gpui_component/
styled.rs

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