Add live watermark preview with position and opacity updates

Shows a thumbnail of the first batch image with the watermark text
overlaid. Preview updates in real-time as user changes text, position
(9-point grid), and opacity.
This commit is contained in:
2026-03-06 15:39:10 +02:00
parent b50147404a
commit eb8da4b3e9

View File

@@ -167,6 +167,88 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
content.append(&position_group); 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: &gtk::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);
}
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(&no_preview_label);
preview_group.add(&preview_stack);
content.append(&preview_group);
// Advanced options // Advanced options
let advanced_group = adw::PreferencesGroup::builder() let advanced_group = adw::PreferencesGroup::builder()
.title("Advanced") .title("Advanced")
@@ -242,8 +324,11 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
} }
{ {
let jc = state.job_config.clone(); let jc = state.job_config.clone();
let wl = watermark_label.clone();
text_row.connect_changed(move |row| { text_row.connect_changed(move |row| {
jc.borrow_mut().watermark_text = row.text().to_string(); let text = row.text().to_string();
wl.set_label(&text);
jc.borrow_mut().watermark_text = text;
}); });
} }
{ {
@@ -258,10 +343,12 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
let label = position_label.clone(); let label = position_label.clone();
let names = position_names; let names = position_names;
let all_buttons = buttons.clone(); let all_buttons = buttons.clone();
let wl = watermark_label.clone();
btn.connect_toggled(move |b| { btn.connect_toggled(move |b| {
if b.is_active() { if b.is_active() {
jc.borrow_mut().watermark_position = i as u32; jc.borrow_mut().watermark_position = i as u32;
label.set_label(names[i]); label.set_label(names[i]);
set_watermark_alignment(&wl, i as u32);
// Update icons // Update icons
for (j, other) in all_buttons.iter().enumerate() { for (j, other) in all_buttons.iter().enumerate() {
let icon_name = if j == i { let icon_name = if j == i {
@@ -276,8 +363,11 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
} }
{ {
let jc = state.job_config.clone(); let jc = state.job_config.clone();
let wl = watermark_label.clone();
opacity_row.connect_value_notify(move |row| { opacity_row.connect_value_notify(move |row| {
jc.borrow_mut().watermark_opacity = row.value() as f32; let val = row.value() as f32;
wl.set_opacity(val as f64);
jc.borrow_mut().watermark_opacity = val;
}); });
} }
// Wire image chooser button // Wire image chooser button