use adw::prelude::*; use gtk::glib; use std::cell::Cell; use std::rc::Rc; use crate::app::AppState; pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { let cfg = state.job_config.borrow(); // === OUTER LAYOUT === let outer = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(0) .vexpand(true) .build(); // --- Enable toggle (full width) --- let enable_group = adw::PreferencesGroup::builder() .margin_start(12) .margin_end(12) .margin_top(12) .build(); let enable_row = adw::SwitchRow::builder() .title("Enable Watermark") .subtitle("Add text or image watermark to processed images") .active(cfg.watermark_enabled) .tooltip_text("Toggle watermark on or off") .build(); enable_group.add(&enable_row); outer.append(&enable_group); // === LEFT SIDE: Preview === let preview_picture = gtk::Picture::builder() .content_fit(gtk::ContentFit::Contain) .hexpand(true) .vexpand(true) .build(); preview_picture.set_can_target(true); preview_picture.set_focusable(true); preview_picture.update_property(&[ gtk::accessible::Property::Label("Watermark preview - press Space to cycle images"), ]); let info_label = gtk::Label::builder() .label("No images loaded") .css_classes(["dim-label", "caption"]) .halign(gtk::Align::Center) .margin_top(4) .margin_bottom(4) .build(); let preview_frame = gtk::Frame::builder() .hexpand(true) .vexpand(true) .build(); preview_frame.set_child(Some(&preview_picture)); let preview_box = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(4) .hexpand(true) .vexpand(true) .build(); preview_box.append(&preview_frame); preview_box.append(&info_label); // === RIGHT SIDE: Controls (scrollable) === let controls = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(12) .margin_start(12) .build(); // --- Watermark type --- let type_group = adw::PreferencesGroup::builder() .title("Watermark Type") .build(); let type_row = adw::ComboRow::builder() .title("Type") .subtitle("Choose text or image watermark") .use_subtitle(true) .tooltip_text("Choose between text or image/logo overlay") .build(); type_row.set_model(Some(>k::StringList::new(&["Text Watermark", "Image Watermark"]))); type_row.set_list_factory(Some(&super::full_text_list_factory())); type_row.set_selected(if cfg.watermark_use_image { 1 } else { 0 }); type_group.add(&type_row); controls.append(&type_group); // --- Text watermark settings --- let text_group = adw::PreferencesGroup::builder() .title("Text Watermark") .visible(!cfg.watermark_use_image) .build(); let text_row = adw::EntryRow::builder() .title("Watermark Text") .text(&cfg.watermark_text) .tooltip_text("The text that appears as a watermark on each image") .build(); let font_row = adw::ActionRow::builder() .title("Font Family") .subtitle("Choose a typeface for the watermark text") .build(); let font_dialog = gtk::FontDialog::builder() .title("Choose Watermark Font") .modal(true) .build(); let font_button = gtk::FontDialogButton::builder() .dialog(&font_dialog) .valign(gtk::Align::Center) .build(); if !cfg.watermark_font_family.is_empty() { let desc = gtk::pango::FontDescription::from_string(&cfg.watermark_font_family); font_button.set_font_desc(&desc); } font_button.update_property(&[ gtk::accessible::Property::Label("Choose watermark font"), ]); font_row.add_suffix(&font_button); let font_size_row = adw::SpinRow::builder() .title("Font Size") .subtitle("Size in pixels") .adjustment(>k::Adjustment::new(cfg.watermark_font_size as f64, 8.0, 200.0, 1.0, 10.0, 0.0)) .tooltip_text("Size of watermark text in pixels") .build(); text_group.add(&text_row); text_group.add(&font_row); text_group.add(&font_size_row); controls.append(&text_group); // --- Image watermark settings --- let image_group = adw::PreferencesGroup::builder() .title("Image Watermark") .visible(cfg.watermark_use_image) .build(); let image_path_row = adw::ActionRow::builder() .title("Logo Image") .subtitle( cfg.watermark_image_path .as_ref() .map(|p| p.display().to_string()) .unwrap_or_else(|| "No image selected".to_string()), ) .activatable(true) .build(); let image_prefix_icon = gtk::Image::from_icon_name("image-x-generic-symbolic"); image_prefix_icon.set_accessible_role(gtk::AccessibleRole::Presentation); image_path_row.add_prefix(&image_prefix_icon); let choose_image_button = gtk::Button::builder() .icon_name("document-open-symbolic") .tooltip_text("Choose logo image") .valign(gtk::Align::Center) .has_frame(false) .build(); choose_image_button.update_property(&[ gtk::accessible::Property::Label("Choose logo image"), ]); image_path_row.add_suffix(&choose_image_button); image_group.add(&image_path_row); controls.append(&image_group); // --- Position group with 3x3 grid --- let position_group = adw::PreferencesGroup::builder() .title("Position") .description("Choose where the watermark appears on the image") .build(); let position_names = [ "Top Left", "Top Center", "Top Right", "Middle Left", "Center", "Middle Right", "Bottom Left", "Bottom Center", "Bottom Right", ]; let grid = gtk::Grid::builder() .row_spacing(4) .column_spacing(4) .halign(gtk::Align::Center) .margin_top(8) .margin_bottom(8) .build(); // Frame styled to look like a miniature image let grid_outer = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .halign(gtk::Align::Center) .build(); let grid_frame = gtk::Frame::builder() .halign(gtk::Align::Center) .build(); grid_frame.set_child(Some(&grid)); grid_frame.update_property(&[ gtk::accessible::Property::Label("Watermark position grid. Select where the watermark appears on the image."), ]); // Image outline label above the grid let grid_title = gtk::Label::builder() .label("Image") .css_classes(["dim-label", "caption"]) .halign(gtk::Align::Center) .margin_bottom(4) .build(); grid_outer.append(&grid_title); grid_outer.append(&grid_frame); let mut first_button: Option = None; let buttons: Vec = position_names.iter().enumerate().map(|(i, name)| { let btn = gtk::ToggleButton::builder() .tooltip_text(*name) .width_request(48) .height_request(48) .build(); btn.update_property(&[ gtk::accessible::Property::Label(&format!("Watermark position: {}", name)), ]); let icon = if i == cfg.watermark_position as usize { "radio-checked-symbolic" } else { "radio-symbolic" }; btn.set_child(Some(>k::Image::from_icon_name(icon))); btn.set_active(i == cfg.watermark_position as usize); if let Some(ref first) = first_button { btn.set_group(Some(first)); } else { first_button = Some(btn.clone()); } let row = i / 3; let col = i % 3; grid.attach(&btn, col as i32, row as i32, 1, 1); btn }).collect(); position_group.add(&grid_outer); let position_label = gtk::Label::builder() .label(position_names.get(cfg.watermark_position as usize).copied().unwrap_or("Center")) .css_classes(["dim-label"]) .halign(gtk::Align::Center) .margin_bottom(4) .build(); position_group.add(&position_label); controls.append(&position_group); // --- Advanced options --- let advanced_group = adw::PreferencesGroup::builder() .title("Advanced") .build(); let advanced_expander = adw::ExpanderRow::builder() .title("Advanced Options") .subtitle("Color, opacity, rotation, tiling, margin, scale") .show_enable_switch(false) .expanded(state.is_section_expanded("watermark-advanced")) .build(); { let st = state.clone(); advanced_expander.connect_expanded_notify(move |row| { st.set_section_expanded("watermark-advanced", row.is_expanded()); }); } // Text color picker let color_row = adw::ActionRow::builder() .title("Text Color") .subtitle("Color of the watermark text") .build(); let initial_color = gtk::gdk::RGBA::new( cfg.watermark_color[0] as f32 / 255.0, cfg.watermark_color[1] as f32 / 255.0, cfg.watermark_color[2] as f32 / 255.0, cfg.watermark_color[3] as f32 / 255.0, ); let color_dialog = gtk::ColorDialog::builder() .with_alpha(true) .title("Watermark Text Color") .build(); let color_button = gtk::ColorDialogButton::builder() .dialog(&color_dialog) .rgba(&initial_color) .valign(gtk::Align::Center) .build(); color_button.update_property(&[ gtk::accessible::Property::Label("Choose watermark text color"), ]); color_row.add_suffix(&color_button); // Opacity slider + reset let opacity_row = adw::ActionRow::builder() .title("Opacity") .subtitle(&format!("{}%", (cfg.watermark_opacity * 100.0).round() as i32)) .build(); let opacity_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, 0.0, 100.0, 1.0); opacity_scale.set_value((cfg.watermark_opacity * 100.0) as f64); opacity_scale.set_draw_value(false); opacity_scale.set_hexpand(false); opacity_scale.set_valign(gtk::Align::Center); opacity_scale.set_width_request(180); opacity_scale.update_property(&[ gtk::accessible::Property::Label("Watermark opacity, 0 to 100 percent"), ]); let opacity_reset = gtk::Button::builder() .icon_name("edit-undo-symbolic") .valign(gtk::Align::Center) .tooltip_text("Reset to 50%") .has_frame(false) .build(); opacity_reset.update_property(&[ gtk::accessible::Property::Label("Reset opacity to 50%"), ]); opacity_reset.set_sensitive((cfg.watermark_opacity - 0.5).abs() > 0.01); opacity_row.add_suffix(&opacity_scale); opacity_row.add_suffix(&opacity_reset); // Rotation slider + reset (-180 to +180) let rotation_row = adw::ActionRow::builder() .title("Rotation") .subtitle(&format!("{} degrees", cfg.watermark_rotation)) .build(); let rotation_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, -180.0, 180.0, 1.0); rotation_scale.set_value(cfg.watermark_rotation as f64); rotation_scale.set_draw_value(false); rotation_scale.set_hexpand(false); rotation_scale.set_valign(gtk::Align::Center); rotation_scale.set_width_request(180); rotation_scale.update_property(&[ gtk::accessible::Property::Label("Watermark rotation, -180 to +180 degrees"), ]); let rotation_reset = gtk::Button::builder() .icon_name("edit-undo-symbolic") .valign(gtk::Align::Center) .tooltip_text("Reset to 0 degrees") .has_frame(false) .build(); rotation_reset.update_property(&[ gtk::accessible::Property::Label("Reset rotation to 0 degrees"), ]); rotation_reset.set_sensitive(cfg.watermark_rotation != 0); rotation_row.add_suffix(&rotation_scale); rotation_row.add_suffix(&rotation_reset); // Tiled toggle let tiled_row = adw::SwitchRow::builder() .title("Tiled / Repeated") .subtitle("Repeat watermark across the entire image") .active(cfg.watermark_tiled) .tooltip_text("Repeat the watermark in a grid pattern across the entire image") .build(); // Margin slider + reset let margin_row = adw::ActionRow::builder() .title("Margin from Edges") .subtitle(&format!("{} px", cfg.watermark_margin)) .build(); let margin_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, 0.0, 200.0, 1.0); margin_scale.set_value(cfg.watermark_margin as f64); margin_scale.set_draw_value(false); margin_scale.set_hexpand(false); margin_scale.set_valign(gtk::Align::Center); margin_scale.set_width_request(180); margin_scale.update_property(&[ gtk::accessible::Property::Label("Watermark margin from edges, 0 to 200 pixels"), ]); let margin_reset = gtk::Button::builder() .icon_name("edit-undo-symbolic") .valign(gtk::Align::Center) .tooltip_text("Reset to 10 px") .has_frame(false) .build(); margin_reset.update_property(&[ gtk::accessible::Property::Label("Reset margin to 10 pixels"), ]); margin_reset.set_sensitive(cfg.watermark_margin != 10); margin_row.add_suffix(&margin_scale); margin_row.add_suffix(&margin_reset); // Scale slider + reset (only relevant for image watermarks) let scale_row = adw::ActionRow::builder() .title("Scale (% of image)") .subtitle(&format!("{}%", cfg.watermark_scale.round() as i32)) .visible(cfg.watermark_use_image) .build(); let scale_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, 1.0, 100.0, 1.0); scale_scale.set_value(cfg.watermark_scale as f64); scale_scale.set_draw_value(false); scale_scale.set_hexpand(false); scale_scale.set_valign(gtk::Align::Center); scale_scale.set_width_request(180); scale_scale.update_property(&[ gtk::accessible::Property::Label("Watermark scale, 1 to 100 percent of image"), ]); let scale_reset = gtk::Button::builder() .icon_name("edit-undo-symbolic") .valign(gtk::Align::Center) .tooltip_text("Reset to 20%") .has_frame(false) .build(); scale_reset.update_property(&[ gtk::accessible::Property::Label("Reset scale to 20%"), ]); scale_reset.set_sensitive((cfg.watermark_scale - 20.0).abs() > 0.5); scale_row.add_suffix(&scale_scale); scale_row.add_suffix(&scale_reset); advanced_expander.add_row(&color_row); advanced_expander.add_row(&opacity_row); advanced_expander.add_row(&rotation_row); advanced_expander.add_row(&tiled_row); advanced_expander.add_row(&margin_row); advanced_expander.add_row(&scale_row); advanced_group.add(&advanced_expander); controls.append(&advanced_group); // Scrollable controls let controls_scrolled = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) .width_request(360) .child(&controls) .build(); // === Main layout: 60/40 side-by-side === let main_box = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(12) .margin_top(12) .margin_bottom(12) .margin_start(12) .margin_end(12) .vexpand(true) .build(); preview_box.set_width_request(400); main_box.append(&preview_box); main_box.append(&controls_scrolled); outer.append(&main_box); // Preview state let preview_index: Rc> = Rc::new(Cell::new(0)); drop(cfg); // === Preview update closure === let preview_gen: Rc> = Rc::new(Cell::new(0)); let update_preview = { let files = state.loaded_files.clone(); let jc = state.job_config.clone(); let pic = preview_picture.clone(); let info = info_label.clone(); let pidx = preview_index.clone(); let bind_gen = preview_gen.clone(); Rc::new(move || { let loaded = files.borrow(); if loaded.is_empty() { info.set_label("No images loaded"); pic.set_paintable(gtk::gdk::Paintable::NONE); return; } let idx = pidx.get().min(loaded.len().saturating_sub(1)); pidx.set(idx); let path = loaded[idx].clone(); let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("image"); info.set_label(&format!("[{}/{}] {}", idx + 1, loaded.len(), name)); let cfg = jc.borrow(); let wm_text = cfg.watermark_text.clone(); let wm_use_image = cfg.watermark_use_image; let wm_image_path = cfg.watermark_image_path.clone(); let wm_position = cfg.watermark_position; let wm_opacity = cfg.watermark_opacity; let wm_font_size = cfg.watermark_font_size; let wm_color = cfg.watermark_color; let wm_font_family = cfg.watermark_font_family.clone(); let wm_tiled = cfg.watermark_tiled; let wm_margin = cfg.watermark_margin; let wm_scale = cfg.watermark_scale; let wm_rotation = cfg.watermark_rotation; let wm_enabled = cfg.watermark_enabled; drop(cfg); let my_gen = bind_gen.get().wrapping_add(1); bind_gen.set(my_gen); let gen_check = bind_gen.clone(); let pic = pic.clone(); let (tx, rx) = std::sync::mpsc::channel::>>(); std::thread::spawn(move || { let result = (|| -> Option> { let img = image::open(&path).ok()?; let img = img.resize(1024, 1024, image::imageops::FilterType::Triangle); let img = if wm_enabled { let position = match wm_position { 0 => pixstrip_core::operations::WatermarkPosition::TopLeft, 1 => pixstrip_core::operations::WatermarkPosition::TopCenter, 2 => pixstrip_core::operations::WatermarkPosition::TopRight, 3 => pixstrip_core::operations::WatermarkPosition::MiddleLeft, 4 => pixstrip_core::operations::WatermarkPosition::Center, 5 => pixstrip_core::operations::WatermarkPosition::MiddleRight, 6 => pixstrip_core::operations::WatermarkPosition::BottomLeft, 7 => pixstrip_core::operations::WatermarkPosition::BottomCenter, _ => pixstrip_core::operations::WatermarkPosition::BottomRight, }; let rotation = if wm_rotation != 0 { Some(pixstrip_core::operations::WatermarkRotation::Custom(wm_rotation as f32)) } else { None }; let wm_config = if wm_use_image { wm_image_path.as_ref().map(|p| { pixstrip_core::operations::WatermarkConfig::Image { path: p.clone(), position, opacity: wm_opacity, scale: wm_scale / 100.0, rotation, tiled: wm_tiled, margin: wm_margin, } }) } else if !wm_text.is_empty() { Some(pixstrip_core::operations::WatermarkConfig::Text { text: wm_text, position, font_size: wm_font_size, opacity: wm_opacity, color: wm_color, font_family: if wm_font_family.is_empty() { None } else { Some(wm_font_family) }, rotation, tiled: wm_tiled, margin: wm_margin, }) } else { None }; if let Some(config) = wm_config { pixstrip_core::operations::watermark::apply_watermark(img, &config).ok()? } else { img } } else { img }; let mut buf = Vec::new(); img.write_to( &mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png, ).ok()?; Some(buf) })(); let _ = tx.send(result); }); glib::timeout_add_local(std::time::Duration::from_millis(100), move || { if gen_check.get() != my_gen { return glib::ControlFlow::Break; } match rx.try_recv() { Ok(Some(bytes)) => { let gbytes = glib::Bytes::from(&bytes); if let Ok(texture) = gtk::gdk::Texture::from_bytes(&gbytes) { pic.set_paintable(Some(&texture)); } glib::ControlFlow::Break } Ok(None) => glib::ControlFlow::Break, Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue, Err(_) => glib::ControlFlow::Break, } }); }) }; // Click-to-cycle on preview { let click = gtk::GestureClick::new(); let pidx = preview_index.clone(); let files = state.loaded_files.clone(); let up = update_preview.clone(); click.connect_released(move |_, _, _, _| { let loaded = files.borrow(); if loaded.len() > 1 { let next = (pidx.get() + 1) % loaded.len(); pidx.set(next); up(); } }); preview_picture.add_controller(click); } // Keyboard support for preview cycling (Space/Enter) { let key = gtk::EventControllerKey::new(); let pidx = preview_index.clone(); let files = state.loaded_files.clone(); let up = update_preview.clone(); key.connect_key_pressed(move |_, keyval, _, _| { if keyval == gtk::gdk::Key::space || keyval == gtk::gdk::Key::Return { let loaded = files.borrow(); if loaded.len() > 1 { let next = (pidx.get() + 1) % loaded.len(); pidx.set(next); up(); } return gtk::glib::Propagation::Stop; } gtk::glib::Propagation::Proceed }); preview_picture.add_controller(key); } // === Wire signals === // Enable toggle { let jc = state.job_config.clone(); let up = update_preview.clone(); enable_row.connect_active_notify(move |row| { jc.borrow_mut().watermark_enabled = row.is_active(); up(); }); } // Type selector { let jc = state.job_config.clone(); let text_group_c = text_group.clone(); let image_group_c = image_group.clone(); let scale_row_c = scale_row.clone(); let up = update_preview.clone(); type_row.connect_selected_notify(move |row| { let use_image = row.selected() == 1; jc.borrow_mut().watermark_use_image = use_image; text_group_c.set_visible(!use_image); image_group_c.set_visible(use_image); scale_row_c.set_visible(use_image); up(); }); } // Text entry (debounced to avoid preview on every keystroke) { let jc = state.job_config.clone(); let up = update_preview.clone(); let debounce_id: Rc> = Rc::new(Cell::new(0)); text_row.connect_changed(move |row| { let text = row.text().to_string(); jc.borrow_mut().watermark_text = text; let up = up.clone(); let did = debounce_id.clone(); let id = did.get().wrapping_add(1); did.set(id); glib::timeout_add_local_once(std::time::Duration::from_millis(300), move || { if did.get() == id { up(); } }); }); } // Font family { let jc = state.job_config.clone(); let up = update_preview.clone(); font_button.connect_font_desc_notify(move |btn| { if let Some(desc) = btn.font_desc() { if let Some(family) = desc.family() { jc.borrow_mut().watermark_font_family = family.to_string(); up(); } } }); } // Font size { let jc = state.job_config.clone(); let up = update_preview.clone(); font_size_row.connect_value_notify(move |row| { jc.borrow_mut().watermark_font_size = row.value() as f32; up(); }); } // Position grid buttons for (i, btn) in buttons.iter().enumerate() { let jc = state.job_config.clone(); let label = position_label.clone(); let names = position_names; let all_buttons = buttons.clone(); let up = update_preview.clone(); btn.connect_toggled(move |b| { if b.is_active() { jc.borrow_mut().watermark_position = i as u32; label.set_label(names[i]); for (j, other) in all_buttons.iter().enumerate() { let icon_name = if j == i { "radio-checked-symbolic" } else { "radio-symbolic" }; other.set_child(Some(>k::Image::from_icon_name(icon_name))); } up(); } }); } // Color picker { let jc = state.job_config.clone(); let up = update_preview.clone(); color_button.connect_rgba_notify(move |btn| { let c = btn.rgba(); jc.borrow_mut().watermark_color = [ (c.red() * 255.0) as u8, (c.green() * 255.0) as u8, (c.blue() * 255.0) as u8, (c.alpha() * 255.0) as u8, ]; up(); }); } // Per-slider debounce counters (separate to avoid cross-slider cancellation) let opacity_debounce: Rc> = Rc::new(Cell::new(0)); let rotation_debounce: Rc> = Rc::new(Cell::new(0)); let margin_debounce: Rc> = Rc::new(Cell::new(0)); let scale_debounce: Rc> = Rc::new(Cell::new(0)); // Opacity slider { let jc = state.job_config.clone(); let row = opacity_row.clone(); let up = update_preview.clone(); let rst = opacity_reset.clone(); let did = opacity_debounce.clone(); opacity_scale.connect_value_changed(move |scale| { let val = scale.value().round() as i32; let opacity = val as f32 / 100.0; jc.borrow_mut().watermark_opacity = opacity; row.set_subtitle(&format!("{}%", val)); rst.set_sensitive(val != 50); let up = up.clone(); let did = did.clone(); let id = did.get().wrapping_add(1); did.set(id); glib::timeout_add_local_once(std::time::Duration::from_millis(150), move || { if did.get() == id { up(); } }); }); } { let scale = opacity_scale.clone(); opacity_reset.connect_clicked(move |_| { scale.set_value(50.0); }); } // Rotation slider { let jc = state.job_config.clone(); let row = rotation_row.clone(); let up = update_preview.clone(); let rst = rotation_reset.clone(); let did = rotation_debounce.clone(); rotation_scale.connect_value_changed(move |scale| { let val = scale.value().round() as i32; jc.borrow_mut().watermark_rotation = val; row.set_subtitle(&format!("{} degrees", val)); rst.set_sensitive(val != 0); let up = up.clone(); let did = did.clone(); let id = did.get().wrapping_add(1); did.set(id); glib::timeout_add_local_once(std::time::Duration::from_millis(150), move || { if did.get() == id { up(); } }); }); } { let scale = rotation_scale.clone(); rotation_reset.connect_clicked(move |_| { scale.set_value(0.0); }); } // Tiled toggle { let jc = state.job_config.clone(); let up = update_preview.clone(); tiled_row.connect_active_notify(move |row| { jc.borrow_mut().watermark_tiled = row.is_active(); up(); }); } // Margin slider { let jc = state.job_config.clone(); let row = margin_row.clone(); let up = update_preview.clone(); let rst = margin_reset.clone(); let did = margin_debounce.clone(); margin_scale.connect_value_changed(move |scale| { let val = scale.value().round() as i32; jc.borrow_mut().watermark_margin = val as u32; row.set_subtitle(&format!("{} px", val)); rst.set_sensitive(val != 10); let up = up.clone(); let did = did.clone(); let id = did.get().wrapping_add(1); did.set(id); glib::timeout_add_local_once(std::time::Duration::from_millis(150), move || { if did.get() == id { up(); } }); }); } { let scale = margin_scale.clone(); margin_reset.connect_clicked(move |_| { scale.set_value(10.0); }); } // Scale slider { let jc = state.job_config.clone(); let row = scale_row.clone(); let up = update_preview.clone(); let rst = scale_reset.clone(); let did = scale_debounce.clone(); scale_scale.connect_value_changed(move |scale| { let val = scale.value().round() as i32; jc.borrow_mut().watermark_scale = val as f32; row.set_subtitle(&format!("{}%", val)); rst.set_sensitive((val - 20).abs() > 0); let up = up.clone(); let did = did.clone(); let id = did.get().wrapping_add(1); did.set(id); glib::timeout_add_local_once(std::time::Duration::from_millis(150), move || { if did.get() == id { up(); } }); }); } { let scale = scale_scale.clone(); scale_reset.connect_clicked(move |_| { scale.set_value(20.0); }); } // Image chooser button { let jc = state.job_config.clone(); let path_row = image_path_row.clone(); let up = update_preview.clone(); choose_image_button.connect_clicked(move |btn| { let jc = jc.clone(); let path_row = path_row.clone(); let up = up.clone(); let dialog = gtk::FileDialog::builder() .title("Choose Watermark Image") .modal(true) .build(); let filter = gtk::FileFilter::new(); filter.set_name(Some("Images")); filter.add_mime_type("image/png"); filter.add_mime_type("image/svg+xml"); let filters = gtk::gio::ListStore::new::(); filters.append(&filter); dialog.set_filters(Some(&filters)); if let Some(window) = btn.root().and_then(|r| r.downcast::().ok()) { dialog.open(Some(&window), gtk::gio::Cancellable::NONE, move |result| { if let Ok(file) = result && let Some(path) = file.path() { path_row.set_subtitle(&path.display().to_string()); jc.borrow_mut().watermark_image_path = Some(path); up(); } }); } }); } let page = adw::NavigationPage::builder() .title("Watermark") .tag("step-watermark") .child(&outer) .build(); // Sync enable toggle, refresh preview and sensitivity when navigating to this page { let up = update_preview.clone(); let lf = state.loaded_files.clone(); let ctrl = controls.clone(); let jc = state.job_config.clone(); let er = enable_row.clone(); page.connect_map(move |_| { let enabled = jc.borrow().watermark_enabled; er.set_active(enabled); ctrl.set_sensitive(!lf.borrow().is_empty()); up(); }); } page }