gpui_component/
styled.rs

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