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(); text_group.add(&text_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); // 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) .build(); 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(&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(); text_row.connect_changed(move |row| { jc.borrow_mut().watermark_text = row.text().to_string(); }); } { 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 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(); btn.connect_toggled(move |b| { if b.is_active() { jc.borrow_mut().watermark_position = i as u32; label.set_label(names[i]); // 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(); opacity_row.connect_value_notify(move |row| { jc.borrow_mut().watermark_opacity = row.value() as f32; }); } // 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() }