Introduce KeybindingHint (#24397)
- Implements scaling for `ui::Keybinding` and it's component parts - Adds the `ui::KeybindingHint` component for creating keybinding hints easily:  Release Notes: - N/A
This commit is contained in:
parent
9c132fece5
commit
00971fbe41
9 changed files with 390 additions and 16 deletions
|
@ -11,6 +11,7 @@ mod image;
|
|||
mod indent_guides;
|
||||
mod indicator;
|
||||
mod keybinding;
|
||||
mod keybinding_hint;
|
||||
mod label;
|
||||
mod list;
|
||||
mod modal;
|
||||
|
@ -47,6 +48,7 @@ pub use image::*;
|
|||
pub use indent_guides::*;
|
||||
pub use indicator::*;
|
||||
pub use keybinding::*;
|
||||
pub use keybinding_hint::*;
|
||||
pub use label::*;
|
||||
pub use list::*;
|
||||
pub use modal::*;
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
use gpui::{AnyView, DefiniteLength};
|
||||
|
||||
use crate::{
|
||||
prelude::*, Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding, TintColor,
|
||||
prelude::*, Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding,
|
||||
KeybindingPosition, TintColor,
|
||||
};
|
||||
use crate::{
|
||||
ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label, LineHeightStyle,
|
||||
|
@ -92,6 +93,7 @@ pub struct Button {
|
|||
selected_icon: Option<IconName>,
|
||||
selected_icon_color: Option<Color>,
|
||||
key_binding: Option<KeyBinding>,
|
||||
keybinding_position: KeybindingPosition,
|
||||
alpha: Option<f32>,
|
||||
}
|
||||
|
||||
|
@ -117,6 +119,7 @@ impl Button {
|
|||
selected_icon: None,
|
||||
selected_icon_color: None,
|
||||
key_binding: None,
|
||||
keybinding_position: KeybindingPosition::default(),
|
||||
alpha: None,
|
||||
}
|
||||
}
|
||||
|
@ -187,6 +190,15 @@ impl Button {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the position of the keybinding relative to the button label.
|
||||
///
|
||||
/// This method allows you to specify where the keybinding should be displayed
|
||||
/// in relation to the button's label.
|
||||
pub fn key_binding_position(mut self, position: KeybindingPosition) -> Self {
|
||||
self.keybinding_position = position;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the alpha property of the color of label.
|
||||
pub fn alpha(mut self, alpha: f32) -> Self {
|
||||
self.alpha = Some(alpha);
|
||||
|
@ -412,6 +424,10 @@ impl RenderOnce for Button {
|
|||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.when(
|
||||
self.keybinding_position == KeybindingPosition::Start,
|
||||
|this| this.flex_row_reverse(),
|
||||
)
|
||||
.gap(DynamicSpacing::Base06.rems(cx))
|
||||
.justify_between()
|
||||
.child(
|
||||
|
|
|
@ -45,6 +45,13 @@ pub enum IconPosition {
|
|||
End,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum KeybindingPosition {
|
||||
Start,
|
||||
#[default]
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
||||
pub enum TintColor {
|
||||
#[default]
|
||||
|
|
|
@ -70,6 +70,7 @@ pub enum IconSize {
|
|||
Medium,
|
||||
/// 48px
|
||||
XLarge,
|
||||
Custom(Pixels),
|
||||
}
|
||||
|
||||
impl IconSize {
|
||||
|
@ -80,6 +81,7 @@ impl IconSize {
|
|||
IconSize::Small => rems_from_px(14.),
|
||||
IconSize::Medium => rems_from_px(16.),
|
||||
IconSize::XLarge => rems_from_px(48.),
|
||||
IconSize::Custom(size) => rems_from_px(size.into()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,6 +98,8 @@ impl IconSize {
|
|||
IconSize::Small => DynamicSpacing::Base02.px(cx),
|
||||
IconSize::Medium => DynamicSpacing::Base02.px(cx),
|
||||
IconSize::XLarge => DynamicSpacing::Base02.px(cx),
|
||||
// TODO: Wire into dynamic spacing
|
||||
IconSize::Custom(size) => px(size.into()),
|
||||
};
|
||||
|
||||
(icon_size, padding)
|
||||
|
|
|
@ -15,6 +15,7 @@ pub struct KeyBinding {
|
|||
|
||||
/// The [`PlatformStyle`] to use when displaying this keybinding.
|
||||
platform_style: PlatformStyle,
|
||||
size: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl KeyBinding {
|
||||
|
@ -47,6 +48,7 @@ impl KeyBinding {
|
|||
Self {
|
||||
key_binding,
|
||||
platform_style: PlatformStyle::platform(),
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,6 +57,12 @@ impl KeyBinding {
|
|||
self.platform_style = platform_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the size for this [`KeyBinding`].
|
||||
pub fn size(mut self, size: Pixels) -> Self {
|
||||
self.size = Some(size);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for KeyBinding {
|
||||
|
@ -83,9 +91,12 @@ impl RenderOnce for KeyBinding {
|
|||
&keystroke.modifiers,
|
||||
self.platform_style,
|
||||
None,
|
||||
self.size,
|
||||
false,
|
||||
))
|
||||
.map(|el| el.child(render_key(&keystroke, self.platform_style, None)))
|
||||
.map(|el| {
|
||||
el.child(render_key(&keystroke, self.platform_style, None, self.size))
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
@ -94,11 +105,14 @@ pub fn render_key(
|
|||
keystroke: &Keystroke,
|
||||
platform_style: PlatformStyle,
|
||||
color: Option<Color>,
|
||||
size: Option<Pixels>,
|
||||
) -> AnyElement {
|
||||
let key_icon = icon_for_key(keystroke, platform_style);
|
||||
match key_icon {
|
||||
Some(icon) => KeyIcon::new(icon, color).into_any_element(),
|
||||
None => Key::new(capitalize(&keystroke.key), color).into_any_element(),
|
||||
Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
|
||||
None => Key::new(capitalize(&keystroke.key), color)
|
||||
.size(size)
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,6 +144,7 @@ pub fn render_modifiers(
|
|||
modifiers: &Modifiers,
|
||||
platform_style: PlatformStyle,
|
||||
color: Option<Color>,
|
||||
size: Option<Pixels>,
|
||||
standalone: bool,
|
||||
) -> impl Iterator<Item = AnyElement> {
|
||||
enum KeyOrIcon {
|
||||
|
@ -200,8 +215,8 @@ pub fn render_modifiers(
|
|||
PlatformStyle::Windows => vec![modifier.windows, KeyOrIcon::Key("+")],
|
||||
})
|
||||
.map(move |key_or_icon| match key_or_icon {
|
||||
KeyOrIcon::Key(key) => Key::new(key, color).into_any_element(),
|
||||
KeyOrIcon::Icon(icon) => KeyIcon::new(icon, color).into_any_element(),
|
||||
KeyOrIcon::Key(key) => Key::new(key, color).size(size).into_any_element(),
|
||||
KeyOrIcon::Icon(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -209,26 +224,26 @@ pub fn render_modifiers(
|
|||
pub struct Key {
|
||||
key: SharedString,
|
||||
color: Option<Color>,
|
||||
size: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl RenderOnce for Key {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let single_char = self.key.len() == 1;
|
||||
let size = self.size.unwrap_or(px(14.));
|
||||
let size_f32: f32 = size.into();
|
||||
|
||||
div()
|
||||
.py_0()
|
||||
.map(|this| {
|
||||
if single_char {
|
||||
this.w(rems_from_px(14.))
|
||||
.flex()
|
||||
.flex_none()
|
||||
.justify_center()
|
||||
this.w(size).flex().flex_none().justify_center()
|
||||
} else {
|
||||
this.px_0p5()
|
||||
}
|
||||
})
|
||||
.h(rems_from_px(14.))
|
||||
.text_ui(cx)
|
||||
.h(rems_from_px(size_f32))
|
||||
.text_size(size)
|
||||
.line_height(relative(1.))
|
||||
.text_color(self.color.unwrap_or(Color::Muted).color(cx))
|
||||
.child(self.key.clone())
|
||||
|
@ -240,27 +255,47 @@ impl Key {
|
|||
Self {
|
||||
key: key.into(),
|
||||
color,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct KeyIcon {
|
||||
icon: IconName,
|
||||
color: Option<Color>,
|
||||
size: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl RenderOnce for KeyIcon {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let size = self
|
||||
.size
|
||||
.unwrap_or(IconSize::Small.rems().to_pixels(window.rem_size()));
|
||||
|
||||
Icon::new(self.icon)
|
||||
.size(IconSize::XSmall)
|
||||
.size(IconSize::Custom(size))
|
||||
.color(self.color.unwrap_or(Color::Muted))
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyIcon {
|
||||
pub fn new(icon: IconName, color: Option<Color>) -> Self {
|
||||
Self { icon, color }
|
||||
Self {
|
||||
icon,
|
||||
color,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
|
307
crates/ui/src/components/keybinding_hint.rs
Normal file
307
crates/ui/src/components/keybinding_hint.rs
Normal file
|
@ -0,0 +1,307 @@
|
|||
use crate::{h_flex, prelude::*};
|
||||
use crate::{ElevationIndex, KeyBinding};
|
||||
use gpui::{point, App, BoxShadow, IntoElement, Window};
|
||||
use smallvec::smallvec;
|
||||
|
||||
/// Represents a hint for a keybinding, optionally with a prefix and suffix.
|
||||
///
|
||||
/// This struct allows for the creation and customization of a keybinding hint,
|
||||
/// which can be used to display keyboard shortcuts or commands in a user interface.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::prelude::*;
|
||||
///
|
||||
/// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+S"))
|
||||
/// .prefix("Save:")
|
||||
/// .size(Pixels::from(14.0));
|
||||
/// ```
|
||||
#[derive(Debug, IntoElement, Clone)]
|
||||
pub struct KeybindingHint {
|
||||
prefix: Option<SharedString>,
|
||||
suffix: Option<SharedString>,
|
||||
keybinding: KeyBinding,
|
||||
size: Option<Pixels>,
|
||||
elevation: Option<ElevationIndex>,
|
||||
}
|
||||
|
||||
impl KeybindingHint {
|
||||
/// Creates a new `KeybindingHint` with the specified keybinding.
|
||||
///
|
||||
/// This method initializes a new `KeybindingHint` instance with the given keybinding,
|
||||
/// setting all other fields to their default values.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::prelude::*;
|
||||
///
|
||||
/// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+C"));
|
||||
/// ```
|
||||
pub fn new(keybinding: KeyBinding) -> Self {
|
||||
Self {
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
keybinding,
|
||||
size: None,
|
||||
elevation: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `KeybindingHint` with a prefix and keybinding.
|
||||
///
|
||||
/// This method initializes a new `KeybindingHint` instance with the given prefix and keybinding,
|
||||
/// setting all other fields to their default values.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::prelude::*;
|
||||
///
|
||||
/// let hint = KeybindingHint::with_prefix("Copy:", KeyBinding::from_str("Ctrl+C"));
|
||||
/// ```
|
||||
pub fn with_prefix(prefix: impl Into<SharedString>, keybinding: KeyBinding) -> Self {
|
||||
Self {
|
||||
prefix: Some(prefix.into()),
|
||||
suffix: None,
|
||||
keybinding,
|
||||
size: None,
|
||||
elevation: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `KeybindingHint` with a keybinding and suffix.
|
||||
///
|
||||
/// This method initializes a new `KeybindingHint` instance with the given keybinding and suffix,
|
||||
/// setting all other fields to their default values.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::prelude::*;
|
||||
///
|
||||
/// let hint = KeybindingHint::with_suffix(KeyBinding::from_str("Ctrl+V"), "Paste");
|
||||
/// ```
|
||||
pub fn with_suffix(keybinding: KeyBinding, suffix: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
prefix: None,
|
||||
suffix: Some(suffix.into()),
|
||||
keybinding,
|
||||
size: None,
|
||||
elevation: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the prefix for the keybinding hint.
|
||||
///
|
||||
/// This method allows adding or changing the prefix text that appears before the keybinding.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::prelude::*;
|
||||
///
|
||||
/// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+X"))
|
||||
/// .prefix("Cut:");
|
||||
/// ```
|
||||
pub fn prefix(mut self, prefix: impl Into<SharedString>) -> Self {
|
||||
self.prefix = Some(prefix.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the suffix for the keybinding hint.
|
||||
///
|
||||
/// This method allows adding or changing the suffix text that appears after the keybinding.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::prelude::*;
|
||||
///
|
||||
/// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+F"))
|
||||
/// .suffix("Find");
|
||||
/// ```
|
||||
pub fn suffix(mut self, suffix: impl Into<SharedString>) -> Self {
|
||||
self.suffix = Some(suffix.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the size of the keybinding hint.
|
||||
///
|
||||
/// This method allows specifying the size of the keybinding hint in pixels.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::prelude::*;
|
||||
///
|
||||
/// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+Z"))
|
||||
/// .size(Pixels::from(16.0));
|
||||
/// ```
|
||||
pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the elevation of the keybinding hint.
|
||||
///
|
||||
/// This method allows specifying the elevation index for the keybinding hint,
|
||||
/// which affects its visual appearance in terms of depth or layering.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::prelude::*;
|
||||
///
|
||||
/// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+A"))
|
||||
/// .elevation(ElevationIndex::new(1));
|
||||
/// ```
|
||||
pub fn elevation(mut self, elevation: impl Into<Option<ElevationIndex>>) -> Self {
|
||||
self.elevation = elevation.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for KeybindingHint {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let colors = cx.theme().colors().clone();
|
||||
|
||||
let size = self
|
||||
.size
|
||||
.unwrap_or(TextSize::Small.rems(cx).to_pixels(window.rem_size()));
|
||||
let kb_size = size - px(2.0);
|
||||
let kb_bg = if let Some(elevation) = self.elevation {
|
||||
elevation.on_elevation_bg(cx)
|
||||
} else {
|
||||
theme::color_alpha(colors.element_background, 0.6)
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_0p5()
|
||||
.font_buffer(cx)
|
||||
.text_size(size)
|
||||
.text_color(colors.text_muted)
|
||||
.children(self.prefix)
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.rounded_md()
|
||||
.px_0p5()
|
||||
.mr_0p5()
|
||||
.border_1()
|
||||
.border_color(kb_bg)
|
||||
.bg(kb_bg.opacity(0.8))
|
||||
.shadow(smallvec![BoxShadow {
|
||||
color: cx.theme().colors().editor_background.opacity(0.8),
|
||||
offset: point(px(0.), px(1.)),
|
||||
blur_radius: px(0.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
.child(self.keybinding.size(kb_size)),
|
||||
)
|
||||
.children(self.suffix)
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentPreview for KeybindingHint {
|
||||
fn description() -> impl Into<Option<&'static str>> {
|
||||
"Used to display hint text for keyboard shortcuts. Can have a prefix and suffix."
|
||||
}
|
||||
|
||||
fn examples(window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
|
||||
let home_fallback = gpui::KeyBinding::new("home", menu::SelectFirst, None);
|
||||
let home = KeyBinding::for_action(&menu::SelectFirst, window)
|
||||
.unwrap_or(KeyBinding::new(home_fallback));
|
||||
|
||||
let end_fallback = gpui::KeyBinding::new("end", menu::SelectLast, None);
|
||||
let end = KeyBinding::for_action(&menu::SelectLast, window)
|
||||
.unwrap_or(KeyBinding::new(end_fallback));
|
||||
|
||||
let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
|
||||
let enter = KeyBinding::for_action(&menu::Confirm, window)
|
||||
.unwrap_or(KeyBinding::new(enter_fallback));
|
||||
|
||||
let escape_fallback = gpui::KeyBinding::new("escape", menu::Cancel, None);
|
||||
let escape = KeyBinding::for_action(&menu::Cancel, window)
|
||||
.unwrap_or(KeyBinding::new(escape_fallback));
|
||||
|
||||
vec![
|
||||
example_group_with_title(
|
||||
"Basic",
|
||||
vec![
|
||||
single_example(
|
||||
"With Prefix",
|
||||
KeybindingHint::with_prefix("Go to Start:", home.clone()),
|
||||
),
|
||||
single_example(
|
||||
"With Suffix",
|
||||
KeybindingHint::with_suffix(end.clone(), "Go to End"),
|
||||
),
|
||||
single_example(
|
||||
"With Prefix and Suffix",
|
||||
KeybindingHint::new(enter.clone())
|
||||
.prefix("Confirm:")
|
||||
.suffix("Execute selected action"),
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group_with_title(
|
||||
"Sizes",
|
||||
vec![
|
||||
single_example(
|
||||
"Small",
|
||||
KeybindingHint::new(home.clone())
|
||||
.size(Pixels::from(12.0))
|
||||
.prefix("Small:"),
|
||||
),
|
||||
single_example(
|
||||
"Medium",
|
||||
KeybindingHint::new(end.clone())
|
||||
.size(Pixels::from(16.0))
|
||||
.suffix("Medium"),
|
||||
),
|
||||
single_example(
|
||||
"Large",
|
||||
KeybindingHint::new(enter.clone())
|
||||
.size(Pixels::from(20.0))
|
||||
.prefix("Large:")
|
||||
.suffix("Size"),
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group_with_title(
|
||||
"Elevations",
|
||||
vec![
|
||||
single_example(
|
||||
"Surface",
|
||||
KeybindingHint::new(home.clone())
|
||||
.elevation(ElevationIndex::Surface)
|
||||
.prefix("Surface:"),
|
||||
),
|
||||
single_example(
|
||||
"Elevated Surface",
|
||||
KeybindingHint::new(end.clone())
|
||||
.elevation(ElevationIndex::ElevatedSurface)
|
||||
.suffix("Elevated"),
|
||||
),
|
||||
single_example(
|
||||
"Editor Surface",
|
||||
KeybindingHint::new(enter.clone())
|
||||
.elevation(ElevationIndex::EditorSurface)
|
||||
.prefix("Editor:")
|
||||
.suffix("Surface"),
|
||||
),
|
||||
single_example(
|
||||
"Modal Surface",
|
||||
KeybindingHint::new(escape.clone())
|
||||
.elevation(ElevationIndex::ModalSurface)
|
||||
.prefix("Modal:")
|
||||
.suffix("Escape"),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue