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