HelpPanel searches articles and opens one, its body in Markdown, with a way back; focus follows to All articles and back to the search, a link to help:<key> opens another, and Contact support and Keyboard shortcuts wait at its foot. KeyboardShortcutCheatsheet puts settings::KeyboardShortcutsList in a dialog with its search focused; WhatsNewDialog puts data_display::Changelog in one with Got it. FeedbackWidget asks how it is going with four faces and a note, and thanks. ContactSupport sends a topic and a message, spins while the owner sends, and empties the message with focus back in it. InlineHelp is a quiet line with Learn more. HelpTooltip moves to primitives, where FormLabel's help mark shows it; ToggleGroup keys its toggles by its own id. EmptyStateWithAction points to feedback::EmptyState.
314 lines
12 KiB
Rust
314 lines
12 KiB
Rust
use ely_gpui_component::{
|
||
buttons::{Button, ButtonVariant},
|
||
data_display::{Change, Release},
|
||
feedback::EmptyState,
|
||
forms::{Choice, FormField, Input, TextInput},
|
||
onboarding::{
|
||
ContactSupport, FeedbackWidget, HelpArticle, HelpPanel, InlineHelp,
|
||
KeyboardShortcutCheatsheet, WhatsNewDialog,
|
||
},
|
||
primitives::{HelpTooltip, IconName},
|
||
settings::Shortcut,
|
||
theme::{ActiveTheme, ControlSize, TextSize},
|
||
};
|
||
use gpui::{
|
||
App, Entity, FontWeight, IntoElement, ParentElement, SharedString, Styled, Window, div,
|
||
prelude::*, px,
|
||
};
|
||
|
||
use crate::{
|
||
probe::probe,
|
||
ui::{change, keep, section},
|
||
};
|
||
|
||
/// The help demos: the article open, whether the panel shows, and which dialog is up.
|
||
#[derive(Default)]
|
||
struct Help {
|
||
open: Option<SharedString>,
|
||
hidden: bool,
|
||
dialog: Option<Dialog>,
|
||
sent: bool,
|
||
}
|
||
|
||
#[derive(Clone, Copy, PartialEq)]
|
||
enum Dialog {
|
||
Shortcuts,
|
||
WhatsNew,
|
||
}
|
||
|
||
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)
|
||
})
|
||
}
|
||
|
||
fn article(key: &str, title: &str, summary: &str, body: &str) -> HelpArticle {
|
||
HelpArticle {
|
||
key: key.to_string().into(),
|
||
title: title.to_string().into(),
|
||
summary: summary.to_string().into(),
|
||
body: body.to_string().into(),
|
||
}
|
||
}
|
||
|
||
fn articles() -> [HelpArticle; 4] {
|
||
[
|
||
article(
|
||
"start",
|
||
"Getting started",
|
||
"Make a project and invite your team.",
|
||
"Press **New project** in the sidebar, give it a name, and pick who can see it.\n\nThen [invite your team](help:team).",
|
||
),
|
||
article(
|
||
"team",
|
||
"Invite your team",
|
||
"Bring people in by email.",
|
||
"Open **Settings › Members** and press **Invite**. Each person gets a link that works for seven days.",
|
||
),
|
||
article(
|
||
"billing",
|
||
"Change billing",
|
||
"Who can, and where.",
|
||
"Only admins can change billing. Open **Settings › Billing** to change the plan or the card.",
|
||
),
|
||
article(
|
||
"keys",
|
||
"Work from the keyboard",
|
||
"The shortcuts worth learning first.",
|
||
"Press `cmd-k` to open anything, and `?` for every shortcut.",
|
||
),
|
||
]
|
||
}
|
||
|
||
fn shortcut(group: &str, name: &str, keys: &str) -> Shortcut {
|
||
Shortcut {
|
||
group: group.to_string().into(),
|
||
name: name.to_string().into(),
|
||
keys: keys.to_string().into(),
|
||
}
|
||
}
|
||
|
||
pub fn tips(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||
let state = keep("onboarding-help", Help::default, window, cx);
|
||
let billing = field(
|
||
"onboarding-billing-email",
|
||
"billing@example.com",
|
||
window,
|
||
cx,
|
||
);
|
||
let theme = cx.theme();
|
||
let more = state.clone();
|
||
section(
|
||
"HelpTooltip · InlineHelp · EmptyStateWithAction",
|
||
"A help mark beside a name shows its tip on hover; FormLabel's help is one. A line of help sits where it is needed, and Learn more opens the article in the panel below. An empty state with an action is feedback::EmptyState.",
|
||
cx,
|
||
)
|
||
.child(
|
||
div()
|
||
.flex()
|
||
.flex_wrap()
|
||
.items_start()
|
||
.gap_6()
|
||
.child(
|
||
div()
|
||
.w(px(320.))
|
||
.flex()
|
||
.flex_col()
|
||
.gap_4()
|
||
.child(
|
||
div()
|
||
.flex()
|
||
.items_center()
|
||
.gap_1()
|
||
.child(div().font_weight(FontWeight::SEMIBOLD).child("Retention"))
|
||
.child(HelpTooltip::new("onboarding-retention-help", "Deleted items stay 30 days before they are gone.")),
|
||
)
|
||
.child(
|
||
FormField::new("onboarding-billing-field", "Billing email")
|
||
.help("Invoices go here as well as to the owner.")
|
||
.child(Input::new(&billing)),
|
||
)
|
||
.child(
|
||
InlineHelp::new("onboarding-billing-hint", "Only admins can change billing.").on_more(move |_, cx| {
|
||
change(&more, cx, |help| {
|
||
help.open = Some("billing".into());
|
||
help.hidden = false;
|
||
})
|
||
}),
|
||
),
|
||
)
|
||
.child(
|
||
div().w(px(320.)).text_size(theme.text_size(TextSize::Sm)).child(
|
||
EmptyState::new("onboarding-empty", IconName::FolderOpen, "No projects yet")
|
||
.body("Projects hold your work and who can see it.")
|
||
.action(
|
||
Button::new("onboarding-empty-new", "New project")
|
||
.variant(ButtonVariant::Primary)
|
||
.size(ControlSize::Sm)
|
||
.on_click(|_, _, _| log::info!("gallery: new project")),
|
||
),
|
||
),
|
||
),
|
||
)
|
||
}
|
||
|
||
pub fn panel(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||
let state = keep("onboarding-help", Help::default, window, cx);
|
||
let search = field("onboarding-help-search", "Search help", window, cx);
|
||
let (open, hidden) = (state.read(cx).open.clone(), state.read(cx).hidden);
|
||
let theme = cx.theme();
|
||
let [opener, keys, close, show] = [(); 4].map(|_| state.clone());
|
||
let body = match hidden {
|
||
true => Button::new("onboarding-help-show", "Show help")
|
||
.variant(ButtonVariant::Ghost)
|
||
.size(ControlSize::Sm)
|
||
.on_click(move |_, _, cx| change(&show, cx, |help| help.hidden = false))
|
||
.into_any_element(),
|
||
false => div()
|
||
.h(px(460.))
|
||
.rounded_lg()
|
||
.border_1()
|
||
.border_color(theme.colors.border)
|
||
.bg(theme.colors.surface)
|
||
.child(
|
||
HelpPanel::new("onboarding-help-panel", articles(), &search, open)
|
||
.on_open(move |key, _, cx| {
|
||
let key = key.cloned();
|
||
change(&opener, cx, |help| help.open = key)
|
||
})
|
||
.on_contact(|_, _| log::info!("gallery: contact support"))
|
||
.on_shortcuts(move |_, cx| {
|
||
change(&keys, cx, |help| help.dialog = Some(Dialog::Shortcuts))
|
||
})
|
||
.on_close(move |_, cx| change(&close, cx, |help| help.hidden = true)),
|
||
)
|
||
.into_any_element(),
|
||
};
|
||
section(
|
||
"HelpPanel / DocsSidebar",
|
||
"Help beside the work: a search over the articles, one article with the way back, and links at its foot. A link to help:<key> opens another article.",
|
||
cx,
|
||
)
|
||
.child(probe("onboarding-help-panel", div().w(px(320.)).child(body)))
|
||
}
|
||
|
||
pub fn dialogs(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||
let state = keep("onboarding-help", Help::default, window, cx);
|
||
let search = field("onboarding-keys-search", "Search shortcuts", window, cx);
|
||
let dialog = state.read(cx).dialog;
|
||
let [keys, news, shut, shut_news] = [(); 4].map(|_| state.clone());
|
||
let open = match dialog {
|
||
Some(Dialog::Shortcuts) => Some(
|
||
KeyboardShortcutCheatsheet::new(
|
||
"onboarding-cheatsheet",
|
||
[
|
||
shortcut("General", "Open anything", "cmd-k"),
|
||
shortcut("General", "Every shortcut", "?"),
|
||
shortcut("General", "Settings", "cmd-,"),
|
||
shortcut("Editing", "Undo", "cmd-z"),
|
||
shortcut("Editing", "Redo", "cmd-shift-z"),
|
||
shortcut("Editing", "Find", "cmd-f"),
|
||
shortcut("View", "Split view", "cmd-\\"),
|
||
shortcut("View", "Zoom in", "cmd-="),
|
||
],
|
||
&search,
|
||
move |_, cx| change(&shut, cx, |help| help.dialog = None),
|
||
)
|
||
.into_any_element(),
|
||
),
|
||
Some(Dialog::WhatsNew) => Some(
|
||
WhatsNewDialog::new("onboarding-whats-new", move |_, cx| {
|
||
change(&shut_news, cx, |help| help.dialog = None)
|
||
})
|
||
.release(
|
||
Release::new("2.4", "Sep 25, 2026")
|
||
.summary("Split view, and a calmer sidebar.")
|
||
.change(Change::Added, "Split view: two files side by side.")
|
||
.change(Change::Added, "Pin a view to the toolbar.")
|
||
.change(Change::Fixed, "Search keeps its place after an edit."),
|
||
)
|
||
.release(
|
||
Release::new("2.3", "Sep 9, 2026")
|
||
.change(Change::Changed, "The sidebar folds with a narrow window.")
|
||
.change(Change::Fixed, "Sync resumes after sleep."),
|
||
)
|
||
.into_any_element(),
|
||
),
|
||
None => None,
|
||
};
|
||
section(
|
||
"KeyboardShortcutCheatsheet · WhatsNewDialog",
|
||
"Every shortcut in one dialog, its search taking focus as it opens; and what changed, newest first.",
|
||
cx,
|
||
)
|
||
.child(
|
||
div()
|
||
.flex()
|
||
.flex_wrap()
|
||
.gap_3()
|
||
.child(probe(
|
||
"onboarding-shortcuts",
|
||
Button::new("onboarding-shortcuts", "Keyboard shortcuts")
|
||
.icon(IconName::Keyboard)
|
||
.on_click(move |_, _, cx| change(&keys, cx, |help| help.dialog = Some(Dialog::Shortcuts))),
|
||
))
|
||
.child(probe(
|
||
"onboarding-whats-new",
|
||
Button::new("onboarding-whats-new", "What's new")
|
||
.icon(IconName::Sparkles)
|
||
.on_click(move |_, _, cx| change(&news, cx, |help| help.dialog = Some(Dialog::WhatsNew))),
|
||
)),
|
||
)
|
||
.children(open)
|
||
}
|
||
|
||
pub fn feedback(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||
let state = keep("onboarding-help", Help::default, window, cx);
|
||
let sent = state.read(cx).sent;
|
||
let done = state.clone();
|
||
let topics = [
|
||
Choice::new("billing", "Billing"),
|
||
Choice::new("bug", "Something broke"),
|
||
Choice::new("account", "My account"),
|
||
Choice::new("other", "Something else"),
|
||
];
|
||
section(
|
||
"FeedbackWidget · ContactSupport",
|
||
"Four faces and a note, sent with one press; and a message to people, sent once it has a topic and words.",
|
||
cx,
|
||
)
|
||
.child(
|
||
div()
|
||
.flex()
|
||
.flex_wrap()
|
||
.items_start()
|
||
.gap_6()
|
||
.child(probe(
|
||
"onboarding-feedback",
|
||
FeedbackWidget::new("onboarding-feedback")
|
||
.on_send(|sentiment, note, _, _| log::info!("gallery: feedback {} {note}", sentiment.key())),
|
||
))
|
||
.child(
|
||
div()
|
||
.w(px(360.))
|
||
.flex()
|
||
.flex_col()
|
||
.gap_3()
|
||
.child(
|
||
ContactSupport::new("onboarding-support", topics)
|
||
.address("help@example.com")
|
||
.on_send(move |topic, _, _, cx| {
|
||
log::info!("gallery: support about {topic}");
|
||
change(&done, cx, |help| help.sent = true)
|
||
}),
|
||
)
|
||
.when(sent, |column| column.child(div().text_sm().child("Sent. We answer within a day."))),
|
||
),
|
||
)
|
||
}
|