Skip to main content

gpui_component/
sizing.rs

1use gpui::{Edges, Pixels, Styled, px};
2use serde::{Deserialize, Serialize};
3
4/// A size for elements.
5#[derive(Clone, Default, Copy, PartialEq, Eq, Debug, Deserialize, Serialize)]
6pub enum Size {
7    Size(Pixels),
8    XSmall,
9    Small,
10    #[default]
11    Medium,
12    Large,
13}
14
15impl Size {
16    fn as_f32(&self) -> f32 {
17        match self {
18            Size::Size(val) => val.as_f32(),
19            Size::XSmall => 0.,
20            Size::Small => 1.,
21            Size::Medium => 2.,
22            Size::Large => 3.,
23        }
24    }
25
26    /// Returns the size as a static string.
27    pub fn as_str(&self) -> &'static str {
28        match self {
29            Size::XSmall => "xs",
30            Size::Small => "sm",
31            Size::Medium => "md",
32            Size::Large => "lg",
33            Size::Size(_) => "custom",
34        }
35    }
36
37    /// Create a Size from a static string.
38    ///
39    /// - "xs" or "xsmall"
40    /// - "sm" or "small"
41    /// - "md" or "medium"
42    /// - "lg" or "large"
43    ///
44    /// Any other value will return Size::Medium.
45    pub fn from_str(size: &str) -> Self {
46        match size.to_lowercase().as_str() {
47            "xs" | "xsmall" => Size::XSmall,
48            "sm" | "small" => Size::Small,
49            "md" | "medium" => Size::Medium,
50            "lg" | "large" => Size::Large,
51            _ => Size::Medium,
52        }
53    }
54
55    /// Returns the height for table row.
56    #[inline]
57    pub fn table_row_height(&self) -> Pixels {
58        match self {
59            Size::Size(size) => *size,
60            Size::XSmall => px(26.),
61            Size::Small => px(30.),
62            Size::Large => px(40.),
63            _ => px(32.),
64        }
65    }
66
67    /// Returns the padding for a table cell.
68    #[inline]
69    pub fn table_cell_padding(&self) -> Edges<Pixels> {
70        match self {
71            Size::XSmall => Edges {
72                top: px(2.),
73                bottom: px(2.),
74                left: px(4.),
75                right: px(4.),
76            },
77            Size::Small => Edges {
78                top: px(3.),
79                bottom: px(3.),
80                left: px(6.),
81                right: px(6.),
82            },
83            Size::Large => Edges {
84                top: px(8.),
85                bottom: px(8.),
86                left: px(12.),
87                right: px(12.),
88            },
89            _ => Edges {
90                top: px(4.),
91                bottom: px(4.),
92                left: px(8.),
93                right: px(8.),
94            },
95        }
96    }
97
98    /// Returns a smaller size.
99    pub fn smaller(&self) -> Self {
100        match self {
101            Size::XSmall => Size::XSmall,
102            Size::Small => Size::XSmall,
103            Size::Medium => Size::Small,
104            Size::Large => Size::Medium,
105            Size::Size(val) => Size::Size(*val * 0.2),
106        }
107    }
108
109    /// Returns a larger size.
110    pub fn larger(&self) -> Self {
111        match self {
112            Size::XSmall => Size::Small,
113            Size::Small => Size::Medium,
114            Size::Medium => Size::Large,
115            Size::Large => Size::Large,
116            Size::Size(val) => Size::Size(*val * 1.2),
117        }
118    }
119
120    /// Return the max size between two sizes.
121    ///
122    /// e.g. `Size::XSmall.max(Size::Small)` will return `Size::XSmall`.
123    pub fn max(&self, other: Self) -> Self {
124        match (self, other) {
125            (Size::Size(a), Size::Size(b)) => Size::Size(px(a.as_f32().min(b.as_f32()))),
126            (Size::Size(a), _) => Size::Size(*a),
127            (_, Size::Size(b)) => Size::Size(b),
128            (a, b) if a.as_f32() < b.as_f32() => *a,
129            _ => other,
130        }
131    }
132
133    /// Return the min size between two sizes.
134    ///
135    /// e.g. `Size::XSmall.min(Size::Small)` will return `Size::Small`.
136    pub fn min(&self, other: Self) -> Self {
137        match (self, other) {
138            (Size::Size(a), Size::Size(b)) => Size::Size(px(a.as_f32().max(b.as_f32()))),
139            (Size::Size(a), _) => Size::Size(*a),
140            (_, Size::Size(b)) => Size::Size(b),
141            (a, b) if a.as_f32() > b.as_f32() => *a,
142            _ => other,
143        }
144    }
145
146    /// Returns the horizontal input padding.
147    pub fn input_px(&self) -> Pixels {
148        match self {
149            Self::Large => px(12.),
150            Self::Medium => px(10.),
151            Self::Small => px(8.),
152            Self::XSmall => px(4.),
153            _ => px(8.),
154        }
155    }
156
157    /// Returns the vertical input padding.
158    pub fn input_py(&self) -> Pixels {
159        match self {
160            Size::Large => px(10.),
161            Size::Medium => px(8.),
162            Size::Small => px(2.),
163            Size::XSmall => px(0.),
164            _ => px(2.),
165        }
166    }
167
168    /// Returns the horizontal padding of a row in an input's dropdown list.
169    ///
170    /// The trigger insets its text by a 1px border plus [`Self::input_px`]; the
171    /// popup insets a row by [`DROPDOWN_LIST_PADDING`]. The row pads the
172    /// difference, so the chosen item's text lines up with the trigger's.
173    pub(crate) fn list_px(&self) -> Pixels {
174        self.input_px() + px(1.) - DROPDOWN_LIST_PADDING
175    }
176}
177
178/// The inset between an input's dropdown popup and its list rows.
179pub(crate) const DROPDOWN_LIST_PADDING: Pixels = px(4.);
180
181impl From<Pixels> for Size {
182    fn from(size: Pixels) -> Self {
183        Size::Size(size)
184    }
185}
186
187/// A trait for setting the size of an element.
188/// Size::Medium is use by default.
189#[allow(patterns_in_fns_without_body)]
190pub trait Sizable: Sized {
191    /// Set the ui::Size of this element.
192    ///
193    /// Also can receive a `ButtonSize` to convert to `IconSize`,
194    /// Or a `Pixels` to set a custom size: `px(30.)`
195    fn with_size(mut self, size: impl Into<Size>) -> Self;
196
197    /// Apply control-specific presentation when this element is hosted by a
198    /// toolbar. Most controls keep their normal presentation; command buttons
199    /// override this hook to use the toolbar's quiet, compact treatment.
200    #[doc(hidden)]
201    fn prepare_for_toolbar(self) -> Self {
202        self
203    }
204
205    /// Set to Size::XSmall
206    #[inline(always)]
207    fn xsmall(self) -> Self {
208        self.with_size(Size::XSmall)
209    }
210
211    /// Set to Size::Small
212    #[inline(always)]
213    fn small(self) -> Self {
214        self.with_size(Size::Small)
215    }
216
217    /// Set to Size::Large
218    #[inline(always)]
219    fn large(self) -> Self {
220        self.with_size(Size::Large)
221    }
222}
223
224#[allow(unused)]
225pub trait StyleSized<T: Styled> {
226    fn input_text_size(self, size: Size) -> Self;
227    fn input_size(self, size: Size) -> Self;
228    fn input_pl(self, size: Size) -> Self;
229    fn input_pr(self, size: Size) -> Self;
230    fn input_px(self, size: Size) -> Self;
231    fn input_py(self, size: Size) -> Self;
232    fn input_h(self, size: Size) -> Self;
233    fn list_size(self, size: Size) -> Self;
234    fn list_px(self, size: Size) -> Self;
235    fn list_py(self, size: Size) -> Self;
236    /// Apply size with the given `Size`.
237    fn size_with(self, size: Size) -> Self;
238    /// Apply the table cell size (Font size, padding) with the given `Size`.
239    fn table_cell_size(self, size: Size) -> Self;
240    fn button_text_size(self, size: Size) -> Self;
241}
242
243impl<T: Styled> StyleSized<T> for T {
244    #[inline]
245    fn input_text_size(self, size: Size) -> Self {
246        match size {
247            Size::XSmall => self.text_xs(),
248            Size::Small => self.text_sm(),
249            Size::Medium => self.text_sm(),
250            Size::Large => self.text_base(),
251            Size::Size(size) => self.text_size(size * 0.875),
252        }
253    }
254
255    #[inline]
256    fn input_size(self, size: Size) -> Self {
257        self.input_px(size).input_py(size).input_h(size)
258    }
259
260    #[inline]
261    fn input_pl(self, size: Size) -> Self {
262        self.pl(size.input_px())
263    }
264
265    #[inline]
266    fn input_pr(self, size: Size) -> Self {
267        self.pr(size.input_px())
268    }
269
270    #[inline]
271    fn input_px(self, size: Size) -> Self {
272        self.px(size.input_px())
273    }
274
275    #[inline]
276    fn input_py(self, size: Size) -> Self {
277        self.py(size.input_py())
278    }
279
280    #[inline]
281    fn input_h(self, size: Size) -> Self {
282        match size {
283            Size::Large => self.h_11(),
284            Size::Medium => self.h_8(),
285            Size::Small => self.h_6(),
286            Size::XSmall => self.h_5(),
287            _ => self.h_6(),
288        }
289    }
290
291    #[inline]
292    fn list_size(self, size: Size) -> Self {
293        self.list_px(size).list_py(size).input_text_size(size)
294    }
295
296    #[inline]
297    fn list_px(self, size: Size) -> Self {
298        self.px(size.list_px())
299    }
300
301    #[inline]
302    fn list_py(self, size: Size) -> Self {
303        match size {
304            Size::Large => self.py_2(),
305            Size::Medium => self.py_1(),
306            Size::Small => self.py_0p5(),
307            _ => self.py_1(),
308        }
309    }
310
311    #[inline]
312    fn size_with(self, size: Size) -> Self {
313        match size {
314            Size::Large => self.size_11(),
315            Size::Medium => self.size_8(),
316            Size::Small => self.size_5(),
317            Size::XSmall => self.size_4(),
318            Size::Size(size) => self.size(size),
319        }
320    }
321
322    #[inline]
323    fn table_cell_size(self, size: Size) -> Self {
324        let padding = size.table_cell_padding();
325        match size {
326            Size::XSmall => self.text_xs(),
327            Size::Small | Size::Medium => self.text_sm(),
328            Size::Large => self.text_base(),
329            _ => self,
330        }
331        .pl(padding.left)
332        .pr(padding.right)
333        .pt(padding.top)
334        .pb(padding.bottom)
335    }
336
337    fn button_text_size(self, size: Size) -> Self {
338        match size {
339            Size::XSmall => self.text_xs(),
340            Size::Small | Size::Medium => self.text_sm(),
341            _ => self.text_base(),
342        }
343    }
344}
345#[cfg(test)]
346mod tests {
347    use gpui::px;
348
349    use crate::Size;
350
351    #[test]
352    fn test_size_max_min() {
353        assert_eq!(Size::Small.min(Size::XSmall), Size::Small);
354        assert_eq!(Size::XSmall.min(Size::Small), Size::Small);
355        assert_eq!(Size::Small.min(Size::Medium), Size::Medium);
356        assert_eq!(Size::Medium.min(Size::Large), Size::Large);
357        assert_eq!(Size::Large.min(Size::Small), Size::Large);
358
359        assert_eq!(
360            Size::Size(px(10.)).min(Size::Size(px(20.))),
361            Size::Size(px(20.))
362        );
363
364        // Min
365        assert_eq!(Size::Small.max(Size::XSmall), Size::XSmall);
366        assert_eq!(Size::XSmall.max(Size::Small), Size::XSmall);
367        assert_eq!(Size::Small.max(Size::Medium), Size::Small);
368        assert_eq!(Size::Medium.max(Size::Large), Size::Medium);
369        assert_eq!(Size::Large.max(Size::Small), Size::Small);
370
371        assert_eq!(
372            Size::Size(px(10.)).max(Size::Size(px(20.))),
373            Size::Size(px(10.))
374        );
375    }
376
377    #[test]
378    fn test_size_as_str() {
379        assert_eq!(Size::XSmall.as_str(), "xs");
380        assert_eq!(Size::Small.as_str(), "sm");
381        assert_eq!(Size::Medium.as_str(), "md");
382        assert_eq!(Size::Large.as_str(), "lg");
383        assert_eq!(Size::Size(px(15.)).as_str(), "custom");
384    }
385
386    #[test]
387    fn test_table_row_height() {
388        assert_eq!(Size::XSmall.table_row_height(), px(26.));
389        assert_eq!(Size::Small.table_row_height(), px(30.));
390        assert_eq!(Size::Medium.table_row_height(), px(32.));
391        assert_eq!(Size::Large.table_row_height(), px(40.));
392        assert_eq!(Size::Size(px(48.)).table_row_height(), px(48.));
393    }
394
395    #[test]
396    fn test_size_from_str() {
397        assert_eq!(Size::from_str("xs"), Size::XSmall);
398        assert_eq!(Size::from_str("xsmall"), Size::XSmall);
399        assert_eq!(Size::from_str("sm"), Size::Small);
400        assert_eq!(Size::from_str("small"), Size::Small);
401        assert_eq!(Size::from_str("md"), Size::Medium);
402        assert_eq!(Size::from_str("medium"), Size::Medium);
403        assert_eq!(Size::from_str("lg"), Size::Large);
404        assert_eq!(Size::from_str("large"), Size::Large);
405        assert_eq!(Size::from_str("unknown"), Size::Medium);
406
407        // Case insensitive
408        assert_eq!(Size::from_str("XS"), Size::XSmall);
409        assert_eq!(Size::from_str("SMALL"), Size::Small);
410        assert_eq!(Size::from_str("Md"), Size::Medium);
411    }
412}