Skip to main content

rosace_widgets/tree/
scaffold.rs

1use rosace_core::types::{Point, Rect, Size};
2use rosace_layout::Constraints;
3use rosace_render::Color;
4use super::{Widget, LayoutCtx, PaintCtx, BoxedWidget, avail_w, avail_h};
5
6/// Full-page layout: optional AppBar + optional NavRail sidebar + body + optional FAB.
7///
8/// This is the root widget for any screen in a ROSACE app — analogous to
9/// Flutter's `Scaffold`, SwiftUI's `NavigationSplitView`, or Compose's `Scaffold`.
10pub struct Scaffold {
11    pub background: Color,
12    pub app_bar: Option<BoxedWidget>,
13    pub nav_rail: Option<BoxedWidget>,
14    pub body: BoxedWidget,
15    pub fab: Option<BoxedWidget>,
16    pub bottom_bar: Option<BoxedWidget>,
17    pub sidebar_right: Option<BoxedWidget>,
18}
19
20impl Scaffold {
21    pub fn new(body: impl Widget + 'static) -> Self {
22        Self {
23            background: Color::rgba(0, 0, 0, 0), // sentinel: use theme.background
24            app_bar: None,
25            nav_rail: None,
26            body: Box::new(body),
27            fab: None,
28            bottom_bar: None,
29            sidebar_right: None,
30        }
31    }
32
33    pub fn background(mut self, c: Color) -> Self { self.background = c; self }
34    pub fn app_bar(mut self, w: impl Widget + 'static) -> Self { self.app_bar = Some(Box::new(w)); self }
35    pub fn nav_rail(mut self, w: impl Widget + 'static) -> Self { self.nav_rail = Some(Box::new(w)); self }
36    pub fn fab(mut self, w: impl Widget + 'static) -> Self { self.fab = Some(Box::new(w)); self }
37    pub fn bottom_bar(mut self, w: impl Widget + 'static) -> Self { self.bottom_bar = Some(Box::new(w)); self }
38    pub fn sidebar_right(mut self, w: impl Widget + 'static) -> Self { self.sidebar_right = Some(Box::new(w)); self }
39}
40
41impl Widget for Scaffold {
42    fn layout(&self, ctx: &LayoutCtx) -> Size {
43        let constraints = ctx.constraints;
44        Size { width: avail_w(constraints), height: avail_h(constraints) }
45    }
46
47    fn paint(&self, ctx: &mut PaintCtx) {
48        let full = ctx.rect;
49
50        // Background — use explicit color or fall back to theme.background.
51        // Painted over the FULL rect (extends behind the status bar/notch,
52        // matching normal mobile-app behavior); only the content below is
53        // inset by the safe area.
54        let bg = if self.background.a == 0 {
55            ctx.tc(ctx.theme.colors.background)
56        } else {
57            self.background
58        };
59        ctx.fill_rect(full, bg);
60
61        // Keep interactive content (AppBar, body, bottom bar, FAB) clear of
62        // platform-reserved regions — iOS status bar/Dynamic Island/home
63        // indicator, Android status/nav bars. Zero on platforms without one
64        // (desktop, web), so this is a no-op there.
65        let sa = rosace_core::use_safe_area();
66        let total = Rect {
67            origin: Point { x: full.origin.x + sa.left, y: full.origin.y + sa.top },
68            size: Size {
69                width: (full.size.width - sa.left - sa.right).max(0.0),
70                height: (full.size.height - sa.top - sa.bottom).max(0.0),
71            },
72        };
73
74        // Measure app bar
75        let bar_h = self.app_bar.as_ref()
76            .map(|w| w.layout(&ctx.layout_ctx(Constraints::tight(total.size.width, 44.0))).height)
77            .unwrap_or(0.0);
78
79        // Measure bottom bar
80        let bottom_h = self.bottom_bar.as_ref()
81            .map(|w| w.layout(&ctx.layout_ctx(Constraints::tight(total.size.width, 48.0))).height)
82            .unwrap_or(0.0);
83
84        // Paint app bar
85        if let Some(bar) = &self.app_bar {
86            bar.paint(&mut ctx.child(Rect {
87                origin: total.origin,
88                size: Size { width: total.size.width, height: bar_h },
89            }));
90        }
91
92        // Content area (below bar, above bottom bar)
93        let content_y = total.origin.y + bar_h;
94        let content_h = total.size.height - bar_h - bottom_h;
95
96        // Measure nav rail
97        let rail_w = self.nav_rail.as_ref()
98            .map(|w| w.layout(&ctx.layout_ctx(Constraints::loose(300.0, content_h))).width)
99            .unwrap_or(0.0);
100
101        // Paint nav rail
102        if let Some(rail) = &self.nav_rail {
103            rail.paint(&mut ctx.child(Rect {
104                origin: Point { x: total.origin.x, y: content_y },
105                size: Size { width: rail_w, height: content_h },
106            }));
107        }
108
109        // Measure right sidebar
110        let rsb_w = self.sidebar_right.as_ref()
111            .map(|w| w.layout(&ctx.layout_ctx(Constraints::loose(400.0, content_h))).width)
112            .unwrap_or(0.0);
113
114        // Paint right sidebar
115        if let Some(rsb) = &self.sidebar_right {
116            rsb.paint(&mut ctx.child(Rect {
117                origin: Point { x: total.origin.x + total.size.width - rsb_w, y: content_y },
118                size: Size { width: rsb_w, height: content_h },
119            }));
120        }
121
122        // Paint body
123        let body_x = total.origin.x + rail_w;
124        let body_w = total.size.width - rail_w - rsb_w;
125        self.body.paint(&mut ctx.child(Rect {
126            origin: Point { x: body_x, y: content_y },
127            size: Size { width: body_w, height: content_h },
128        }));
129
130        // Paint bottom bar
131        if let Some(bb) = &self.bottom_bar {
132            super::set_bottom_overlay_inset(bottom_h);
133            bb.paint(&mut ctx.child(Rect {
134                origin: Point { x: total.origin.x, y: total.origin.y + total.size.height - bottom_h },
135                size: Size { width: total.size.width, height: bottom_h },
136            }));
137        }
138
139        // FAB (bottom-right)
140        if let Some(fab) = &self.fab {
141            let fab_size = fab.layout(&ctx.layout_ctx(Constraints::loose(60.0, 60.0)));
142            let fab_x = total.origin.x + total.size.width - fab_size.width - 20.0;
143            let fab_y = total.origin.y + total.size.height - bottom_h - fab_size.height - 20.0;
144            fab.paint(&mut ctx.child(Rect {
145                origin: Point { x: fab_x, y: fab_y },
146                size: fab_size,
147            }));
148        }
149    }
150}