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

Danilo Leal created

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

Release Notes:

- N/A

Change summary

crates/onboarding/src/ai_setup_page.rs | 42 +++++++++++++
crates/onboarding/src/editing_page.rs  | 79 ++++++++++++++------------
crates/ui/src/components/badge.rs      | 30 +++++++++
crates/ui/src/components/toggle.rs     | 82 ++++++++++++++++++++++++++-
4 files changed, 188 insertions(+), 45 deletions(-)

Detailed changes

crates/onboarding/src/ai_setup_page.rs 🔗

@@ -11,7 +11,7 @@ use project::DisableAiSettings;
 use settings::{Settings, update_settings_file};
 use ui::{
     Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState,
-    prelude::*,
+    prelude::*, tooltip_container,
 };
 use util::ResultExt;
 use workspace::ModalView;
@@ -41,7 +41,11 @@ fn render_llm_provider_section(
 }
 
 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()
         .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),
+                ),
+            )
+        })
+    }
+}

crates/onboarding/src/editing_page.rs 🔗

@@ -9,7 +9,7 @@ use settings::{Settings as _, update_settings_file};
 use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
 use ui::{
     ButtonLike, ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup,
-    ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, prelude::*,
+    ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*,
 };
 
 use crate::{ImportCursorSettings, ImportVsCodeSettings};
@@ -357,34 +357,65 @@ 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 {
+    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()
         .gap_5()
         .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8())
         .child(render_font_customization_section(window, cx))
+        .child(
+            SwitchField::new(
+                "onboarding-font-ligatures",
+                "Font Ligatures",
+                Some("Combine text characters into their associated symbols.".into()),
+                if read_font_ligatures(cx) {
+                    ui::ToggleState::Selected
+                } else {
+                    ui::ToggleState::Unselected
+                },
+                |toggle_state, _, cx| {
+                    write_font_ligatures(toggle_state == &ToggleState::Selected, cx);
+                },
+            )
+            .tooltip(Tooltip::text(LIGATURE_TOOLTIP)),
+        )
         .child(SwitchField::new(
-            "onboarding-font-ligatures",
-            "Font Ligatures",
-            Some("Combine text characters into their associated symbols.".into()),
-            if read_font_ligatures(cx) {
+            "onboarding-format-on-save",
+            "Format on Save",
+            Some("Format code automatically when saving.".into()),
+            if read_format_on_save(cx) {
                 ui::ToggleState::Selected
             } else {
                 ui::ToggleState::Unselected
             },
             |toggle_state, _, cx| {
-                write_font_ligatures(toggle_state == &ToggleState::Selected, cx);
+                write_format_on_save(toggle_state == &ToggleState::Selected, cx);
             },
         ))
         .child(SwitchField::new(
-            "onboarding-format-on-save",
-            "Format on Save",
-            Some("Format code automatically when saving.".into()),
-            if read_format_on_save(cx) {
+            "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_format_on_save(toggle_state == &ToggleState::Selected, 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(
@@ -421,32 +452,6 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In
                     .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 {

crates/ui/src/components/badge.rs 🔗

@@ -1,13 +1,18 @@
+use std::rc::Rc;
+
 use crate::Divider;
 use crate::DividerColor;
+use crate::Tooltip;
 use crate::component_prelude::*;
 use crate::prelude::*;
+use gpui::AnyView;
 use gpui::{AnyElement, IntoElement, SharedString, Window};
 
 #[derive(IntoElement, RegisterComponent)]
 pub struct Badge {
     label: SharedString,
     icon: IconName,
+    tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>,
 }
 
 impl Badge {
@@ -15,6 +20,7 @@ impl Badge {
         Self {
             label: label.into(),
             icon: IconName::Check,
+            tooltip: None,
         }
     }
 
@@ -22,11 +28,19 @@ impl Badge {
         self.icon = icon;
         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 {
     fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let tooltip = self.tooltip;
+
         h_flex()
+            .id(self.label.clone())
             .h_full()
             .gap_1()
             .pl_1()
@@ -43,6 +57,9 @@ impl RenderOnce for Badge {
             )
             .child(Divider::vertical().color(DividerColor::Border))
             .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> {
         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(),
         )
     }

crates/ui/src/components/toggle.rs 🔗

@@ -2,10 +2,10 @@ use gpui::{
     AnyElement, AnyView, ClickEvent, ElementId, Hsla, IntoElement, Styled, Window, div, hsla,
     prelude::*,
 };
-use std::sync::Arc;
+use std::{rc::Rc, sync::Arc};
 
 use crate::utils::is_light;
-use crate::{Color, Icon, IconName, ToggleState};
+use crate::{Color, Icon, IconName, ToggleState, Tooltip};
 use crate::{ElevationIndex, KeyBinding, prelude::*};
 
 // 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>,
     disabled: bool,
     color: SwitchColor,
+    tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>,
 }
 
 impl SwitchField {
@@ -589,6 +590,7 @@ impl SwitchField {
             on_click: Arc::new(on_click),
             disabled: false,
             color: SwitchColor::Accent,
+            tooltip: None,
         }
     }
 
@@ -608,10 +610,17 @@ impl SwitchField {
         self.color = color;
         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 {
     fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+        let tooltip = self.tooltip;
+
         h_flex()
             .id(SharedString::from(format!("{}-container", self.id)))
             .when(!self.disabled, |this| {
@@ -621,14 +630,48 @@ impl RenderOnce for SwitchField {
             .gap_4()
             .justify_between()
             .flex_wrap()
-            .child(match &self.description {
-                Some(description) => v_flex()
+            .child(match (&self.description, &tooltip) {
+                (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()
                     .max_w_5_6()
                     .child(Label::new(self.label.clone()))
                     .child(Label::new(description.clone()).color(Color::Muted))
                     .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(
                 Switch::new(
@@ -754,6 +797,35 @@ impl Component for SwitchField {
                             .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(),
         )