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
234 .size
235 .unwrap_or_else(|| TextSize::default().rems(cx).into());
236
237 div()
238 .py_0()
239 .map(|this| {
240 if single_char {
241 this.w(size).flex().flex_none().justify_center()
242 } else {
243 this.px_0p5()
244 }
245 })
246 .h(size)
247 .text_size(size)
248 .line_height(relative(1.))
249 .text_color(self.color.unwrap_or(Color::Muted).color(cx))
250 .child(self.key.clone())
251 }
252}
253
254impl Key {
255 pub fn new(key: impl Into<SharedString>, color: Option<Color>) -> Self {
256 Self {
257 key: key.into(),
258 color,
259 size: None,
260 }
261 }
262
263 pub fn size(mut self, size: impl Into<Option<AbsoluteLength>>) -> Self {
264 self.size = size.into();
265 self
266 }
267}
268
269#[derive(IntoElement)]
270pub struct KeyIcon {
271 icon: IconName,
272 color: Option<Color>,
273 size: Option<AbsoluteLength>,
274}
275
276impl RenderOnce for KeyIcon {
277 fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
278 let size = self.size.unwrap_or(IconSize::Small.rems().into());
279
280 Icon::new(self.icon)
281 .size(IconSize::Custom(size.to_rems(window.rem_size())))
282 .color(self.color.unwrap_or(Color::Muted))
283 }
284}
285
286impl KeyIcon {
287 pub fn new(icon: IconName, color: Option<Color>) -> Self {
288 Self {
289 icon,
290 color,
291 size: None,
292 }
293 }
294
295 pub fn size(mut self, size: impl Into<Option<AbsoluteLength>>) -> Self {
296 self.size = size.into();
297 self
298 }
299}
300
301/// Returns a textual representation of the key binding for the given [`Action`].
302pub fn text_for_action(action: &dyn Action, window: &Window) -> Option<String> {
303 let bindings = window.bindings_for_action(action);
304 let key_binding = bindings.last()?;
305 Some(text_for_key_binding(key_binding, PlatformStyle::platform()))
306}
307
308/// Returns a textual representation of the key binding for the given [`Action`]
309/// as if the provided [`FocusHandle`] was focused.
310pub fn text_for_action_in(
311 action: &dyn Action,
312 focus: &FocusHandle,
313 window: &mut Window,
314) -> Option<String> {
315 let bindings = window.bindings_for_action_in(action, focus);
316 let key_binding = bindings.last()?;
317 Some(text_for_key_binding(key_binding, PlatformStyle::platform()))
318}
319
320/// Returns a textual representation of the given key binding for the specified platform.
321pub fn text_for_key_binding(
322 key_binding: &gpui::KeyBinding,
323 platform_style: PlatformStyle,
324) -> String {
325 key_binding
326 .keystrokes()
327 .iter()
328 .map(|keystroke| text_for_keystroke(keystroke, platform_style))
329 .collect::<Vec<_>>()
330 .join(" ")
331}
332
333/// Returns a textual representation of the given [`Keystroke`].
334pub fn text_for_keystroke(keystroke: &Keystroke, platform_style: PlatformStyle) -> String {
335 let mut text = String::new();
336
337 let delimiter = match platform_style {
338 PlatformStyle::Mac => '-',
339 PlatformStyle::Linux | PlatformStyle::Windows => '+',
340 };
341
342 if keystroke.modifiers.function {
343 match platform_style {
344 PlatformStyle::Mac => text.push_str("fn"),
345 PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Fn"),
346 }
347
348 text.push(delimiter);
349 }
350
351 if keystroke.modifiers.control {
352 match platform_style {
353 PlatformStyle::Mac => text.push_str("Control"),
354 PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Ctrl"),
355 }
356
357 text.push(delimiter);
358 }
359
360 if keystroke.modifiers.alt {
361 match platform_style {
362 PlatformStyle::Mac => text.push_str("Option"),
363 PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Alt"),
364 }
365
366 text.push(delimiter);
367 }
368
369 if keystroke.modifiers.platform {
370 match platform_style {
371 PlatformStyle::Mac => text.push_str("Command"),
372 PlatformStyle::Linux => text.push_str("Super"),
373 PlatformStyle::Windows => text.push_str("Win"),
374 }
375
376 text.push(delimiter);
377 }
378
379 if keystroke.modifiers.shift {
380 match platform_style {
381 PlatformStyle::Mac | PlatformStyle::Linux | PlatformStyle::Windows => {
382 text.push_str("Shift")
383 }
384 }
385
386 text.push(delimiter);
387 }
388
389 let key = match keystroke.key.as_str() {
390 "pageup" => "PageUp",
391 "pagedown" => "PageDown",
392 key => &capitalize(key),
393 };
394
395 text.push_str(key);
396
397 text
398}
399
400fn capitalize(str: &str) -> String {
401 let mut chars = str.chars();
402 match chars.next() {
403 None => String::new(),
404 Some(first_char) => first_char.to_uppercase().collect::<String>() + chars.as_str(),
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 #[test]
413 fn test_text_for_keystroke() {
414 assert_eq!(
415 text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Mac),
416 "Command-C".to_string()
417 );
418 assert_eq!(
419 text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Linux),
420 "Super+C".to_string()
421 );
422 assert_eq!(
423 text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Windows),
424 "Win+C".to_string()
425 );
426
427 assert_eq!(
428 text_for_keystroke(
429 &Keystroke::parse("ctrl-alt-delete").unwrap(),
430 PlatformStyle::Mac
431 ),
432 "Control-Option-Delete".to_string()
433 );
434 assert_eq!(
435 text_for_keystroke(
436 &Keystroke::parse("ctrl-alt-delete").unwrap(),
437 PlatformStyle::Linux
438 ),
439 "Ctrl+Alt+Delete".to_string()
440 );
441 assert_eq!(
442 text_for_keystroke(
443 &Keystroke::parse("ctrl-alt-delete").unwrap(),
444 PlatformStyle::Windows
445 ),
446 "Ctrl+Alt+Delete".to_string()
447 );
448
449 assert_eq!(
450 text_for_keystroke(
451 &Keystroke::parse("shift-pageup").unwrap(),
452 PlatformStyle::Mac
453 ),
454 "Shift-PageUp".to_string()
455 );
456 assert_eq!(
457 text_for_keystroke(
458 &Keystroke::parse("shift-pageup").unwrap(),
459 PlatformStyle::Linux
460 ),
461 "Shift+PageUp".to_string()
462 );
463 assert_eq!(
464 text_for_keystroke(
465 &Keystroke::parse("shift-pageup").unwrap(),
466 PlatformStyle::Windows
467 ),
468 "Shift+PageUp".to_string()
469 );
470 }
471}