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