1use crate::KeyBinding;
2use crate::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 const 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();
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
216 let bg_color = self
217 .background_color
218 .blend(colors.text_accent.alpha(if is_light { 0.05 } else { 0.1 }));
219
220 let shadow_color = colors.text.alpha(if is_light { 0.04 } else { 0.08 });
221
222 let size = self
223 .size
224 .unwrap_or(TextSize::Small.rems(cx).to_pixels(window.rem_size()));
225
226 let kb_size = size - px(2.0);
227
228 let mut base = h_flex();
229
230 base.text_style()
231 .get_or_insert_with(Default::default)
232 .font_style = Some(FontStyle::Italic);
233
234 base.gap_1()
235 .font_buffer(cx)
236 .text_size(size)
237 .text_color(colors.text_disabled)
238 .children(self.prefix)
239 .child(
240 h_flex()
241 .rounded_sm()
242 .px_0p5()
243 .mr_0p5()
244 .border_1()
245 .border_color(border_color)
246 .bg(bg_color)
247 .shadow(vec![BoxShadow {
248 color: shadow_color,
249 offset: point(px(0.), px(1.)),
250 blur_radius: px(0.),
251 spread_radius: px(0.),
252 }])
253 .child(self.keybinding.size(rems_from_px(kb_size))),
254 )
255 .children(self.suffix)
256 }
257}
258
259impl Component for KeybindingHint {
260 fn scope() -> ComponentScope {
261 ComponentScope::DataDisplay
262 }
263
264 fn description() -> Option<&'static str> {
265 Some("Displays a keyboard shortcut hint with optional prefix and suffix text")
266 }
267
268 fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
269 let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
270 let enter = KeyBinding::for_action(&menu::Confirm, window, cx)
271 .unwrap_or(KeyBinding::new_from_gpui(enter_fallback, cx));
272
273 let bg_color = cx.theme().colors().surface_background;
274
275 Some(
276 v_flex()
277 .gap_6()
278 .children(vec![
279 example_group_with_title(
280 "Basic",
281 vec![
282 single_example(
283 "With Prefix",
284 KeybindingHint::with_prefix(
285 "Go to Start:",
286 enter.clone(),
287 bg_color,
288 )
289 .into_any_element(),
290 ),
291 single_example(
292 "With Suffix",
293 KeybindingHint::with_suffix(enter.clone(), "Go to End", bg_color)
294 .into_any_element(),
295 ),
296 single_example(
297 "With Prefix and Suffix",
298 KeybindingHint::new(enter.clone(), bg_color)
299 .prefix("Confirm:")
300 .suffix("Execute selected action")
301 .into_any_element(),
302 ),
303 ],
304 ),
305 example_group_with_title(
306 "Sizes",
307 vec![
308 single_example(
309 "Small",
310 KeybindingHint::new(enter.clone(), bg_color)
311 .size(Pixels::from(12.0))
312 .prefix("Small:")
313 .into_any_element(),
314 ),
315 single_example(
316 "Medium",
317 KeybindingHint::new(enter.clone(), bg_color)
318 .size(Pixels::from(16.0))
319 .suffix("Medium")
320 .into_any_element(),
321 ),
322 single_example(
323 "Large",
324 KeybindingHint::new(enter, bg_color)
325 .size(Pixels::from(20.0))
326 .prefix("Large:")
327 .suffix("Size")
328 .into_any_element(),
329 ),
330 ],
331 ),
332 ])
333 .into_any_element(),
334 )
335 }
336}