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