1use std::rc::Rc;
11
12use teksilo_canvas::{Rect, SizeProposal};
13use teksilo_core::accessibility::AccessNodeBuilder;
14use teksilo_core::build_context::BuildContext;
15use teksilo_core::event::{EventResponse, Key, WidgetEvent};
16use teksilo_core::modal::{ModalCloseBehavior, ModalPresentation, ModalRequest};
17use teksilo_core::signal::Prop;
18use teksilo_core::widget::{
19 CursorIcon, EventContext, LayoutContext, LayoutResponse, Widget, WidgetPlacement,
20};
21use teksilo_core::widget_builder::HandlerSet;
22use teksilo_core::widget_id::WidgetId;
23use teksilo_i18n::{LocalizedString, lit};
24
25use super::Stepper;
26use super::controller::StepperController;
27use super::nav::{FinishAction, FinishOutcome, IntoFinishOutcome};
28use super::step::Step;
29use crate::button::{Button, ButtonVariant};
30use crate::dialog::ModalContainer;
31use crate::overlay_trigger::OverlayTrigger;
32
33const DEFAULT_WIZARD_WIDTH: u32 = 640;
34const DEFAULT_WIZARD_HEIGHT: u32 = 460;
35
36struct WizardSpec {
38 title: LocalizedString,
39 steps: Vec<Step>,
40 presentation: ModalPresentation,
41 close_behavior: ModalCloseBehavior,
42 size: (u32, u32),
43 non_linear: bool,
44 back_label: LocalizedString,
45 next_label: LocalizedString,
46 finish_label: LocalizedString,
47 skip_label: LocalizedString,
48 cancel_label: LocalizedString,
49 finish_action: Option<FinishAction>,
50}
51
52fn present_wizard(spec: &Rc<WizardSpec>, ctx: &mut EventContext) {
53 if spec.steps.is_empty() {
54 return;
55 }
56 let spec = spec.clone();
57 let presentation = spec.presentation;
58 let close_behavior = spec.close_behavior;
59 let (w, h) = spec.size;
60 let title = spec.title.resolve_now();
61 ctx.present_modal(
62 ModalRequest::deferred(move |tree| {
63 let finish = spec.finish_action.clone();
64 let stepper = Stepper::new()
65 .steps(spec.steps.clone())
66 .non_linear(spec.non_linear)
67 .back_label(spec.back_label.clone())
68 .next_label(spec.next_label.clone())
69 .finish_label(spec.finish_label.clone())
70 .skip_label(spec.skip_label.clone())
71 .cancel(spec.cancel_label.clone(), |ctx, _ctrl| ctx.dismiss_modal())
72 .on_finish(move |ctx, ctrl| {
76 let outcome = match &finish {
77 Some(action) => action(ctx, ctrl),
78 None => FinishOutcome::Finished,
79 };
80 if outcome == FinishOutcome::Finished {
81 ctx.dismiss_modal();
82 }
83 outcome
84 });
85 tree.add(ModalContainer::boxed(Box::new(stepper)))
86 })
87 .presentation(presentation)
88 .close_behavior(close_behavior)
89 .title(title)
90 .size(w, h),
91 );
92}
93
94pub struct Wizard {
102 label: LocalizedString,
103 variant: ButtonVariant,
104 enabled: Prop<bool>,
107 presentation: ModalPresentation,
108 close_behavior: ModalCloseBehavior,
109 size: (u32, u32),
110 non_linear: bool,
111 steps: Vec<Step>,
112 back_label: LocalizedString,
113 next_label: LocalizedString,
114 finish_label: LocalizedString,
115 skip_label: LocalizedString,
116 cancel_label: LocalizedString,
117 finish_action: Option<FinishAction>,
118 pending_trigger: Option<Box<dyn Widget>>,
119 root_child_id: Option<WidgetId>,
120}
121
122impl Wizard {
123 pub fn new(label: impl Into<LocalizedString>) -> Self {
126 Self {
127 label: label.into(),
128 variant: ButtonVariant::Filled,
129 enabled: Prop::Static(true),
130 presentation: ModalPresentation::Auto,
131 close_behavior: ModalCloseBehavior::Manual,
132 size: (DEFAULT_WIZARD_WIDTH, DEFAULT_WIZARD_HEIGHT),
133 non_linear: false,
134 steps: Vec::new(),
135 back_label: lit!("Back"),
136 next_label: lit!("Next"),
137 finish_label: lit!("Finish"),
138 skip_label: lit!("Skip"),
139 cancel_label: lit!("Cancel"),
140 finish_action: None,
141 pending_trigger: None,
142 root_child_id: None,
143 }
144 }
145
146 pub fn step(mut self, step: Step) -> Self {
148 self.steps.push(step);
149 self
150 }
151 pub fn steps(mut self, steps: impl IntoIterator<Item = Step>) -> Self {
153 self.steps.extend(steps);
154 self
155 }
156 pub fn variant(mut self, variant: ButtonVariant) -> Self {
158 self.variant = variant;
159 self
160 }
161 pub fn enabled(mut self, enabled: impl Into<Prop<bool>>) -> Self {
164 self.enabled = enabled.into();
165 self
166 }
167 pub fn non_linear(mut self, non_linear: bool) -> Self {
170 self.non_linear = non_linear;
171 self
172 }
173 pub fn presentation(mut self, presentation: ModalPresentation) -> Self {
175 self.presentation = presentation;
176 self
177 }
178 pub fn close_behavior(mut self, close_behavior: ModalCloseBehavior) -> Self {
180 self.close_behavior = close_behavior;
181 self
182 }
183 pub fn size(mut self, width: u32, height: u32) -> Self {
185 self.size = (width, height);
186 self
187 }
188 pub fn back_label(mut self, label: impl Into<LocalizedString>) -> Self {
190 self.back_label = label.into();
191 self
192 }
193 pub fn next_label(mut self, label: impl Into<LocalizedString>) -> Self {
195 self.next_label = label.into();
196 self
197 }
198 pub fn finish_label(mut self, label: impl Into<LocalizedString>) -> Self {
200 self.finish_label = label.into();
201 self
202 }
203 pub fn skip_label(mut self, label: impl Into<LocalizedString>) -> Self {
205 self.skip_label = label.into();
206 self
207 }
208 pub fn cancel_label(mut self, label: impl Into<LocalizedString>) -> Self {
210 self.cancel_label = label.into();
211 self
212 }
213 pub fn on_finish<R: IntoFinishOutcome>(
222 mut self,
223 action: impl Fn(&mut EventContext, &StepperController) -> R + 'static,
224 ) -> Self {
225 self.finish_action = Some(Rc::new(move |ctx, ctrl| {
226 action(ctx, ctrl).into_finish_outcome()
227 }));
228 self
229 }
230 pub fn trigger(mut self, trigger: impl Widget + 'static) -> Self {
231 self.pending_trigger = Some(Box::new(trigger));
232 self
233 }
234
235 fn spec(&self) -> Rc<WizardSpec> {
236 Rc::new(WizardSpec {
237 title: self.label.clone(),
238 steps: self.steps.clone(),
239 presentation: self.presentation,
240 close_behavior: self.close_behavior,
241 size: self.size,
242 non_linear: self.non_linear,
243 back_label: self.back_label.clone(),
244 next_label: self.next_label.clone(),
245 finish_label: self.finish_label.clone(),
246 skip_label: self.skip_label.clone(),
247 cancel_label: self.cancel_label.clone(),
248 finish_action: self.finish_action.clone(),
249 })
250 }
251}
252
253impl std::fmt::Debug for Wizard {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 f.debug_struct("Wizard")
256 .field("label", &self.label)
257 .field("steps", &self.steps.len())
258 .finish()
259 }
260}
261
262impl Widget for Wizard {
263 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
264 let enabled = self.enabled.clone();
265 let spec = self.spec();
266
267 let root_id = if let Some(trigger) = self.pending_trigger.take() {
268 let handlers = HandlerSet::new()
269 .focusable(true)
270 .cursor(CursorIcon::Pointer)
271 .on_tap({
272 let spec = spec.clone();
273 let enabled = enabled.clone();
274 move |_pos, ctx| {
275 if enabled.get() {
276 present_wizard(&spec, ctx);
277 }
278 }
279 })
280 .on_key({
281 let spec = spec.clone();
282 let enabled = enabled.clone();
283 move |event, ctx| match event {
284 WidgetEvent::KeyUp {
285 key: Key::Enter | Key::Space,
286 ..
287 } if enabled.get() => {
288 present_wizard(&spec, ctx);
289 EventResponse::Handled
290 }
291 _ => EventResponse::Ignored,
292 }
293 })
294 .on_access_action({
295 let spec = spec.clone();
296 let enabled = enabled.clone();
297 move |action, ctx| {
298 if action == teksilo_core::accesskit::Action::Click && enabled.get() {
299 present_wizard(&spec, ctx);
300 EventResponse::Handled
301 } else {
302 EventResponse::Ignored
303 }
304 }
305 });
306 ctx.add(
307 OverlayTrigger::new(trigger, handlers)
308 .enabled(self.enabled.clone())
309 .name(self.label.clone()),
310 )
311 } else {
312 ctx.add(
313 Button::new(self.label.clone())
314 .variant(self.variant)
315 .enabled(enabled.clone())
316 .on_activate_fn(move |ctx| {
317 if enabled.get() {
318 present_wizard(&spec, ctx);
319 }
320 }),
321 )
322 };
323
324 self.root_child_id = Some(root_id);
325 vec![root_id]
326 }
327
328 fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
329 self.root_child_id
330 .and_then(|id| ctx.child_size(id, proposal))
331 .unwrap_or_else(|| proposal.resolve(140.0, 40.0))
332 .into()
333 }
334
335 fn place_children(
336 &self,
337 bounds: Rect,
338 _proposal: SizeProposal,
339 children: &mut [WidgetPlacement],
340 _ctx: &LayoutContext,
341 ) {
342 for child in children.iter_mut() {
343 child.origin = bounds.origin();
344 child.size = bounds.size();
345 }
346 }
347
348 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
349 builder.set_role(teksilo_core::accesskit::Role::GenericContainer);
350 }
351
352 fn children(&self) -> Vec<WidgetId> {
353 self.root_child_id.into_iter().collect()
354 }
355}