From 3ae84297d593cc5103d292e03191e2788ce4eaaa Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 13:09:45 +0200 Subject: [PATCH] Add crop/trim/canvas padding to adjustments, wire all sliders to config Add crop to aspect ratio (8 ratios), trim whitespace, and canvas padding controls to the adjustments step per design doc. Wire brightness, contrast, saturation, sharpen, grayscale, and sepia to JobConfig. Add Select All / Deselect All toolbar buttons to images step. Include new adjustment operations in output step summary. --- pixstrip-gtk/src/app.rs | 55 ++++++++ pixstrip-gtk/src/steps/step_adjustments.rs | 154 ++++++++++++++++----- pixstrip-gtk/src/steps/step_images.rs | 14 ++ 3 files changed, 191 insertions(+), 32 deletions(-) diff --git a/pixstrip-gtk/src/app.rs b/pixstrip-gtk/src/app.rs index b80079b..708107a 100644 --- a/pixstrip-gtk/src/app.rs +++ b/pixstrip-gtk/src/app.rs @@ -21,6 +21,15 @@ pub struct JobConfig { // Adjustments pub rotation: u32, pub flip: u32, + pub brightness: i32, + pub contrast: i32, + pub saturation: i32, + pub sharpen: bool, + pub grayscale: bool, + pub sepia: bool, + pub crop_aspect_ratio: u32, + pub trim_whitespace: bool, + pub canvas_padding: u32, // Convert pub convert_enabled: bool, pub convert_format: Option, @@ -142,6 +151,15 @@ fn build_ui(app: &adw::Application) { allow_upscale: false, rotation: 0, flip: 0, + brightness: 0, + contrast: 0, + saturation: 0, + sharpen: false, + grayscale: false, + sepia: false, + crop_aspect_ratio: 0, + trim_whitespace: false, + canvas_padding: 0, convert_enabled: if remember { sess_state.convert_enabled.unwrap_or(false) } else { false }, convert_format: None, compress_enabled: if remember { sess_state.compress_enabled.unwrap_or(true) } else { true }, @@ -1705,6 +1723,43 @@ fn update_output_summary(ui: &WizardUi) { }; ops.push(fl.to_string()); } + if cfg.brightness != 0 { + ops.push(format!("Brightness {:+}", cfg.brightness)); + } + if cfg.contrast != 0 { + ops.push(format!("Contrast {:+}", cfg.contrast)); + } + if cfg.saturation != 0 { + ops.push(format!("Saturation {:+}", cfg.saturation)); + } + if cfg.sharpen { + ops.push("Sharpen".to_string()); + } + if cfg.grayscale { + ops.push("Grayscale".to_string()); + } + if cfg.sepia { + ops.push("Sepia".to_string()); + } + if cfg.crop_aspect_ratio > 0 { + let ratio = match cfg.crop_aspect_ratio { + 1 => "1:1", + 2 => "4:3", + 3 => "3:2", + 4 => "16:9", + 5 => "9:16", + 6 => "3:4", + 7 => "2:3", + _ => "Custom", + }; + ops.push(format!("Crop {}", ratio)); + } + if cfg.trim_whitespace { + ops.push("Trim whitespace".to_string()); + } + if cfg.canvas_padding > 0 { + ops.push(format!("Padding {}px", cfg.canvas_padding)); + } let summary_text = if ops.is_empty() { "No operations configured".to_string() diff --git a/pixstrip-gtk/src/steps/step_adjustments.rs b/pixstrip-gtk/src/steps/step_adjustments.rs index 3ef591b..aa687ef 100644 --- a/pixstrip-gtk/src/steps/step_adjustments.rs +++ b/pixstrip-gtk/src/steps/step_adjustments.rs @@ -50,12 +50,51 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { rotate_group.add(&flip_row); content.append(&rotate_group); - // Advanced adjustments in an expander - let advanced_group = adw::PreferencesGroup::builder() + // Crop and canvas group + let crop_group = adw::PreferencesGroup::builder() + .title("Crop and Canvas") + .build(); + + let crop_row = adw::ComboRow::builder() + .title("Crop to Aspect Ratio") + .subtitle("Crop images to a specific aspect ratio from center") + .build(); + let crop_model = gtk::StringList::new(&[ + "None", + "1:1 (Square)", + "4:3", + "3:2", + "16:9", + "9:16 (Portrait)", + "3:4 (Portrait)", + "2:3 (Portrait)", + ]); + crop_row.set_model(Some(&crop_model)); + crop_row.set_selected(cfg.crop_aspect_ratio); + + let trim_row = adw::SwitchRow::builder() + .title("Trim Whitespace") + .subtitle("Remove uniform borders around the image") + .active(cfg.trim_whitespace) + .build(); + + let padding_row = adw::SpinRow::builder() + .title("Canvas Padding") + .subtitle("Add uniform padding around the image (pixels)") + .adjustment(>k::Adjustment::new(cfg.canvas_padding as f64, 0.0, 500.0, 1.0, 10.0, 0.0)) + .build(); + + crop_group.add(&crop_row); + crop_group.add(&trim_row); + crop_group.add(&padding_row); + content.append(&crop_group); + + // Image adjustments + let adjust_group = adw::PreferencesGroup::builder() .title("Image Adjustments") .build(); - let advanced_expander = adw::ExpanderRow::builder() + let adjust_expander = adw::ExpanderRow::builder() .title("Advanced Adjustments") .subtitle("Brightness, contrast, saturation, effects") .show_enable_switch(false) @@ -64,83 +103,71 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { // Brightness slider (-100 to +100) let brightness_row = adw::ActionRow::builder() .title("Brightness") - .subtitle("0") + .subtitle(format!("{}", cfg.brightness)) .build(); let brightness_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, -100.0, 100.0, 1.0); - brightness_scale.set_value(0.0); + brightness_scale.set_value(cfg.brightness as f64); brightness_scale.set_hexpand(true); brightness_scale.set_valign(gtk::Align::Center); brightness_scale.set_size_request(200, -1); brightness_scale.set_draw_value(false); - let br_label = brightness_row.clone(); - brightness_scale.connect_value_changed(move |scale| { - br_label.set_subtitle(&format!("{:.0}", scale.value())); - }); brightness_row.add_suffix(&brightness_scale); - advanced_expander.add_row(&brightness_row); + adjust_expander.add_row(&brightness_row); // Contrast slider (-100 to +100) let contrast_row = adw::ActionRow::builder() .title("Contrast") - .subtitle("0") + .subtitle(format!("{}", cfg.contrast)) .build(); let contrast_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, -100.0, 100.0, 1.0); - contrast_scale.set_value(0.0); + contrast_scale.set_value(cfg.contrast as f64); contrast_scale.set_hexpand(true); contrast_scale.set_valign(gtk::Align::Center); contrast_scale.set_size_request(200, -1); contrast_scale.set_draw_value(false); - let ct_label = contrast_row.clone(); - contrast_scale.connect_value_changed(move |scale| { - ct_label.set_subtitle(&format!("{:.0}", scale.value())); - }); contrast_row.add_suffix(&contrast_scale); - advanced_expander.add_row(&contrast_row); + adjust_expander.add_row(&contrast_row); // Saturation slider (-100 to +100) let saturation_row = adw::ActionRow::builder() .title("Saturation") - .subtitle("0") + .subtitle(format!("{}", cfg.saturation)) .build(); let saturation_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, -100.0, 100.0, 1.0); - saturation_scale.set_value(0.0); + saturation_scale.set_value(cfg.saturation as f64); saturation_scale.set_hexpand(true); saturation_scale.set_valign(gtk::Align::Center); saturation_scale.set_size_request(200, -1); saturation_scale.set_draw_value(false); - let sat_label = saturation_row.clone(); - saturation_scale.connect_value_changed(move |scale| { - sat_label.set_subtitle(&format!("{:.0}", scale.value())); - }); saturation_row.add_suffix(&saturation_scale); - advanced_expander.add_row(&saturation_row); + adjust_expander.add_row(&saturation_row); // Sharpen after resize let sharpen_row = adw::SwitchRow::builder() .title("Sharpen after resize") .subtitle("Apply subtle sharpening to resized images") - .active(false) + .active(cfg.sharpen) .build(); - advanced_expander.add_row(&sharpen_row); + adjust_expander.add_row(&sharpen_row); // Grayscale let grayscale_row = adw::SwitchRow::builder() .title("Grayscale") .subtitle("Convert images to black and white") - .active(false) + .active(cfg.grayscale) .build(); - advanced_expander.add_row(&grayscale_row); + adjust_expander.add_row(&grayscale_row); // Sepia let sepia_row = adw::SwitchRow::builder() .title("Sepia") .subtitle("Apply a warm vintage tone") - .active(false) + .active(cfg.sepia) .build(); - advanced_expander.add_row(&sepia_row); + adjust_expander.add_row(&sepia_row); - advanced_group.add(&advanced_expander); - content.append(&advanced_group); + adjust_group.add(&adjust_expander); + content.append(&adjust_group); drop(cfg); @@ -157,6 +184,69 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { jc.borrow_mut().flip = row.selected(); }); } + { + let jc = state.job_config.clone(); + crop_row.connect_selected_notify(move |row| { + jc.borrow_mut().crop_aspect_ratio = row.selected(); + }); + } + { + let jc = state.job_config.clone(); + trim_row.connect_active_notify(move |row| { + jc.borrow_mut().trim_whitespace = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + padding_row.connect_value_notify(move |row| { + jc.borrow_mut().canvas_padding = row.value() as u32; + }); + } + { + let jc = state.job_config.clone(); + let label = brightness_row; + brightness_scale.connect_value_changed(move |scale| { + let val = scale.value().round() as i32; + jc.borrow_mut().brightness = val; + label.set_subtitle(&format!("{}", val)); + }); + } + { + let jc = state.job_config.clone(); + let label = contrast_row; + contrast_scale.connect_value_changed(move |scale| { + let val = scale.value().round() as i32; + jc.borrow_mut().contrast = val; + label.set_subtitle(&format!("{}", val)); + }); + } + { + let jc = state.job_config.clone(); + let label = saturation_row; + saturation_scale.connect_value_changed(move |scale| { + let val = scale.value().round() as i32; + jc.borrow_mut().saturation = val; + label.set_subtitle(&format!("{}", val)); + }); + } + { + let jc = state.job_config.clone(); + sharpen_row.connect_active_notify(move |row| { + jc.borrow_mut().sharpen = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + grayscale_row.connect_active_notify(move |row| { + jc.borrow_mut().grayscale = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + sepia_row.connect_active_notify(move |row| { + jc.borrow_mut().sepia = row.is_active(); + }); + } scrolled.set_child(Some(&content)); diff --git a/pixstrip-gtk/src/steps/step_images.rs b/pixstrip-gtk/src/steps/step_images.rs index 3ce54f0..36b06df 100644 --- a/pixstrip-gtk/src/steps/step_images.rs +++ b/pixstrip-gtk/src/steps/step_images.rs @@ -304,6 +304,18 @@ fn build_loaded_state(state: &AppState) -> gtk::Box { .build(); add_button.add_css_class("flat"); + let select_all_button = gtk::Button::builder() + .icon_name("edit-select-all-symbolic") + .tooltip_text("Select all images (Ctrl+A)") + .build(); + select_all_button.add_css_class("flat"); + + let deselect_all_button = gtk::Button::builder() + .icon_name("edit-clear-symbolic") + .tooltip_text("Deselect all images (Ctrl+Shift+A)") + .build(); + deselect_all_button.add_css_class("flat"); + let clear_button = gtk::Button::builder() .icon_name("edit-clear-all-symbolic") .tooltip_text("Remove all images") @@ -326,6 +338,8 @@ fn build_loaded_state(state: &AppState) -> gtk::Box { } toolbar.append(&count_label); + toolbar.append(&select_all_button); + toolbar.append(&deselect_all_button); toolbar.append(&add_button); toolbar.append(&clear_button);