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