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 .map(|el| match self.display {
101 KeyBindingDisplay::Mac => el.gap_0p5(),
102 KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => el,
103 })
104 .p_0p5()
105 .rounded_sm()
106 .text_color(cx.theme().colors().text_muted)
107 .when(keystroke.modifiers.function, |el| match self.display {
108 KeyBindingDisplay::Mac => el.child(Key::new("fn")),
109 KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
110 el.child(Key::new("Fn")).child(Key::new("+"))
111 }
112 })
113 .when(keystroke.modifiers.control, |el| match self.display {
114 KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Control)),
115 KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
116 el.child(Key::new("Ctrl")).child(Key::new("+"))
117 }
118 })
119 .when(keystroke.modifiers.alt, |el| match self.display {
120 KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Option)),
121 KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
122 el.child(Key::new("Alt")).child(Key::new("+"))
123 }
124 })
125 .when(keystroke.modifiers.command, |el| match self.display {
126 KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Command)),
127 KeyBindingDisplay::Linux => {
128 el.child(Key::new("Super")).child(Key::new("+"))
129 }
130 KeyBindingDisplay::Windows => {
131 el.child(Key::new("Win")).child(Key::new("+"))
132 }
133 })
134 .when(keystroke.modifiers.shift, |el| match self.display {
135 KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Shift)),
136 KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
137 el.child(Key::new("Shift")).child(Key::new("+"))
138 }
139 })
140 .map(|el| match key_icon {
141 Some(icon) => el.child(KeyIcon::new(icon)),
142 None => el.child(Key::new(keystroke.key.to_uppercase())),
143 })
144 }))
145 }
146}
147
148#[derive(IntoElement)]
149pub struct Key {
150 key: SharedString,
151}
152
153impl RenderOnce for Key {
154 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
155 let single_char = self.key.len() == 1;
156
157 div()
158 .py_0()
159 .map(|this| {
160 if single_char {
161 this.w(rems(14. / 16.)).flex().flex_none().justify_center()
162 } else {
163 this.px_0p5()
164 }
165 })
166 .h(rems(14. / 16.))
167 .text_ui()
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 div().w(rems(14. / 16.)).child(
188 Icon::new(self.icon)
189 .size(IconSize::Small)
190 .color(Color::Muted),
191 )
192 }
193}
194
195impl KeyIcon {
196 pub fn new(icon: IconName) -> Self {
197 Self { icon }
198 }
199}