From 8e50fa5e8763aad418c5830862ea1ce575556995 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 14:05:52 +0200 Subject: [PATCH] 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. --- pixstrip-gtk/Cargo.toml | 1 + pixstrip-gtk/src/steps/step_resize.rs | 126 +++++++++++++++++++++++++- 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/pixstrip-gtk/Cargo.toml b/pixstrip-gtk/Cargo.toml index 36bab94..525dbdf 100644 --- a/pixstrip-gtk/Cargo.toml +++ b/pixstrip-gtk/Cargo.toml @@ -8,3 +8,4 @@ license.workspace = true pixstrip-core = { workspace = true } gtk = { package = "gtk4", version = "0.11.0", features = ["gnome_49"] } adw = { package = "libadwaita", version = "0.9.1", features = ["v1_8"] } +image = "0.25" diff --git a/pixstrip-gtk/src/steps/step_resize.rs b/pixstrip-gtk/src/steps/step_resize.rs index b262b75..da7e5c9 100644 --- a/pixstrip-gtk/src/steps/step_resize.rs +++ b/pixstrip-gtk/src/steps/step_resize.rs @@ -224,6 +224,110 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { 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) let orientation_group = adw::PreferencesGroup::builder() .title("Orientation") @@ -308,14 +412,24 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { } { let jc = state.job_config.clone(); + let pw = preview_width.clone(); + let draw = drawing.clone(); 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 ph = preview_height.clone(); + let draw = drawing.clone(); 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) .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)) +}