1#![allow(missing_docs)]
2use crate::PlatformStyle;
3use crate::{h_flex, prelude::*, Icon, IconName, IconSize};
4use gpui::{
5 relative, Action, AnyElement, App, FocusHandle, IntoElement, Keystroke, Modifiers, Window,
6};
7
8#[derive(Debug, IntoElement, Clone)]
9pub struct KeyBinding {
10 /// A keybinding consists of a key and a set of modifier keys.
11 /// More then one keybinding produces a chord.
12 ///
13 /// This should always contain at least one element.
14 key_binding: gpui::KeyBinding,
15
16 /// The [`PlatformStyle`] to use when displaying this keybinding.
17 platform_style: PlatformStyle,
18 size: Option<Pixels>,
19}
20
21impl KeyBinding {
22 /// Returns the highest precedence keybinding for an action. This is the last binding added to
23 /// the keymap. User bindings are added after built-in bindings so that they take precedence.
24 pub fn for_action(action: &dyn Action, window: &mut Window) -> Option<Self> {
25 let key_binding = window
26 .bindings_for_action(action)
27 .into_iter()
28 .rev()
29 .next()?;
30 Some(Self::new(key_binding))
31 }
32
33 /// Like `for_action`, but lets you specify the context from which keybindings are matched.
34 pub fn for_action_in(
35 action: &dyn Action,
36 focus: &FocusHandle,
37 window: &mut Window,
38 ) -> Option<Self> {
39 let key_binding = window
40 .bindings_for_action_in(action, focus)
41 .into_iter()
42 .rev()
43 .next()?;
44 Some(Self::new(key_binding))
45 }
46
47 pub fn new(key_binding: gpui::KeyBinding) -> Self {
48 Self {
49 key_binding,
50 platform_style: PlatformStyle::platform(),
51 size: None,
52 }
53 }
54
55 /// Sets the [`PlatformStyle`] for this [`KeyBinding`].
56 pub fn platform_style(mut self, platform_style: PlatformStyle) -> Self {
57 self.platform_style = platform_style;
58 self
59 }
60
61 /// Sets the size for this [`KeyBinding`].
62 pub fn size(mut self, size: Pixels) -> Self {
63 self.size = Some(size);
64 self
65 }
66}
67
68impl RenderOnce for KeyBinding {
69 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
70 h_flex()
71 .debug_selector(|| {
72 format!(
73 "KEY_BINDING-{}",
74 self.key_binding
75 .keystrokes()
76 .iter()
77 .map(|k| k.key.to_string())
78 .collect::<Vec<_>>()
79 .join(" ")
80 )
81 })
82 .gap(DynamicSpacing::Base04.rems(cx))
83 .flex_none()
84 .children(self.key_binding.keystrokes().iter().map(|keystroke| {
85 h_flex()
86 .flex_none()
87 .py_0p5()
88 .rounded_sm()
89 .text_color(cx.theme().colors().text_muted)
90 .children(render_modifiers(
91 &keystroke.modifiers,
92 self.platform_style,
93 None,
94 self.size,
95 false,
96 ))
97 .map(|el| {
98 el.child(render_key(&keystroke, self.platform_style, None, self.size))
99 })
100 }))
101 }
102}
103
104pub fn render_key(
105 keystroke: &Keystroke,
106 platform_style: PlatformStyle,
107 color: Option<Color>,
108 size: Option<Pixels>,
109) -> AnyElement {
110 let key_icon = icon_for_key(keystroke, platform_style);
111 match key_icon {
112 Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
113 None => Key::new(capitalize(&keystroke.key), color)
114 .size(size)
115 .into_any_element(),
116 }
117}
118
119fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
120 match keystroke.key.as_str() {
121 "left" => Some(IconName::ArrowLeft),
122 "right" => Some(IconName::ArrowRight),
123 "up" => Some(IconName::ArrowUp),
124 "down" => Some(IconName::ArrowDown),
125 "backspace" => Some(IconName::Backspace),
126 "delete" => Some(IconName::Delete),
127 "return" => Some(IconName::Return),
128 "enter" => Some(IconName::Return),
129 "tab" => Some(IconName::Tab),
130 "space" => Some(IconName::Space),
131 "escape" => Some(IconName::Escape),
132 "pagedown" => Some(IconName::PageDown),
133 "pageup" => Some(IconName::PageUp),
134 "shift" if platform_style == PlatformStyle::Mac => Some(IconName::Shift),
135 "control" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
136 "platform" if platform_style == PlatformStyle::Mac => Some(IconName::Command),
137 "function" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
138 "alt" if platform_style == PlatformStyle::Mac => Some(IconName::Option),
139 _ => None,
140 }
141}
142
143pub fn render_modifiers(
144 modifiers: &Modifiers,
145 platform_style: PlatformStyle,
146 color: Option<Color>,
147 size: Option<Pixels>,
148 standalone: bool,
149) -> impl Iterator<Item = AnyElement> {
150 enum KeyOrIcon {
151 Key(&'static str),
152 Icon(IconName),
153 }
154
155 struct Modifier {
156 enabled: bool,
157 mac: KeyOrIcon,
158 linux: KeyOrIcon,
159 windows: KeyOrIcon,
160 }
161
162 let table = {
163 use KeyOrIcon::*;
164
165 [
166 Modifier {
167 enabled: modifiers.function,
168 mac: Icon(IconName::Control),
169 linux: Key("Fn"),
170 windows: Key("Fn"),
171 },
172 Modifier {
173 enabled: modifiers.control,
174 mac: Icon(IconName::Control),
175 linux: Key("Ctrl"),
176 windows: Key("Ctrl"),
177 },
178 Modifier {
179 enabled: modifiers.alt,
180 mac: Icon(IconName::Option),
181 linux: Key("Alt"),
182 windows: Key("Alt"),
183 },
184 Modifier {
185 enabled: modifiers.platform,
186 mac: Icon(IconName::Command),
187 linux: Key("Super"),
188 windows: Key("Win"),
189 },
190 Modifier {
191 enabled: modifiers.shift,
192 mac: Icon(IconName::Shift),
193 linux: Key("Shift"),
194 windows: Key("Shift"),
195 },
196 ]
197 };
198
199 let filtered = table
200 .into_iter()
201 .filter(|modifier| modifier.enabled)
202 .collect::<Vec<_>>();
203 let last_ix = filtered.len().saturating_sub(1);
204
205 filtered
206 .into_iter()
207 .enumerate()
208 .flat_map(move |(ix, modifier)| match platform_style {
209 PlatformStyle::Mac => vec![modifier.mac],
210 PlatformStyle::Linux if standalone && ix == last_ix => vec![modifier.linux],
211 PlatformStyle::Linux => vec![modifier.linux, KeyOrIcon::Key("+")],
212 PlatformStyle::Windows if standalone && ix == last_ix => {
213 vec![modifier.windows]
214 }
215 PlatformStyle::Windows => vec![modifier.windows, KeyOrIcon::Key("+")],
216 })
217 .map(move |key_or_icon| match key_or_icon {
218 KeyOrIcon::Key(key) => Key::new(key, color).size(size).into_any_element(),
219 KeyOrIcon::Icon(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
220 })
221}
222
223#[derive(IntoElement)]
224pub struct Key {
225 key: SharedString,
226 color: Option<Color>,
227 size: Option<Pixels>,
228}
229
230impl RenderOnce for Key {
231 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
232 let single_char = self.key.len() == 1;
233 let size = self.size.unwrap_or(px(14.));
234 let size_f32: f32 = size.into();
235
236 div()
237 .py_0()
238 .map(|this| {
239 if single_char {
240 this.w(size).flex().flex_none().justify_center()
241 } else {
242 this.px_0p5()
243 }
244 })
245 .h(rems_from_px(size_f32))
246 .text_size(size)
247 .line_height(relative(1.))
248 .text_color(self.color.unwrap_or(Color::Muted).color(cx))
249 .child(self.key.clone())
250 }
251}
252
253impl Key {
254 pub fn new(key: impl Into<SharedString>, color: Option<Color>) -> Self {
255 Self {
256 key: key.into(),
257 color,
258 size: None,
259 }
260 }
261
262 pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
263 self.size = size.into();
264 self
265 }
266}
267
268#[derive(IntoElement)]
269pub struct KeyIcon {
270 icon: IconName,
271 color: Option<Color>,
272 size: Option<Pixels>,
273}
274
275impl RenderOnce for KeyIcon {
276 fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
277 let size = self
278 .size
279 .unwrap_or(IconSize::Small.rems().to_pixels(window.rem_size()));
280
281 Icon::new(self.icon)
282 .size(IconSize::Custom(size))
283 .color(self.color.unwrap_or(Color::Muted))
284 }
285}
286
287impl KeyIcon {
288 pub fn new(icon: IconName, color: Option<Color>) -> Self {
289 Self {
290 icon,
291 color,
292 size: None,
293 }
294 }
295
296 pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
297 self.size = size.into();
298 self
299 }
300}
301
302/// Returns a textual representation of the key binding for the given [`Action`].
303pub fn text_for_action(action: &dyn Action, window: &Window) -> Option<String> {
304 let bindings = window.bindings_for_action(action);
305 let key_binding = bindings.last()?;
306 Some(text_for_key_binding(key_binding, PlatformStyle::platform()))
307}
308
309/// Returns a textual representation of the key binding for the given [`Action`]
310/// as if the provided [`FocusHandle`] was focused.
311pub fn text_for_action_in(
312 action: &dyn Action,
313 focus: &FocusHandle,
314 window: &mut Window,
315) -> Option<String> {
316 let bindings = window.bindings_for_action_in(action, focus);
317 let key_binding = bindings.last()?;
318 Some(text_for_key_binding(key_binding, PlatformStyle::platform()))
319}
320
321/// Returns a textual representation of the given key binding for the specified platform.
322pub fn text_for_key_binding(
323 key_binding: &gpui::KeyBinding,
324 platform_style: PlatformStyle,
325) -> String {
326 key_binding
327 .keystrokes()
328 .iter()
329 .map(|keystroke| text_for_keystroke(keystroke, platform_style))
330 .collect::<Vec<_>>()
331 .join(" ")
332}
333
334/// Returns a textual representation of the given [`Keystroke`].
335pub fn text_for_keystroke(keystroke: &Keystroke, platform_style: PlatformStyle) -> String {
336 let mut text = String::new();
337
338 let delimiter = match platform_style {
339 PlatformStyle::Mac => '-',
340 PlatformStyle::Linux | PlatformStyle::Windows => '+',
341 };
342
343 if keystroke.modifiers.function {
344 match platform_style {
345 PlatformStyle::Mac => text.push_str("fn"),
346 PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Fn"),
347 }
348
349 text.push(delimiter);
350 }
351
352 if keystroke.modifiers.control {
353 match platform_style {
354 PlatformStyle::Mac => text.push_str("Control"),
355 PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Ctrl"),
356 }
357
358 text.push(delimiter);
359 }
360
361 if keystroke.modifiers.alt {
362 match platform_style {
363 PlatformStyle::Mac => text.push_str("Option"),
364 PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Alt"),
365 }
366
367 text.push(delimiter);
368 }
369
370 if keystroke.modifiers.platform {
371 match platform_style {
372 PlatformStyle::Mac => text.push_str("Command"),
373 PlatformStyle::Linux => text.push_str("Super"),
374 PlatformStyle::Windows => text.push_str("Win"),
375 }
376
377 text.push(delimiter);
378 }
379
380 if keystroke.modifiers.shift {
381 match platform_style {
382 PlatformStyle::Mac | PlatformStyle::Linux | PlatformStyle::Windows => {
383 text.push_str("Shift")
384 }
385 }
386
387 text.push(delimiter);
388 }
389
390 let key = match keystroke.key.as_str() {
391 "pageup" => "PageUp",
392 "pagedown" => "PageDown",
393 key => &capitalize(key),
394 };
395
396 text.push_str(key);
397
398 text
399}
400
401fn capitalize(str: &str) -> String {
402 let mut chars = str.chars();
403 match chars.next() {
404 None => String::new(),
405 Some(first_char) => first_char.to_uppercase().collect::<String>() + chars.as_str(),
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_text_for_keystroke() {
415 assert_eq!(
416 text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Mac),
417 "Command-C".to_string()
418 );
419 assert_eq!(
420 text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Linux),
421 "Super+C".to_string()
422 );
423 assert_eq!(
424 text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Windows),
425 "Win+C".to_string()
426 );
427
428 assert_eq!(
429 text_for_keystroke(
430 &Keystroke::parse("ctrl-alt-delete").unwrap(),
431 PlatformStyle::Mac
432 ),
433 "Control-Option-Delete".to_string()
434 );
435 assert_eq!(
436 text_for_keystroke(
437 &Keystroke::parse("ctrl-alt-delete").unwrap(),
438 PlatformStyle::Linux
439 ),
440 "Ctrl+Alt+Delete".to_string()
441 );
442 assert_eq!(
443 text_for_keystroke(
444 &Keystroke::parse("ctrl-alt-delete").unwrap(),
445 PlatformStyle::Windows
446 ),
447 "Ctrl+Alt+Delete".to_string()
448 );
449
450 assert_eq!(
451 text_for_keystroke(
452 &Keystroke::parse("shift-pageup").unwrap(),
453 PlatformStyle::Mac
454 ),
455 "Shift-PageUp".to_string()
456 );
457 assert_eq!(
458 text_for_keystroke(
459 &Keystroke::parse("shift-pageup").unwrap(),
460 PlatformStyle::Linux
461 ),
462 "Shift+PageUp".to_string()
463 );
464 assert_eq!(
465 text_for_keystroke(
466 &Keystroke::parse("shift-pageup").unwrap(),
467 PlatformStyle::Windows
468 ),
469 "Shift+PageUp".to_string()
470 );
471 }
472}