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