diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index b4b043196bc05d688d0290dc87acb87ae07e648b..b5dda7601f9111d0d90452cd2578f4cf826d5afa 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/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) -> 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), + ), + ) + }) + } +} diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 2972f41348ed7d195566784f1ee0215f65a0238c..20ef17c7aa26a18becada8808145fd6bb79b95e8 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/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 { diff --git a/crates/ui/src/components/badge.rs b/crates/ui/src/components/badge.rs index 2eee084bbb34b2fa6cb7bd5f27835202afb9e82b..f36e03291c5915f70e8370c6cc1e037d097622b0 100644 --- a/crates/ui/src/components/badge.rs +++ b/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 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 { 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(), ) } diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index 0d8f5c4107af2b9d2a1fabbe8ca9c7b088d89588..a3a3f23889f7a6221bd5c31195e0cc8d2907279e 100644 --- a/crates/ui/src/components/toggle.rs +++ b/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, disabled: bool, color: SwitchColor, + tooltip: Option 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(), )