1#![warn(missing_docs)]
2#![deny(unsafe_code)]
3
4pub mod animation;
29pub mod bounds;
30mod card;
31mod manager;
32pub 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#[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#[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 integration_checklist(&state, &theme);
190 }
191}