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