Onboarding · Flows and highlights: wizard, hotspot, highlight, setup checklist
OnboardingWizard runs a first run on navigation::Wizard without its numbered steps: "Step 2 of 4" over a bar in as many parts, the step's title, line and content, Back and Next once ready, Finish last, and Skip for now. Hotspot pulses on a dot that opens a tip on a press or Enter, with Got it. FeatureHighlight names something new under a New badge, with Try it and Not now, and rises in. SetupChecklist counts first things to do in words and a bar, offers Start on each left, eases a check in as one gets done, and offers Hide once all are. navigation::Wizard now keys its buttons, steps and motion by its own id and takes on_step and on_finish at render, failing loud without them.
This commit is contained in:
1 parent
c68e3229bb
commit
eea99b8e41
16 files changed
+1288
-27
No files matched your search
@@ -0,0 +1,124 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, FontWeight, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
forms::Run,
|
||||
motion::Pulse,
|
||||
overlays::Popover,
|
||||
primitives::FocusRing,
|
||||
theme::{ActiveTheme, ControlSize, TextSize},
|
||||
};
|
||||
|
||||
/// The dot: a Tab stop that opens the tip on a press or on Enter.
|
||||
#[derive(IntoElement)]
|
||||
struct Dot {
|
||||
id: ElementId,
|
||||
toggle: Run,
|
||||
}
|
||||
|
||||
impl RenderOnce for Dot {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let side = theme.onboarding().hotspot;
|
||||
let toggle = self.toggle;
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(side * 2.0)
|
||||
.rounded_full()
|
||||
.border_1()
|
||||
.border_color(gpui::transparent_black())
|
||||
.tab_index(0)
|
||||
.focus_ring(cx)
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.on_click(move |_, window, cx| toggle(window, cx))
|
||||
.child(
|
||||
Pulse::new((self.id, "pulse"))
|
||||
.size(side)
|
||||
.rounded_full()
|
||||
.bg(theme.colors.accent),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A dot that calls out something new, with rings that widen from it; a press or Enter opens a tip beside it with a title, a line and Got it. Under reduced motion no ring shows.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Hotspot {
|
||||
id: ElementId,
|
||||
title: SharedString,
|
||||
body: SharedString,
|
||||
on_dismiss: Option<Run>,
|
||||
}
|
||||
|
||||
impl Hotspot {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
title: impl Into<SharedString>,
|
||||
body: impl Into<SharedString>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
title: title.into(),
|
||||
body: body.into(),
|
||||
on_dismiss: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs on Got it; the owner stops showing the hotspot.
|
||||
pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_dismiss = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Hotspot {
|
||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
let id = self.id;
|
||||
let dismiss = self
|
||||
.on_dismiss
|
||||
.unwrap_or_else(|| panic!("hotspot {id:?} has no on_dismiss"));
|
||||
let (dot, got_it) = ((id.clone(), "dot"), (id.clone(), "got-it"));
|
||||
let (title, body) = (self.title, self.body);
|
||||
Popover::with_opener(
|
||||
id.clone(),
|
||||
move |toggle| Dot {
|
||||
id: dot.into(),
|
||||
toggle,
|
||||
},
|
||||
move |_, cx| {
|
||||
let theme = cx.theme();
|
||||
let prose =
|
||||
|text: SharedString| div().flex().child(div().flex_1().min_w_0().child(text));
|
||||
div()
|
||||
.debug_selector(|| "hotspot-tip".into())
|
||||
.w(theme.tooltip_max_width())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.child(prose(title).font_weight(FontWeight::SEMIBOLD))
|
||||
.child(prose(body).text_color(theme.colors.fg_muted))
|
||||
.child(
|
||||
div().flex().justify_end().pt_1().child(
|
||||
Button::new(got_it, "Got it")
|
||||
.variant(ButtonVariant::Primary)
|
||||
.size(ControlSize::Sm)
|
||||
.on_click(move |_, window, cx| {
|
||||
log::info!("hotspot: dismissed");
|
||||
dismiss(window, cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user