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);