1use std::rc::Rc;
11
12use teksilo_canvas::{Rect, SizeProposal};
13use teksilo_core::accessibility::AccessNodeBuilder;
14use teksilo_core::build_context::BuildContext;
15use teksilo_core::modal::{ModalCloseBehavior, ModalPresentation, ModalRequest};
16use teksilo_core::signal::Prop;
17use teksilo_core::widget::{EventContext, LayoutContext, LayoutResponse, Widget, WidgetPlacement};
18use teksilo_core::widget_id::WidgetId;
19use teksilo_i18n::{LocalizedString, lit};
20
21use super::Stepper;
22use super::controller::StepperController;
23use super::nav::{FinishAction, FinishOutcome, IntoFinishOutcome};
24use super::step::Step;
25use crate::button::{Button, ButtonVariant};
26use crate::dialog::ModalContainer;
27use crate::overlay_trigger::OverlayTrigger;
28
29const DEFAULT_WIZARD_WIDTH: u32 = 640;
30const DEFAULT_WIZARD_HEIGHT: u32 = 460;
31
32struct WizardSpec {
34 title: LocalizedString,
35 steps: Vec<Step>,
36 presentation: ModalPresentation,
37 close_behavior: ModalCloseBehavior,
38 size: (u32, u32),
39 non_linear: bool,
40 back_label: LocalizedString,
41 next_label: LocalizedString,
42 finish_label: LocalizedString,
43 skip_label: LocalizedString,
44 cancel_label: LocalizedString,
45 finish_action: Option<FinishAction>,
46}
47
48fn present_wizard(spec: &Rc<WizardSpec>, ctx: &mut EventContext) {
49 if spec.steps.is_empty() {
50 return;
51 }
52 let spec = spec.clone();
53 let presentation = spec.presentation;
54 let close_behavior = spec.close_behavior;
55 let (w, h) = spec.size;
56 let title = spec.title.resolve_now();
57 ctx.present_modal(
58 ModalRequest::deferred(move |tree| {
59 let finish = spec.finish_action.clone();
60 let stepper = Stepper::new()
61 .steps(spec.steps.clone())
62 .non_linear(spec.non_linear)
63 .back_label(spec.back_label.clone())
64 .next_label(spec.next_label.clone())
65 .finish_label(spec.finish_label.clone())
66 .skip_label(spec.skip_label.clone())
67 .cancel(spec.cancel_label.clone(), |ctx, _ctrl| ctx.dismiss_modal())
68 .on_finish(move |ctx, ctrl| {
72 let outcome = match &finish {
73 Some(action) => action(ctx, ctrl),
74 None => FinishOutcome::Finished,
75 };
76 if outcome == FinishOutcome::Finished {
77 ctx.dismiss_modal();
78 }
79 outcome
80 });
81 tree.add(ModalContainer::boxed(Box::new(stepper)))
82 })
83 .presentation(presentation)
84 .close_behavior(close_behavior)
85 .title(title)
86 .size(w, h),
87 );
88}
89
90pub struct Wizard {
98 label: LocalizedString,
99 variant: ButtonVariant,
100 enabled: Prop<bool>,
103 presentation: ModalPresentation,
104 close_behavior: ModalCloseBehavior,
105 size: (u32, u32),
106 non_linear: bool,
107 steps: Vec<Step>,
108 back_label: LocalizedString,
109 next_label: LocalizedString,
110 finish_label: LocalizedString,
111 skip_label: LocalizedString,
112 cancel_label: LocalizedString,
113 finish_action: Option<FinishAction>,
114 pending_trigger: Option<Box<dyn Widget>>,
115 root_child_id: Option<WidgetId>,
116}
117
118impl Wizard {
119 pub fn new(label: impl Into<LocalizedString>) -> Self {
122 Self {
123 label: label.into(),
124 variant: ButtonVariant::Filled,
125 enabled: Prop::Static(true),
126 presentation: ModalPresentation::Auto,
127 close_behavior: ModalCloseBehavior::Manual,
128 size: (DEFAULT_WIZARD_WIDTH, DEFAULT_WIZARD_HEIGHT),
129 non_linear: false,
130 steps: Vec::new(),
131 back_label: lit!("Back"),
132 next_label: lit!("Next"),
133 finish_label: lit!("Finish"),
134 skip_label: lit!("Skip"),
135 cancel_label: lit!("Cancel"),
136 finish_action: None,
137 pending_trigger: None,
138 root_child_id: None,
139 }
140 }
141
142 pub fn step(mut self, step: Step) -> Self {
144 self.steps.push(step);
145 self
146 }
147 pub fn steps(mut self, steps: impl IntoIterator<Item = Step>) -> Self {
149 self.steps.extend(steps);
150 self
151 }
152 pub fn variant(mut self, variant: ButtonVariant) -> Self {
154 self.variant = variant;
155 self
156 }
157 pub fn enabled(mut self, enabled: impl Into<Prop<bool>>) -> Self {
160 self.enabled = enabled.into();
161 self
162 }
163 pub fn non_linear(mut self, non_linear: bool) -> Self {
166 self.non_linear = non_linear;
167 self
168 }
169 pub fn presentation(mut self, presentation: ModalPresentation) -> Self {
171 self.presentation = presentation;
172 self
173 }
174 pub fn close_behavior(mut self, close_behavior: ModalCloseBehavior) -> Self {
176 self.close_behavior = close_behavior;
177 self
178 }
179 pub fn size(mut self, width: u32, height: u32) -> Self {
181 self.size = (width, height);
182 self
183 }
184 pub fn back_label(mut self, label: impl Into<LocalizedString>) -> Self {
186 self.back_label = label.into();
187 self
188 }
189 pub fn next_label(mut self, label: impl Into<LocalizedString>) -> Self {
191 self.next_label = label.into();
192 self
193 }
194 pub fn finish_label(mut self, label: impl Into<LocalizedString>) -> Self {
196 self.finish_label = label.into();
197 self
198 }
199 pub fn skip_label(mut self, label: impl Into<LocalizedString>) -> Self {
201 self.skip_label = label.into();
202 self
203 }
204 pub fn cancel_label(mut self, label: impl Into<LocalizedString>) -> Self {
206 self.cancel_label = label.into();
207 self
208 }
209 pub fn on_finish<R: IntoFinishOutcome>(
218 mut self,
219 action: impl Fn(&mut EventContext, &StepperController) -> R + 'static,
220 ) -> Self {
221 self.finish_action = Some(Rc::new(move |ctx, ctrl| {
222 action(ctx, ctrl).into_finish_outcome()
223 }));
224 self
225 }
226 pub fn trigger(mut self, trigger: impl Widget + 'static) -> Self {
227 self.pending_trigger = Some(Box::new(trigger));
228 self
229 }
230
231 fn spec(&self) -> Rc<WizardSpec> {
232 Rc::new(WizardSpec {
233 title: self.label.clone(),
234 steps: self.steps.clone(),
235 presentation: self.presentation,
236 close_behavior: self.close_behavior,
237 size: self.size,
238 non_linear: self.non_linear,
239 back_label: self.back_label.clone(),
240 next_label: self.next_label.clone(),
241 finish_label: self.finish_label.clone(),
242 skip_label: self.skip_label.clone(),
243 cancel_label: self.cancel_label.clone(),
244 finish_action: self.finish_action.clone(),
245 })
246 }
247}
248
249impl std::fmt::Debug for Wizard {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 f.debug_struct("Wizard")
252 .field("label", &self.label)
253 .field("steps", &self.steps.len())
254 .finish()
255 }
256}
257
258impl Widget for Wizard {
259 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
260 let enabled = self.enabled.clone();
261 let spec = self.spec();
262
263 let root_id = if let Some(trigger) = self.pending_trigger.take() {
264 let open = {
267 let spec = spec.clone();
268 let enabled = enabled.clone();
269 move |ctx: &mut EventContext| {
270 if enabled.get() {
271 present_wizard(&spec, ctx);
272 }
273 }
274 };
275 ctx.add(
276 OverlayTrigger::new(trigger)
277 .on_activate(open)
278 .activate_on_key_up()
279 .enabled(self.enabled.clone())
280 .name(self.label.clone()),
281 )
282 } else {
283 ctx.add(
284 Button::new(self.label.clone())
285 .variant(self.variant)
286 .enabled(enabled.clone())
287 .on_activate_fn(move |ctx| {
288 if enabled.get() {
289 present_wizard(&spec, ctx);
290 }
291 }),
292 )
293 };
294
295 self.root_child_id = Some(root_id);
296 vec![root_id]
297 }
298
299 fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
300 self.root_child_id
301 .and_then(|id| ctx.child_size(id, proposal))
302 .unwrap_or_else(|| proposal.resolve(140.0, 40.0))
303 .into()
304 }
305
306 fn place_children(
307 &self,
308 bounds: Rect,
309 _proposal: SizeProposal,
310 children: &mut [WidgetPlacement],
311 _ctx: &LayoutContext,
312 ) {
313 for child in children.iter_mut() {
314 child.origin = bounds.origin();
315 child.size = bounds.size();
316 }
317 }
318
319 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
320 builder.set_role(teksilo_core::accesskit::Role::GenericContainer);
321 }
322
323 fn children(&self) -> Vec<WidgetId> {
324 self.root_child_id.into_iter().collect()
325 }
326}