Skip to main content

herogpui_components/
surface.rs

1//! Surface — port of `@heroui/surface`.
2//!
3//! A container that applies surface-level styling. Mirrors the React API:
4//! `variant` of `transparent | default | secondary | tertiary`. Upstream
5//! `.surface` is only `relative text-foreground` plus each variant's
6//! fill/foreground classes; the docs examples add their `flex flex-col gap-3
7//! rounded-3xl p-6` skeleton through `className`, so it is not a Surface
8//! default. This port keeps a minimal column skeleton with zero default
9//! padding and gap so the repository `padding`/`gap` builders work; they are
10//! conveniences, not upstream props. Upstream also publishes its variant
11//! through `SurfaceContext`; GPUI has no ancestor context propagation, so
12//! nothing here reads the surrounding surface.
13
14use gpui::{
15    div, px, AnyElement, App, IntoElement, ParentElement, Pixels, RenderOnce, Styled, Window,
16};
17use herogpui_theme::ActiveTheme;
18
19/// Prominence level of a surface (`variant` prop).
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21pub enum SurfaceVariant {
22    /// No background — for overlays and cards with a custom background.
23    Transparent,
24    /// `bg-surface`.
25    #[default]
26    Default,
27    /// `bg-surface-secondary`.
28    Secondary,
29    /// `bg-surface-tertiary`.
30    Tertiary,
31}
32
33/// HeroUI Surface.
34#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
35#[derive(IntoElement)]
36pub struct Surface {
37    variant: SurfaceVariant,
38    padding: Pixels,
39    gap: Pixels,
40    children: Vec<AnyElement>,
41    /// The `sx` slot, refined over the root style at the end of render.
42    sx: Option<Box<gpui::StyleRefinement>>,
43}
44
45impl Surface {
46    /// Creates a surface.
47    pub fn new() -> Self {
48        Self {
49            variant: SurfaceVariant::default(),
50            padding: px(0.),
51            gap: px(0.),
52            children: Vec::new(),
53            sx: None,
54        }
55    }
56
57    /// Sets the surface variant.
58    pub fn variant(mut self, variant: SurfaceVariant) -> Self {
59        self.variant = variant;
60        self
61    }
62
63    /// The one slot for caller-owned low-level styling: GPUI's styling methods
64    /// (`bg`, `text_color`, `w`, `h`, `p`, `rounded`, `border_color`, …)
65    /// applied to the surface's root element after every value the variant and
66    /// the active theme chose, so they win.
67    pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
68        crate::util::refine_sx(&mut self.sx, style);
69        self
70    }
71
72    /// Repository convenience, not an upstream prop.
73    pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
74        self.padding = padding.into();
75        self
76    }
77
78    /// Repository convenience, not an upstream prop.
79    pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
80        self.gap = gap.into();
81        self
82    }
83}
84
85impl Default for Surface {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91impl ParentElement for Surface {
92    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
93        self.children.extend(elements);
94    }
95}
96
97impl RenderOnce for Surface {
98    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
99        let colors = cx.colors();
100        let mut el = div()
101            .flex()
102            .flex_col()
103            .gap(self.gap)
104            .p(self.padding)
105            .text_color(colors.foreground);
106
107        el = match self.variant {
108            // `.surface--transparent` is only `bg-transparent`; GPUI's default
109            // div background is already transparent, so nothing extra to paint.
110            SurfaceVariant::Transparent => el,
111            // Each fill brings its own foreground: `.surface--secondary` is
112            // `bg-surface-secondary text-surface-secondary-foreground`, and the
113            // text colour was going unset.
114            SurfaceVariant::Default => el
115                .bg(colors.surface.background)
116                .text_color(colors.surface.foreground),
117            SurfaceVariant::Secondary => el
118                .bg(colors.surface_secondary)
119                .text_color(colors.surface_secondary_foreground()),
120            SurfaceVariant::Tertiary => el
121                .bg(colors.surface_tertiary)
122                .text_color(colors.surface_tertiary_foreground()),
123        };
124
125        let el = el.children(self.children);
126        crate::util::apply_sx(el, &self.sx)
127    }
128}
129
130crate::util::impl_component_styled!(Surface);