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