keybinding_hint.rs

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