Replace watermark position dropdown with visual 3x3 grid

- 9-point position grid using ToggleButtons in a Grid layout
- Visual feedback with radio-checked/radio-symbolic icons
- Position label below grid shows current selection name
- Much better UX than ComboRow for spatial position selection
This commit is contained in:
2026-03-06 13:38:12 +02:00
parent e02d677e5c
commit 07d47b6b3f

View File

@@ -94,7 +94,7 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
image_group.add(&image_path_row);
content.append(&image_group);
// Position grid (9-point)
// Visual 9-point position grid
let position_group = adw::PreferencesGroup::builder()
.title("Position")
.description("Choose where the watermark appears on the image")
@@ -106,14 +106,62 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
"Bottom Left", "Bottom Center", "Bottom Right",
];
let position_row = adw::ComboRow::builder()
.title("Watermark Position")
// 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();
let position_model = gtk::StringList::new(&position_names);
position_row.set_model(Some(&position_model));
position_row.set_selected(cfg.watermark_position);
position_group.add(&position_row);
// 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));
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
@@ -200,10 +248,26 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
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();
position_row.connect_selected_notify(move |row| {
jc.borrow_mut().watermark_position = row.selected();
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)));
}
}
});
}
{