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