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 = if self.vim_mode {
96 if keystroke.modifiers.shift && keystroke.key.len() == 1 {
97 keystroke.key.to_ascii_uppercase().to_string()
98 } else {
99 keystroke.key.to_string()
100 }
101 } else {
102 util::capitalize(&keystroke.key)
103 };
104 Key::new(&key, color).size(self.size).into_any_element()
105 }
106 }
107 }
108}
109
110impl RenderOnce for KeyBinding {
111 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
112 h_flex()
113 .debug_selector(|| {
114 format!(
115 "KEY_BINDING-{}",
116 self.key_binding
117 .keystrokes()
118 .iter()
119 .map(|k| k.key.to_string())
120 .collect::<Vec<_>>()
121 .join(" ")
122 )
123 })
124 .gap(DynamicSpacing::Base04.rems(cx))
125 .flex_none()
126 .children(self.key_binding.keystrokes().iter().map(|keystroke| {
127 h_flex()
128 .flex_none()
129 .py_0p5()
130 .rounded_sm()
131 .text_color(cx.theme().colors().text_muted)
132 .children(render_modifiers(
133 &keystroke.modifiers,
134 self.platform_style,
135 None,
136 self.size,
137 true,
138 ))
139 .map(|el| el.child(self.render_key(&keystroke, None)))
140 }))
141 }
142}
143
144fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
145 match keystroke.key.as_str() {
146 "left" => Some(IconName::ArrowLeft),
147 "right" => Some(IconName::ArrowRight),
148 "up" => Some(IconName::ArrowUp),
149 "down" => Some(IconName::ArrowDown),
150 "backspace" => Some(IconName::Backspace),
151 "delete" => Some(IconName::Delete),
152 "return" => Some(IconName::Return),
153 "enter" => Some(IconName::Return),
154 "tab" => Some(IconName::Tab),
155 "space" => Some(IconName::Space),
156 "escape" => Some(IconName::Escape),
157 "pagedown" => Some(IconName::PageDown),
158 "pageup" => Some(IconName::PageUp),
159 "shift" if platform_style == PlatformStyle::Mac => Some(IconName::Shift),
160 "control" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
161 "platform" if platform_style == PlatformStyle::Mac => Some(IconName::Command),
162 "function" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
163 "alt" if platform_style == PlatformStyle::Mac => Some(IconName::Option),
164 _ => None,
165 }
166}
167
168pub fn render_modifiers(
169 modifiers: &Modifiers,
170 platform_style: PlatformStyle,
171 color: Option<Color>,
172 size: Option<AbsoluteLength>,
173 trailing_separator: bool,
174) -> impl Iterator<Item = AnyElement> {
175 #[derive(Clone)]
176 enum KeyOrIcon {
177 Key(&'static str),
178 Plus,
179 Icon(IconName),
180 }
181
182 struct Modifier {
183 enabled: bool,
184 mac: KeyOrIcon,
185 linux: KeyOrIcon,
186 windows: KeyOrIcon,
187 }
188
189 let table = {
190 use KeyOrIcon::*;
191
192 [
193 Modifier {
194 enabled: modifiers.function,
195 mac: Icon(IconName::Control),
196 linux: Key("Fn"),
197 windows: Key("Fn"),
198 },
199 Modifier {
200 enabled: modifiers.control,
201 mac: Icon(IconName::Control),
202 linux: Key("Ctrl"),
203 windows: Key("Ctrl"),
204 },
205 Modifier {
206 enabled: modifiers.alt,
207 mac: Icon(IconName::Option),
208 linux: Key("Alt"),
209 windows: Key("Alt"),
210 },
211 Modifier {
212 enabled: modifiers.platform,
213 mac: Icon(IconName::Command),
214 linux: Key("Super"),
215 windows: Key("Win"),
216 },
217 Modifier {
218 enabled: modifiers.shift,
219 mac: Icon(IconName::Shift),
220 linux: Key("Shift"),
221 windows: Key("Shift"),
222 },
223 ]
224 };
225
226 let filtered = table
227 .into_iter()
228 .filter(|modifier| modifier.enabled)
229 .collect::<Vec<_>>();
230
231 let platform_keys = filtered
232 .into_iter()
233 .map(move |modifier| match platform_style {
234 PlatformStyle::Mac => Some(modifier.mac),
235 PlatformStyle::Linux => Some(modifier.linux),
236 PlatformStyle::Windows => Some(modifier.windows),
237 });
238
239 let separator = match platform_style {
240 PlatformStyle::Mac => None,
241 PlatformStyle::Linux => Some(KeyOrIcon::Plus),
242 PlatformStyle::Windows => Some(KeyOrIcon::Plus),
243 };
244
245 let platform_keys = itertools::intersperse(platform_keys, separator.clone());
246
247 platform_keys
248 .chain(if modifiers.modified() && trailing_separator {
249 Some(separator)
250 } else {
251 None
252 })
253 .flatten()
254 .map(move |key_or_icon| match key_or_icon {
255 KeyOrIcon::Key(key) => Key::new(key, color).size(size).into_any_element(),
256 KeyOrIcon::Icon(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
257 KeyOrIcon::Plus => "+".into_any_element(),
258 })
259}
260
261#[derive(IntoElement)]
262pub struct Key {
263 key: SharedString,
264 color: Option<Color>,
265 size: Option<AbsoluteLength>,
266}
267
268impl RenderOnce for Key {
269 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
270 let single_char = self.key.len() == 1;
271 let size = self
272 .size
273 .unwrap_or_else(|| TextSize::default().rems(cx).into());
274
275 div()
276 .py_0()
277 .map(|this| {
278 if single_char {
279 this.w(size).flex().flex_none().justify_center()
280 } else {
281 this.px_0p5()
282 }
283 })
284 .h(size)
285 .text_size(size)
286 .line_height(relative(1.))
287 .text_color(self.color.unwrap_or(Color::Muted).color(cx))
288 .child(self.key.clone())
289 }
290}
291
292impl Key {
293 pub fn new(key: impl Into<SharedString>, color: Option<Color>) -> Self {
294 Self {
295 key: key.into(),
296 color,
297 size: None,
298 }
299 }
300
301 pub fn size(mut self, size: impl Into<Option<AbsoluteLength>>) -> Self {
302 self.size = size.into();
303 self
304 }
305}
306
307#[derive(IntoElement)]
308pub struct KeyIcon {
309 icon: IconName,
310 color: Option<Color>,
311 size: Option<AbsoluteLength>,
312}
313
314impl RenderOnce for KeyIcon {
315 fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
316 let size = self.size.unwrap_or(IconSize::Small.rems().into());
317
318 Icon::new(self.icon)
319 .size(IconSize::Custom(size.to_rems(window.rem_size())))
320 .color(self.color.unwrap_or(Color::Muted))
321 }
322}
323
324impl KeyIcon {
325 pub fn new(icon: IconName, color: Option<Color>) -> Self {
326 Self {
327 icon,
328 color,
329 size: None,
330 }
331 }
332
333 pub fn size(mut self, size: impl Into<Option<AbsoluteLength>>) -> Self {
334 self.size = size.into();
335 self
336 }
337}
338
339/// Returns a textual representation of the key binding for the given [`Action`].
340pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option<String> {
341 let bindings = window.bindings_for_action(action);
342 let key_binding = bindings.last()?;
343 Some(text_for_keystrokes(key_binding.keystrokes(), cx))
344}
345
346pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
347 let platform_style = PlatformStyle::platform();
348 let vim_enabled = cx.try_global::<VimStyle>().is_some();
349 keystrokes
350 .iter()
351 .map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled))
352 .join(" ")
353}
354
355pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String {
356 let platform_style = PlatformStyle::platform();
357 let vim_enabled = cx.try_global::<VimStyle>().is_some();
358 keystroke_text(keystroke, platform_style, vim_enabled)
359}
360
361/// Returns a textual representation of the given [`Keystroke`].
362fn keystroke_text(
363 keystroke: &Keystroke,
364 platform_style: PlatformStyle,
365 vim_enabled: bool,
366) -> String {
367 let mut text = String::new();
368
369 let delimiter = match platform_style {
370 PlatformStyle::Mac => '-',
371 PlatformStyle::Linux | PlatformStyle::Windows => '+',
372 };
373
374 if keystroke.modifiers.function {
375 match platform_style {
376 PlatformStyle::Mac => text.push_str("Fn"),
377 PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Fn"),
378 }
379
380 text.push(delimiter);
381 }
382
383 if keystroke.modifiers.control {
384 match platform_style {
385 PlatformStyle::Mac => text.push_str("Control"),
386 PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Ctrl"),
387 }
388
389 text.push(delimiter);
390 }
391
392 if keystroke.modifiers.alt {
393 match platform_style {
394 PlatformStyle::Mac => text.push_str("Option"),
395 PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Alt"),
396 }
397
398 text.push(delimiter);
399 }
400
401 if keystroke.modifiers.platform {
402 match platform_style {
403 PlatformStyle::Mac => text.push_str("Command"),
404 PlatformStyle::Linux => text.push_str("Super"),
405 PlatformStyle::Windows => text.push_str("Win"),
406 }
407
408 text.push(delimiter);
409 }
410
411 if keystroke.modifiers.shift {
412 if !(vim_enabled && keystroke.key.len() == 1) {
413 match platform_style {
414 PlatformStyle::Mac | PlatformStyle::Linux | PlatformStyle::Windows => {
415 text.push_str("Shift")
416 }
417 }
418 text.push(delimiter);
419 }
420 }
421
422 let key = match keystroke.key.as_str() {
423 "pageup" => "PageUp",
424 "pagedown" => "PageDown",
425 key if vim_enabled => {
426 if !keystroke.modifiers.shift && key.len() == 1 {
427 key
428 } else {
429 &util::capitalize(key)
430 }
431 }
432 key => &util::capitalize(key),
433 };
434
435 text.push_str(key);
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}