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 false,
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(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 standalone: bool,
149) -> impl Iterator<Item = AnyElement> {
150 enum KeyOrIcon {
151 Key(&'static str),
152 Icon(IconName),
153 }
154
155 struct Modifier {
156 enabled: bool,
157 mac: KeyOrIcon,
158 linux: KeyOrIcon,
159 windows: KeyOrIcon,
160 }
161
162 let table = {
163 use KeyOrIcon::*;
164
165 [
166 Modifier {
167 enabled: modifiers.function,
168 mac: Icon(IconName::Control),
169 linux: Key("Fn"),
170 windows: Key("Fn"),
171 },
172 Modifier {
173 enabled: modifiers.control,
174 mac: Icon(IconName::Control),
175 linux: Key("Ctrl"),
176 windows: Key("Ctrl"),
177 },
178 Modifier {
179 enabled: modifiers.alt,
180 mac: Icon(IconName::Option),
181 linux: Key("Alt"),
182 windows: Key("Alt"),
183 },
184 Modifier {
185 enabled: modifiers.platform,
186 mac: Icon(IconName::Command),
187 linux: Key("Super"),
188 windows: Key("Win"),
189 },
190 Modifier {
191 enabled: modifiers.shift,
192 mac: Icon(IconName::Shift),
193 linux: Key("Shift"),
194 windows: Key("Shift"),
195 },
196 ]
197 };
198
199 let filtered = table
200 .into_iter()
201 .filter(|modifier| modifier.enabled)
202 .collect::<Vec<_>>();
203 let last_ix = filtered.len().saturating_sub(1);
204
205 filtered
206 .into_iter()
207 .enumerate()
208 .flat_map(move |(ix, modifier)| match platform_style {
209 PlatformStyle::Mac => vec![modifier.mac],
210 PlatformStyle::Linux if standalone && ix == last_ix => vec![modifier.linux],
211 PlatformStyle::Linux => vec![modifier.linux, KeyOrIcon::Key("+")],
212 PlatformStyle::Windows if standalone && ix == last_ix => {
213 vec![modifier.windows]
214 }
215 PlatformStyle::Windows => vec![modifier.windows, KeyOrIcon::Key("+")],
216 })
217 .map(move |key_or_icon| match key_or_icon {
218 KeyOrIcon::Key(key) => Key::new(key, color).size(size).into_any_element(),
219 KeyOrIcon::Icon(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
220 })
221}
222
223#[derive(IntoElement)]
224pub struct Key {
225 key: SharedString,
226 color: Option<Color>,
227 size: Option<AbsoluteLength>,
228}
229
230impl RenderOnce for Key {
231 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
232 let single_char = self.key.len() == 1;
233 let size = self.size.unwrap_or(px(14.).into());
234
235 div()
236 .py_0()
237 .map(|this| {
238 if single_char {
239 this.w(size).flex().flex_none().justify_center()
240 } else {
241 this.px_0p5()
242 }
243 })
244 .h(size)
245 .text_size(size)
246 .line_height(relative(1.))
247 .text_color(self.color.unwrap_or(Color::Muted).color(cx))
248 .child(self.key.clone())
249 }
250}
251
252impl Key {
253 pub fn new(key: impl Into<SharedString>, color: Option<Color>) -> Self {
254 Self {
255 key: key.into(),
256 color,
257 size: None,
258 }
259 }
260
261 pub fn size(mut self, size: impl Into<Option<AbsoluteLength>>) -> Self {
262 self.size = size.into();
263 self
264 }
265}
266
267#[derive(IntoElement)]
268pub struct KeyIcon {
269 icon: IconName,
270 color: Option<Color>,
271 size: Option<AbsoluteLength>,
272}
273
274impl RenderOnce for KeyIcon {
275 fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
276 let size = self.size.unwrap_or(IconSize::Small.rems().into());
277
278 Icon::new(self.icon)
279 .size(IconSize::Custom(size.to_rems(window.rem_size())))
280 .color(self.color.unwrap_or(Color::Muted))
281 }
282}
283
284impl KeyIcon {
285 pub fn new(icon: IconName, color: Option<Color>) -> Self {
286 Self {
287 icon,
288 color,
289 size: None,
290 }
291 }
292
293 pub fn size(mut self, size: impl Into<Option<AbsoluteLength>>) -> Self {
294 self.size = size.into();
295 self
296 }
297}
298
299/// Returns a textual representation of the key binding for the given [`Action`].
300pub fn text_for_action(action: &dyn Action, window: &Window) -> Option<String> {
301 let bindings = window.bindings_for_action(action);
302 let key_binding = bindings.last()?;
303 Some(text_for_key_binding(key_binding, PlatformStyle::platform()))
304}
305
306/// Returns a textual representation of the key binding for the given [`Action`]
307/// as if the provided [`FocusHandle`] was focused.
308pub fn text_for_action_in(
309 action: &dyn Action,
310 focus: &FocusHandle,
311 window: &mut Window,
312) -> Option<String> {
313 let bindings = window.bindings_for_action_in(action, focus);
314 let key_binding = bindings.last()?;
315 Some(text_for_key_binding(key_binding, PlatformStyle::platform()))
316}
317
318/// Returns a textual representation of the given key binding for the specified platform.
319pub fn text_for_key_binding(
320 key_binding: &gpui::KeyBinding,
321 platform_style: PlatformStyle,
322) -> String {
323 key_binding
324 .keystrokes()
325 .iter()
326 .map(|keystroke| text_for_keystroke(keystroke, platform_style))
327 .collect::<Vec<_>>()
328 .join(" ")
329}
330
331/// Returns a textual representation of the given [`Keystroke`].
332pub fn text_for_keystroke(keystroke: &Keystroke, platform_style: PlatformStyle) -> String {
333 let mut text = String::new();
334
335 let delimiter = match platform_style {
336 PlatformStyle::Mac => '-',
337 PlatformStyle::Linux | PlatformStyle::Windows => '+',
338 };
339
340 if keystroke.modifiers.function {
341 match platform_style {
342 PlatformStyle::Mac => text.push_str("fn"),
343 PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Fn"),
344 }
345
346 text.push(delimiter);
347 }
348
349 if keystroke.modifiers.control {
350 match platform_style {
351 PlatformStyle::Mac => text.push_str("Control"),
352 PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Ctrl"),
353 }
354
355 text.push(delimiter);
356 }
357
358 if keystroke.modifiers.alt {
359 match platform_style {
360 PlatformStyle::Mac => text.push_str("Option"),
361 PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Alt"),
362 }
363
364 text.push(delimiter);
365 }
366
367 if keystroke.modifiers.platform {
368 match platform_style {
369 PlatformStyle::Mac => text.push_str("Command"),
370 PlatformStyle::Linux => text.push_str("Super"),
371 PlatformStyle::Windows => text.push_str("Win"),
372 }
373
374 text.push(delimiter);
375 }
376
377 if keystroke.modifiers.shift {
378 match platform_style {
379 PlatformStyle::Mac | PlatformStyle::Linux | PlatformStyle::Windows => {
380 text.push_str("Shift")
381 }
382 }
383
384 text.push(delimiter);
385 }
386
387 let key = match keystroke.key.as_str() {
388 "pageup" => "PageUp",
389 "pagedown" => "PageDown",
390 key => &capitalize(key),
391 };
392
393 text.push_str(key);
394
395 text
396}
397
398fn capitalize(str: &str) -> String {
399 let mut chars = str.chars();
400 match chars.next() {
401 None => String::new(),
402 Some(first_char) => first_char.to_uppercase().collect::<String>() + chars.as_str(),
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
411 fn test_text_for_keystroke() {
412 assert_eq!(
413 text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Mac),
414 "Command-C".to_string()
415 );
416 assert_eq!(
417 text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Linux),
418 "Super+C".to_string()
419 );
420 assert_eq!(
421 text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Windows),
422 "Win+C".to_string()
423 );
424
425 assert_eq!(
426 text_for_keystroke(
427 &Keystroke::parse("ctrl-alt-delete").unwrap(),
428 PlatformStyle::Mac
429 ),
430 "Control-Option-Delete".to_string()
431 );
432 assert_eq!(
433 text_for_keystroke(
434 &Keystroke::parse("ctrl-alt-delete").unwrap(),
435 PlatformStyle::Linux
436 ),
437 "Ctrl+Alt+Delete".to_string()
438 );
439 assert_eq!(
440 text_for_keystroke(
441 &Keystroke::parse("ctrl-alt-delete").unwrap(),
442 PlatformStyle::Windows
443 ),
444 "Ctrl+Alt+Delete".to_string()
445 );
446
447 assert_eq!(
448 text_for_keystroke(
449 &Keystroke::parse("shift-pageup").unwrap(),
450 PlatformStyle::Mac
451 ),
452 "Shift-PageUp".to_string()
453 );
454 assert_eq!(
455 text_for_keystroke(
456 &Keystroke::parse("shift-pageup").unwrap(),
457 PlatformStyle::Linux
458 ),
459 "Shift+PageUp".to_string()
460 );
461 assert_eq!(
462 text_for_keystroke(
463 &Keystroke::parse("shift-pageup").unwrap(),
464 PlatformStyle::Windows
465 ),
466 "Shift+PageUp".to_string()
467 );
468 }
469}