Files
pixstrip/pixstrip-gtk/src/step_indicator.rs
lashman b78f1cd7c4 Add What's New dialog, accessibility labels, focus management
Add What's New dialog accessible from hamburger menu showing version
changelog. Add accessible property labels to step indicator for screen
reader support with current step/total announcements. Add focus
management on step transitions - focus moves to first interactive
element when navigating to a new step.
2026-03-06 13:13:39 +02:00

146 lines
4.4 KiB
Rust

use gtk::prelude::*;
use gtk::glib::prelude::ToVariant;
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();
container.update_property(&[
gtk::accessible::Property::Label("Wizard step indicator"),
]);
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 {}: {} (Alt+{})", i + 1, name, i + 1))
.sensitive(false)
.action_name("win.goto-step")
.action_target(&(i as i32 + 1).to_variant())
.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();
let total = dots.len();
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");
// Update accessible description for screen readers
dot.button.update_property(&[
gtk::accessible::Property::Label(
&format!("Step {} of {}: {} (current)", i + 1, total, dot.label.label())
),
]);
} 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");
dot.button.update_property(&[
gtk::accessible::Property::Label(
&format!("Step {} of {}: {}", i + 1, total, dot.label.label())
),
]);
}
}
}
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
}
}