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