use gtk::prelude::*; use gtk::glib::prelude::ToVariant; use std::cell::RefCell; #[derive(Clone)] pub struct StepIndicator { container: gtk::Box, grid: gtk::Grid, dots: RefCell>, /// Maps visual index -> actual step index step_map: RefCell>, } #[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.set_overflow(gtk::Overflow::Hidden); container.update_property(&[ gtk::accessible::Property::Label("Wizard step indicator"), ]); let grid = gtk::Grid::builder() .column_homogeneous(false) .row_spacing(2) .column_spacing(0) .hexpand(false) .build(); let indices: Vec = (0..step_names.len()).collect(); let dots = Self::build_dots(&grid, step_names, &indices); container.append(&grid); // First step starts as current let total = dots.len(); 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"); first.button.update_property(&[ gtk::accessible::Property::Label( &format!("Step 1 of {}: {} (current)", total, first.label.label()) ), ]); } // Label all non-current dots for (i, dot) in dots.iter().enumerate().skip(1) { dot.button.update_property(&[ gtk::accessible::Property::Label( &format!("Step {} of {}: {}", i + 1, total, dot.label.label()) ), ]); } Self { container, grid, dots: RefCell::new(dots), step_map: RefCell::new(indices), } } fn build_dots(grid: >k::Grid, names: &[String], step_indices: &[usize]) -> Vec { let mut dots = Vec::new(); for (visual_i, (name, &actual_i)) in names.iter().zip(step_indices.iter()).enumerate() { let col = (visual_i * 2) as i32; if visual_i > 0 { let line = gtk::Separator::builder() .orientation(gtk::Orientation::Horizontal) .hexpand(false) .valign(gtk::Align::Center) .build(); line.set_size_request(12, -1); grid.attach(&line, col - 1, 0, 1, 1); } 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+{})", visual_i + 1, name, actual_i + 1)) .sensitive(false) .action_name("win.goto-step") .action_target(&(actual_i as i32 + 1).to_variant()) .halign(gtk::Align::Center) .build(); button.add_css_class("circular"); let label = gtk::Label::builder() .label(name) .css_classes(["caption"]) .ellipsize(gtk::pango::EllipsizeMode::End) .width_chars(10) .halign(gtk::Align::Center) .build(); grid.attach(&button, col, 0, 1, 1); grid.attach(&label, col, 1, 1, 1); dots.push(StepDot { button, icon, label, }); } dots } /// Rebuild the indicator to show only the given steps. /// `visible_steps` is a list of (actual_step_index, name). pub fn rebuild(&self, visible_steps: &[(usize, String)]) { // Clear the grid while let Some(child) = self.grid.first_child() { self.grid.remove(&child); } let names: Vec = visible_steps.iter().map(|(_, n)| n.clone()).collect(); let indices: Vec = visible_steps.iter().map(|(i, _)| *i).collect(); let dots = Self::build_dots(&self.grid, &names, &indices); *self.dots.borrow_mut() = dots; *self.step_map.borrow_mut() = indices; } /// Set the current step by actual step index. Finds the visual position. pub fn set_current(&self, actual_index: usize) { let dots = self.dots.borrow(); let map = self.step_map.borrow(); let total = dots.len(); for (visual_i, dot) in dots.iter().enumerate() { let is_current = map.get(visual_i) == Some(&actual_index); if is_current { dot.icon.set_icon_name(Some("radio-checked-symbolic")); dot.button.set_sensitive(true); dot.label.add_css_class("accent"); dot.button.update_property(&[ gtk::accessible::Property::Label( &format!("Step {} of {}: {} (current)", visual_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 {}: {}", visual_i + 1, total, dot.label.label()) ), ]); } } } /// Mark a step as completed by actual step index. pub fn set_completed(&self, actual_index: usize) { let dots = self.dots.borrow(); let map = self.step_map.borrow(); let total = dots.len(); if let Some(visual_i) = map.iter().position(|&i| i == actual_index) { if let Some(dot) = dots.get(visual_i) { dot.icon.set_icon_name(Some("emblem-ok-symbolic")); dot.button.set_sensitive(true); dot.label.remove_css_class("accent"); dot.button.update_property(&[ gtk::accessible::Property::Label( &format!("Step {} of {}: {} (completed)", visual_i + 1, total, dot.label.label()) ), ]); } } } pub fn widget(&self) -> >k::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 } }