1use crate::{h_flex, prelude::*, Icon, IconName, IconSize};
2use gpui::{relative, 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_from_px(14.))
162 .flex()
163 .flex_none()
164 .justify_center()
165 } else {
166 this.px_0p5()
167 }
168 })
169 .h(rems_from_px(14.))
170 .text_ui()
171 .line_height(relative(1.))
172 .text_color(cx.theme().colors().text_muted)
173 .child(self.key.clone())
174 }
175}
176
177impl Key {
178 pub fn new(key: impl Into<SharedString>) -> Self {
179 Self { key: key.into() }
180 }
181}
182
183#[derive(IntoElement)]
184pub struct KeyIcon {
185 icon: IconName,
186}
187
188impl RenderOnce for KeyIcon {
189 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
190 div().w(rems_from_px(14.)).child(
191 Icon::new(self.icon)
192 .size(IconSize::Small)
193 .color(Color::Muted),
194 )
195 }
196}
197
198impl KeyIcon {
199 pub fn new(icon: IconName) -> Self {
200 Self { icon }
201 }
202}