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