gpui_component/
styled.rs

1use crate::{ActiveTheme, PixelsExt as _};
2use gpui::{
3    App, BoxShadow, Corners, DefiniteLength, Div, Edges, FocusHandle, Hsla, ParentElement, Pixels,
4    Refineable, StyleRefinement, Styled, Window, div, point, px,
5};
6use serde::{Deserialize, Serialize};
7
8/// Returns a `Div` as horizontal flex layout.
9#[inline(always)]
10pub fn h_flex() -> Div {
11    div().h_flex()
12}
13
14/// Returns a `Div` as vertical flex layout.
15#[inline(always)]
16pub fn v_flex() -> Div {
17    div().v_flex()
18}
19
20/// Create a [`BoxShadow`] like CSS.
21///
22/// e.g:
23///
24/// If CSS is `box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);`
25///
26/// Then the equivalent in Rust is `box_shadow(0., 0., 10., 0., hsla(0., 0., 0., 0.1))`
27#[inline(always)]
28pub fn box_shadow(
29    x: impl Into<Pixels>,
30    y: impl Into<Pixels>,
31    blur: impl Into<Pixels>,
32    spread: impl Into<Pixels>,
33    color: Hsla,
34) -> BoxShadow {
35    BoxShadow {
36        offset: point(x.into(), y.into()),
37        blur_radius: blur.into(),
38        spread_radius: spread.into(),
39        color,
40    }
41}
42
43macro_rules! font_weight {
44    ($fn:ident, $const:ident) => {
45        /// [docs](https://tailwindcss.com/docs/font-weight)
46        #[inline]
47        fn $fn(self) -> Self {
48            self.font_weight(gpui::FontWeight::$const)
49        }
50    };
51}
52
53/// Extends [`gpui::Styled`] with specific styling methods.
54#[cfg_attr(
55    any(feature = "inspector", debug_assertions),
56    gpui_macros::derive_inspector_reflection
57)]
58pub trait StyledExt: Styled + Sized {
59    /// Refine the style of this element, applying the given style refinement.
60    fn refine_style(mut self, style: &StyleRefinement) -> Self {
61        self.style().refine(style);
62        self
63    }
64
65    /// Apply self into a horizontal flex layout.
66    #[inline(always)]
67    fn h_flex(self) -> Self {
68        self.flex().flex_row().items_center()
69    }
70
71    /// Apply self into a vertical flex layout.
72    #[inline(always)]
73    fn v_flex(self) -> Self {
74        self.flex().flex_col()
75    }
76
77    /// Apply paddings to the element.
78    fn paddings<L>(self, paddings: impl Into<Edges<L>>) -> Self
79    where
80        L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug + PartialEq,
81    {
82        let paddings = paddings.into();
83        self.pt(paddings.top.into())
84            .pb(paddings.bottom.into())
85            .pl(paddings.left.into())
86            .pr(paddings.right.into())
87    }
88
89    /// Apply margins to the element.
90    fn margins<L>(self, margins: impl Into<Edges<L>>) -> Self
91    where
92        L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug + PartialEq,
93    {
94        let margins = margins.into();
95        self.mt(margins.top.into())
96            .mb(margins.bottom.into())
97            .ml(margins.left.into())
98            .mr(margins.right.into())
99    }
100
101    /// Render a border with a width of 1px, color red
102    fn debug_red(self) -> Self {
103        if cfg!(debug_assertions) {
104            self.border_1().border_color(crate::red_500())
105        } else {
106            self
107        }
108    }
109
110    /// Render a border with a width of 1px, color blue
111    fn debug_blue(self) -> Self {
112        if cfg!(debug_assertions) {
113            self.border_1().border_color(crate::blue_500())
114        } else {
115            self
116        }
117    }
118
119    /// Render a border with a width of 1px, color yellow
120    fn debug_yellow(self) -> Self {
121        if cfg!(debug_assertions) {
122            self.border_1().border_color(crate::yellow_500())
123        } else {
124            self
125        }
126    }
127
128    /// Render a border with a width of 1px, color green
129    fn debug_green(self) -> Self {
130        if cfg!(debug_assertions) {
131            self.border_1().border_color(crate::green_500())
132        } else {
133            self
134        }
135    }
136
137    /// Render a border with a width of 1px, color pink
138    fn debug_pink(self) -> Self {
139        if cfg!(debug_assertions) {
140            self.border_1().border_color(crate::pink_500())
141        } else {
142            self
143        }
144    }
145
146    /// Render a 1px blue border, when if the element is focused
147    fn debug_focused(self, focus_handle: &FocusHandle, window: &Window, cx: &App) -> Self {
148        if cfg!(debug_assertions) {
149            if focus_handle.contains_focused(window, cx) {
150                self.debug_blue()
151            } else {
152                self
153            }
154        } else {
155            self
156        }
157    }
158
159    /// Render a border with a width of 1px, color ring color
160    #[inline]
161    fn focused_border(self, cx: &App) -> Self {
162        self.border_1().border_color(cx.theme().ring)
163    }
164
165    font_weight!(font_thin, THIN);
166    font_weight!(font_extralight, EXTRA_LIGHT);
167    font_weight!(font_light, LIGHT);
168    font_weight!(font_normal, NORMAL);
169    font_weight!(font_medium, MEDIUM);
170    font_weight!(font_semibold, SEMIBOLD);
171    font_weight!(font_bold, BOLD);
172    font_weight!(font_extrabold, EXTRA_BOLD);
173    font_weight!(font_black, BLACK);
174
175    /// Set as Popover style
176    #[inline]
177    fn popover_style(self, cx: &App) -> Self {
178        self.bg(cx.theme().popover)
179            .text_color(cx.theme().popover_foreground)
180            .border_1()
181            .border_color(cx.theme().border)
182            .shadow_lg()
183            .rounded(cx.theme().radius)
184    }
185
186    /// Set corner radii for the element.
187    fn corner_radii(self, radius: Corners<Pixels>) -> Self {
188        self.rounded_tl(radius.top_left)
189            .rounded_tr(radius.top_right)
190            .rounded_bl(radius.bottom_left)
191            .rounded_br(radius.bottom_right)
192    }
193}
194
195impl<E: Styled> StyledExt for E {}
196
197/// A size for elements.
198#[derive(Clone, Default, Copy, PartialEq, Eq, Debug, Deserialize, Serialize)]
199pub enum Size {
200    Size(Pixels),
201    XSmall,
202    Small,
203    #[default]
204    Medium,
205    Large,
206}
207
208impl Size {
209    fn as_f32(&self) -> f32 {
210        match self {
211            Size::Size(val) => val.as_f32(),
212            Size::XSmall => 0.,
213            Size::Small => 1.,
214            Size::Medium => 2.,
215            Size::Large => 3.,
216        }
217    }
218
219    /// Returns the size as a static string.
220    pub fn as_str(&self) -> &'static str {
221        match self {
222            Size::XSmall => "xs",
223            Size::Small => "sm",
224            Size::Medium => "md",
225            Size::Large => "lg",
226            Size::Size(_) => "custom",
227        }
228    }
229
230    /// Create a Size from a static string.
231    ///
232    /// - "xs" or "xsmall"
233    /// - "sm" or "small"
234    /// - "md" or "medium"
235    /// - "lg" or "large"
236    ///
237    /// Any other value will return Size::Medium.
238    pub fn from_str(size: &str) -> Self {
239        match size.to_lowercase().as_str() {
240            "xs" | "xsmall" => Size::XSmall,
241            "sm" | "small" => Size::Small,
242            "md" | "medium" => Size::Medium,
243            "lg" | "large" => Size::Large,
244            _ => Size::Medium,
245        }
246    }
247
248    /// Returns the height for table row.
249    #[inline]
250    pub fn table_row_height(&self) -> Pixels {
251        match self {
252            Size::XSmall => px(26.),
253            Size::Small => px(30.),
254            Size::Large => px(40.),
255            _ => px(32.),
256        }
257    }
258
259    /// Returns the padding for a table cell.
260    #[inline]
261    pub fn table_cell_padding(&self) -> Edges<Pixels> {
262        match self {
263            Size::XSmall => Edges {
264                top: px(2.),
265                bottom: px(2.),
266                left: px(4.),
267                right: px(4.),
268            },
269            Size::Small => Edges {
270                top: px(3.),
271                bottom: px(3.),
272                left: px(6.),
273                right: px(6.),
274            },
275            Size::Large => Edges {
276                top: px(8.),
277                bottom: px(8.),
278                left: px(12.),
279                right: px(12.),
280            },
281            _ => Edges {
282                top: px(4.),
283                bottom: px(4.),
284                left: px(8.),
285                right: px(8.),
286            },
287        }
288    }
289
290    /// Returns a smaller size.
291    pub fn smaller(&self) -> Self {
292        match self {
293            Size::XSmall => Size::XSmall,
294            Size::Small => Size::XSmall,
295            Size::Medium => Size::Small,
296            Size::Large => Size::Medium,
297            Size::Size(val) => Size::Size(*val * 0.2),
298        }
299    }
300
301    /// Returns a larger size.
302    pub fn larger(&self) -> Self {
303        match self {
304            Size::XSmall => Size::Small,
305            Size::Small => Size::Medium,
306            Size::Medium => Size::Large,
307            Size::Large => Size::Large,
308            Size::Size(val) => Size::Size(*val * 1.2),
309        }
310    }
311
312    /// Return the max size between two sizes.
313    ///
314    /// e.g. `Size::XSmall.max(Size::Small)` will return `Size::XSmall`.
315    pub fn max(&self, other: Self) -> Self {
316        match (self, other) {
317            (Size::Size(a), Size::Size(b)) => Size::Size(px(a.as_f32().min(b.as_f32()))),
318            (Size::Size(a), _) => Size::Size(*a),
319            (_, Size::Size(b)) => Size::Size(b),
320            (a, b) if a.as_f32() < b.as_f32() => *a,
321            _ => other,
322        }
323    }
324
325    /// Return the min size between two sizes.
326    ///
327    /// e.g. `Size::XSmall.min(Size::Small)` will return `Size::Small`.
328    pub fn min(&self, other: Self) -> Self {
329        match (self, other) {
330            (Size::Size(a), Size::Size(b)) => Size::Size(px(a.as_f32().max(b.as_f32()))),
331            (Size::Size(a), _) => Size::Size(*a),
332            (_, Size::Size(b)) => Size::Size(b),
333            (a, b) if a.as_f32() > b.as_f32() => *a,
334            _ => other,
335        }
336    }
337
338    /// Returns the horizontal input padding.
339    pub fn input_px(&self) -> Pixels {
340        match self {
341            Self::Large => px(16.),
342            Self::Medium => px(12.),
343            Self::Small => px(8.),
344            Self::XSmall => px(4.),
345            _ => px(8.),
346        }
347    }
348
349    /// Returns the vertical input padding.
350    pub fn input_py(&self) -> Pixels {
351        match self {
352            Size::Large => px(10.),
353            Size::Medium => px(8.),
354            Size::Small => px(2.),
355            Size::XSmall => px(0.),
356            _ => px(2.),
357        }
358    }
359}
360
361impl From<Pixels> for Size {
362    fn from(size: Pixels) -> Self {
363        Size::Size(size)
364    }
365}
366
367/// A trait for defining element that can be selected.
368pub trait Selectable: Sized {
369    /// Set the selected state of the element.
370    fn selected(self, selected: bool) -> Self;
371
372    /// Returns true if the element is selected.
373    fn is_selected(&self) -> bool;
374
375    /// Set is the element mouse right clicked, default do nothing.
376    fn secondary_selected(self, _: bool) -> Self {
377        self
378    }
379}
380
381/// A trait for defining element that can be disabled.
382pub trait Disableable {
383    /// Set the disabled state of the element.
384    fn disabled(self, disabled: bool) -> Self;
385}
386
387/// A trait for setting the size of an element.
388/// Size::Medium is use by default.
389pub trait Sizable: Sized {
390    /// Set the ui::Size of this element.
391    ///
392    /// Also can receive a `ButtonSize` to convert to `IconSize`,
393    /// Or a `Pixels` to set a custom size: `px(30.)`
394    fn with_size(self, size: impl Into<Size>) -> Self;
395
396    /// Set to Size::XSmall
397    #[inline(always)]
398    fn xsmall(self) -> Self {
399        self.with_size(Size::XSmall)
400    }
401
402    /// Set to Size::Small
403    #[inline(always)]
404    fn small(self) -> Self {
405        self.with_size(Size::Small)
406    }
407
408    /// Set to Size::Large
409    #[inline(always)]
410    fn large(self) -> Self {
411        self.with_size(Size::Large)
412    }
413}
414
415#[allow(unused)]
416pub trait StyleSized<T: Styled> {
417    fn input_text_size(self, size: Size) -> Self;
418    fn input_size(self, size: Size) -> Self;
419    fn input_pl(self, size: Size) -> Self;
420    fn input_pr(self, size: Size) -> Self;
421    fn input_px(self, size: Size) -> Self;
422    fn input_py(self, size: Size) -> Self;
423    fn input_h(self, size: Size) -> Self;
424    fn list_size(self, size: Size) -> Self;
425    fn list_px(self, size: Size) -> Self;
426    fn list_py(self, size: Size) -> Self;
427    /// Apply size with the given `Size`.
428    fn size_with(self, size: Size) -> Self;
429    /// Apply the table cell size (Font size, padding) with the given `Size`.
430    fn table_cell_size(self, size: Size) -> Self;
431    fn button_text_size(self, size: Size) -> Self;
432}
433
434impl<T: Styled> StyleSized<T> for T {
435    #[inline]
436    fn input_text_size(self, size: Size) -> Self {
437        match size {
438            Size::XSmall => self.text_xs(),
439            Size::Small => self.text_sm(),
440            Size::Medium => self.text_sm(),
441            Size::Large => self.text_base(),
442            Size::Size(size) => self.text_size(size * 0.875),
443        }
444    }
445
446    #[inline]
447    fn input_size(self, size: Size) -> Self {
448        self.input_px(size).input_py(size).input_h(size)
449    }
450
451    #[inline]
452    fn input_pl(self, size: Size) -> Self {
453        self.pl(size.input_px())
454    }
455
456    #[inline]
457    fn input_pr(self, size: Size) -> Self {
458        self.pr(size.input_px())
459    }
460
461    #[inline]
462    fn input_px(self, size: Size) -> Self {
463        self.px(size.input_px())
464    }
465
466    #[inline]
467    fn input_py(self, size: Size) -> Self {
468        self.py(size.input_py())
469    }
470
471    #[inline]
472    fn input_h(self, size: Size) -> Self {
473        match size {
474            Size::Large => self.h_11(),
475            Size::Medium => self.h_8(),
476            Size::Small => self.h_6(),
477            Size::XSmall => self.h_5(),
478            _ => self.h_6(),
479        }
480    }
481
482    #[inline]
483    fn list_size(self, size: Size) -> Self {
484        self.list_px(size).list_py(size).input_text_size(size)
485    }
486
487    #[inline]
488    fn list_px(self, size: Size) -> Self {
489        match size {
490            Size::Small => self.px_2(),
491            _ => self.px_3(),
492        }
493    }
494
495    #[inline]
496    fn list_py(self, size: Size) -> Self {
497        match size {
498            Size::Large => self.py_2(),
499            Size::Medium => self.py_1(),
500            Size::Small => self.py_0p5(),
501            _ => self.py_1(),
502        }
503    }
504
505    #[inline]
506    fn size_with(self, size: Size) -> Self {
507        match size {
508            Size::Large => self.size_11(),
509            Size::Medium => self.size_8(),
510            Size::Small => self.size_5(),
511            Size::XSmall => self.size_4(),
512            Size::Size(size) => self.size(size),
513        }
514    }
515
516    #[inline]
517    fn table_cell_size(self, size: Size) -> Self {
518        let padding = size.table_cell_padding();
519        match size {
520            Size::XSmall => self.text_sm(),
521            Size::Small => self.text_sm(),
522            _ => self,
523        }
524        .pl(padding.left)
525        .pr(padding.right)
526        .pt(padding.top)
527        .pb(padding.bottom)
528    }
529
530    fn button_text_size(self, size: Size) -> Self {
531        match size {
532            Size::XSmall => self.text_xs(),
533            Size::Small => self.text_sm(),
534            _ => self.text_base(),
535        }
536    }
537}
538
539pub(crate) trait FocusableExt<T: ParentElement + Styled + Sized> {
540    /// Add focus ring to the element.
541    fn focus_ring(self, is_focused: bool, margins: Pixels, window: &Window, cx: &App) -> Self;
542}
543
544impl<T: ParentElement + Styled + Sized> FocusableExt<T> for T {
545    fn focus_ring(mut self, is_focused: bool, margins: Pixels, window: &Window, cx: &App) -> Self {
546        if !is_focused {
547            return self;
548        }
549
550        const RING_BORDER_WIDTH: Pixels = px(1.5);
551        let rem_size = window.rem_size();
552        let style = self.style();
553
554        let border_widths = Edges::<Pixels> {
555            top: style
556                .border_widths
557                .top
558                .map(|v| v.to_pixels(rem_size))
559                .unwrap_or_default(),
560            bottom: style
561                .border_widths
562                .bottom
563                .map(|v| v.to_pixels(rem_size))
564                .unwrap_or_default(),
565            left: style
566                .border_widths
567                .left
568                .map(|v| v.to_pixels(rem_size))
569                .unwrap_or_default(),
570            right: style
571                .border_widths
572                .right
573                .map(|v| v.to_pixels(rem_size))
574                .unwrap_or_default(),
575        };
576
577        // Update the radius based on element's corner radii and the ring border width.
578        let radius = Corners::<Pixels> {
579            top_left: style
580                .corner_radii
581                .top_left
582                .map(|v| v.to_pixels(rem_size))
583                .unwrap_or_default(),
584            top_right: style
585                .corner_radii
586                .top_right
587                .map(|v| v.to_pixels(rem_size))
588                .unwrap_or_default(),
589            bottom_left: style
590                .corner_radii
591                .bottom_left
592                .map(|v| v.to_pixels(rem_size))
593                .unwrap_or_default(),
594            bottom_right: style
595                .corner_radii
596                .bottom_right
597                .map(|v| v.to_pixels(rem_size))
598                .unwrap_or_default(),
599        }
600        .map(|v| *v + RING_BORDER_WIDTH);
601
602        let mut inner_style = StyleRefinement::default();
603        inner_style.corner_radii.top_left = Some(radius.top_left.into());
604        inner_style.corner_radii.top_right = Some(radius.top_right.into());
605        inner_style.corner_radii.bottom_left = Some(radius.bottom_left.into());
606        inner_style.corner_radii.bottom_right = Some(radius.bottom_right.into());
607
608        let inset = RING_BORDER_WIDTH + margins;
609
610        self.child(
611            div()
612                .flex_none()
613                .absolute()
614                .top(-(inset + border_widths.top))
615                .left(-(inset + border_widths.left))
616                .right(-(inset + border_widths.right))
617                .bottom(-(inset + border_widths.bottom))
618                .border(RING_BORDER_WIDTH)
619                .border_color(cx.theme().ring.alpha(0.2))
620                .refine_style(&inner_style),
621        )
622    }
623}
624
625/// A trait for defining element that can be collapsed.
626pub trait Collapsible {
627    fn collapsed(self, collapsed: bool) -> Self;
628    fn is_collapsed(&self) -> bool;
629}
630
631#[cfg(test)]
632mod tests {
633    use gpui::px;
634
635    use crate::Size;
636
637    #[test]
638    fn test_size_max_min() {
639        assert_eq!(Size::Small.min(Size::XSmall), Size::Small);
640        assert_eq!(Size::XSmall.min(Size::Small), Size::Small);
641        assert_eq!(Size::Small.min(Size::Medium), Size::Medium);
642        assert_eq!(Size::Medium.min(Size::Large), Size::Large);
643        assert_eq!(Size::Large.min(Size::Small), Size::Large);
644
645        assert_eq!(
646            Size::Size(px(10.)).min(Size::Size(px(20.))),
647            Size::Size(px(20.))
648        );
649
650        // Min
651        assert_eq!(Size::Small.max(Size::XSmall), Size::XSmall);
652        assert_eq!(Size::XSmall.max(Size::Small), Size::XSmall);
653        assert_eq!(Size::Small.max(Size::Medium), Size::Small);
654        assert_eq!(Size::Medium.max(Size::Large), Size::Medium);
655        assert_eq!(Size::Large.max(Size::Small), Size::Small);
656
657        assert_eq!(
658            Size::Size(px(10.)).max(Size::Size(px(20.))),
659            Size::Size(px(10.))
660        );
661    }
662
663    #[test]
664    fn test_size_as_str() {
665        assert_eq!(Size::XSmall.as_str(), "xs");
666        assert_eq!(Size::Small.as_str(), "sm");
667        assert_eq!(Size::Medium.as_str(), "md");
668        assert_eq!(Size::Large.as_str(), "lg");
669        assert_eq!(Size::Size(px(15.)).as_str(), "custom");
670    }
671
672    #[test]
673    fn test_size_from_str() {
674        assert_eq!(Size::from_str("xs"), Size::XSmall);
675        assert_eq!(Size::from_str("xsmall"), Size::XSmall);
676        assert_eq!(Size::from_str("sm"), Size::Small);
677        assert_eq!(Size::from_str("small"), Size::Small);
678        assert_eq!(Size::from_str("md"), Size::Medium);
679        assert_eq!(Size::from_str("medium"), Size::Medium);
680        assert_eq!(Size::from_str("lg"), Size::Large);
681        assert_eq!(Size::from_str("large"), Size::Large);
682        assert_eq!(Size::from_str("unknown"), Size::Medium);
683
684        // Case insensitive
685        assert_eq!(Size::from_str("XS"), Size::XSmall);
686        assert_eq!(Size::from_str("SMALL"), Size::Small);
687        assert_eq!(Size::from_str("Md"), Size::Medium);
688    }
689}