1use fission::prelude::*;
2
3use crate::{
4 OnboardingFlow, OnboardingStyle, card::OnboardingCard, focus_ring::SpotlightFocusRing,
5};
6
7#[derive(Clone, Debug, PartialEq)]
8pub struct OnboardingLabels {
9 pub progress: TextContent,
10 pub back: TextContent,
11 pub continue_label: TextContent,
12 pub finish: TextContent,
13 pub skip: TextContent,
14 pub dialog_label: String,
15}
16
17impl Default for OnboardingLabels {
18 fn default() -> Self {
19 Self {
20 progress: "Getting started".into(),
21 back: "Back".into(),
22 continue_label: "Continue".into(),
23 finish: "Finish".into(),
24 skip: "Skip tour".into(),
25 dialog_label: "Guided tour".into(),
26 }
27 }
28}
29
30#[derive(Clone)]
31pub struct OnboardingHost {
32 pub id: String,
33 pub flow: OnboardingFlow,
34 pub current_step: usize,
35 pub labels: OnboardingLabels,
36 pub style: OnboardingStyle,
37 pub busy: bool,
38 pub continue_enabled: bool,
39 pub contextual_action: Option<(TextContent, ActionEnvelope)>,
40 pub contextual_status: Option<TextContent>,
41 pub on_back: Option<ActionEnvelope>,
42 pub on_continue: ActionEnvelope,
43 pub on_skip: Option<ActionEnvelope>,
44 pub on_complete: ActionEnvelope,
45}
46
47impl OnboardingHost {
48 pub fn new(
51 id: impl Into<String>,
52 flow: OnboardingFlow,
53 current_step: usize,
54 on_continue: ActionEnvelope,
55 on_complete: ActionEnvelope,
56 ) -> Self {
57 Self {
58 id: id.into(),
59 flow,
60 current_step,
61 labels: OnboardingLabels::default(),
62 style: OnboardingStyle::default(),
63 busy: false,
64 continue_enabled: true,
65 contextual_action: None,
66 contextual_status: None,
67 on_back: None,
68 on_continue,
69 on_skip: None,
70 on_complete,
71 }
72 }
73}
74
75impl From<OnboardingHost> for Widget {
76 fn from(host: OnboardingHost) -> Self {
77 let (ctx, _) = fission::build::current::<()>();
78 let Some(step) = host.flow.step(host.current_step).cloned() else {
79 return Spacer::default().into();
80 };
81
82 let dim_panel = |suffix: &str| {
83 GestureDetector {
84 id: Some(WidgetId::explicit(&format!(
85 "{}-spotlight-{suffix}",
86 host.id
87 ))),
88 child: Container::new(Spacer::default())
89 .bg(host.style.backdrop)
90 .into(),
91 on_tap: None,
92 ..Default::default()
93 }
94 .into()
95 };
96 let focus_ring: Widget = SpotlightFocusRing {
97 color: host.style.focus_ring,
98 width: host.style.focus_ring_width,
99 }
100 .into();
101 let overlay = Positioned {
102 left: Some(0.0),
103 top: Some(0.0),
104 right: Some(0.0),
105 bottom: Some(0.0),
106 child: Some(
107 Spotlight {
108 anchor: step.anchor,
109 padding: host.style.spotlight_padding,
110 children: [
111 dim_panel("top"),
112 dim_panel("bottom"),
113 dim_panel("left"),
114 dim_panel("right"),
115 focus_ring,
116 ],
117 }
118 .into(),
119 ),
120 ..Default::default()
121 };
122 ctx.register_portal_with_layer(
123 fission::core::PortalLayer::Modal,
124 Some(WidgetId::explicit(&format!("{}-spotlight", host.id))),
125 overlay.into(),
126 );
127
128 let card: Widget = OnboardingCard {
129 id: format!("{}-card", host.id),
130 step: step.clone(),
131 labels: host.labels,
132 style: host.style.clone(),
133 first_step: host.current_step == 0,
134 last_step: host.flow.is_last_step(host.current_step),
135 busy: host.busy,
136 continue_enabled: host.continue_enabled,
137 contextual_action: host.contextual_action,
138 contextual_status: host.contextual_status,
139 on_back: host.on_back,
140 on_continue: host.on_continue,
141 on_skip: host.on_skip,
142 on_complete: host.on_complete,
143 }
144 .into();
145 let callout: Widget = Column {
146 gap: Some(0.0),
147 children: vec![
148 Spacer {
149 height: Some(host.style.callout_gap),
150 ..Default::default()
151 }
152 .into(),
153 card,
154 Spacer {
155 height: Some(host.style.callout_gap),
156 ..Default::default()
157 }
158 .into(),
159 ],
160 ..Default::default()
161 }
162 .into();
163 ctx.register_portal_with_layer(
164 fission::core::PortalLayer::Flyout,
165 Some(WidgetId::explicit(&format!("{}-callout", host.id))),
166 fission::widgets::flyout(step.anchor, callout),
167 );
168
169 Spacer::default().into()
170 }
171}