Files
pixstrip/pixstrip-gtk/src/steps/step_watermark.rs

328 lines
11 KiB
Rust

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(&gtk::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(&gtk::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<gtk::ToggleButton> = None;
let buttons: Vec<gtk::ToggleButton> = 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(&gtk::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)
.expanded(state.detailed_mode)
.build();
let opacity_row = adw::SpinRow::builder()
.title("Opacity")
.subtitle("0.0 (invisible) to 1.0 (fully opaque)")
.adjustment(&gtk::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(&gtk::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(&gtk::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(&gtk::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::<gtk::FileFilter>();
filters.append(&filter);
dialog.set_filters(Some(&filters));
if let Some(window) = btn.root().and_then(|r| r.downcast::<gtk::Window>().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()
}