Files
Ely-GPUI-Components/examples/gallery/pages/onboarding/flows.rs
T

274 lines
9.4 KiB
Rust

use ely_gpui_component::{
buttons::{Button, ButtonVariant, IconButton},
data_display::{Badge, Tone},
forms::{FormField, Input, TextInput},
lists::{List, ListItem},
onboarding::{
FeatureHighlight, Hotspot, OnboardingStep, OnboardingWizard, SetupChecklist, SetupTask,
},
primitives::{Icon, IconName},
theme::{ActiveTheme, ControlSize, IconSize},
};
use gpui::{App, Entity, IntoElement, ParentElement, Styled, Window, div, prelude::*, px};
use crate::{
probe::probe,
ui::{change, keep, section},
};
/// The wizard demo: its step, and whether it was finished.
#[derive(Default)]
struct Welcome {
step: usize,
finished: bool,
}
fn field(
key: &'static str,
hint: &'static str,
window: &mut Window,
cx: &mut App,
) -> Entity<TextInput> {
window.use_keyed_state(key, cx, move |window, cx| {
TextInput::new(window, cx).placeholder(hint)
})
}
pub fn wizard(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let state = keep("onboarding-welcome", Welcome::default, window, cx);
let name = field("onboarding-name", "Acme", window, cx);
let invite = field("onboarding-invite", "name@example.com", window, cx);
let (step, finished) = (state.read(cx).step, state.read(cx).finished);
let step_of = |key: &str, title: &str, body: &str| OnboardingStep {
key: key.to_string().into(),
title: title.to_string().into(),
body: body.to_string().into(),
};
let steps = [
step_of(
"name",
"Name your workspace",
"It shows in the sidebar and in every invitation.",
),
step_of(
"invite",
"Bring your team",
"Add an address now, or later from Settings.",
),
step_of(
"ready",
"You're set",
"Your workspace is ready. Press Finish to open it.",
),
];
let ready = step != 0 || !name.read(cx).text().trim().is_empty();
let content = match step {
0 => FormField::new("onboarding-name-field", "Workspace name")
.child(Input::new(&name))
.into_any_element(),
1 => FormField::new("onboarding-invite-field", "Email")
.child(Input::new(&invite))
.into_any_element(),
_ => div().into_any_element(),
};
let [walk, finish, skip, again] = [(); 4].map(|_| state.clone());
let body = match finished {
true => div()
.flex()
.flex_wrap()
.items_center()
.gap_3()
.child(format!("{} is ready.", name.read(cx).text()))
.child(
Button::new("onboarding-again", "Start over")
.variant(ButtonVariant::Ghost)
.size(ControlSize::Sm)
.on_click(move |_, _, cx| {
change(&again, cx, |welcome| *welcome = Welcome::default())
}),
)
.into_any_element(),
false => OnboardingWizard::new(
"onboarding-wizard",
steps,
step,
move |to, _, cx| change(&walk, cx, |welcome| welcome.step = to),
move |_, cx| change(&finish, cx, |welcome| welcome.finished = true),
)
.content(content)
.ready(ready)
.on_skip(move |_, cx| change(&skip, cx, |welcome| welcome.finished = true))
.into_any_element(),
};
section(
"OnboardingWizard · WelcomeTour",
"A first run in a few steps: where it stands over a bar in as many parts, each step's title and line, and Next once its fields are ready. The welcome tour that follows is overlays::Tour.",
cx,
)
.child(probe("onboarding-wizard", div().w(px(440.)).child(body)))
}
/// The highlights demo: what was dismissed.
#[derive(Default)]
struct Seen {
hotspot: bool,
highlight: bool,
}
pub fn highlights(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let state = keep("onboarding-seen", Seen::default, window, cx);
let (spot_seen, card_seen) = (state.read(cx).hotspot, state.read(cx).highlight);
let [spot, card, reset] = [(); 3].map(|_| state.clone());
let theme = cx.theme();
let tool =
|id: &'static str, icon: IconName| IconButton::new(id, icon).variant(ButtonVariant::Ghost);
let pin = div()
.relative()
.child(tool("onboarding-pin", IconName::Pin))
.when(!spot_seen, |pin| {
pin.child(div().absolute().top_neg_1p5().right_neg_1p5().child(probe(
"onboarding-hotspot",
Hotspot::new(
"onboarding-hotspot",
"Pin a view",
"Keep the views you use most one press away.",
move |_, cx| change(&spot, cx, |seen| seen.hotspot = true),
),
)))
});
let toolbar = div()
.flex()
.items_center()
.gap_1()
.p_1()
.rounded_lg()
.border_1()
.border_color(theme.colors.border)
.child(tool("onboarding-search", IconName::Search))
.child(pin)
.child(tool("onboarding-share", IconName::Share2));
let highlight = (!card_seen).then(|| {
FeatureHighlight::new(
"onboarding-split",
IconName::Columns2,
"Split view",
"Work on two files side by side, each with its own history.",
|_, _| log::info!("gallery: split view tried"),
move |_, cx| change(&card, cx, |seen| seen.highlight = true),
)
});
let menu = List::new()
.child(
ListItem::new("onboarding-inbox", "Inbox")
.leading(Icon::new(IconName::Inbox).size(IconSize::Sm)),
)
.child(
ListItem::new("onboarding-automations", "Automations")
.leading(Icon::new(IconName::Zap).size(IconSize::Sm))
.trailing(Badge::new("New").tone(Tone::Accent)),
);
section(
"Coachmark / Hotspot · FeatureHighlight / NewBadge",
"A hotspot's rings call out something new, and a press or Enter opens its tip. A highlight names a feature with a New badge over it; the badge alone marks a new row. Coachmarks are overlays::Spotlight.",
cx,
)
.child(
div()
.flex()
.flex_wrap()
.items_start()
.gap_6()
.child(div().w(px(240.)).flex().flex_col().gap_6().child(toolbar).child(menu))
.child(div().w(px(360.)).children(highlight))
.when(spot_seen || card_seen, |row| {
row.child(
Button::new("onboarding-show-again", "Show again")
.variant(ButtonVariant::Ghost)
.size(ControlSize::Sm)
.on_click(move |_, _, cx| change(&reset, cx, |seen| *seen = Seen::default())),
)
}),
)
}
/// Which setup tasks are done, and whether the list was hidden.
struct Setup {
done: [bool; 4],
hidden: bool,
}
pub fn checklist(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let state = keep(
"onboarding-setup",
|| Setup {
done: [true, false, false, false],
hidden: false,
},
window,
cx,
);
let (done, hidden) = (state.read(cx).done, state.read(cx).hidden);
let [start, hide, reset] = [(); 3].map(|_| state.clone());
const TASKS: [(&str, &str, &str); 4] = [
(
"profile",
"Add your photo",
"So your team knows who is who.",
),
("invite", "Invite a teammate", "Work is better shared."),
(
"connect",
"Connect your calendar",
"See meetings beside your tasks.",
),
(
"shortcuts",
"Learn three shortcuts",
"Command K opens everything.",
),
];
let tasks = TASKS
.iter()
.zip(done)
.map(|((key, title, body), done)| SetupTask {
key: (*key).into(),
title: (*title).into(),
body: (*body).into(),
done,
});
let body = match hidden {
true => Button::new("onboarding-setup-again", "Show the checklist")
.variant(ButtonVariant::Ghost)
.size(ControlSize::Sm)
.on_click(move |_, _, cx| {
change(&reset, cx, |setup| {
*setup = Setup {
done: [true, false, false, false],
hidden: false,
}
})
})
.into_any_element(),
false => SetupChecklist::new(
"onboarding-setup",
"Get started",
tasks,
move |key, _, cx| {
let at = TASKS
.iter()
.position(|(each, _, _)| *each == key.as_ref())
.expect("a setup task");
change(&start, cx, |setup| setup.done[at] = true)
},
)
.on_dismiss(move |_, cx| change(&hide, cx, |setup| setup.hidden = true))
.into_any_element(),
};
section(
"Checklist (first-run tasks)",
"First things to do, counted in words and a bar. Start opens each; here it marks it done, and the check eases in. Once all are done, Hide puts it away.",
cx,
)
.child(probe("onboarding-setup", div().w(px(400.)).child(body)))
}