Skip to main content

rgpui_component/
styled.rs

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