1use crate::{h_flex, prelude::*};
2use crate::{ElevationIndex, KeyBinding};
3use gpui::{point, AnyElement, 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, IntoComponent)]
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(rems_from_px(kb_size.0))),
203 )
204 .children(self.suffix)
205 }
206}
207
208// View this component preview using `workspace: open component-preview`
209impl ComponentPreview for KeybindingHint {
210 fn preview(window: &mut Window, cx: &App) -> AnyElement {
211 let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
212 let enter = KeyBinding::for_action(&menu::Confirm, window, cx)
213 .unwrap_or(KeyBinding::new(enter_fallback, cx));
214
215 v_flex()
216 .gap_6()
217 .children(vec![
218 example_group_with_title(
219 "Basic",
220 vec![
221 single_example(
222 "With Prefix",
223 KeybindingHint::with_prefix("Go to Start:", enter.clone())
224 .into_any_element(),
225 ),
226 single_example(
227 "With Suffix",
228 KeybindingHint::with_suffix(enter.clone(), "Go to End")
229 .into_any_element(),
230 ),
231 single_example(
232 "With Prefix and Suffix",
233 KeybindingHint::new(enter.clone())
234 .prefix("Confirm:")
235 .suffix("Execute selected action")
236 .into_any_element(),
237 ),
238 ],
239 ),
240 example_group_with_title(
241 "Sizes",
242 vec![
243 single_example(
244 "Small",
245 KeybindingHint::new(enter.clone())
246 .size(Pixels::from(12.0))
247 .prefix("Small:")
248 .into_any_element(),
249 ),
250 single_example(
251 "Medium",
252 KeybindingHint::new(enter.clone())
253 .size(Pixels::from(16.0))
254 .suffix("Medium")
255 .into_any_element(),
256 ),
257 single_example(
258 "Large",
259 KeybindingHint::new(enter.clone())
260 .size(Pixels::from(20.0))
261 .prefix("Large:")
262 .suffix("Size")
263 .into_any_element(),
264 ),
265 ],
266 ),
267 example_group_with_title(
268 "Elevations",
269 vec![
270 single_example(
271 "Surface",
272 KeybindingHint::new(enter.clone())
273 .elevation(ElevationIndex::Surface)
274 .prefix("Surface:")
275 .into_any_element(),
276 ),
277 single_example(
278 "Elevated Surface",
279 KeybindingHint::new(enter.clone())
280 .elevation(ElevationIndex::ElevatedSurface)
281 .suffix("Elevated")
282 .into_any_element(),
283 ),
284 single_example(
285 "Editor Surface",
286 KeybindingHint::new(enter.clone())
287 .elevation(ElevationIndex::EditorSurface)
288 .prefix("Editor:")
289 .suffix("Surface")
290 .into_any_element(),
291 ),
292 single_example(
293 "Modal Surface",
294 KeybindingHint::new(enter.clone())
295 .elevation(ElevationIndex::ModalSurface)
296 .prefix("Modal:")
297 .suffix("Enter")
298 .into_any_element(),
299 ),
300 ],
301 ),
302 ])
303 .into_any_element()
304 }
305}