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.
This commit is contained in:
@@ -21,6 +21,15 @@ pub struct JobConfig {
|
|||||||
// Adjustments
|
// Adjustments
|
||||||
pub rotation: u32,
|
pub rotation: u32,
|
||||||
pub flip: 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
|
// Convert
|
||||||
pub convert_enabled: bool,
|
pub convert_enabled: bool,
|
||||||
pub convert_format: Option<pixstrip_core::types::ImageFormat>,
|
pub convert_format: Option<pixstrip_core::types::ImageFormat>,
|
||||||
@@ -142,6 +151,15 @@ fn build_ui(app: &adw::Application) {
|
|||||||
allow_upscale: false,
|
allow_upscale: false,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
flip: 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_enabled: if remember { sess_state.convert_enabled.unwrap_or(false) } else { false },
|
||||||
convert_format: None,
|
convert_format: None,
|
||||||
compress_enabled: if remember { sess_state.compress_enabled.unwrap_or(true) } else { true },
|
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());
|
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() {
|
let summary_text = if ops.is_empty() {
|
||||||
"No operations configured".to_string()
|
"No operations configured".to_string()
|
||||||
|
|||||||
@@ -50,12 +50,51 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
rotate_group.add(&flip_row);
|
rotate_group.add(&flip_row);
|
||||||
content.append(&rotate_group);
|
content.append(&rotate_group);
|
||||||
|
|
||||||
// Advanced adjustments in an expander
|
// Crop and canvas group
|
||||||
let advanced_group = adw::PreferencesGroup::builder()
|
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")
|
.title("Image Adjustments")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let advanced_expander = adw::ExpanderRow::builder()
|
let adjust_expander = adw::ExpanderRow::builder()
|
||||||
.title("Advanced Adjustments")
|
.title("Advanced Adjustments")
|
||||||
.subtitle("Brightness, contrast, saturation, effects")
|
.subtitle("Brightness, contrast, saturation, effects")
|
||||||
.show_enable_switch(false)
|
.show_enable_switch(false)
|
||||||
@@ -64,83 +103,71 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
// Brightness slider (-100 to +100)
|
// Brightness slider (-100 to +100)
|
||||||
let brightness_row = adw::ActionRow::builder()
|
let brightness_row = adw::ActionRow::builder()
|
||||||
.title("Brightness")
|
.title("Brightness")
|
||||||
.subtitle("0")
|
.subtitle(format!("{}", cfg.brightness))
|
||||||
.build();
|
.build();
|
||||||
let brightness_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, -100.0, 100.0, 1.0);
|
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_hexpand(true);
|
||||||
brightness_scale.set_valign(gtk::Align::Center);
|
brightness_scale.set_valign(gtk::Align::Center);
|
||||||
brightness_scale.set_size_request(200, -1);
|
brightness_scale.set_size_request(200, -1);
|
||||||
brightness_scale.set_draw_value(false);
|
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);
|
brightness_row.add_suffix(&brightness_scale);
|
||||||
advanced_expander.add_row(&brightness_row);
|
adjust_expander.add_row(&brightness_row);
|
||||||
|
|
||||||
// Contrast slider (-100 to +100)
|
// Contrast slider (-100 to +100)
|
||||||
let contrast_row = adw::ActionRow::builder()
|
let contrast_row = adw::ActionRow::builder()
|
||||||
.title("Contrast")
|
.title("Contrast")
|
||||||
.subtitle("0")
|
.subtitle(format!("{}", cfg.contrast))
|
||||||
.build();
|
.build();
|
||||||
let contrast_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, -100.0, 100.0, 1.0);
|
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_hexpand(true);
|
||||||
contrast_scale.set_valign(gtk::Align::Center);
|
contrast_scale.set_valign(gtk::Align::Center);
|
||||||
contrast_scale.set_size_request(200, -1);
|
contrast_scale.set_size_request(200, -1);
|
||||||
contrast_scale.set_draw_value(false);
|
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);
|
contrast_row.add_suffix(&contrast_scale);
|
||||||
advanced_expander.add_row(&contrast_row);
|
adjust_expander.add_row(&contrast_row);
|
||||||
|
|
||||||
// Saturation slider (-100 to +100)
|
// Saturation slider (-100 to +100)
|
||||||
let saturation_row = adw::ActionRow::builder()
|
let saturation_row = adw::ActionRow::builder()
|
||||||
.title("Saturation")
|
.title("Saturation")
|
||||||
.subtitle("0")
|
.subtitle(format!("{}", cfg.saturation))
|
||||||
.build();
|
.build();
|
||||||
let saturation_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, -100.0, 100.0, 1.0);
|
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_hexpand(true);
|
||||||
saturation_scale.set_valign(gtk::Align::Center);
|
saturation_scale.set_valign(gtk::Align::Center);
|
||||||
saturation_scale.set_size_request(200, -1);
|
saturation_scale.set_size_request(200, -1);
|
||||||
saturation_scale.set_draw_value(false);
|
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);
|
saturation_row.add_suffix(&saturation_scale);
|
||||||
advanced_expander.add_row(&saturation_row);
|
adjust_expander.add_row(&saturation_row);
|
||||||
|
|
||||||
// Sharpen after resize
|
// Sharpen after resize
|
||||||
let sharpen_row = adw::SwitchRow::builder()
|
let sharpen_row = adw::SwitchRow::builder()
|
||||||
.title("Sharpen after resize")
|
.title("Sharpen after resize")
|
||||||
.subtitle("Apply subtle sharpening to resized images")
|
.subtitle("Apply subtle sharpening to resized images")
|
||||||
.active(false)
|
.active(cfg.sharpen)
|
||||||
.build();
|
.build();
|
||||||
advanced_expander.add_row(&sharpen_row);
|
adjust_expander.add_row(&sharpen_row);
|
||||||
|
|
||||||
// Grayscale
|
// Grayscale
|
||||||
let grayscale_row = adw::SwitchRow::builder()
|
let grayscale_row = adw::SwitchRow::builder()
|
||||||
.title("Grayscale")
|
.title("Grayscale")
|
||||||
.subtitle("Convert images to black and white")
|
.subtitle("Convert images to black and white")
|
||||||
.active(false)
|
.active(cfg.grayscale)
|
||||||
.build();
|
.build();
|
||||||
advanced_expander.add_row(&grayscale_row);
|
adjust_expander.add_row(&grayscale_row);
|
||||||
|
|
||||||
// Sepia
|
// Sepia
|
||||||
let sepia_row = adw::SwitchRow::builder()
|
let sepia_row = adw::SwitchRow::builder()
|
||||||
.title("Sepia")
|
.title("Sepia")
|
||||||
.subtitle("Apply a warm vintage tone")
|
.subtitle("Apply a warm vintage tone")
|
||||||
.active(false)
|
.active(cfg.sepia)
|
||||||
.build();
|
.build();
|
||||||
advanced_expander.add_row(&sepia_row);
|
adjust_expander.add_row(&sepia_row);
|
||||||
|
|
||||||
advanced_group.add(&advanced_expander);
|
adjust_group.add(&adjust_expander);
|
||||||
content.append(&advanced_group);
|
content.append(&adjust_group);
|
||||||
|
|
||||||
drop(cfg);
|
drop(cfg);
|
||||||
|
|
||||||
@@ -157,6 +184,69 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
jc.borrow_mut().flip = row.selected();
|
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));
|
scrolled.set_child(Some(&content));
|
||||||
|
|
||||||
|
|||||||
@@ -304,6 +304,18 @@ fn build_loaded_state(state: &AppState) -> gtk::Box {
|
|||||||
.build();
|
.build();
|
||||||
add_button.add_css_class("flat");
|
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()
|
let clear_button = gtk::Button::builder()
|
||||||
.icon_name("edit-clear-all-symbolic")
|
.icon_name("edit-clear-all-symbolic")
|
||||||
.tooltip_text("Remove all images")
|
.tooltip_text("Remove all images")
|
||||||
@@ -326,6 +338,8 @@ fn build_loaded_state(state: &AppState) -> gtk::Box {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toolbar.append(&count_label);
|
toolbar.append(&count_label);
|
||||||
|
toolbar.append(&select_all_button);
|
||||||
|
toolbar.append(&deselect_all_button);
|
||||||
toolbar.append(&add_button);
|
toolbar.append(&add_button);
|
||||||
toolbar.append(&clear_button);
|
toolbar.append(&clear_button);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user