Add GTK app shell with wizard navigation, step indicator, and actions

7-step wizard flow (Workflow, Images, Resize, Convert, Compress,
Metadata, Output) with AdwNavigationView, step indicator dots,
Back/Next buttons, keyboard shortcuts (Alt+arrows, Alt+1-9),
and hamburger menu with Settings and History placeholders.
This commit is contained in:
2026-03-06 11:03:11 +02:00
parent be7d345aa9
commit 20f4c24538
4 changed files with 501 additions and 33 deletions

View File

@@ -0,0 +1,126 @@
use gtk::prelude::*;
use std::cell::RefCell;
#[derive(Clone)]
pub struct StepIndicator {
container: gtk::Box,
dots: RefCell<Vec<StepDot>>,
}
#[derive(Clone)]
struct StepDot {
button: gtk::Button,
icon: gtk::Image,
label: gtk::Label,
}
impl StepIndicator {
pub fn new(step_names: &[String]) -> Self {
let container = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.halign(gtk::Align::Center)
.spacing(0)
.margin_top(8)
.margin_bottom(8)
.margin_start(12)
.margin_end(12)
.build();
let mut dots = Vec::new();
for (i, name) in step_names.iter().enumerate() {
if i > 0 {
// Connector line between dots
let line = gtk::Separator::builder()
.orientation(gtk::Orientation::Horizontal)
.hexpand(false)
.valign(gtk::Align::Center)
.build();
line.set_size_request(24, -1);
container.append(&line);
}
let dot_box = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(2)
.halign(gtk::Align::Center)
.build();
let icon = gtk::Image::builder()
.icon_name("radio-symbolic")
.pixel_size(16)
.build();
let button = gtk::Button::builder()
.child(&icon)
.has_frame(false)
.tooltip_text(format!("Step {}: {}", i + 1, name))
.sensitive(false)
.build();
button.add_css_class("circular");
let label = gtk::Label::builder()
.label(name)
.css_classes(["caption"])
.build();
dot_box.append(&button);
dot_box.append(&label);
container.append(&dot_box);
dots.push(StepDot {
button,
icon,
label,
});
}
// First step starts as current
if let Some(first) = dots.first() {
first.icon.set_icon_name(Some("radio-checked-symbolic"));
first.button.set_sensitive(true);
first.label.add_css_class("accent");
}
Self {
container,
dots: RefCell::new(dots),
}
}
pub fn set_current(&self, index: usize) {
let dots = self.dots.borrow();
for (i, dot) in dots.iter().enumerate() {
if i == index {
dot.icon.set_icon_name(Some("radio-checked-symbolic"));
dot.button.set_sensitive(true);
dot.label.add_css_class("accent");
} else if dot.icon.icon_name().as_deref() != Some("emblem-ok-symbolic") {
dot.icon.set_icon_name(Some("radio-symbolic"));
dot.label.remove_css_class("accent");
}
}
}
pub fn set_completed(&self, index: usize) {
let dots = self.dots.borrow();
if let Some(dot) = dots.get(index) {
dot.icon.set_icon_name(Some("emblem-ok-symbolic"));
dot.button.set_sensitive(true);
dot.label.remove_css_class("accent");
}
}
pub fn widget(&self) -> &gtk::Box {
&self.container
}
}
// Allow appending the step indicator to containers
impl std::ops::Deref for StepIndicator {
type Target = gtk::Box;
fn deref(&self) -> &Self::Target {
&self.container
}
}