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, IntoComponent)]
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
208// View this component preview using `workspace: open component-preview`
209impl ComponentPreview for KeybindingHint {
210 fn preview(window: &mut Window, cx: &mut 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 let bg_color = cx.theme().colors().surface_background;
216
217 v_flex()
218 .gap_6()
219 .children(vec![
220 example_group_with_title(
221 "Basic",
222 vec![
223 single_example(
224 "With Prefix",
225 KeybindingHint::with_prefix("Go to Start:", enter.clone(), bg_color)
226 .into_any_element(),
227 ),
228 single_example(
229 "With Suffix",
230 KeybindingHint::with_suffix(enter.clone(), "Go to End", bg_color)
231 .into_any_element(),
232 ),
233 single_example(
234 "With Prefix and Suffix",
235 KeybindingHint::new(enter.clone(), bg_color)
236 .prefix("Confirm:")
237 .suffix("Execute selected action")
238 .into_any_element(),
239 ),
240 ],
241 ),
242 example_group_with_title(
243 "Sizes",
244 vec![
245 single_example(
246 "Small",
247 KeybindingHint::new(enter.clone(), bg_color)
248 .size(Pixels::from(12.0))
249 .prefix("Small:")
250 .into_any_element(),
251 ),
252 single_example(
253 "Medium",
254 KeybindingHint::new(enter.clone(), bg_color)
255 .size(Pixels::from(16.0))
256 .suffix("Medium")
257 .into_any_element(),
258 ),
259 single_example(
260 "Large",
261 KeybindingHint::new(enter.clone(), bg_color)
262 .size(Pixels::from(20.0))
263 .prefix("Large:")
264 .suffix("Size")
265 .into_any_element(),
266 ),
267 ],
268 ),
269 ])
270 .into_any_element()
271 }
272}