Add crop/trim/canvas padding to adjustments, wire all sliders to config

This commit is contained in:
2026-03-06 13:09:45 +02:00
parent 6ef0f14804
commit ac6f305bcc
3 changed files with 191 additions and 32 deletions

View File

@@ -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<pixstrip_core::types::ImageFormat>,
@@ -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()

View File

@@ -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(&gtk::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));

View File

@@ -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);