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<Text>, _: &mut Window, cx: &mut App) ->
AnyView
+ pub fn new(text: impl Into<Text>) -> Self
```
This commit is contained in:
parent
95fb86ec08
commit
d0a73011de
10 changed files with 362 additions and 107 deletions
40
crates/story/examples/tooltip.rs
Normal file
40
crates/story/examples/tooltip.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
use gpui::*;
|
||||||
|
use story::{Assets, TooltipStory};
|
||||||
|
|
||||||
|
pub struct Example {
|
||||||
|
root: Entity<TooltipStory>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Example {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let root = TooltipStory::view(window, cx);
|
||||||
|
|
||||||
|
Self { root }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
|
cx.new(|cx| Self::new(window, cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Example {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -36,8 +36,9 @@ pub use toggle_story::ToggleStory;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, impl_internal_actions, prelude::FluentBuilder as _, px, size, AnyElement,
|
actions, div, impl_internal_actions, prelude::FluentBuilder as _, px, size, AnyElement,
|
||||||
AnyView, App, AppContext, Bounds, Context, Div, Entity, EventEmitter, Focusable, Global, Hsla,
|
AnyView, App, AppContext, Bounds, Context, Div, Entity, EventEmitter, Focusable, Global, Hsla,
|
||||||
InteractiveElement, IntoElement, KeyBinding, ParentElement, Render, SharedString,
|
InteractiveElement, IntoElement, KeyBinding, Menu, MenuItem, ParentElement, Render,
|
||||||
StatefulInteractiveElement, Styled as _, Window, WindowBounds, WindowKind, WindowOptions,
|
SharedString, StatefulInteractiveElement, Styled as _, Window, WindowBounds, WindowKind,
|
||||||
|
WindowOptions,
|
||||||
};
|
};
|
||||||
pub use icon_story::IconStory;
|
pub use icon_story::IconStory;
|
||||||
pub use image_story::ImageStory;
|
pub use image_story::ImageStory;
|
||||||
|
|
@ -210,13 +211,21 @@ pub fn init(cx: &mut App) {
|
||||||
dropdown_story::init(cx);
|
dropdown_story::init(cx);
|
||||||
popup_story::init(cx);
|
popup_story::init(cx);
|
||||||
webview_story::init(cx);
|
webview_story::init(cx);
|
||||||
|
tooltip_story::init(cx);
|
||||||
|
|
||||||
let http_client = std::sync::Arc::new(
|
let http_client = std::sync::Arc::new(
|
||||||
reqwest_client::ReqwestClient::user_agent("gpui-component/story").unwrap(),
|
reqwest_client::ReqwestClient::user_agent("gpui-component/story").unwrap(),
|
||||||
);
|
);
|
||||||
cx.set_http_client(http_client);
|
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| {
|
register_panel(cx, PANEL_NAME, |_, _, info, window, cx| {
|
||||||
let story_state = match info {
|
let story_state = match info {
|
||||||
|
|
@ -250,6 +259,30 @@ pub fn init(cx: &mut App) {
|
||||||
});
|
});
|
||||||
Box::new(view)
|
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]);
|
actions!(story, [ShowPanelInfo]);
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ use std::{sync::Arc, time::Duration};
|
||||||
use story::{
|
use story::{
|
||||||
AccordionStory, AppState, AppTitleBar, Assets, ButtonStory, CalendarStory, DropdownStory,
|
AccordionStory, AppState, AppTitleBar, Assets, ButtonStory, CalendarStory, DropdownStory,
|
||||||
FormStory, IconStory, ImageStory, InputStory, ListStory, ModalStory, Open, PopupStory,
|
FormStory, IconStory, ImageStory, InputStory, ListStory, ModalStory, Open, PopupStory,
|
||||||
ProgressStory, Quit, ResizableStory, ScrollableStory, SidebarStory, StoryContainer,
|
ProgressStory, ResizableStory, ScrollableStory, SidebarStory, StoryContainer, SwitchStory,
|
||||||
SwitchStory, TableStory, TextStory, TooltipStory, WebViewStory,
|
TableStory, TextStory, TooltipStory, WebViewStory,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||||
|
|
@ -38,12 +38,12 @@ const STATE_FILE: &str = "target/layout.json";
|
||||||
const STATE_FILE: &str = "layout.json";
|
const STATE_FILE: &str = "layout.json";
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
|
||||||
|
|
||||||
cx.on_action(|_action: &Open, _cx: &mut App| {});
|
cx.on_action(|_action: &Open, _cx: &mut App| {});
|
||||||
|
|
||||||
gpui_component::init(cx);
|
gpui_component::init(cx);
|
||||||
story::init(cx);
|
story::init(cx);
|
||||||
|
|
||||||
|
cx.activate(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StoryWorkspace {
|
pub struct StoryWorkspace {
|
||||||
|
|
@ -534,44 +534,14 @@ impl Render for StoryWorkspace {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
use gpui_component::input::{Copy, Cut, Paste, Redo, Undo};
|
|
||||||
|
|
||||||
let app = Application::new().with_assets(Assets);
|
let app = Application::new().with_assets(Assets);
|
||||||
|
|
||||||
app.run(move |cx| {
|
app.run(move |cx| {
|
||||||
init(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, |_, _, _| {
|
open_new(cx, |_, _, _| {
|
||||||
// do something
|
// do something
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quit(_: &Quit, cx: &mut App) {
|
|
||||||
cx.quit();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, App, AppContext, Context, Entity, Focusable, InteractiveElement, ParentElement, Render,
|
actions, div, App, AppContext, Context, Entity, Focusable, InteractiveElement, KeyBinding,
|
||||||
StatefulInteractiveElement, Styled, Window,
|
ParentElement, Render, StatefulInteractiveElement, Styled, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
|
|
@ -13,6 +13,12 @@ use gpui_component::{
|
||||||
v_flex, ActiveTheme, IconName,
|
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 {
|
pub struct TooltipStory {
|
||||||
focus_handle: gpui::FocusHandle,
|
focus_handle: gpui::FocusHandle,
|
||||||
}
|
}
|
||||||
|
|
@ -50,45 +56,51 @@ impl Focusable for TooltipStory {
|
||||||
impl Render for TooltipStory {
|
impl Render for TooltipStory {
|
||||||
fn render(
|
fn render(
|
||||||
&mut self,
|
&mut self,
|
||||||
_window: &mut gpui::Window,
|
window: &mut gpui::Window,
|
||||||
_cx: &mut gpui::Context<Self>,
|
_cx: &mut gpui::Context<Self>,
|
||||||
) -> impl gpui::IntoElement {
|
) -> impl gpui::IntoElement {
|
||||||
v_flex()
|
v_flex()
|
||||||
.p_4()
|
.p_4()
|
||||||
.gap_5()
|
.gap_5()
|
||||||
.child(
|
.child(
|
||||||
div()
|
h_flex()
|
||||||
|
.gap_3()
|
||||||
.child(
|
.child(
|
||||||
Button::new("button")
|
Button::new("btn0")
|
||||||
.label("Hover me")
|
.label("Search")
|
||||||
.with_variant(ButtonVariant::Primary),
|
.with_variant(ButtonVariant::Primary)
|
||||||
|
.tooltip("This is a search Button."),
|
||||||
)
|
)
|
||||||
.id("tooltip-1")
|
.child(Button::new("btn1").label("Info").tooltip_with_action(
|
||||||
.tooltip(|window, cx| Tooltip::new("This is a Button", window, cx)),
|
"This is a tooltip with Action for display keybinding.",
|
||||||
|
&Info,
|
||||||
|
Some("Tooltip"),
|
||||||
|
window,
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.child(Label::new("Hover me"))
|
.child(Label::new("Hover me"))
|
||||||
.id("tooltip-2")
|
.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(
|
.child(
|
||||||
div()
|
div()
|
||||||
.child(Checkbox::new("check").label("Remember me").checked(true))
|
.child(Checkbox::new("check").label("Remember me").checked(true))
|
||||||
.id("tooltip-3")
|
.id("tooltip-3")
|
||||||
.tooltip(|window, cx| Tooltip::new("Checked!", window, cx)),
|
.tooltip(|window, cx| Tooltip::new("Checked!").build(window, cx)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.child(
|
.child(
|
||||||
Button::new("button")
|
Button::new("btn3")
|
||||||
.label("Hover me")
|
.label("Hover me")
|
||||||
.with_variant(ButtonVariant::Primary),
|
.with_variant(ButtonVariant::Primary),
|
||||||
)
|
)
|
||||||
.id("tooltip-4")
|
.id("tooltip-4")
|
||||||
.tooltip(|window, cx| {
|
.tooltip(|window, cx| {
|
||||||
Tooltip::new_element(window, cx, |_, cx| {
|
Tooltip::element(|_, cx| {
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_x_1()
|
.gap_x_1()
|
||||||
.child(IconName::Info)
|
.child(IconName::Info)
|
||||||
|
|
@ -100,6 +112,7 @@ impl Render for TooltipStory {
|
||||||
.child(div().child("Danger").text_color(cx.theme().danger))
|
.child(div().child("Danger").text_color(cx.theme().danger))
|
||||||
.child(IconName::ArrowUp)
|
.child(IconName::ArrowUp)
|
||||||
})
|
})
|
||||||
|
.build(window, cx)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
h_flex, indicator::Indicator, tooltip::Tooltip, ActiveTheme, Colorize as _, Disableable, Icon,
|
h_flex, indicator::Indicator, tooltip::Tooltip, ActiveTheme, Colorize as _, Disableable, Icon,
|
||||||
Selectable, Sizable, Size, StyleSized,
|
Kbd, Selectable, Sizable, Size, StyleSized,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::FluentBuilder as _, relative, AnyElement, App, ClickEvent, Corners, Div, Edges,
|
div, prelude::FluentBuilder as _, relative, Action, AnyElement, App, ClickEvent, Corners, Div,
|
||||||
ElementId, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels,
|
Edges, ElementId, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels,
|
||||||
RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, Window,
|
RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -184,6 +184,7 @@ pub struct Button {
|
||||||
size: Size,
|
size: Size,
|
||||||
compact: bool,
|
compact: bool,
|
||||||
tooltip: Option<SharedString>,
|
tooltip: Option<SharedString>,
|
||||||
|
key_binding: Option<Kbd>,
|
||||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||||
pub(crate) stop_propagation: bool,
|
pub(crate) stop_propagation: bool,
|
||||||
loading: bool,
|
loading: bool,
|
||||||
|
|
@ -211,6 +212,7 @@ impl Button {
|
||||||
border_edges: Edges::all(true),
|
border_edges: Edges::all(true),
|
||||||
size: Size::Medium,
|
size: Size::Medium,
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
|
key_binding: None,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
stop_propagation: true,
|
stop_propagation: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
@ -263,6 +265,18 @@ impl Button {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tooltip_with_action(
|
||||||
|
mut self,
|
||||||
|
tooltip: impl Into<SharedString>,
|
||||||
|
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.
|
/// Set true to show the loading indicator.
|
||||||
pub fn loading(mut self, loading: bool) -> Self {
|
pub fn loading(mut self, loading: bool) -> Self {
|
||||||
self.loading = loading;
|
self.loading = loading;
|
||||||
|
|
@ -483,8 +497,12 @@ impl RenderOnce for Button {
|
||||||
.children(self.children)
|
.children(self.children)
|
||||||
})
|
})
|
||||||
.when(self.loading, |this| this.bg(normal_style.bg.opacity(0.8)))
|
.when(self.loading, |this| this.bg(normal_style.bg.opacity(0.8)))
|
||||||
.when_some(self.tooltip.clone(), |this, tooltip| {
|
.when_some(self.tooltip, |this, tooltip| {
|
||||||
this.tooltip(move |window, cx| Tooltip::new(tooltip.clone(), window, cx))
|
this.tooltip(move |window, cx| {
|
||||||
|
Tooltip::new(tooltip.clone())
|
||||||
|
.key_binding(self.key_binding.clone())
|
||||||
|
.build(window, cx)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -353,8 +353,8 @@ impl Render for ColorPicker {
|
||||||
.when(self.open, |this| this.border_2())
|
.when(self.open, |this| this.border_2())
|
||||||
})
|
})
|
||||||
.when(!display_title.is_empty(), |this| {
|
.when(!display_title.is_empty(), |this| {
|
||||||
this.tooltip(move |window, cx| {
|
this.tooltip(move |_, cx| {
|
||||||
Tooltip::new(display_title.clone(), window, cx)
|
cx.new(|_| Tooltip::new(display_title.clone())).into()
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
use crate::ActiveTheme;
|
||||||
|
|
||||||
/// A key binding tag
|
/// A key binding tag
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement, Clone, Debug)]
|
||||||
pub struct Kbd {
|
pub struct Kbd {
|
||||||
stroke: gpui::Keystroke,
|
stroke: gpui::Keystroke,
|
||||||
|
appearance: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Keystroke> for Kbd {
|
impl From<Keystroke> for Kbd {
|
||||||
fn from(stroke: Keystroke) -> Self {
|
fn from(stroke: Keystroke) -> Self {
|
||||||
Self { stroke }
|
Self {
|
||||||
|
stroke,
|
||||||
|
appearance: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Kbd {
|
impl Kbd {
|
||||||
pub fn new(stroke: Keystroke) -> Self {
|
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<Self> {
|
||||||
|
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
|
/// 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 {
|
pub fn format(key: &Keystroke) -> String {
|
||||||
if cfg!(target_os = "macos") {
|
#[cfg(target_os = "macos")]
|
||||||
return format!("{}", key);
|
const DIVIDER: &str = "";
|
||||||
}
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
const DIVIDER: &str = "+";
|
||||||
|
|
||||||
let mut parts = vec![];
|
let mut parts = vec![];
|
||||||
if key.modifiers.control {
|
|
||||||
parts.push("Ctrl");
|
|
||||||
}
|
|
||||||
if key.modifiers.alt {
|
|
||||||
parts.push("Alt");
|
|
||||||
}
|
|
||||||
if key.modifiers.platform {
|
if key.modifiers.platform {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
parts.push("⌘");
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
parts.push("Win");
|
parts.push("Win");
|
||||||
}
|
}
|
||||||
if key.modifiers.shift {
|
if key.modifiers.shift {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
parts.push("⇧");
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
parts.push("Shift");
|
parts.push("Shift");
|
||||||
}
|
}
|
||||||
|
if key.modifiers.control {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
parts.push("⌃");
|
||||||
|
|
||||||
// Capitalize the first letter
|
#[cfg(not(target_os = "macos"))]
|
||||||
let key = if let Some(first_c) = key.key.chars().next() {
|
parts.push("Ctrl");
|
||||||
format!("{}{}", first_c.to_uppercase(), &key.key[1..])
|
}
|
||||||
|
if key.modifiers.alt {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
parts.push("⌥");
|
||||||
|
|
||||||
|
#[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 {
|
} else {
|
||||||
key.key.to_string()
|
if let Some(first_char) = key.chars().next() {
|
||||||
};
|
keys.push_str(&format!("{}{}", first_char.to_uppercase(), &key[1..]));
|
||||||
|
} else {
|
||||||
parts.push(&key);
|
keys.push_str(&key);
|
||||||
parts.join("+")
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.push(&keys);
|
||||||
|
parts.join(DIVIDER)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Kbd {
|
impl RenderOnce for Kbd {
|
||||||
fn render(self, _: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
|
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()
|
div()
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
|
|
@ -66,6 +193,7 @@ impl RenderOnce for Kbd {
|
||||||
.line_height(relative(1.))
|
.line_height(relative(1.))
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.child(Self::format(&self.stroke))
|
.child(Self::format(&self.stroke))
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,14 +225,39 @@ mod tests {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
assert_eq!(Kbd::format(&Keystroke::parse("cmd-a").unwrap()), "⌘A");
|
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!(
|
assert_eq!(
|
||||||
Kbd::format(&Keystroke::parse("cmd-ctrl-shift-a").unwrap()),
|
Kbd::format(&Keystroke::parse("cmd-ctrl-shift-a").unwrap()),
|
||||||
"^⌘⇧A"
|
"⌘⇧⌃A"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Kbd::format(&Keystroke::parse("cmd-ctrl-shift-alt-a").unwrap()),
|
Kbd::format(&Keystroke::parse("cmd-ctrl-shift-alt-a").unwrap()),
|
||||||
"^⌥⌘⇧A"
|
"⌘⇧⌃⌥A"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -623,15 +623,15 @@ impl PopupMenu {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_keybinding(
|
fn render_key_binding(
|
||||||
action: Option<Box<dyn Action>>,
|
action: Option<Box<dyn Action>>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Option<impl IntoElement> {
|
) -> Option<impl IntoElement> {
|
||||||
if let Some(action) = action {
|
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(
|
let el = div().text_color(cx.theme().muted_foreground).children(
|
||||||
keybinding
|
key_binding
|
||||||
.keystrokes()
|
.keystrokes()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|key| Kbd::format(key)),
|
.map(|key| Kbd::format(key)),
|
||||||
|
|
@ -746,7 +746,7 @@ impl PopupMenu {
|
||||||
} => {
|
} => {
|
||||||
let show_link_icon = *is_link && self.external_link_icon;
|
let show_link_icon = *is_link && self.external_link_icon;
|
||||||
let action = action.as_ref().map(|action| action.boxed_clone());
|
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.when(!disabled, |this| {
|
||||||
this.on_click(
|
this.on_click(
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ impl Slider {
|
||||||
.border_color(cx.theme().slider_bar.opacity(0.9))
|
.border_color(cx.theme().slider_bar.opacity(0.9))
|
||||||
.when(cx.theme().shadow, |this| this.shadow_md())
|
.when(cx.theme().shadow, |this| this.shadow_md())
|
||||||
.bg(cx.theme().slider_thumb)
|
.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(
|
fn on_mouse_down(
|
||||||
|
|
|
||||||
|
|
@ -3,42 +3,60 @@ use gpui::{
|
||||||
ParentElement, Render, Styled, Window,
|
ParentElement, Render, Styled, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{text::Text, ActiveTheme};
|
use crate::{h_flex, text::Text, ActiveTheme, Kbd};
|
||||||
|
|
||||||
|
enum TooltipContext {
|
||||||
|
Text(Text),
|
||||||
|
Element(Box<dyn Fn(&mut Window, &mut App) -> AnyElement>),
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Tooltip {
|
pub struct Tooltip {
|
||||||
text: Text,
|
content: TooltipContext,
|
||||||
element_builder: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
|
key_binding: Option<Kbd>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tooltip {
|
impl Tooltip {
|
||||||
pub fn new(text: impl Into<Text>, _: &mut Window, cx: &mut App) -> AnyView {
|
/// Create a Tooltip with a text content.
|
||||||
cx.new(|_| Self {
|
pub fn new(text: impl Into<Text>) -> Self {
|
||||||
text: text.into(),
|
Self {
|
||||||
element_builder: None,
|
content: TooltipContext::Text(text.into()),
|
||||||
})
|
key_binding: None,
|
||||||
.into()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_element<E, F>(_: &mut Window, cx: &mut App, builder: F) -> AnyView
|
/// Create a Tooltip with a custom element.
|
||||||
|
pub fn element<E, F>(builder: F) -> Self
|
||||||
where
|
where
|
||||||
E: IntoElement,
|
E: IntoElement,
|
||||||
F: Fn(&mut Window, &mut App) -> E + 'static,
|
F: Fn(&mut Window, &mut App) -> E + 'static,
|
||||||
{
|
{
|
||||||
cx.new(|_| Self {
|
Self {
|
||||||
text: "".into(),
|
key_binding: None,
|
||||||
element_builder: Some(Box::new(move |window, cx| {
|
content: TooltipContext::Element(Box::new(move |window, cx| {
|
||||||
builder(window, cx).into_any_element()
|
builder(window, cx).into_any_element()
|
||||||
})),
|
})),
|
||||||
})
|
}
|
||||||
.into()
|
}
|
||||||
|
|
||||||
|
/// Set KeyBinding information for the tooltip.
|
||||||
|
pub(crate) fn key_binding(mut self, kbd: Option<impl Into<Kbd>>) -> 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 {
|
impl Render for Tooltip {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
div().child(
|
div().child(
|
||||||
// Wrap in a child, to ensure the left margin is applied to the tooltip
|
// Wrap in a child, to ensure the left margin is applied to the tooltip
|
||||||
div()
|
h_flex()
|
||||||
.font_family(".SystemUIFont")
|
.font_family(".SystemUIFont")
|
||||||
.m_3()
|
.m_3()
|
||||||
.bg(cx.theme().popover)
|
.bg(cx.theme().popover)
|
||||||
|
|
@ -48,15 +66,25 @@ impl Render for Tooltip {
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.shadow_md()
|
.shadow_md()
|
||||||
.rounded(px(6.))
|
.rounded(px(6.))
|
||||||
|
.justify_between()
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.px_2()
|
.px_2()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
|
.gap_3()
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if let Some(builder) = &self.element_builder {
|
this.child(div().map(|this| match self.content {
|
||||||
this.child(builder(window, cx))
|
TooltipContext::Text(ref text) => this.child(text.clone()),
|
||||||
} else {
|
TooltipContext::Element(ref builder) => this.child(builder(window, cx)),
|
||||||
this.child(self.text.clone())
|
}))
|
||||||
}
|
})
|
||||||
|
.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)),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue