keybinding_hint.rs

  1use crate::KeyBinding;
  2use crate::{h_flex, prelude::*};
  3use gpui::{AnyElement, App, BoxShadow, FontStyle, Hsla, IntoElement, Window, point};
  4use theme::Appearance;
  5
  6/// Represents a hint for a keybinding, optionally with a prefix and suffix.
  7///
  8/// This struct allows for the creation and customization of a keybinding hint,
  9/// which can be used to display keyboard shortcuts or commands in a user interface.
 10///
 11/// # Examples
 12///
 13/// ```
 14/// use ui::prelude::*;
 15///
 16/// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+S"))
 17///     .prefix("Save:")
 18///     .size(Pixels::from(14.0));
 19/// ```
 20#[derive(Debug, IntoElement, RegisterComponent)]
 21pub struct KeybindingHint {
 22    prefix: Option<SharedString>,
 23    suffix: Option<SharedString>,
 24    keybinding: KeyBinding,
 25    size: Option<Pixels>,
 26    background_color: Hsla,
 27}
 28
 29impl KeybindingHint {
 30    /// Creates a new `KeybindingHint` with the specified keybinding.
 31    ///
 32    /// This method initializes a new `KeybindingHint` instance with the given keybinding,
 33    /// setting all other fields to their default values.
 34    ///
 35    /// # Examples
 36    ///
 37    /// ```
 38    /// use ui::prelude::*;
 39    ///
 40    /// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+C"), Hsla::new(0.0, 0.0, 0.0, 1.0));
 41    /// ```
 42    pub fn new(keybinding: KeyBinding, background_color: Hsla) -> Self {
 43        Self {
 44            prefix: None,
 45            suffix: None,
 46            keybinding,
 47            size: None,
 48            background_color,
 49        }
 50    }
 51
 52    /// Creates a new `KeybindingHint` with a prefix and keybinding.
 53    ///
 54    /// This method initializes a new `KeybindingHint` instance with the given prefix and keybinding,
 55    /// setting all other fields to their default values.
 56    ///
 57    /// # Examples
 58    ///
 59    /// ```
 60    /// use ui::prelude::*;
 61    ///
 62    /// let hint = KeybindingHint::with_prefix("Copy:", KeyBinding::from_str("Ctrl+C"), Hsla::new(0.0, 0.0, 0.0, 1.0));
 63    /// ```
 64    pub fn with_prefix(
 65        prefix: impl Into<SharedString>,
 66        keybinding: KeyBinding,
 67        background_color: Hsla,
 68    ) -> Self {
 69        Self {
 70            prefix: Some(prefix.into()),
 71            suffix: None,
 72            keybinding,
 73            size: None,
 74            background_color,
 75        }
 76    }
 77
 78    /// Creates a new `KeybindingHint` with a keybinding and suffix.
 79    ///
 80    /// This method initializes a new `KeybindingHint` instance with the given keybinding and suffix,
 81    /// setting all other fields to their default values.
 82    ///
 83    /// # Examples
 84    ///
 85    /// ```
 86    /// use ui::prelude::*;
 87    ///
 88    /// let hint = KeybindingHint::with_suffix(KeyBinding::from_str("Ctrl+V"), "Paste", Hsla::new(0.0, 0.0, 0.0, 1.0));
 89    /// ```
 90    pub fn with_suffix(
 91        keybinding: KeyBinding,
 92        suffix: impl Into<SharedString>,
 93        background_color: Hsla,
 94    ) -> Self {
 95        Self {
 96            prefix: None,
 97            suffix: Some(suffix.into()),
 98            keybinding,
 99            size: None,
100            background_color,
101        }
102    }
103
104    /// Sets the prefix for the keybinding hint.
105    ///
106    /// This method allows adding or changing the prefix text that appears before the keybinding.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use ui::prelude::*;
112    ///
113    /// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+X"))
114    ///     .prefix("Cut:");
115    /// ```
116    pub fn prefix(mut self, prefix: impl Into<SharedString>) -> Self {
117        self.prefix = Some(prefix.into());
118        self
119    }
120
121    /// Sets the suffix for the keybinding hint.
122    ///
123    /// This method allows adding or changing the suffix text that appears after the keybinding.
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use ui::prelude::*;
129    ///
130    /// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+F"))
131    ///     .suffix("Find");
132    /// ```
133    pub fn suffix(mut self, suffix: impl Into<SharedString>) -> Self {
134        self.suffix = Some(suffix.into());
135        self
136    }
137
138    /// Sets the size of the keybinding hint.
139    ///
140    /// This method allows specifying the size of the keybinding hint in pixels.
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use ui::prelude::*;
146    ///
147    /// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+Z"))
148    ///     .size(Pixels::from(16.0));
149    /// ```
150    pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
151        self.size = size.into();
152        self
153    }
154}
155
156impl RenderOnce for KeybindingHint {
157    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
158        let colors = cx.theme().colors().clone();
159        let is_light = cx.theme().appearance() == Appearance::Light;
160
161        let border_color =
162            self.background_color
163                .blend(colors.text.alpha(if is_light { 0.08 } else { 0.16 }));
164        let bg_color =
165            self.background_color
166                .blend(colors.text.alpha(if is_light { 0.06 } else { 0.12 }));
167        let shadow_color = colors.text.alpha(if is_light { 0.04 } else { 0.08 });
168
169        let size = self
170            .size
171            .unwrap_or(TextSize::Small.rems(cx).to_pixels(window.rem_size()));
172        let kb_size = size - px(2.0);
173
174        let mut base = h_flex();
175
176        base.text_style()
177            .get_or_insert_with(Default::default)
178            .font_style = Some(FontStyle::Italic);
179
180        base.items_center()
181            .gap_0p5()
182            .font_buffer(cx)
183            .text_size(size)
184            .text_color(colors.text_disabled)
185            .children(self.prefix)
186            .child(
187                h_flex()
188                    .items_center()
189                    .rounded_sm()
190                    .px_0p5()
191                    .mr_0p5()
192                    .border_1()
193                    .border_color(border_color)
194                    .bg(bg_color)
195                    .shadow(vec![BoxShadow {
196                        color: shadow_color,
197                        offset: point(px(0.), px(1.)),
198                        blur_radius: px(0.),
199                        spread_radius: px(0.),
200                    }])
201                    .child(self.keybinding.size(rems_from_px(kb_size.0))),
202            )
203            .children(self.suffix)
204    }
205}
206
207impl Component for KeybindingHint {
208    fn scope() -> ComponentScope {
209        ComponentScope::DataDisplay
210    }
211
212    fn description() -> Option<&'static str> {
213        Some("Displays a keyboard shortcut hint with optional prefix and suffix text")
214    }
215
216    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
217        let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
218        let enter = KeyBinding::for_action(&menu::Confirm, window, cx)
219            .unwrap_or(KeyBinding::new_from_gpui(enter_fallback, cx));
220
221        let bg_color = cx.theme().colors().surface_background;
222
223        Some(
224            v_flex()
225                .gap_6()
226                .children(vec![
227                    example_group_with_title(
228                        "Basic",
229                        vec![
230                            single_example(
231                                "With Prefix",
232                                KeybindingHint::with_prefix(
233                                    "Go to Start:",
234                                    enter.clone(),
235                                    bg_color,
236                                )
237                                .into_any_element(),
238                            ),
239                            single_example(
240                                "With Suffix",
241                                KeybindingHint::with_suffix(enter.clone(), "Go to End", bg_color)
242                                    .into_any_element(),
243                            ),
244                            single_example(
245                                "With Prefix and Suffix",
246                                KeybindingHint::new(enter.clone(), bg_color)
247                                    .prefix("Confirm:")
248                                    .suffix("Execute selected action")
249                                    .into_any_element(),
250                            ),
251                        ],
252                    ),
253                    example_group_with_title(
254                        "Sizes",
255                        vec![
256                            single_example(
257                                "Small",
258                                KeybindingHint::new(enter.clone(), bg_color)
259                                    .size(Pixels::from(12.0))
260                                    .prefix("Small:")
261                                    .into_any_element(),
262                            ),
263                            single_example(
264                                "Medium",
265                                KeybindingHint::new(enter.clone(), bg_color)
266                                    .size(Pixels::from(16.0))
267                                    .suffix("Medium")
268                                    .into_any_element(),
269                            ),
270                            single_example(
271                                "Large",
272                                KeybindingHint::new(enter, bg_color)
273                                    .size(Pixels::from(20.0))
274                                    .prefix("Large:")
275                                    .suffix("Size")
276                                    .into_any_element(),
277                            ),
278                        ],
279                    ),
280                ])
281                .into_any_element(),
282        )
283    }
284}