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