Onboarding · Help: help panel, cheatsheet, what's new, feedback, support, inline help
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.
This commit is contained in:
1 parent
d4ba8f7037
commit
fe1b00d414
20 files changed
+1662
-122
No files matched your search
@@ -0,0 +1,183 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
forms::{Choice, FormField, Input, Run, Select, TextInput},
|
||||
primitives::{Icon, IconName},
|
||||
theme::{ActiveTheme, IconSize, TextSize},
|
||||
typography::literal,
|
||||
};
|
||||
|
||||
type OnMessage = Rc<dyn Fn(&SharedString, &str, &mut Window, &mut App)>;
|
||||
|
||||
/// A way to reach people: a topic and a message, and Send once both are given. It rests while the owner sends, and empties the message once sent, with focus back in it. With `address`, the address to write to instead shows under it.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ContactSupport {
|
||||
id: ElementId,
|
||||
topics: Vec<Choice>,
|
||||
busy: bool,
|
||||
address: Option<SharedString>,
|
||||
on_send: Option<OnMessage>,
|
||||
}
|
||||
|
||||
impl ContactSupport {
|
||||
pub fn new(id: impl Into<ElementId>, topics: impl IntoIterator<Item = Choice>) -> Self {
|
||||
let topics: Vec<Choice> = topics.into_iter().collect();
|
||||
assert!(!topics.is_empty(), "contact support needs a topic");
|
||||
Self {
|
||||
id: id.into(),
|
||||
topics,
|
||||
busy: false,
|
||||
address: None,
|
||||
on_send: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// While the owner sends: Send spins and takes no press, keeping its focus.
|
||||
pub fn busy(mut self, busy: bool) -> Self {
|
||||
self.busy = busy;
|
||||
self
|
||||
}
|
||||
|
||||
/// The address to write to instead.
|
||||
pub fn address(mut self, address: impl Into<SharedString>) -> Self {
|
||||
self.address = Some(address.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs with the topic's key and the message, trimmed.
|
||||
pub fn on_send(
|
||||
mut self,
|
||||
handler: impl Fn(&SharedString, &str, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_send = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ContactSupport {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let id = self.id;
|
||||
let on_send = self
|
||||
.on_send
|
||||
.unwrap_or_else(|| panic!("contact support {id:?} has no on_send"));
|
||||
let topic = window.use_keyed_state((id.clone(), "topic"), cx, |_, _| None::<SharedString>);
|
||||
let message = window.use_keyed_state((id.clone(), "message"), cx, |window, cx| {
|
||||
TextInput::new(window, cx)
|
||||
.multi_line(4, 8)
|
||||
.placeholder("What happened, and what you expected")
|
||||
});
|
||||
let chosen = topic.read(cx).clone();
|
||||
let words = message.read(cx).text().trim().to_string();
|
||||
let ready = chosen.is_some() && !words.is_empty();
|
||||
let theme = cx.theme();
|
||||
let (picked, emptied, field) = (topic, message.clone(), message.read(cx).focus().clone());
|
||||
let mut select = Select::new((id.clone(), "topic-select"), self.topics)
|
||||
.placeholder("Choose a topic")
|
||||
.on_change(move |key, _, cx| {
|
||||
let key = key.clone();
|
||||
picked.update(cx, |topic, cx| {
|
||||
*topic = Some(key);
|
||||
cx.notify();
|
||||
})
|
||||
});
|
||||
if let Some(key) = &chosen {
|
||||
select = select.selected(key.clone());
|
||||
}
|
||||
let send = move |window: &mut Window, cx: &mut App| {
|
||||
let key = chosen.clone().expect("Send rests until a topic is chosen");
|
||||
log::info!("contact support: send about {key}");
|
||||
on_send(&key, &words, window, cx);
|
||||
emptied.update(cx, |field, cx| field.set_text("", cx));
|
||||
window.focus(&field);
|
||||
};
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.child(FormField::new((id.clone(), "topic-field"), "Topic").child(select))
|
||||
.child(
|
||||
FormField::new((id.clone(), "message-field"), "Message")
|
||||
.child(Input::new(&message)),
|
||||
)
|
||||
.child(
|
||||
div().flex().child(
|
||||
Button::new((id.clone(), "send"), "Send message")
|
||||
.variant(ButtonVariant::Primary)
|
||||
.loading(self.busy)
|
||||
.disabled(!ready)
|
||||
.on_click(move |_, window, cx| send(window, cx)),
|
||||
),
|
||||
)
|
||||
.children(self.address.map(|address| {
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.items_center()
|
||||
.gap_x_1()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.child("Or write to")
|
||||
.child(literal(div()).text_color(theme.colors.fg).child(address))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// A quiet line of help beside what it explains; with `on_more`, Learn more follows it.
|
||||
#[derive(IntoElement)]
|
||||
pub struct InlineHelp {
|
||||
id: ElementId,
|
||||
text: SharedString,
|
||||
on_more: Option<Run>,
|
||||
}
|
||||
|
||||
impl InlineHelp {
|
||||
pub fn new(id: impl Into<ElementId>, text: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
text: text.into(),
|
||||
on_more: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows Learn more, which runs `handler`.
|
||||
pub fn on_more(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_more = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for InlineHelp {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let id = self.id;
|
||||
let more = self.on_more.map(|run| {
|
||||
div().flex().child(
|
||||
Button::new((id.clone(), "more"), "Learn more")
|
||||
.variant(ButtonVariant::Link)
|
||||
.on_click(move |_, window, cx| {
|
||||
log::info!("inline help: learn more");
|
||||
run(window, cx)
|
||||
}),
|
||||
)
|
||||
});
|
||||
div()
|
||||
.flex()
|
||||
.items_start()
|
||||
.gap_2()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.child(
|
||||
div().flex_none().pt_0p5().child(
|
||||
Icon::new(IconName::Info)
|
||||
.size(IconSize::Sm)
|
||||
.color(theme.colors.fg_subtle),
|
||||
),
|
||||
)
|
||||
.child(div().flex_1().min_w_0().child(self.text).children(more))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user