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