onboarding: Add explainer tooltips for the editing and AI section (#35619)

Includes the ability to add a tooltip for both the badge and switch
field components.

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2025-08-04 20:52:22 -03:00 committed by GitHub
parent afc4f50300
commit e1d0e3fc34
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 191 additions and 48 deletions

View file

@ -11,7 +11,7 @@ use project::DisableAiSettings;
use settings::{Settings, update_settings_file}; use settings::{Settings, update_settings_file};
use ui::{ use ui::{
Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState, Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState,
prelude::*, prelude::*, tooltip_container,
}; };
use util::ResultExt; use util::ResultExt;
use workspace::ModalView; use workspace::ModalView;
@ -41,7 +41,11 @@ fn render_llm_provider_section(
} }
fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement { fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement {
let privacy_badge = || Badge::new("Privacy").icon(IconName::ShieldCheck); let privacy_badge = || {
Badge::new("Privacy")
.icon(IconName::ShieldCheck)
.tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into())
};
v_flex() v_flex()
.relative() .relative()
@ -355,3 +359,37 @@ impl Render for AiConfigurationModal {
) )
} }
} }
pub struct AiPrivacyTooltip {}
impl AiPrivacyTooltip {
pub fn new() -> Self {
Self {}
}
}
impl Render for AiPrivacyTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
const DESCRIPTION: &'static str = "One of Zed's most important principles is transparency. This is why we are and value open-source so much. And it wouldn't be any different with AI.";
tooltip_container(window, cx, move |this, _, _| {
this.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::ShieldCheck)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new("Privacy Principle")),
)
.child(
div().max_w_64().child(
Label::new(DESCRIPTION)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
})
}
}

View file

@ -9,7 +9,7 @@ use settings::{Settings as _, update_settings_file};
use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
use ui::{ use ui::{
ButtonLike, ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup, ButtonLike, ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup,
ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, prelude::*, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*,
}; };
use crate::{ImportCursorSettings, ImportVsCodeSettings}; use crate::{ImportCursorSettings, ImportVsCodeSettings};
@ -357,23 +357,28 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl
} }
fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement { fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
const LIGATURE_TOOLTIP: &'static str = "Ligatures are when a font creates a special character out of combining two characters into one. For example, with ligatures turned on, =/= would become ≠.";
v_flex() v_flex()
.gap_5() .gap_5()
.child(Label::new("Popular Settings").size(LabelSize::Large).mt_8()) .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8())
.child(render_font_customization_section(window, cx)) .child(render_font_customization_section(window, cx))
.child(SwitchField::new( .child(
"onboarding-font-ligatures", SwitchField::new(
"Font Ligatures", "onboarding-font-ligatures",
Some("Combine text characters into their associated symbols.".into()), "Font Ligatures",
if read_font_ligatures(cx) { Some("Combine text characters into their associated symbols.".into()),
ui::ToggleState::Selected if read_font_ligatures(cx) {
} else { ui::ToggleState::Selected
ui::ToggleState::Unselected } else {
}, ui::ToggleState::Unselected
|toggle_state, _, cx| { },
write_font_ligatures(toggle_state == &ToggleState::Selected, cx); |toggle_state, _, cx| {
}, write_font_ligatures(toggle_state == &ToggleState::Selected, cx);
)) },
)
.tooltip(Tooltip::text(LIGATURE_TOOLTIP)),
)
.child(SwitchField::new( .child(SwitchField::new(
"onboarding-format-on-save", "onboarding-format-on-save",
"Format on Save", "Format on Save",
@ -387,6 +392,32 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In
write_format_on_save(toggle_state == &ToggleState::Selected, cx); write_format_on_save(toggle_state == &ToggleState::Selected, cx);
}, },
)) ))
.child(SwitchField::new(
"onboarding-enable-inlay-hints",
"Inlay Hints",
Some("See parameter names for function and method calls inline.".into()),
if read_inlay_hints(cx) {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
|toggle_state, _, cx| {
write_inlay_hints(toggle_state == &ToggleState::Selected, cx);
},
))
.child(SwitchField::new(
"onboarding-git-blame-switch",
"Git Blame",
Some("See who committed each line on a given file.".into()),
if read_git_blame(cx) {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
|toggle_state, _, cx| {
set_git_blame(toggle_state == &ToggleState::Selected, cx);
},
))
.child( .child(
h_flex() h_flex()
.items_start() .items_start()
@ -421,32 +452,6 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In
.button_width(ui::rems_from_px(64.)), .button_width(ui::rems_from_px(64.)),
), ),
) )
.child(SwitchField::new(
"onboarding-enable-inlay-hints",
"Inlay Hints",
Some("See parameter names for function and method calls inline.".into()),
if read_inlay_hints(cx) {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
|toggle_state, _, cx| {
write_inlay_hints(toggle_state == &ToggleState::Selected, cx);
},
))
.child(SwitchField::new(
"onboarding-git-blame-switch",
"Git Blame",
Some("See who committed each line on a given file.".into()),
if read_git_blame(cx) {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
|toggle_state, _, cx| {
set_git_blame(toggle_state == &ToggleState::Selected, cx);
},
))
} }
pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement { pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {

View file

@ -1,13 +1,18 @@
use std::rc::Rc;
use crate::Divider; use crate::Divider;
use crate::DividerColor; use crate::DividerColor;
use crate::Tooltip;
use crate::component_prelude::*; use crate::component_prelude::*;
use crate::prelude::*; use crate::prelude::*;
use gpui::AnyView;
use gpui::{AnyElement, IntoElement, SharedString, Window}; use gpui::{AnyElement, IntoElement, SharedString, Window};
#[derive(IntoElement, RegisterComponent)] #[derive(IntoElement, RegisterComponent)]
pub struct Badge { pub struct Badge {
label: SharedString, label: SharedString,
icon: IconName, icon: IconName,
tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>,
} }
impl Badge { impl Badge {
@ -15,6 +20,7 @@ impl Badge {
Self { Self {
label: label.into(), label: label.into(),
icon: IconName::Check, icon: IconName::Check,
tooltip: None,
} }
} }
@ -22,11 +28,19 @@ impl Badge {
self.icon = icon; self.icon = icon;
self self
} }
pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
self.tooltip = Some(Rc::new(tooltip));
self
}
} }
impl RenderOnce for Badge { impl RenderOnce for Badge {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let tooltip = self.tooltip;
h_flex() h_flex()
.id(self.label.clone())
.h_full() .h_full()
.gap_1() .gap_1()
.pl_1() .pl_1()
@ -43,6 +57,9 @@ impl RenderOnce for Badge {
) )
.child(Divider::vertical().color(DividerColor::Border)) .child(Divider::vertical().color(DividerColor::Border))
.child(Label::new(self.label.clone()).size(LabelSize::Small).ml_1()) .child(Label::new(self.label.clone()).size(LabelSize::Small).ml_1())
.when_some(tooltip, |this, tooltip| {
this.tooltip(move |window, cx| tooltip(window, cx))
})
} }
} }
@ -59,7 +76,18 @@ impl Component for Badge {
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> { fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some( Some(
single_example("Basic Badge", Badge::new("Default").into_any_element()) v_flex()
.gap_6()
.child(single_example(
"Basic Badge",
Badge::new("Default").into_any_element(),
))
.child(single_example(
"With Tooltip",
Badge::new("Tooltip")
.tooltip(Tooltip::text("This is a tooltip."))
.into_any_element(),
))
.into_any_element(), .into_any_element(),
) )
} }

View file

@ -2,10 +2,10 @@ use gpui::{
AnyElement, AnyView, ClickEvent, ElementId, Hsla, IntoElement, Styled, Window, div, hsla, AnyElement, AnyView, ClickEvent, ElementId, Hsla, IntoElement, Styled, Window, div, hsla,
prelude::*, prelude::*,
}; };
use std::sync::Arc; use std::{rc::Rc, sync::Arc};
use crate::utils::is_light; use crate::utils::is_light;
use crate::{Color, Icon, IconName, ToggleState}; use crate::{Color, Icon, IconName, ToggleState, Tooltip};
use crate::{ElevationIndex, KeyBinding, prelude::*}; use crate::{ElevationIndex, KeyBinding, prelude::*};
// TODO: Checkbox, CheckboxWithLabel, and Switch could all be // TODO: Checkbox, CheckboxWithLabel, and Switch could all be
@ -571,6 +571,7 @@ pub struct SwitchField {
on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>, on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
disabled: bool, disabled: bool,
color: SwitchColor, color: SwitchColor,
tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>,
} }
impl SwitchField { impl SwitchField {
@ -589,6 +590,7 @@ impl SwitchField {
on_click: Arc::new(on_click), on_click: Arc::new(on_click),
disabled: false, disabled: false,
color: SwitchColor::Accent, color: SwitchColor::Accent,
tooltip: None,
} }
} }
@ -608,10 +610,17 @@ impl SwitchField {
self.color = color; self.color = color;
self self
} }
pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
self.tooltip = Some(Rc::new(tooltip));
self
}
} }
impl RenderOnce for SwitchField { impl RenderOnce for SwitchField {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let tooltip = self.tooltip;
h_flex() h_flex()
.id(SharedString::from(format!("{}-container", self.id))) .id(SharedString::from(format!("{}-container", self.id)))
.when(!self.disabled, |this| { .when(!self.disabled, |this| {
@ -621,14 +630,48 @@ impl RenderOnce for SwitchField {
.gap_4() .gap_4()
.justify_between() .justify_between()
.flex_wrap() .flex_wrap()
.child(match &self.description { .child(match (&self.description, &tooltip) {
Some(description) => v_flex() (Some(description), Some(tooltip)) => v_flex()
.gap_0p5()
.max_w_5_6()
.child(
h_flex()
.gap_0p5()
.child(Label::new(self.label.clone()))
.child(
IconButton::new("tooltip_button", IconName::Info)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.shape(crate::IconButtonShape::Square)
.tooltip({
let tooltip = tooltip.clone();
move |window, cx| tooltip(window, cx)
}),
),
)
.child(Label::new(description.clone()).color(Color::Muted))
.into_any_element(),
(Some(description), None) => v_flex()
.gap_0p5() .gap_0p5()
.max_w_5_6() .max_w_5_6()
.child(Label::new(self.label.clone())) .child(Label::new(self.label.clone()))
.child(Label::new(description.clone()).color(Color::Muted)) .child(Label::new(description.clone()).color(Color::Muted))
.into_any_element(), .into_any_element(),
None => Label::new(self.label.clone()).into_any_element(), (None, Some(tooltip)) => h_flex()
.gap_0p5()
.child(Label::new(self.label.clone()))
.child(
IconButton::new("tooltip_button", IconName::Info)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.shape(crate::IconButtonShape::Square)
.tooltip({
let tooltip = tooltip.clone();
move |window, cx| tooltip(window, cx)
}),
)
.into_any_element(),
(None, None) => Label::new(self.label.clone()).into_any_element(),
}) })
.child( .child(
Switch::new( Switch::new(
@ -754,6 +797,35 @@ impl Component for SwitchField {
.into_any_element(), .into_any_element(),
)], )],
), ),
example_group_with_title(
"With Tooltip",
vec![
single_example(
"Tooltip with Description",
SwitchField::new(
"switch_field_tooltip_with_desc",
"Nice Feature",
Some("Enable advanced configuration options.".into()),
ToggleState::Unselected,
|_, _, _| {},
)
.tooltip(Tooltip::text("This is content for this tooltip!"))
.into_any_element(),
),
single_example(
"Tooltip without Description",
SwitchField::new(
"switch_field_tooltip_no_desc",
"Nice Feature",
None,
ToggleState::Selected,
|_, _, _| {},
)
.tooltip(Tooltip::text("This is content for this tooltip!"))
.into_any_element(),
),
],
),
]) ])
.into_any_element(), .into_any_element(),
) )