Skip to main content

fission_onboarding/
host.rs

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    /// Start a tour over the application's live widget tree. The application
49    /// owns step transitions and persistence; this widget only presents them.
50    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}