use adw::prelude::*; use gtk::glib; /// Show the tutorial tour if the user hasn't completed it yet. /// Called after the welcome wizard closes on first launch. pub fn show_tutorial_if_needed(window: &adw::ApplicationWindow) { let config_store = pixstrip_core::storage::ConfigStore::new(); if let Ok(cfg) = config_store.load() && cfg.tutorial_complete { return; } // Small delay to let the welcome dialog fully dismiss let win = window.clone(); glib::timeout_add_local_once(std::time::Duration::from_millis(400), move || { show_tour_stop(&win, 0); }); } /// Tour stops: (title, description, widget_name, popover_position) fn tour_stops() -> Vec<(&'static str, &'static str, &'static str, gtk::PositionType)> { vec![ ( "Choose a Workflow", "Pick a preset that matches your needs, or scroll down to build a custom workflow from scratch.", "tour-content", gtk::PositionType::Bottom, ), ( "Track Your Progress", "This bar shows where you are in the wizard. Click any completed step to jump back to it.", "tour-step-indicator", gtk::PositionType::Bottom, ), ( "Navigation", "Use Back and Next to move between steps, or press Alt+Left / Alt+Right. Disabled steps are automatically skipped.", "tour-next-button", gtk::PositionType::Top, ), ( "Main Menu", "Settings, keyboard shortcuts, processing history, and preset management live here.", "tour-menu-button", gtk::PositionType::Bottom, ), ( "Get Help", "Every step has a help button with detailed guidance specific to that step.", "tour-help-button", gtk::PositionType::Bottom, ), ] } fn show_tour_stop(window: &adw::ApplicationWindow, index: usize) { let stops = tour_stops(); let total = stops.len(); if index >= total { mark_tutorial_complete(); return; } let (title, description, widget_name, position) = stops[index]; // Find the target widget by name in the widget tree let Some(root) = window.content() else { return }; let Some(target) = find_widget_by_name(&root, widget_name) else { // Widget not found - skip to next stop show_tour_stop(window, index + 1); return; }; // Build popover content let content = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(8) .margin_top(12) .margin_bottom(12) .margin_start(16) .margin_end(16) .build(); content.set_size_request(280, -1); // Progress dots let dots_box = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(6) .halign(gtk::Align::Center) .build(); for i in 0..total { let dot = gtk::Label::builder() .label(if i == index { "\u{25CF}" } else { "\u{25CB}" }) .build(); if i == index { dot.add_css_class("accent"); } else { dot.add_css_class("dim-label"); } dots_box.append(&dot); } content.append(&dots_box); // Title let title_label = gtk::Label::builder() .label(title) .css_classes(["title-3"]) .halign(gtk::Align::Start) .build(); content.append(&title_label); // Description let desc_label = gtk::Label::builder() .label(description) .wrap(true) .max_width_chars(36) .halign(gtk::Align::Start) .xalign(0.0) .build(); content.append(&desc_label); // Step counter let counter_label = gtk::Label::builder() .label(&format!("{} of {}", index + 1, total)) .css_classes(["dim-label", "caption"]) .halign(gtk::Align::Start) .build(); content.append(&counter_label); // Buttons let button_box = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(12) .halign(gtk::Align::End) .margin_top(4) .build(); let skip_btn = gtk::Button::builder() .label("Skip Tour") .tooltip_text("Close the tour and start using Pixstrip") .build(); skip_btn.add_css_class("flat"); let is_last = index + 1 >= total; let next_btn = gtk::Button::builder() .label(if is_last { "Done" } else { "Next" }) .tooltip_text(if is_last { "Finish the tour" } else { "Go to the next tour stop" }) .build(); next_btn.add_css_class("suggested-action"); next_btn.add_css_class("pill"); button_box.append(&skip_btn); button_box.append(&next_btn); content.append(&button_box); // Create popover attached to the target widget let popover = gtk::Popover::builder() .child(&content) .position(position) .autohide(false) .has_arrow(true) .build(); popover.set_parent(&target); // For the content area (large widget), point to the upper portion // where the preset cards are visible if widget_name == "tour-content" { let w = target.width(); if w > 0 { let rect = gtk::gdk::Rectangle::new(w / 2 - 10, 40, 20, 20); popover.set_pointing_to(Some(&rect)); } } // Accessible label for screen readers popover.update_property(&[ gtk::accessible::Property::Label( &format!("Tour step {} of {}: {}", index + 1, total, title) ), ]); // Wire skip button { let pop = popover.clone(); skip_btn.connect_clicked(move |_| { mark_tutorial_complete(); pop.popdown(); let p = pop.clone(); glib::idle_add_local_once(move || { p.unparent(); }); }); } // Wire next button { let pop = popover.clone(); let win = window.clone(); next_btn.connect_clicked(move |_| { pop.popdown(); let p = pop.clone(); let w = win.clone(); glib::idle_add_local_once(move || { p.unparent(); if is_last { mark_tutorial_complete(); } else { show_tour_stop(&w, index + 1); } }); }); } popover.popup(); } /// Recursively search the widget tree for a widget with the given name. fn find_widget_by_name(root: >k::Widget, name: &str) -> Option { if root.widget_name().as_str() == name { return Some(root.clone()); } let mut child = root.first_child(); while let Some(c) = child { if let Some(found) = find_widget_by_name(&c, name) { return Some(found); } child = c.next_sibling(); } None } fn mark_tutorial_complete() { let config_store = pixstrip_core::storage::ConfigStore::new(); if let Ok(mut cfg) = config_store.load() { cfg.tutorial_complete = true; let _ = config_store.save(&cfg); } }