From d0a73011de4a8c19908687473a3f82b7c4de5ee2 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 14 Apr 2025 16:40:40 +0800 Subject: [PATCH] tooltip: Add KeyBinding to tooltip and Button. (#783) - Improve `Kbd::format` method. - Add `tooltip_with_action` method to Button for display key binding in tooltip. - ## Break change - The `Tooltip::new` has been changed it argument and return type. ```diff - pub fn new(text: impl Into, _: &mut Window, cx: &mut App) -> AnyView + pub fn new(text: impl Into) -> Self ``` --- crates/story/examples/tooltip.rs | 40 ++++++ crates/story/src/lib.rs | 39 +++++- crates/story/src/main.rs | 38 +----- crates/story/src/tooltip_story.rs | 39 ++++-- crates/ui/src/button/button.rs | 28 ++++- crates/ui/src/color_picker.rs | 4 +- crates/ui/src/kbd.rs | 201 ++++++++++++++++++++++++++---- crates/ui/src/popup_menu.rs | 8 +- crates/ui/src/slider.rs | 2 +- crates/ui/src/tooltip.rs | 70 +++++++---- 10 files changed, 362 insertions(+), 107 deletions(-) create mode 100644 crates/story/examples/tooltip.rs diff --git a/crates/story/examples/tooltip.rs b/crates/story/examples/tooltip.rs new file mode 100644 index 00000000..3f458ac7 --- /dev/null +++ b/crates/story/examples/tooltip.rs @@ -0,0 +1,40 @@ +use gpui::*; +use story::{Assets, TooltipStory}; + +pub struct Example { + root: Entity, +} + +impl Example { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let root = TooltipStory::view(window, cx); + + Self { root } + } + + fn view(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| Self::new(window, cx)) + } +} + +impl Render for Example { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .p_4() + .id("example") + .overflow_y_scroll() + .size_full() + .child(self.root.clone()) + } +} + +fn main() { + let app = Application::new().with_assets(Assets); + + app.run(move |cx| { + story::init(cx); + cx.activate(true); + + story::create_new_window("Tooltip Example", Example::view, cx); + }); +} diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index 81a120eb..6affe9e6 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -36,8 +36,9 @@ pub use toggle_story::ToggleStory; use gpui::{ actions, div, impl_internal_actions, prelude::FluentBuilder as _, px, size, AnyElement, AnyView, App, AppContext, Bounds, Context, Div, Entity, EventEmitter, Focusable, Global, Hsla, - InteractiveElement, IntoElement, KeyBinding, ParentElement, Render, SharedString, - StatefulInteractiveElement, Styled as _, Window, WindowBounds, WindowKind, WindowOptions, + InteractiveElement, IntoElement, KeyBinding, Menu, MenuItem, ParentElement, Render, + SharedString, StatefulInteractiveElement, Styled as _, Window, WindowBounds, WindowKind, + WindowOptions, }; pub use icon_story::IconStory; pub use image_story::ImageStory; @@ -210,13 +211,21 @@ pub fn init(cx: &mut App) { dropdown_story::init(cx); popup_story::init(cx); webview_story::init(cx); + tooltip_story::init(cx); let http_client = std::sync::Arc::new( reqwest_client::ReqwestClient::user_agent("gpui-component/story").unwrap(), ); cx.set_http_client(http_client); - cx.bind_keys([KeyBinding::new("/", ToggleSearch, None)]); + cx.bind_keys([ + KeyBinding::new("/", ToggleSearch, None), + KeyBinding::new("cmd-q", Quit, None), + ]); + + cx.on_action(|_: &Quit, cx: &mut App| { + cx.quit(); + }); register_panel(cx, PANEL_NAME, |_, _, info, window, cx| { let story_state = match info { @@ -250,6 +259,30 @@ pub fn init(cx: &mut App) { }); Box::new(view) }); + + use gpui_component::input::{Copy, Cut, Paste, Redo, Undo}; + cx.set_menus(vec![ + Menu { + name: "GPUI App".into(), + items: vec![MenuItem::action("Quit", Quit)], + }, + Menu { + name: "Edit".into(), + items: vec![ + MenuItem::os_action("Undo", Undo, gpui::OsAction::Undo), + MenuItem::os_action("Redo", Redo, gpui::OsAction::Redo), + MenuItem::separator(), + MenuItem::os_action("Cut", Cut, gpui::OsAction::Cut), + MenuItem::os_action("Copy", Copy, gpui::OsAction::Copy), + MenuItem::os_action("Paste", Paste, gpui::OsAction::Paste), + ], + }, + Menu { + name: "Window".into(), + items: vec![], + }, + ]); + cx.activate(true); } actions!(story, [ShowPanelInfo]); diff --git a/crates/story/src/main.rs b/crates/story/src/main.rs index 03ae2f5f..8b7afd82 100644 --- a/crates/story/src/main.rs +++ b/crates/story/src/main.rs @@ -14,8 +14,8 @@ use std::{sync::Arc, time::Duration}; use story::{ AccordionStory, AppState, AppTitleBar, Assets, ButtonStory, CalendarStory, DropdownStory, FormStory, IconStory, ImageStory, InputStory, ListStory, ModalStory, Open, PopupStory, - ProgressStory, Quit, ResizableStory, ScrollableStory, SidebarStory, StoryContainer, - SwitchStory, TableStory, TextStory, TooltipStory, WebViewStory, + ProgressStory, ResizableStory, ScrollableStory, SidebarStory, StoryContainer, SwitchStory, + TableStory, TextStory, TooltipStory, WebViewStory, }; #[derive(Clone, PartialEq, Eq, Deserialize)] @@ -38,12 +38,12 @@ const STATE_FILE: &str = "target/layout.json"; const STATE_FILE: &str = "layout.json"; pub fn init(cx: &mut App) { - cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); - cx.on_action(|_action: &Open, _cx: &mut App| {}); gpui_component::init(cx); story::init(cx); + + cx.activate(true); } pub struct StoryWorkspace { @@ -534,44 +534,14 @@ impl Render for StoryWorkspace { } fn main() { - use gpui_component::input::{Copy, Cut, Paste, Redo, Undo}; - let app = Application::new().with_assets(Assets); app.run(move |cx| { init(cx); - cx.on_action(quit); - cx.set_menus(vec![ - Menu { - name: "GPUI App".into(), - items: vec![MenuItem::action("Quit", Quit)], - }, - Menu { - name: "Edit".into(), - items: vec![ - MenuItem::os_action("Undo", Undo, gpui::OsAction::Undo), - MenuItem::os_action("Redo", Redo, gpui::OsAction::Redo), - MenuItem::separator(), - MenuItem::os_action("Cut", Cut, gpui::OsAction::Cut), - MenuItem::os_action("Copy", Copy, gpui::OsAction::Copy), - MenuItem::os_action("Paste", Paste, gpui::OsAction::Paste), - ], - }, - Menu { - name: "Window".into(), - items: vec![], - }, - ]); - cx.activate(true); - open_new(cx, |_, _, _| { // do something }) .detach(); }); } - -fn quit(_: &Quit, cx: &mut App) { - cx.quit(); -} diff --git a/crates/story/src/tooltip_story.rs b/crates/story/src/tooltip_story.rs index 2a2c8368..5837d93e 100644 --- a/crates/story/src/tooltip_story.rs +++ b/crates/story/src/tooltip_story.rs @@ -1,6 +1,6 @@ use gpui::{ - div, App, AppContext, Context, Entity, Focusable, InteractiveElement, ParentElement, Render, - StatefulInteractiveElement, Styled, Window, + actions, div, App, AppContext, Context, Entity, Focusable, InteractiveElement, KeyBinding, + ParentElement, Render, StatefulInteractiveElement, Styled, Window, }; use gpui_component::{ @@ -13,6 +13,12 @@ use gpui_component::{ v_flex, ActiveTheme, IconName, }; +actions!(tooltip, [Info]); + +pub fn init(cx: &mut App) { + cx.bind_keys([KeyBinding::new("ctrl-shift-delete", Info, Some("Tooltip"))]); +} + pub struct TooltipStory { focus_handle: gpui::FocusHandle, } @@ -50,45 +56,51 @@ impl Focusable for TooltipStory { impl Render for TooltipStory { fn render( &mut self, - _window: &mut gpui::Window, + window: &mut gpui::Window, _cx: &mut gpui::Context, ) -> impl gpui::IntoElement { v_flex() .p_4() .gap_5() .child( - div() + h_flex() + .gap_3() .child( - Button::new("button") - .label("Hover me") - .with_variant(ButtonVariant::Primary), + Button::new("btn0") + .label("Search") + .with_variant(ButtonVariant::Primary) + .tooltip("This is a search Button."), ) - .id("tooltip-1") - .tooltip(|window, cx| Tooltip::new("This is a Button", window, cx)), + .child(Button::new("btn1").label("Info").tooltip_with_action( + "This is a tooltip with Action for display keybinding.", + &Info, + Some("Tooltip"), + window, + )), ) .child( h_flex() .justify_center() .child(Label::new("Hover me")) .id("tooltip-2") - .tooltip(|window, cx| Tooltip::new("This is a Label", window, cx)), + .tooltip(|window, cx| Tooltip::new("This is a Label").build(window, cx)), ) .child( div() .child(Checkbox::new("check").label("Remember me").checked(true)) .id("tooltip-3") - .tooltip(|window, cx| Tooltip::new("Checked!", window, cx)), + .tooltip(|window, cx| Tooltip::new("Checked!").build(window, cx)), ) .child( div() .child( - Button::new("button") + Button::new("btn3") .label("Hover me") .with_variant(ButtonVariant::Primary), ) .id("tooltip-4") .tooltip(|window, cx| { - Tooltip::new_element(window, cx, |_, cx| { + Tooltip::element(|_, cx| { h_flex() .gap_x_1() .child(IconName::Info) @@ -100,6 +112,7 @@ impl Render for TooltipStory { .child(div().child("Danger").text_color(cx.theme().danger)) .child(IconName::ArrowUp) }) + .build(window, cx) }), ) } diff --git a/crates/ui/src/button/button.rs b/crates/ui/src/button/button.rs index dd43a0c6..311a63f8 100644 --- a/crates/ui/src/button/button.rs +++ b/crates/ui/src/button/button.rs @@ -1,10 +1,10 @@ use crate::{ h_flex, indicator::Indicator, tooltip::Tooltip, ActiveTheme, Colorize as _, Disableable, Icon, - Selectable, Sizable, Size, StyleSized, + Kbd, Selectable, Sizable, Size, StyleSized, }; use gpui::{ - div, prelude::FluentBuilder as _, relative, AnyElement, App, ClickEvent, Corners, Div, Edges, - ElementId, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, + div, prelude::FluentBuilder as _, relative, Action, AnyElement, App, ClickEvent, Corners, Div, + Edges, ElementId, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, Window, }; @@ -184,6 +184,7 @@ pub struct Button { size: Size, compact: bool, tooltip: Option, + key_binding: Option, on_click: Option>, pub(crate) stop_propagation: bool, loading: bool, @@ -211,6 +212,7 @@ impl Button { border_edges: Edges::all(true), size: Size::Medium, tooltip: None, + key_binding: None, on_click: None, stop_propagation: true, loading: false, @@ -263,6 +265,18 @@ impl Button { self } + pub fn tooltip_with_action( + mut self, + tooltip: impl Into, + action: &dyn Action, + context: Option<&str>, + window: &Window, + ) -> Self { + self.tooltip = Some(tooltip.into()); + self.key_binding = Kbd::binding_for_action(action, context, window); + self + } + /// Set true to show the loading indicator. pub fn loading(mut self, loading: bool) -> Self { self.loading = loading; @@ -483,8 +497,12 @@ impl RenderOnce for Button { .children(self.children) }) .when(self.loading, |this| this.bg(normal_style.bg.opacity(0.8))) - .when_some(self.tooltip.clone(), |this, tooltip| { - this.tooltip(move |window, cx| Tooltip::new(tooltip.clone(), window, cx)) + .when_some(self.tooltip, |this, tooltip| { + this.tooltip(move |window, cx| { + Tooltip::new(tooltip.clone()) + .key_binding(self.key_binding.clone()) + .build(window, cx) + }) }) } } diff --git a/crates/ui/src/color_picker.rs b/crates/ui/src/color_picker.rs index 5c3ca725..b8f1f084 100644 --- a/crates/ui/src/color_picker.rs +++ b/crates/ui/src/color_picker.rs @@ -353,8 +353,8 @@ impl Render for ColorPicker { .when(self.open, |this| this.border_2()) }) .when(!display_title.is_empty(), |this| { - this.tooltip(move |window, cx| { - Tooltip::new(display_title.clone(), window, cx) + this.tooltip(move |_, cx| { + cx.new(|_| Tooltip::new(display_title.clone())).into() }) }), ) diff --git a/crates/ui/src/kbd.rs b/crates/ui/src/kbd.rs index 407f13e0..54fc1180 100644 --- a/crates/ui/src/kbd.rs +++ b/crates/ui/src/kbd.rs @@ -1,58 +1,185 @@ -use gpui::{div, relative, IntoElement, Keystroke, ParentElement as _, RenderOnce, Styled as _}; +use gpui::{ + div, relative, Action, IntoElement, KeyContext, Keystroke, ParentElement as _, RenderOnce, + Styled as _, Window, +}; use crate::ActiveTheme; /// A key binding tag -#[derive(IntoElement)] +#[derive(IntoElement, Clone, Debug)] pub struct Kbd { stroke: gpui::Keystroke, + appearance: bool, } impl From for Kbd { fn from(stroke: Keystroke) -> Self { - Self { stroke } + Self { + stroke, + appearance: true, + } } } impl Kbd { pub fn new(stroke: Keystroke) -> Self { - Self { stroke } + Self { + stroke, + appearance: true, + } + } + + /// Set the appearance of the keybinding. + pub fn appearance(mut self, appearance: bool) -> Self { + self.appearance = appearance; + self + } + + /// Return the first keybinding for the given action and context. + pub fn binding_for_action( + action: &dyn Action, + context: Option<&str>, + window: &Window, + ) -> Option { + let key_context = context.and_then(|context| KeyContext::parse(context).ok()); + let bindings = match key_context { + Some(context) => window.bindings_for_action_in_context(action, context), + None => window.bindings_for_action(action), + }; + + bindings.first().and_then(|binding| { + if let Some(key) = binding.keystrokes().first() { + Some(Self::new(key.clone())) + } else { + None + } + }) } /// Return the Platform specific keybinding string by KeyStroke + /// + /// macOS: https://support.apple.com/en-us/HT201236 + /// Windows: https://support.microsoft.com/en-us/windows/keyboard-shortcuts-in-windows-dcc61a57-8ff0-cffe-9796-cb9706c75eec pub fn format(key: &Keystroke) -> String { - if cfg!(target_os = "macos") { - return format!("{}", key); - } + #[cfg(target_os = "macos")] + const DIVIDER: &str = ""; + #[cfg(not(target_os = "macos"))] + const DIVIDER: &str = "+"; let mut parts = vec![]; - if key.modifiers.control { - parts.push("Ctrl"); - } - if key.modifiers.alt { - parts.push("Alt"); - } if key.modifiers.platform { + #[cfg(target_os = "macos")] + parts.push("⌘"); + + #[cfg(not(target_os = "macos"))] parts.push("Win"); } if key.modifiers.shift { + #[cfg(target_os = "macos")] + parts.push("⇧"); + + #[cfg(not(target_os = "macos"))] parts.push("Shift"); } + if key.modifiers.control { + #[cfg(target_os = "macos")] + parts.push("⌃"); - // Capitalize the first letter - let key = if let Some(first_c) = key.key.chars().next() { - format!("{}{}", first_c.to_uppercase(), &key.key[1..]) - } else { - key.key.to_string() - }; + #[cfg(not(target_os = "macos"))] + parts.push("Ctrl"); + } + if key.modifiers.alt { + #[cfg(target_os = "macos")] + parts.push("⌥"); - parts.push(&key); - parts.join("+") + #[cfg(not(target_os = "macos"))] + parts.push("Alt"); + } + + let mut keys = String::new(); + + for key in key.key.split("-") { + if parts.len() > 0 || keys.len() > 0 { + keys.push_str(DIVIDER); + } + + match key { + #[cfg(target_os = "macos")] + "ctrl" => keys.push('⌃'), + #[cfg(not(target_os = "macos"))] + "ctrl" => keys.push_str("Ctrl"), + #[cfg(target_os = "macos")] + "alt" => keys.push('⌥'), + #[cfg(not(target_os = "macos"))] + "alt" => keys.push_str("Alt"), + #[cfg(target_os = "macos")] + "shift" => keys.push('⇧'), + #[cfg(not(target_os = "macos"))] + "shift" => keys.push_str("Shift"), + #[cfg(target_os = "macos")] + "cmd" => keys.push('⌘'), + #[cfg(not(target_os = "macos"))] + "cmd" => keys.push_str("Win"), + #[cfg(target_os = "macos")] + "space" => keys.push_str("Space"), + #[cfg(target_os = "macos")] + "backspace" => keys.push('⌫'), + #[cfg(not(target_os = "macos"))] + "backspace" => keys.push_str("Backspace"), + #[cfg(target_os = "macos")] + "delete" => keys.push('⌫'), + #[cfg(not(target_os = "macos"))] + "delete" => keys.push_str("Delete"), + #[cfg(target_os = "macos")] + "escape" => keys.push('⎋'), + #[cfg(not(target_os = "macos"))] + "escape" => keys.push_str("Esc"), + #[cfg(target_os = "macos")] + "enter" => keys.push('⏎'), + #[cfg(not(target_os = "macos"))] + "enter" => keys.push_str("Enter"), + "pagedown" => keys.push_str("Page Down"), + "pageup" => keys.push_str("Page Up"), + #[cfg(target_os = "macos")] + "left" => keys.push('←'), + #[cfg(not(target_os = "macos"))] + "left" => keys.push_str("Left"), + #[cfg(target_os = "macos")] + "right" => keys.push('→'), + #[cfg(not(target_os = "macos"))] + "right" => keys.push_str("Right"), + #[cfg(target_os = "macos")] + "up" => keys.push('↑'), + #[cfg(not(target_os = "macos"))] + "up" => keys.push_str("Up"), + #[cfg(target_os = "macos")] + "down" => keys.push('↓'), + #[cfg(not(target_os = "macos"))] + "down" => keys.push_str("Down"), + _ => { + if key.len() == 1 { + keys.push_str(&key.to_uppercase()); + } else { + if let Some(first_char) = key.chars().next() { + keys.push_str(&format!("{}{}", first_char.to_uppercase(), &key[1..])); + } else { + keys.push_str(&key); + } + } + } + } + } + parts.push(&keys); + parts.join(DIVIDER) } } impl RenderOnce for Kbd { fn render(self, _: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement { + if !self.appearance { + return Self::format(&self.stroke).into_any_element(); + } + div() .border_1() .border_color(cx.theme().border) @@ -66,6 +193,7 @@ impl RenderOnce for Kbd { .line_height(relative(1.)) .text_xs() .child(Self::format(&self.stroke)) + .into_any_element() } } @@ -97,14 +225,39 @@ mod tests { ); } else { assert_eq!(Kbd::format(&Keystroke::parse("cmd-a").unwrap()), "⌘A"); - assert_eq!(Kbd::format(&Keystroke::parse("cmd-ctrl-a").unwrap()), "^⌘A"); + assert_eq!(Kbd::format(&Keystroke::parse("cmd-enter").unwrap()), "⌘⏎"); + assert_eq!( + Kbd::format(&Keystroke::parse("secondary-f12").unwrap()), + "⌘F12" + ); + assert_eq!( + Kbd::format(&Keystroke::parse("shift-pagedown").unwrap()), + "⇧Page Down" + ); + assert_eq!( + Kbd::format(&Keystroke::parse("shift-pageup").unwrap()), + "⇧Page Up" + ); + assert_eq!( + Kbd::format(&Keystroke::parse("shift-space").unwrap()), + "⇧Space" + ); + assert_eq!(Kbd::format(&Keystroke::parse("cmd-ctrl-a").unwrap()), "⌘⌃A"); + assert_eq!( + Kbd::format(&Keystroke::parse("cmd-alt-backspace").unwrap()), + "⌘⌥⌫" + ); + assert_eq!( + Kbd::format(&Keystroke::parse("shift-delete").unwrap()), + "⇧⌫" + ); assert_eq!( Kbd::format(&Keystroke::parse("cmd-ctrl-shift-a").unwrap()), - "^⌘⇧A" + "⌘⇧⌃A" ); assert_eq!( Kbd::format(&Keystroke::parse("cmd-ctrl-shift-alt-a").unwrap()), - "^⌥⌘⇧A" + "⌘⇧⌃⌥A" ); } } diff --git a/crates/ui/src/popup_menu.rs b/crates/ui/src/popup_menu.rs index b6e0568d..0311c266 100644 --- a/crates/ui/src/popup_menu.rs +++ b/crates/ui/src/popup_menu.rs @@ -623,15 +623,15 @@ impl PopupMenu { }); } - fn render_keybinding( + fn render_key_binding( action: Option>, window: &mut Window, cx: &mut Context, ) -> Option { if let Some(action) = action { - if let Some(keybinding) = window.bindings_for_action(action.deref()).first() { + if let Some(key_binding) = window.bindings_for_action(action.deref()).first() { let el = div().text_color(cx.theme().muted_foreground).children( - keybinding + key_binding .keystrokes() .into_iter() .map(|key| Kbd::format(key)), @@ -746,7 +746,7 @@ impl PopupMenu { } => { let show_link_icon = *is_link && self.external_link_icon; let action = action.as_ref().map(|action| action.boxed_clone()); - let key = Self::render_keybinding(action, window, cx); + let key = Self::render_key_binding(action, window, cx); this.when(!disabled, |this| { this.on_click( diff --git a/crates/ui/src/slider.rs b/crates/ui/src/slider.rs index b5f47764..472d921a 100644 --- a/crates/ui/src/slider.rs +++ b/crates/ui/src/slider.rs @@ -203,7 +203,7 @@ impl Slider { .border_color(cx.theme().slider_bar.opacity(0.9)) .when(cx.theme().shadow, |this| this.shadow_md()) .bg(cx.theme().slider_thumb) - .tooltip(move |window, cx| Tooltip::new(format!("{}", value), window, cx)) + .tooltip(move |window, cx| Tooltip::new(format!("{}", value)).build(window, cx)) } fn on_mouse_down( diff --git a/crates/ui/src/tooltip.rs b/crates/ui/src/tooltip.rs index 70edc712..486db96c 100644 --- a/crates/ui/src/tooltip.rs +++ b/crates/ui/src/tooltip.rs @@ -3,42 +3,60 @@ use gpui::{ ParentElement, Render, Styled, Window, }; -use crate::{text::Text, ActiveTheme}; +use crate::{h_flex, text::Text, ActiveTheme, Kbd}; + +enum TooltipContext { + Text(Text), + Element(Box AnyElement>), +} pub struct Tooltip { - text: Text, - element_builder: Option AnyElement>>, + content: TooltipContext, + key_binding: Option, } impl Tooltip { - pub fn new(text: impl Into, _: &mut Window, cx: &mut App) -> AnyView { - cx.new(|_| Self { - text: text.into(), - element_builder: None, - }) - .into() + /// Create a Tooltip with a text content. + pub fn new(text: impl Into) -> Self { + Self { + content: TooltipContext::Text(text.into()), + key_binding: None, + } } - pub fn new_element(_: &mut Window, cx: &mut App, builder: F) -> AnyView + /// Create a Tooltip with a custom element. + pub fn element(builder: F) -> Self where E: IntoElement, F: Fn(&mut Window, &mut App) -> E + 'static, { - cx.new(|_| Self { - text: "".into(), - element_builder: Some(Box::new(move |window, cx| { + Self { + key_binding: None, + content: TooltipContext::Element(Box::new(move |window, cx| { builder(window, cx).into_any_element() })), - }) - .into() + } + } + + /// Set KeyBinding information for the tooltip. + pub(crate) fn key_binding(mut self, kbd: Option>) -> Self { + self.key_binding = kbd.map(Into::into); + self + } + + /// Build the tooltip and return it as an `AnyView`. + pub fn build(self, _: &mut Window, cx: &mut App) -> AnyView { + cx.new(|_| self).into() } } +impl FluentBuilder for Tooltip {} + impl Render for Tooltip { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { div().child( // Wrap in a child, to ensure the left margin is applied to the tooltip - div() + h_flex() .font_family(".SystemUIFont") .m_3() .bg(cx.theme().popover) @@ -48,15 +66,25 @@ impl Render for Tooltip { .border_color(cx.theme().border) .shadow_md() .rounded(px(6.)) + .justify_between() .py_0p5() .px_2() .text_sm() + .gap_3() .map(|this| { - if let Some(builder) = &self.element_builder { - this.child(builder(window, cx)) - } else { - this.child(self.text.clone()) - } + this.child(div().map(|this| match self.content { + TooltipContext::Text(ref text) => this.child(text.clone()), + TooltipContext::Element(ref builder) => this.child(builder(window, cx)), + })) + }) + .when_some(self.key_binding.clone(), |this, kbd| { + this.child( + div() + .text_xs() + .flex_shrink_0() + .text_color(cx.theme().muted_foreground) + .child(kbd.appearance(false)), + ) }), ) }