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