Detailed changes
@@ -5613,6 +5613,7 @@ impl Editor {
} else {
Color::Default
}),
+ None,
true,
),
))
@@ -5784,6 +5784,7 @@ fn inline_completion_accept_indicator(
&accept_keystroke.modifiers,
PlatformStyle::platform(),
Some(Color::Default),
+ None,
false,
))
})
@@ -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
}
}
@@ -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"),
+ ),
+ ],
+ ),
+ ]
+ }
+}
@@ -6,7 +6,7 @@ use ui::{
element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus,
Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike,
Checkbox, CheckboxWithLabel, ContentGroup, DecoratedIcon, ElevationIndex, Facepile,
- IconDecoration, Indicator, Switch, Table, TintColor, Tooltip,
+ IconDecoration, Indicator, KeybindingHint, Switch, Table, TintColor, Tooltip,
};
use crate::{Item, Workspace};
@@ -408,6 +408,7 @@ impl ThemePreview {
.child(Facepile::render_component_previews(window, cx))
.child(Icon::render_component_previews(window, cx))
.child(IconDecoration::render_component_previews(window, cx))
+ .child(KeybindingHint::render_component_previews(window, cx))
.child(Indicator::render_component_previews(window, cx))
.child(Switch::render_component_previews(window, cx))
.child(Table::render_component_previews(window, cx))