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