Skip to main content

aetna_core/widgets/
sheet.rs

1//! Sheet anatomy — edge-attached dialog surfaces.
2//!
3//! This mirrors shadcn's Sheet shape without adding a new runtime
4//! primitive: a sheet is an overlay, a dismiss scrim, and a panel aligned
5//! to one viewport edge.
6
7use std::panic::Location;
8
9use crate::metrics::MetricsRole;
10use crate::style::StyleProfile;
11use crate::tokens;
12use crate::tree::*;
13use crate::widgets::overlay::{overlay, scrim};
14use crate::widgets::text::{h3, text};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum SheetSide {
19    Left,
20    Right,
21    Top,
22    Bottom,
23}
24
25/// A blocking edge-attached sheet with a keyed dismiss scrim.
26///
27/// Keys:
28/// - `{key}:dismiss` — emitted when the user clicks outside the sheet.
29#[track_caller]
30pub fn sheet<I, E>(key: impl Into<String>, side: SheetSide, body: I) -> El
31where
32    I: IntoIterator<Item = E>,
33    E: Into<El>,
34{
35    let key = key.into();
36    let layer = overlay([
37        scrim(format!("{key}:dismiss")),
38        sheet_content(side, body).block_pointer(),
39    ]);
40
41    match side {
42        SheetSide::Left => layer.align(Align::Start).justify(Justify::Center),
43        SheetSide::Right => layer.align(Align::End).justify(Justify::Center),
44        SheetSide::Top => layer.align(Align::Center).justify(Justify::Start),
45        SheetSide::Bottom => layer.align(Align::Center).justify(Justify::End),
46    }
47}
48
49#[track_caller]
50pub fn sheet_content<I, E>(side: SheetSide, children: I) -> El
51where
52    I: IntoIterator<Item = E>,
53    E: Into<El>,
54{
55    let mut content = El::new(Kind::Custom("sheet_content"))
56        .at_loc(Location::caller())
57        .style_profile(StyleProfile::Surface)
58        .metrics_role(MetricsRole::Panel)
59        .surface_role(SurfaceRole::Popover)
60        .children(children)
61        .fill(tokens::POPOVER)
62        .stroke(tokens::BORDER)
63        .default_radius(0.0)
64        .shadow(tokens::SHADOW_LG)
65        .default_padding(tokens::SPACE_4)
66        .default_gap(tokens::SPACE_4)
67        .axis(Axis::Column)
68        .align(Align::Stretch)
69        .clip();
70
71    match side {
72        SheetSide::Left | SheetSide::Right => {
73            content.width = Size::Fixed(360.0);
74            content.height = Size::Fill(1.0);
75        }
76        SheetSide::Top | SheetSide::Bottom => {
77            content.width = Size::Fill(1.0);
78            content.height = Size::Hug;
79        }
80    }
81
82    content
83}
84
85#[track_caller]
86pub fn sheet_header<I, E>(children: I) -> El
87where
88    I: IntoIterator<Item = E>,
89    E: Into<El>,
90{
91    column(children)
92        .at_loc(Location::caller())
93        .width(Size::Fill(1.0))
94        .height(Size::Hug)
95        .gap(tokens::SPACE_1)
96}
97
98#[track_caller]
99pub fn sheet_footer<I, E>(children: I) -> El
100where
101    I: IntoIterator<Item = E>,
102    E: Into<El>,
103{
104    row(children)
105        .at_loc(Location::caller())
106        .width(Size::Fill(1.0))
107        .height(Size::Hug)
108        .gap(tokens::SPACE_2)
109        .align(Align::Center)
110        .justify(Justify::End)
111}
112
113#[track_caller]
114pub fn sheet_title(title: impl Into<String>) -> El {
115    h3(title)
116        .at_loc(Location::caller())
117        .line_height(tokens::TEXT_BASE.size)
118}
119
120#[track_caller]
121pub fn sheet_description(description: impl Into<String>) -> El {
122    text(description)
123        .at_loc(Location::caller())
124        .muted()
125        .wrap_text()
126        .width(Size::Fill(1.0))
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn sheet_aligns_layer_by_side() {
135        let right = sheet("settings", SheetSide::Right, [sheet_title("Settings")]);
136        assert_eq!(right.align, Align::End);
137        assert_eq!(right.justify, Justify::Center);
138        assert_eq!(right.children[0].key.as_deref(), Some("settings:dismiss"));
139        assert!(right.children[1].block_pointer);
140
141        let bottom = sheet("activity", SheetSide::Bottom, [sheet_title("Activity")]);
142        assert_eq!(bottom.align, Align::Center);
143        assert_eq!(bottom.justify, Justify::End);
144    }
145
146    #[test]
147    fn vertical_sheets_fill_height_and_horizontal_sheets_fill_width() {
148        let side = sheet_content(SheetSide::Right, [sheet_title("Settings")]);
149        assert_eq!(side.width, Size::Fixed(360.0));
150        assert_eq!(side.height, Size::Fill(1.0));
151        assert_eq!(side.radius, crate::tree::Corners::ZERO);
152
153        let bottom = sheet_content(SheetSide::Bottom, [sheet_title("Activity")]);
154        assert_eq!(bottom.width, Size::Fill(1.0));
155        assert_eq!(bottom.height, Size::Hug);
156    }
157}