use adw::prelude::*; use crate::app::AppState; pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { let scrolled = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) .build(); let content = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(12) .margin_top(12) .margin_bottom(12) .margin_start(24) .margin_end(24) .build(); let cfg = state.job_config.borrow(); // Enable toggle let enable_row = adw::SwitchRow::builder() .title("Enable Watermark") .subtitle("Add text or image watermark to processed images") .active(cfg.watermark_enabled) .build(); let enable_group = adw::PreferencesGroup::new(); enable_group.add(&enable_row); content.append(&enable_group); // Watermark type selection let type_group = adw::PreferencesGroup::builder() .title("Watermark Type") .build(); let type_row = adw::ComboRow::builder() .title("Type") .subtitle("Choose text or image watermark") .build(); let type_model = gtk::StringList::new(&["Text Watermark", "Image Watermark"]); type_row.set_model(Some(&type_model)); type_row.set_selected(if cfg.watermark_use_image { 1 } else { 0 }); type_group.add(&type_row); content.append(&type_group); // Text watermark settings let text_group = adw::PreferencesGroup::builder() .title("Text Watermark") .build(); let text_row = adw::EntryRow::builder() .title("Watermark Text") .text(&cfg.watermark_text) .build(); 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)) .build(); // Font family picker 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(); // Set initial font if one was previously selected 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_row.add_suffix(&font_button); text_group.add(&text_row); text_group.add(&font_row); text_group.add(&font_size_row); content.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(); image_path_row.add_prefix(>k::Image::from_icon_name("image-x-generic-symbolic")); let choose_image_button = gtk::Button::builder() .icon_name("document-open-symbolic") .tooltip_text("Choose logo image") .valign(gtk::Align::Center) .build(); choose_image_button.add_css_class("flat"); image_path_row.add_suffix(&choose_image_button); image_group.add(&image_path_row); content.append(&image_group); // Visual 9-point position 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", ]; // Build a 3x3 grid of toggle buttons let grid = gtk::Grid::builder() .row_spacing(4) .column_spacing(4) .halign(gtk::Align::Center) .margin_top(8) .margin_bottom(8) .build(); // Create a visual "image" area as background context 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."), ]); 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(); // Use a dot icon for each position 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_frame); // Position label showing current selection let position_label = gtk::Label::builder() .label(position_names[cfg.watermark_position as usize]) .css_classes(["dim-label"]) .halign(gtk::Align::Center) .margin_bottom(4) .build(); position_group.add(&position_label); content.append(&position_group); // Live preview section let preview_group = adw::PreferencesGroup::builder() .title("Preview") .description("Shows how the watermark will appear on your image") .build(); // Overlay container for image + watermark text let preview_overlay = gtk::Overlay::builder() .halign(gtk::Align::Center) .build(); let preview_picture = gtk::Picture::builder() .content_fit(gtk::ContentFit::Contain) .width_request(300) .height_request(200) .build(); preview_picture.add_css_class("card"); preview_overlay.set_child(Some(&preview_picture)); // Watermark text label overlay let watermark_label = gtk::Label::builder() .label(&cfg.watermark_text) .css_classes(["heading"]) .opacity(cfg.watermark_opacity as f64) .build(); preview_overlay.add_overlay(&watermark_label); // Position the watermark label according to grid position fn set_watermark_alignment(label: >k::Label, position: u32) { let (h, v) = match position { 0 => (gtk::Align::Start, gtk::Align::Start), // Top Left 1 => (gtk::Align::Center, gtk::Align::Start), // Top Center 2 => (gtk::Align::End, gtk::Align::Start), // Top Right 3 => (gtk::Align::Start, gtk::Align::Center), // Middle Left 4 => (gtk::Align::Center, gtk::Align::Center), // Center 5 => (gtk::Align::End, gtk::Align::Center), // Middle Right 6 => (gtk::Align::Start, gtk::Align::End), // Bottom Left 7 => (gtk::Align::Center, gtk::Align::End), // Bottom Center _ => (gtk::Align::End, gtk::Align::End), // Bottom Right }; label.set_halign(h); label.set_valign(v); label.set_margin_start(8); label.set_margin_end(8); label.set_margin_top(8); label.set_margin_bottom(8); } set_watermark_alignment(&watermark_label, cfg.watermark_position); // Load first image from batch as preview background { let files = state.loaded_files.borrow(); if let Some(first) = files.first() { preview_picture.set_filename(Some(first)); } } // "No preview" placeholder let no_preview_label = gtk::Label::builder() .label("Add images to see a preview") .css_classes(["dim-label"]) .halign(gtk::Align::Center) .valign(gtk::Align::Center) .build(); { let has_files = !state.loaded_files.borrow().is_empty(); no_preview_label.set_visible(!has_files); preview_picture.set_visible(has_files); } // Thumbnail strip for selecting preview image let wm_thumb_box = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(4) .halign(gtk::Align::Center) .margin_top(4) .build(); { let files = state.loaded_files.borrow(); let max_thumbs = files.len().min(10); for i in 0..max_thumbs { let pic = gtk::Picture::builder() .content_fit(gtk::ContentFit::Cover) .width_request(40) .height_request(40) .build(); pic.set_filename(Some(&files[i])); let frame = gtk::Frame::builder() .child(&pic) .build(); if i == 0 { frame.add_css_class("accent"); } let btn = gtk::Button::builder() .child(&frame) .has_frame(false) .tooltip_text(files[i].file_name().and_then(|n| n.to_str()).unwrap_or("image")) .build(); let pp = preview_picture.clone(); let path = files[i].clone(); let tb = wm_thumb_box.clone(); let current_idx = i; btn.connect_clicked(move |_| { pp.set_filename(Some(&path)); let mut c = tb.first_child(); let mut j = 0usize; while let Some(w) = c { if let Some(b) = w.downcast_ref::() { if let Some(f) = b.child().and_then(|c| c.downcast::().ok()) { if j == current_idx { f.add_css_class("accent"); } else { f.remove_css_class("accent"); } } } c = w.next_sibling(); j += 1; } }); wm_thumb_box.append(&btn); } wm_thumb_box.set_visible(max_thumbs > 1); } let preview_stack = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(4) .margin_top(8) .margin_bottom(8) .build(); preview_stack.append(&preview_overlay); preview_stack.append(&wm_thumb_box); preview_stack.append(&no_preview_label); preview_group.add(&preview_stack); content.append(&preview_group); // Advanced options let advanced_group = adw::PreferencesGroup::builder() .title("Advanced") .build(); let advanced_expander = adw::ExpanderRow::builder() .title("Advanced Options") .subtitle("Opacity, rotation, tiling, margin") .show_enable_switch(false) .expanded(state.detailed_mode) .build(); // 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_row.add_suffix(&color_button); let opacity_row = adw::SpinRow::builder() .title("Opacity") .subtitle("0.0 (invisible) to 1.0 (fully opaque)") .adjustment(>k::Adjustment::new(cfg.watermark_opacity as f64, 0.0, 1.0, 0.05, 0.1, 0.0)) .digits(2) .build(); let rotation_row = adw::ComboRow::builder() .title("Rotation") .subtitle("Rotate the watermark") .build(); let rotation_model = gtk::StringList::new(&["None", "45 degrees", "-45 degrees", "90 degrees"]); rotation_row.set_model(Some(&rotation_model)); let tiled_row = adw::SwitchRow::builder() .title("Tiled / Repeated") .subtitle("Repeat watermark across the entire image") .active(false) .build(); let margin_row = adw::SpinRow::builder() .title("Margin from Edges") .subtitle("Padding in pixels from image edges") .adjustment(>k::Adjustment::new(10.0, 0.0, 200.0, 1.0, 10.0, 0.0)) .build(); let scale_row = adw::SpinRow::builder() .title("Scale (% of image)") .subtitle("Watermark size relative to image") .adjustment(>k::Adjustment::new(20.0, 1.0, 100.0, 1.0, 5.0, 0.0)) .build(); 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); content.append(&advanced_group); drop(cfg); // Wire signals { let jc = state.job_config.clone(); enable_row.connect_active_notify(move |row| { jc.borrow_mut().watermark_enabled = row.is_active(); }); } { let jc = state.job_config.clone(); let text_group_c = text_group.clone(); let image_group_c = image_group.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); }); } { let jc = state.job_config.clone(); let wl = watermark_label.clone(); text_row.connect_changed(move |row| { let text = row.text().to_string(); wl.set_label(&text); jc.borrow_mut().watermark_text = text; }); } { let jc = state.job_config.clone(); font_size_row.connect_value_notify(move |row| { jc.borrow_mut().watermark_font_size = row.value() as f32; }); } // Wire font family picker { let jc = state.job_config.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(); } } }); } // Wire 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 wl = watermark_label.clone(); btn.connect_toggled(move |b| { if b.is_active() { jc.borrow_mut().watermark_position = i as u32; label.set_label(names[i]); set_watermark_alignment(&wl, i as u32); // Update icons 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))); } } }); } { let jc = state.job_config.clone(); let wl = watermark_label.clone(); opacity_row.connect_value_notify(move |row| { let val = row.value() as f32; wl.set_opacity(val as f64); jc.borrow_mut().watermark_opacity = val; }); } // Wire color picker { let jc = state.job_config.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, ]; }); } // Wire image chooser button { let jc = state.job_config.clone(); let path_row = image_path_row.clone(); choose_image_button.connect_clicked(move |btn| { let jc = jc.clone(); let path_row = path_row.clone(); let dialog = gtk::FileDialog::builder() .title("Choose Watermark Image") .modal(true) .build(); let filter = gtk::FileFilter::new(); filter.set_name(Some("PNG images")); filter.add_mime_type("image/png"); 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); } }); } }); } scrolled.set_child(Some(&content)); let clamp = adw::Clamp::builder() .maximum_size(600) .child(&scrolled) .build(); adw::NavigationPage::builder() .title("Watermark") .tag("step-watermark") .child(&clamp) .build() }