Add visual size preview to resize step
Show proportional rectangles comparing original image dimensions (gray) vs target output dimensions (blue). Preview updates live as the user changes width/height values. Uses first loaded image for actual dimensions when available.
This commit is contained in:
@@ -8,3 +8,4 @@ license.workspace = true
|
|||||||
pixstrip-core = { workspace = true }
|
pixstrip-core = { workspace = true }
|
||||||
gtk = { package = "gtk4", version = "0.11.0", features = ["gnome_49"] }
|
gtk = { package = "gtk4", version = "0.11.0", features = ["gnome_49"] }
|
||||||
adw = { package = "libadwaita", version = "0.9.1", features = ["v1_8"] }
|
adw = { package = "libadwaita", version = "0.9.1", features = ["v1_8"] }
|
||||||
|
image = "0.25"
|
||||||
|
|||||||
@@ -224,6 +224,110 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
|
|
||||||
content.append(&mode_stack);
|
content.append(&mode_stack);
|
||||||
|
|
||||||
|
// Size preview visualization
|
||||||
|
let preview_group = adw::PreferencesGroup::builder()
|
||||||
|
.title("Size Preview")
|
||||||
|
.description("Visual comparison of original and output dimensions")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Use shared state for the preview drawing
|
||||||
|
let preview_width = std::rc::Rc::new(std::cell::RefCell::new(cfg.resize_width));
|
||||||
|
let preview_height = std::rc::Rc::new(std::cell::RefCell::new(cfg.resize_height));
|
||||||
|
let loaded_files = state.loaded_files.clone();
|
||||||
|
|
||||||
|
let drawing = gtk::DrawingArea::builder()
|
||||||
|
.content_width(300)
|
||||||
|
.content_height(150)
|
||||||
|
.halign(gtk::Align::Center)
|
||||||
|
.margin_top(8)
|
||||||
|
.margin_bottom(8)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let pw = preview_width.clone();
|
||||||
|
let ph = preview_height.clone();
|
||||||
|
let lf = loaded_files.clone();
|
||||||
|
drawing.set_draw_func(move |_area, cr, width, height| {
|
||||||
|
let target_w = *pw.borrow() as f64;
|
||||||
|
let target_h = *ph.borrow() as f64;
|
||||||
|
|
||||||
|
// Try to get actual first image dimensions
|
||||||
|
let (orig_w, orig_h) = {
|
||||||
|
let files = lf.borrow();
|
||||||
|
if let Some(first) = files.first() {
|
||||||
|
image_dimensions(first).unwrap_or((4000.0, 3000.0))
|
||||||
|
} else {
|
||||||
|
(4000.0, 3000.0)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let actual_target_w = if target_w > 0.0 { target_w } else { orig_w };
|
||||||
|
let actual_target_h = if target_h > 0.0 {
|
||||||
|
target_h
|
||||||
|
} else if target_w > 0.0 {
|
||||||
|
orig_h * target_w / orig_w
|
||||||
|
} else {
|
||||||
|
orig_h
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_dim = orig_w.max(orig_h).max(actual_target_w).max(actual_target_h);
|
||||||
|
if max_dim == 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pad = 20.0;
|
||||||
|
let avail_w = width as f64 - pad * 2.0;
|
||||||
|
let avail_h = height as f64 - pad * 2.0;
|
||||||
|
let scale = (avail_w / max_dim).min(avail_h / max_dim);
|
||||||
|
|
||||||
|
let ow = orig_w * scale;
|
||||||
|
let oh = orig_h * scale;
|
||||||
|
let tw = actual_target_w * scale;
|
||||||
|
let th = actual_target_h * scale;
|
||||||
|
|
||||||
|
// Draw original rectangle (semi-transparent)
|
||||||
|
let _ = cr.set_source_rgba(0.5, 0.5, 0.5, 0.3);
|
||||||
|
let _ = cr.rectangle(pad, pad, ow, oh);
|
||||||
|
let _ = cr.fill();
|
||||||
|
let _ = cr.set_source_rgba(0.5, 0.5, 0.5, 0.7);
|
||||||
|
let _ = cr.rectangle(pad, pad, ow, oh);
|
||||||
|
let _ = cr.stroke();
|
||||||
|
|
||||||
|
// Draw target rectangle (accent color)
|
||||||
|
let _ = cr.set_source_rgba(0.2, 0.5, 0.9, 0.3);
|
||||||
|
let _ = cr.rectangle(pad, pad, tw, th);
|
||||||
|
let _ = cr.fill();
|
||||||
|
let _ = cr.set_source_rgba(0.2, 0.5, 0.9, 0.9);
|
||||||
|
let _ = cr.rectangle(pad, pad, tw, th);
|
||||||
|
let _ = cr.stroke();
|
||||||
|
|
||||||
|
// Labels
|
||||||
|
let _ = cr.set_source_rgba(0.6, 0.6, 0.6, 1.0);
|
||||||
|
let _ = cr.set_font_size(10.0);
|
||||||
|
let _ = cr.move_to(pad + ow + 4.0, pad + oh / 2.0);
|
||||||
|
let _ = cr.show_text(&format!("{}x{}", orig_w as u32, orig_h as u32));
|
||||||
|
|
||||||
|
let _ = cr.set_source_rgba(0.2, 0.5, 0.9, 1.0);
|
||||||
|
let _ = cr.move_to(pad + tw + 4.0, pad + th / 2.0 + 12.0);
|
||||||
|
let _ = cr.show_text(&format!("{}x{}", actual_target_w as u32, actual_target_h as u32));
|
||||||
|
});
|
||||||
|
|
||||||
|
let preview_label = gtk::Label::builder()
|
||||||
|
.label("Gray = original, Blue = target size")
|
||||||
|
.css_classes(["dim-label", "caption"])
|
||||||
|
.halign(gtk::Align::Center)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let preview_box = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
.spacing(4)
|
||||||
|
.margin_top(4)
|
||||||
|
.margin_bottom(4)
|
||||||
|
.build();
|
||||||
|
preview_box.append(&drawing);
|
||||||
|
preview_box.append(&preview_label);
|
||||||
|
preview_group.add(&preview_box);
|
||||||
|
content.append(&preview_group);
|
||||||
|
|
||||||
// Basic orientation adjustments (folded into resize step per design doc)
|
// Basic orientation adjustments (folded into resize step per design doc)
|
||||||
let orientation_group = adw::PreferencesGroup::builder()
|
let orientation_group = adw::PreferencesGroup::builder()
|
||||||
.title("Orientation")
|
.title("Orientation")
|
||||||
@@ -308,14 +412,24 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
let jc = state.job_config.clone();
|
let jc = state.job_config.clone();
|
||||||
|
let pw = preview_width.clone();
|
||||||
|
let draw = drawing.clone();
|
||||||
width_row.connect_value_notify(move |row| {
|
width_row.connect_value_notify(move |row| {
|
||||||
jc.borrow_mut().resize_width = row.value() as u32;
|
let val = row.value() as u32;
|
||||||
|
jc.borrow_mut().resize_width = val;
|
||||||
|
*pw.borrow_mut() = val;
|
||||||
|
draw.queue_draw();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let jc = state.job_config.clone();
|
let jc = state.job_config.clone();
|
||||||
|
let ph = preview_height.clone();
|
||||||
|
let draw = drawing.clone();
|
||||||
height_row.connect_value_notify(move |row| {
|
height_row.connect_value_notify(move |row| {
|
||||||
jc.borrow_mut().resize_height = row.value() as u32;
|
let val = row.value() as u32;
|
||||||
|
jc.borrow_mut().resize_height = val;
|
||||||
|
*ph.borrow_mut() = val;
|
||||||
|
draw.queue_draw();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -350,3 +464,11 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
.child(&clamp)
|
.child(&clamp)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get dimensions of an image file without loading the full image
|
||||||
|
fn image_dimensions(path: &std::path::Path) -> Option<(f64, f64)> {
|
||||||
|
let reader = image::ImageReader::open(path).ok()?;
|
||||||
|
let reader = reader.with_guessed_format().ok()?;
|
||||||
|
let (w, h) = reader.into_dimensions().ok()?;
|
||||||
|
Some((w as f64, h as f64))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user