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