1use crate::{h_flex, prelude::*};
2use crate::{ElevationIndex, KeyBinding};
3use gpui::{point, App, BoxShadow, IntoElement, Window};
4use smallvec::smallvec;
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/// ```
14/// use ui::prelude::*;
15///
16/// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+S"))
17/// .prefix("Save:")
18/// .size(Pixels::from(14.0));
19/// ```
20#[derive(Debug, IntoElement, Clone)]
21pub struct KeybindingHint {
22 prefix: Option<SharedString>,
23 suffix: Option<SharedString>,
24 keybinding: KeyBinding,
25 size: Option<Pixels>,
26 elevation: Option<ElevationIndex>,
27}
28
29impl KeybindingHint {
30 /// Creates a new `KeybindingHint` with the specified keybinding.
31 ///
32 /// This method initializes a new `KeybindingHint` instance with the given keybinding,
33 /// setting all other fields to their default values.
34 ///
35 /// # Examples
36 ///
37 /// ```
38 /// use ui::prelude::*;
39 ///
40 /// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+C"));
41 /// ```
42 pub fn new(keybinding: KeyBinding) -> Self {
43 Self {
44 prefix: None,
45 suffix: None,
46 keybinding,
47 size: None,
48 elevation: None,
49 }
50 }
51
52 /// Creates a new `KeybindingHint` with a prefix and keybinding.
53 ///
54 /// This method initializes a new `KeybindingHint` instance with the given prefix and keybinding,
55 /// setting all other fields to their default values.
56 ///
57 /// # Examples
58 ///
59 /// ```
60 /// use ui::prelude::*;
61 ///
62 /// let hint = KeybindingHint::with_prefix("Copy:", KeyBinding::from_str("Ctrl+C"));
63 /// ```
64 pub fn with_prefix(prefix: impl Into<SharedString>, keybinding: KeyBinding) -> Self {
65 Self {
66 prefix: Some(prefix.into()),
67 suffix: None,
68 keybinding,
69 size: None,
70 elevation: None,
71 }
72 }
73
74 /// Creates a new `KeybindingHint` with a keybinding and suffix.
75 ///
76 /// This method initializes a new `KeybindingHint` instance with the given keybinding and suffix,
77 /// setting all other fields to their default values.
78 ///
79 /// # Examples
80 ///
81 /// ```
82 /// use ui::prelude::*;
83 ///
84 /// let hint = KeybindingHint::with_suffix(KeyBinding::from_str("Ctrl+V"), "Paste");
85 /// ```
86 pub fn with_suffix(keybinding: KeyBinding, suffix: impl Into<SharedString>) -> Self {
87 Self {
88 prefix: None,
89 suffix: Some(suffix.into()),
90 keybinding,
91 size: None,
92 elevation: None,
93 }
94 }
95
96 /// Sets the prefix for the keybinding hint.
97 ///
98 /// This method allows adding or changing the prefix text that appears before the keybinding.
99 ///
100 /// # Examples
101 ///
102 /// ```
103 /// use ui::prelude::*;
104 ///
105 /// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+X"))
106 /// .prefix("Cut:");
107 /// ```
108 pub fn prefix(mut self, prefix: impl Into<SharedString>) -> Self {
109 self.prefix = Some(prefix.into());
110 self
111 }
112
113 /// Sets the suffix for the keybinding hint.
114 ///
115 /// This method allows adding or changing the suffix text that appears after the keybinding.
116 ///
117 /// # Examples
118 ///
119 /// ```
120 /// use ui::prelude::*;
121 ///
122 /// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+F"))
123 /// .suffix("Find");
124 /// ```
125 pub fn suffix(mut self, suffix: impl Into<SharedString>) -> Self {
126 self.suffix = Some(suffix.into());
127 self
128 }
129
130 /// Sets the size of the keybinding hint.
131 ///
132 /// This method allows specifying the size of the keybinding hint in pixels.
133 ///
134 /// # Examples
135 ///
136 /// ```
137 /// use ui::prelude::*;
138 ///
139 /// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+Z"))
140 /// .size(Pixels::from(16.0));
141 /// ```
142 pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
143 self.size = size.into();
144 self
145 }
146
147 /// Sets the elevation of the keybinding hint.
148 ///
149 /// This method allows specifying the elevation index for the keybinding hint,
150 /// which affects its visual appearance in terms of depth or layering.
151 ///
152 /// # Examples
153 ///
154 /// ```
155 /// use ui::prelude::*;
156 ///
157 /// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+A"))
158 /// .elevation(ElevationIndex::new(1));
159 /// ```
160 pub fn elevation(mut self, elevation: impl Into<Option<ElevationIndex>>) -> Self {
161 self.elevation = elevation.into();
162 self
163 }
164}
165
166impl RenderOnce for KeybindingHint {
167 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
168 let colors = cx.theme().colors().clone();
169
170 let size = self
171 .size
172 .unwrap_or(TextSize::Small.rems(cx).to_pixels(window.rem_size()));
173 let kb_size = size - px(2.0);
174 let kb_bg = if let Some(elevation) = self.elevation {
175 elevation.on_elevation_bg(cx)
176 } else {
177 theme::color_alpha(colors.element_background, 0.6)
178 };
179
180 h_flex()
181 .items_center()
182 .gap_0p5()
183 .font_buffer(cx)
184 .text_size(size)
185 .text_color(colors.text_muted)
186 .children(self.prefix)
187 .child(
188 h_flex()
189 .items_center()
190 .rounded_md()
191 .px_0p5()
192 .mr_0p5()
193 .border_1()
194 .border_color(kb_bg)
195 .bg(kb_bg.opacity(0.8))
196 .shadow(smallvec![BoxShadow {
197 color: cx.theme().colors().editor_background.opacity(0.8),
198 offset: point(px(0.), px(1.)),
199 blur_radius: px(0.),
200 spread_radius: px(0.),
201 }])
202 .child(self.keybinding.size(kb_size)),
203 )
204 .children(self.suffix)
205 }
206}
207
208impl ComponentPreview for KeybindingHint {
209 fn description() -> impl Into<Option<&'static str>> {
210 "Used to display hint text for keyboard shortcuts. Can have a prefix and suffix."
211 }
212
213 fn examples(window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
214 let home_fallback = gpui::KeyBinding::new("home", menu::SelectFirst, None);
215 let home = KeyBinding::for_action(&menu::SelectFirst, window)
216 .unwrap_or(KeyBinding::new(home_fallback));
217
218 let end_fallback = gpui::KeyBinding::new("end", menu::SelectLast, None);
219 let end = KeyBinding::for_action(&menu::SelectLast, window)
220 .unwrap_or(KeyBinding::new(end_fallback));
221
222 let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
223 let enter = KeyBinding::for_action(&menu::Confirm, window)
224 .unwrap_or(KeyBinding::new(enter_fallback));
225
226 let escape_fallback = gpui::KeyBinding::new("escape", menu::Cancel, None);
227 let escape = KeyBinding::for_action(&menu::Cancel, window)
228 .unwrap_or(KeyBinding::new(escape_fallback));
229
230 vec![
231 example_group_with_title(
232 "Basic",
233 vec![
234 single_example(
235 "With Prefix",
236 KeybindingHint::with_prefix("Go to Start:", home.clone()),
237 ),
238 single_example(
239 "With Suffix",
240 KeybindingHint::with_suffix(end.clone(), "Go to End"),
241 ),
242 single_example(
243 "With Prefix and Suffix",
244 KeybindingHint::new(enter.clone())
245 .prefix("Confirm:")
246 .suffix("Execute selected action"),
247 ),
248 ],
249 ),
250 example_group_with_title(
251 "Sizes",
252 vec![
253 single_example(
254 "Small",
255 KeybindingHint::new(home.clone())
256 .size(Pixels::from(12.0))
257 .prefix("Small:"),
258 ),
259 single_example(
260 "Medium",
261 KeybindingHint::new(end.clone())
262 .size(Pixels::from(16.0))
263 .suffix("Medium"),
264 ),
265 single_example(
266 "Large",
267 KeybindingHint::new(enter.clone())
268 .size(Pixels::from(20.0))
269 .prefix("Large:")
270 .suffix("Size"),
271 ),
272 ],
273 ),
274 example_group_with_title(
275 "Elevations",
276 vec![
277 single_example(
278 "Surface",
279 KeybindingHint::new(home.clone())
280 .elevation(ElevationIndex::Surface)
281 .prefix("Surface:"),
282 ),
283 single_example(
284 "Elevated Surface",
285 KeybindingHint::new(end.clone())
286 .elevation(ElevationIndex::ElevatedSurface)
287 .suffix("Elevated"),
288 ),
289 single_example(
290 "Editor Surface",
291 KeybindingHint::new(enter.clone())
292 .elevation(ElevationIndex::EditorSurface)
293 .prefix("Editor:")
294 .suffix("Surface"),
295 ),
296 single_example(
297 "Modal Surface",
298 KeybindingHint::new(escape.clone())
299 .elevation(ElevationIndex::ModalSurface)
300 .prefix("Modal:")
301 .suffix("Escape"),
302 ),
303 ],
304 ),
305 ]
306 }
307}