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,134 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce,
|
||||
SharedString, Styled, Window, div,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
data_display::{Badge, Tone},
|
||||
forms::Run,
|
||||
layout::Card,
|
||||
motion,
|
||||
primitives::{Icon, IconName},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
|
||||
};
|
||||
|
||||
/// Word of something new: its icon, a New badge over its name, a line on what it does, then Try it and Not now. It rises in when it appears.
|
||||
#[derive(IntoElement)]
|
||||
pub struct FeatureHighlight {
|
||||
id: ElementId,
|
||||
icon: IconName,
|
||||
title: SharedString,
|
||||
body: SharedString,
|
||||
on_try: Option<Run>,
|
||||
on_dismiss: Option<Run>,
|
||||
}
|
||||
|
||||
impl FeatureHighlight {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
icon: IconName,
|
||||
title: impl Into<SharedString>,
|
||||
body: impl Into<SharedString>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
icon,
|
||||
title: title.into(),
|
||||
body: body.into(),
|
||||
on_try: None,
|
||||
on_dismiss: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_try(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_try = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs on Not now; the owner stops showing it.
|
||||
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 FeatureHighlight {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let id = self.id;
|
||||
let try_it = self
|
||||
.on_try
|
||||
.unwrap_or_else(|| panic!("feature highlight {id:?} has no on_try"));
|
||||
let dismiss = self
|
||||
.on_dismiss
|
||||
.unwrap_or_else(|| panic!("feature highlight {id:?} has no on_dismiss"));
|
||||
let theme = cx.theme();
|
||||
let title = self.title.clone();
|
||||
let actions = div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.gap_2()
|
||||
.mt_3()
|
||||
.child(
|
||||
Button::new((id.clone(), "try"), "Try it")
|
||||
.variant(ButtonVariant::Primary)
|
||||
.size(ControlSize::Sm)
|
||||
.on_click(move |_, window, cx| {
|
||||
log::info!("feature highlight: try {title}");
|
||||
try_it(window, cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new((id.clone(), "dismiss"), "Not now")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.on_click(move |_, window, cx| dismiss(window, cx)),
|
||||
);
|
||||
let words = div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(div().flex().child(Badge::new("New").tone(Tone::Accent)))
|
||||
.child(
|
||||
div()
|
||||
.mt_2()
|
||||
.text_size(theme.text_size(TextSize::Base))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.child(self.title),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt_1()
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.child(self.body),
|
||||
)
|
||||
.child(actions);
|
||||
let mark = div()
|
||||
.flex_none()
|
||||
.p_2()
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.bg(theme.colors.sunken)
|
||||
.child(
|
||||
Icon::new(self.icon)
|
||||
.size(IconSize::Md)
|
||||
.color(theme.colors.fg_muted),
|
||||
);
|
||||
Card::new()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_start()
|
||||
.gap_3()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.child(mark)
|
||||
.child(words),
|
||||
)
|
||||
.with_animation(
|
||||
(id, "in"),
|
||||
Animation::new(motion::duration(motion::SLOW, cx))
|
||||
.with_easing(motion::ease_out_cubic),
|
||||
|card, t| card.opacity(t).mt(motion::NUDGE * 2.0 * (1.0 - t)),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user