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