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