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