1use crate::KeyBinding;
2use crate::{h_flex, prelude::*};
3use gpui::{AnyElement, App, BoxShadow, FontStyle, Hsla, IntoElement, Window, point};
4use theme::Appearance;
5
6/// Represents a hint for a keybinding, optionally with a prefix and suffix.
7///
8/// This struct allows for the creation and customization of a keybinding hint,
9/// which can be used to display keyboard shortcuts or commands in a user interface.
10///
11/// # Examples
12///
13/// ```no_run
14/// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke};
15/// use ui::prelude::*;
16/// use ui::{KeyBinding, KeybindingHint};
17///
18/// # fn example(cx: &App) {
19/// let hint = KeybindingHint::new(
20/// KeyBinding::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-s").unwrap())], cx),
21/// Hsla::black()
22/// )
23/// .prefix("Save:")
24/// .size(Pixels::from(14.0));
25/// # }
26/// ```
27#[derive(Debug, IntoElement, RegisterComponent)]
28pub struct KeybindingHint {
29 prefix: Option<SharedString>,
30 suffix: Option<SharedString>,
31 keybinding: KeyBinding,
32 size: Option<Pixels>,
33 background_color: Hsla,
34}
35
36impl KeybindingHint {
37 /// Creates a new `KeybindingHint` with the specified keybinding.
38 ///
39 /// This method initializes a new `KeybindingHint` instance with the given keybinding,
40 /// setting all other fields to their default values.
41 ///
42 /// # Examples
43 ///
44 /// ```no_run
45 /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke};
46 /// use ui::prelude::*;
47 /// use ui::{KeyBinding, KeybindingHint};
48 ///
49 /// # fn example(cx: &App) {
50 /// let hint = KeybindingHint::new(
51 /// KeyBinding::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-c").unwrap())], cx),
52 /// Hsla::black()
53 /// );
54 /// # }
55 /// ```
56 pub fn new(keybinding: KeyBinding, background_color: Hsla) -> Self {
57 Self {
58 prefix: None,
59 suffix: None,
60 keybinding,
61 size: None,
62 background_color,
63 }
64 }
65
66 /// Creates a new `KeybindingHint` with a prefix and keybinding.
67 ///
68 /// This method initializes a new `KeybindingHint` instance with the given prefix and keybinding,
69 /// setting all other fields to their default values.
70 ///
71 /// # Examples
72 ///
73 /// ```no_run
74 /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke};
75 /// use ui::prelude::*;
76 /// use ui::{KeyBinding, KeybindingHint};
77 ///
78 /// # fn example(cx: &App) {
79 /// let hint = KeybindingHint::with_prefix(
80 /// "Copy:",
81 /// KeyBinding::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-c").unwrap())], cx),
82 /// Hsla::black()
83 /// );
84 /// # }
85 /// ```
86 pub fn with_prefix(
87 prefix: impl Into<SharedString>,
88 keybinding: KeyBinding,
89 background_color: Hsla,
90 ) -> Self {
91 Self {
92 prefix: Some(prefix.into()),
93 suffix: None,
94 keybinding,
95 size: None,
96 background_color,
97 }
98 }
99
100 /// Creates a new `KeybindingHint` with a keybinding and suffix.
101 ///
102 /// This method initializes a new `KeybindingHint` instance with the given keybinding and suffix,
103 /// setting all other fields to their default values.
104 ///
105 /// # Examples
106 ///
107 /// ```no_run
108 /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke};
109 /// use ui::prelude::*;
110 /// use ui::{KeyBinding, KeybindingHint};
111 ///
112 /// # fn example(cx: &App) {
113 /// let hint = KeybindingHint::with_suffix(
114 /// KeyBinding::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-v").unwrap())], cx),
115 /// "Paste",
116 /// Hsla::black()
117 /// );
118 /// # }
119 /// ```
120 pub fn with_suffix(
121 keybinding: KeyBinding,
122 suffix: impl Into<SharedString>,
123 background_color: Hsla,
124 ) -> Self {
125 Self {
126 prefix: None,
127 suffix: Some(suffix.into()),
128 keybinding,
129 size: None,
130 background_color,
131 }
132 }
133
134 /// Sets the prefix for the keybinding hint.
135 ///
136 /// This method allows adding or changing the prefix text that appears before the keybinding.
137 ///
138 /// # Examples
139 ///
140 /// ```no_run
141 /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke};
142 /// use ui::prelude::*;
143 /// use ui::{KeyBinding, KeybindingHint};
144 ///
145 /// # fn example(cx: &App) {
146 /// let hint = KeybindingHint::new(
147 /// KeyBinding::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-x").unwrap())], cx),
148 /// Hsla::black()
149 /// )
150 /// .prefix("Cut:");
151 /// # }
152 /// ```
153 pub fn prefix(mut self, prefix: impl Into<SharedString>) -> Self {
154 self.prefix = Some(prefix.into());
155 self
156 }
157
158 /// Sets the suffix for the keybinding hint.
159 ///
160 /// This method allows adding or changing the suffix text that appears after the keybinding.
161 ///
162 /// # Examples
163 ///
164 /// ```no_run
165 /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke};
166 /// use ui::prelude::*;
167 /// use ui::{KeyBinding, KeybindingHint};
168 ///
169 /// # fn example(cx: &App) {
170 /// let hint = KeybindingHint::new(
171 /// KeyBinding::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-f").unwrap())], cx),
172 /// Hsla::black()
173 /// )
174 /// .suffix("Find");
175 /// # }
176 /// ```
177 pub fn suffix(mut self, suffix: impl Into<SharedString>) -> Self {
178 self.suffix = Some(suffix.into());
179 self
180 }
181
182 /// Sets the size of the keybinding hint.
183 ///
184 /// This method allows specifying the size of the keybinding hint in pixels.
185 ///
186 /// # Examples
187 ///
188 /// ```no_run
189 /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke};
190 /// use ui::prelude::*;
191 /// use ui::{KeyBinding, KeybindingHint};
192 ///
193 /// # fn example(cx: &App) {
194 /// let hint = KeybindingHint::new(
195 /// KeyBinding::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-z").unwrap())], cx),
196 /// Hsla::black()
197 /// )
198 /// .size(Pixels::from(16.0));
199 /// # }
200 /// ```
201 pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
202 self.size = size.into();
203 self
204 }
205}
206
207impl RenderOnce for KeybindingHint {
208 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
209 let colors = cx.theme().colors().clone();
210 let is_light = cx.theme().appearance() == Appearance::Light;
211
212 let border_color =
213 self.background_color
214 .blend(colors.text.alpha(if is_light { 0.08 } else { 0.16 }));
215 let bg_color =
216 self.background_color
217 .blend(colors.text.alpha(if is_light { 0.06 } else { 0.12 }));
218 let shadow_color = colors.text.alpha(if is_light { 0.04 } else { 0.08 });
219
220 let size = self
221 .size
222 .unwrap_or(TextSize::Small.rems(cx).to_pixels(window.rem_size()));
223 let kb_size = size - px(2.0);
224
225 let mut base = h_flex();
226
227 base.text_style()
228 .get_or_insert_with(Default::default)
229 .font_style = Some(FontStyle::Italic);
230
231 base.items_center()
232 .gap_0p5()
233 .font_buffer(cx)
234 .text_size(size)
235 .text_color(colors.text_disabled)
236 .children(self.prefix)
237 .child(
238 h_flex()
239 .items_center()
240 .rounded_sm()
241 .px_0p5()
242 .mr_0p5()
243 .border_1()
244 .border_color(border_color)
245 .bg(bg_color)
246 .shadow(vec![BoxShadow {
247 color: shadow_color,
248 offset: point(px(0.), px(1.)),
249 blur_radius: px(0.),
250 spread_radius: px(0.),
251 }])
252 .child(self.keybinding.size(rems_from_px(kb_size.0))),
253 )
254 .children(self.suffix)
255 }
256}
257
258impl Component for KeybindingHint {
259 fn scope() -> ComponentScope {
260 ComponentScope::DataDisplay
261 }
262
263 fn description() -> Option<&'static str> {
264 Some("Displays a keyboard shortcut hint with optional prefix and suffix text")
265 }
266
267 fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
268 let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
269 let enter = KeyBinding::for_action(&menu::Confirm, window, cx)
270 .unwrap_or(KeyBinding::new_from_gpui(enter_fallback, cx));
271
272 let bg_color = cx.theme().colors().surface_background;
273
274 Some(
275 v_flex()
276 .gap_6()
277 .children(vec![
278 example_group_with_title(
279 "Basic",
280 vec![
281 single_example(
282 "With Prefix",
283 KeybindingHint::with_prefix(
284 "Go to Start:",
285 enter.clone(),
286 bg_color,
287 )
288 .into_any_element(),
289 ),
290 single_example(
291 "With Suffix",
292 KeybindingHint::with_suffix(enter.clone(), "Go to End", bg_color)
293 .into_any_element(),
294 ),
295 single_example(
296 "With Prefix and Suffix",
297 KeybindingHint::new(enter.clone(), bg_color)
298 .prefix("Confirm:")
299 .suffix("Execute selected action")
300 .into_any_element(),
301 ),
302 ],
303 ),
304 example_group_with_title(
305 "Sizes",
306 vec![
307 single_example(
308 "Small",
309 KeybindingHint::new(enter.clone(), bg_color)
310 .size(Pixels::from(12.0))
311 .prefix("Small:")
312 .into_any_element(),
313 ),
314 single_example(
315 "Medium",
316 KeybindingHint::new(enter.clone(), bg_color)
317 .size(Pixels::from(16.0))
318 .suffix("Medium")
319 .into_any_element(),
320 ),
321 single_example(
322 "Large",
323 KeybindingHint::new(enter, bg_color)
324 .size(Pixels::from(20.0))
325 .prefix("Large:")
326 .suffix("Size")
327 .into_any_element(),
328 ),
329 ],
330 ),
331 ])
332 .into_any_element(),
333 )
334 }
335}