Skip to main content

iced_tour/
lib.rs

1#![warn(missing_docs)]
2#![deny(unsafe_code)]
3
4//! # iced_tour
5//!
6//! Guided tour / onboarding overlay for iced applications.
7//!
8//! There are **zero** existing Rust crates for in-app onboarding/guided tours.
9//! This is the first.
10//!
11//! ## Quick Start
12//!
13//! ```rust,no_run
14//! use iced_tour::{TourState, TourStep, TourMessage, TourTheme, CardPosition, tour_steps};
15//!
16//! // Define steps (no target = centered card, works without knowing layout)
17//! let steps = tour_steps![
18//!     "Welcome" => "Let's take a quick tour of the app",
19//!     "Editor" => "This is where you edit your content",
20//!     "Timeline" => "Arrange your clips here",
21//! ];
22//!
23//! let state = TourState::new(steps);
24//! ```
25//!
26//! See the [README](https://github.com/bartlomein/iced_tour) for full integration instructions.
27
28pub mod animation;
29pub mod bounds;
30mod card;
31mod manager;
32/// Tour overlay rendering (backdrop, spotlight cutout, tooltip card).
33pub mod overlay;
34mod state;
35mod step;
36mod theme;
37
38pub use animation::TourAnimation;
39pub use bounds::visible_bounds;
40pub use manager::{TourManager, TourManagerEvent, TourManagerMessage};
41pub use overlay::{tour_manager_overlay, tour_overlay};
42pub use state::{TourEvent, TourMessage, TourState};
43pub use step::{CardPosition, TourStep, TourTarget};
44pub use theme::{ThemeMode, TourTheme};
45
46/// Convenience macro for quick step definitions.
47///
48/// Creates a `Vec<TourStep>` with no targets (all centered cards).
49/// Targets and positions can be set afterwards with `.target()` and `.card_position()`.
50///
51/// # Examples
52///
53/// ```
54/// use iced_tour::tour_steps;
55///
56/// let steps = tour_steps![
57///     "Video Preview" => "Drop a cycling video here to get started",
58///     "Timeline" => "Arrange your clips and telemetry tracks here",
59///     "Inspector" => "Customize overlay styles, fonts, and colors",
60/// ];
61/// assert_eq!(steps.len(), 3);
62/// assert_eq!(steps[0].title(), "Video Preview");
63/// ```
64#[macro_export]
65macro_rules! tour_steps {
66    ($($title:expr => $desc:expr),* $(,)?) => {
67        vec![
68            $(
69                $crate::TourStep::new($title, $desc),
70            )*
71        ]
72    };
73}
74
75/// Verify your tour integration. Only available in debug builds.
76///
77/// Prints a checklist of what's configured and what's missing to stdout.
78///
79/// # Example output
80///
81/// ```text
82/// [iced_tour] Integration checklist:
83///   + TourState created with 5 steps
84///   - 3 steps have no target rectangle (will show centered)
85///   + Theme: Dark mode
86///   + Custom title font: Inter
87///   Reminder: Make sure tour_overlay() is in your view's stack![]
88/// ```
89#[cfg(debug_assertions)]
90pub fn integration_checklist(state: &TourState, theme: &TourTheme) {
91    let total = state.steps().len();
92    let no_target = state.steps().iter().filter(|s| s.is_centered()).count();
93    let has_target = total - no_target;
94
95    println!("[iced_tour] Integration checklist:");
96
97    if total > 0 {
98        println!("  + TourState created with {total} steps");
99    } else {
100        println!("  - TourState has no steps (add steps with TourState::new(vec![...]))");
101    }
102
103    if has_target > 0 {
104        println!("  + {has_target} steps have target rectangles (spotlight cutout)");
105    }
106    if no_target > 0 {
107        println!(
108            "  {} {} steps have no target rectangle (will show centered)",
109            if no_target == total { "-" } else { "~" },
110            no_target
111        );
112    }
113
114    println!("  + Theme: {:?} mode", theme.mode);
115
116    if theme.title_font != iced::Font::DEFAULT {
117        println!("  + Custom title font set");
118    } else {
119        println!("  ~ Using default title font (set with .with_title_font())");
120    }
121
122    if theme.description_font != iced::Font::DEFAULT {
123        println!("  + Custom description font set");
124    }
125
126    if state.is_active() {
127        println!(
128            "  + Tour is currently active (step {})",
129            state.step_index().0 + 1
130        );
131    } else if state.is_finished() {
132        println!("  + Tour has been completed/skipped");
133    } else {
134        println!("  ~ Tour is inactive (call .start() to begin)");
135    }
136
137    println!("  Reminder: Make sure tour_overlay() is in your view's stack![]");
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn tour_steps_macro_creates_steps() {
146        let steps = tour_steps![
147            "Step 1" => "First",
148            "Step 2" => "Second",
149            "Step 3" => "Third",
150        ];
151        assert_eq!(steps.len(), 3);
152        assert_eq!(steps[0].title(), "Step 1");
153        assert_eq!(steps[0].description(), "First");
154        assert_eq!(steps[2].title(), "Step 3");
155        assert!(steps.iter().all(|s| s.is_centered()));
156    }
157
158    #[test]
159    fn tour_steps_macro_empty() {
160        let steps: Vec<TourStep> = tour_steps![];
161        assert!(steps.is_empty());
162    }
163
164    #[test]
165    fn tour_steps_macro_single() {
166        let steps = tour_steps![
167            "Only" => "One step",
168        ];
169        assert_eq!(steps.len(), 1);
170    }
171
172    #[test]
173    fn tour_steps_macro_no_trailing_comma() {
174        let steps = tour_steps![
175            "A" => "First",
176            "B" => "Second"
177        ];
178        assert_eq!(steps.len(), 2);
179    }
180
181    #[test]
182    fn integration_checklist_runs_without_panic() {
183        let state = TourState::new(tour_steps![
184            "A" => "First",
185            "B" => "Second",
186        ]);
187        let theme = TourTheme::dark();
188        // Just verify it doesn't panic
189        integration_checklist(&state, &theme);
190    }
191}