1use crate::{h_flex, prelude::*, Icon, IconName, IconSize};
2use gpui::{relative, rems, Action, FocusHandle, IntoElement, Keystroke};
3
4/// The way a [`KeyBinding`] should be displayed.
5#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
6pub enum KeyBindingDisplay {
7 /// Display in macOS style.
8 Mac,
9 /// Display in Linux style.
10 Linux,
11 /// Display in Windows style.
12 Windows,
13}
14
15impl KeyBindingDisplay {
16 /// Returns the [`KeyBindingDisplay`] for the current platform.
17 pub const fn platform() -> Self {
18 if cfg!(target_os = "linux") {
19 KeyBindingDisplay::Linux
20 } else if cfg!(target_os = "windows") {
21 KeyBindingDisplay::Windows
22 } else {
23 KeyBindingDisplay::Mac
24 }
25 }
26}
27
28#[derive(IntoElement, Clone)]
29pub struct KeyBinding {
30 /// A keybinding consists of a key and a set of modifier keys.
31 /// More then one keybinding produces a chord.
32 ///
33 /// This should always contain at least one element.
34 key_binding: gpui::KeyBinding,
35
36 /// How keybindings should be displayed.
37 display: KeyBindingDisplay,
38}
39
40impl KeyBinding {
41 pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option<Self> {
42 let key_binding = cx.bindings_for_action(action).last().cloned()?;
43 Some(Self::new(key_binding))
44 }
45
46 // like for_action(), but lets you specify the context from which keybindings
47 // are matched.
48 pub fn for_action_in(
49 action: &dyn Action,
50 focus: &FocusHandle,
51 cx: &mut WindowContext,
52 ) -> Option<Self> {
53 let key_binding = cx.bindings_for_action_in(action, focus).last().cloned()?;
54 Some(Self::new(key_binding))
55 }
56
57 fn icon_for_key(keystroke: &Keystroke) -> Option<IconName> {
58 match keystroke.key.as_str() {
59 "left" => Some(IconName::ArrowLeft),
60 "right" => Some(IconName::ArrowRight),
61 "up" => Some(IconName::ArrowUp),
62 "down" => Some(IconName::ArrowDown),
63 "backspace" => Some(IconName::Backspace),
64 "delete" => Some(IconName::Delete),
65 "return" => Some(IconName::Return),
66 "enter" => Some(IconName::Return),
67 "tab" => Some(IconName::Tab),
68 "space" => Some(IconName::Space),
69 "escape" => Some(IconName::Escape),
70 "pagedown" => Some(IconName::PageDown),
71 "pageup" => Some(IconName::PageUp),
72 _ => None,
73 }
74 }
75
76 pub fn new(key_binding: gpui::KeyBinding) -> Self {
77 Self {
78 key_binding,
79 display: KeyBindingDisplay::platform(),
80 }
81 }
82
83 /// Sets how this [`KeyBinding`] should be displayed.
84 pub fn display(mut self, display: KeyBindingDisplay) -> Self {
85 self.display = display;
86 self
87 }
88}
89
90impl RenderOnce for KeyBinding {
91 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
92 h_flex()
93 .flex_none()
94 .gap_2()
95 .children(self.key_binding.keystrokes().iter().map(|keystroke| {
96 let key_icon = Self::icon_for_key(keystroke);
97
98 h_flex()
99 .flex_none()
100 .gap_0p5()
101 .p_0p5()
102 .rounded_sm()
103 .text_color(cx.theme().colors().text_muted)
104 .when(keystroke.modifiers.function, |el| match self.display {
105 KeyBindingDisplay::Mac => el.child(Key::new("fn")),
106 KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
107 el.child(Key::new("Fn"))
108 }
109 })
110 .when(keystroke.modifiers.control, |el| match self.display {
111 KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Control)),
112 KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
113 el.child(Key::new("Ctrl"))
114 }
115 })
116 .when(keystroke.modifiers.alt, |el| match self.display {
117 KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Option)),
118 KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
119 el.child(Key::new("Alt"))
120 }
121 })
122 .when(keystroke.modifiers.command, |el| match self.display {
123 KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Command)),
124 KeyBindingDisplay::Linux => el.child(Key::new("Super")),
125 KeyBindingDisplay::Windows => el.child(Key::new("Win")),
126 })
127 .when(keystroke.modifiers.shift, |el| match self.display {
128 KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Option)),
129 KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
130 el.child(Key::new("Shift"))
131 }
132 })
133 .map(|el| match key_icon {
134 Some(icon) => el.child(KeyIcon::new(icon)),
135 None => el.child(Key::new(keystroke.key.to_uppercase())),
136 })
137 }))
138 }
139}
140
141#[derive(IntoElement)]
142pub struct Key {
143 key: SharedString,
144}
145
146impl RenderOnce for Key {
147 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
148 let single_char = self.key.len() == 1;
149
150 div()
151 .py_0()
152 .map(|this| {
153 if single_char {
154 this.w(rems(14. / 16.)).flex().flex_none().justify_center()
155 } else {
156 this.px_0p5()
157 }
158 })
159 .h(rems(14. / 16.))
160 .text_ui()
161 .line_height(relative(1.))
162 .text_color(cx.theme().colors().text_muted)
163 .child(self.key.clone())
164 }
165}
166
167impl Key {
168 pub fn new(key: impl Into<SharedString>) -> Self {
169 Self { key: key.into() }
170 }
171}
172
173#[derive(IntoElement)]
174pub struct KeyIcon {
175 icon: IconName,
176}
177
178impl RenderOnce for KeyIcon {
179 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
180 div().w(rems(14. / 16.)).child(
181 Icon::new(self.icon)
182 .size(IconSize::Small)
183 .color(Color::Muted),
184 )
185 }
186}
187
188impl KeyIcon {
189 pub fn new(icon: IconName) -> Self {
190 Self { icon }
191 }
192}