tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
1//! `llimphi-widget-scaffold` — chasis de página.
2//!
3//! Inspiración Flutter `Scaffold`/Material 3 layout. Compone un layout
4//! de página común:
5//!
6//! ```text
7//! ┌──────────────────────────────┐
8//! │ app bar (opcional) │ ← 48 px
9//! ├──────────────────────────────┤
10//! │ │
11//! │ body (flex-grow 1) │
12//! │ ╭──╮│
13//! │ │+ ││ ← FAB opcional (bottom-end)
14//! │ ╰──╯│
15//! ├──────────────────────────────┤
16//! │ bottom bar (opcional) │ ← 56 px
17//! └──────────────────────────────┘
18//! ```
19//!
20//! Los drawers laterales se ofrecen como **overlays** (la app los pasa
21//! por `view_overlay`, no por el body — así no roban el layout cuando
22//! están cerrados). Este widget sólo aporta el chasis central; los
23//! drawers son responsabilidad de quien los abre.
24
25#![forbid(unsafe_code)]
26
27
28// Sin `std`: este widget no usa nada del sistema —sólo `alloc` para
29// `String`/`Vec`—, y declararlo es lo que le permite cruzar a una jaula
30// WASM o al kernel. Antes enlazaba `std` por omisión (W4.6).
31#![no_std]
32
33extern crate alloc;
34
35use alloc::string::String;
36use alloc::vec::Vec;
37use alloc::vec;
38use llimphi_compositor::llimphi_layout::taffy::{
39 prelude::{length, percent, FlexDirection, Size, Style},
40 AlignItems, JustifyContent, Position,
41};
42use llimphi_compositor::peniko::Color;
43use llimphi_compositor::View;
44use llimphi_theme::Theme;
45
46#[derive(Debug, Clone, Copy)]
47pub struct ScaffoldPalette {
48 pub bg: Color,
49}
50
51impl ScaffoldPalette {
52 pub fn from_theme(t: &Theme) -> Self {
53 Self { bg: t.bg_app }
54 }
55}
56
57/// Opciones del scaffold — todas las superficies son opcionales (un
58/// scaffold con sólo `body` es válido y degenera en `View` con bg).
59pub struct ScaffoldSpec<Msg: Clone + 'static> {
60 pub app_bar: Option<View<Msg>>,
61 pub body: View<Msg>,
62 pub bottom_bar: Option<View<Msg>>,
63 pub fab: Option<View<Msg>>,
64}
65
66/// Compone el scaffold. Llamado típicamente desde el `view(model)` de la
67/// app como root. Para drawers, pasarlos por `view_overlay`.
68pub fn scaffold_view<Msg: Clone + 'static>(
69 spec: ScaffoldSpec<Msg>,
70 palette: &ScaffoldPalette,
71) -> View<Msg> {
72 let mut col_children: Vec<View<Msg>> = Vec::with_capacity(3);
73
74 if let Some(bar) = spec.app_bar {
75 col_children.push(
76 View::new(Style {
77 size: Size { width: percent(1.0_f32), height: length(48.0_f32) },
78 flex_shrink: 0.0,
79 ..Default::default()
80 })
81 .children(vec![bar]),
82 );
83 }
84
85 // Body con FAB anclado.
86 let mut body_layer_children: Vec<View<Msg>> = vec![spec.body];
87 if let Some(f) = spec.fab {
88 body_layer_children.push(
89 View::new(Style {
90 position: Position::Absolute,
91 inset: llimphi_compositor::llimphi_layout::taffy::Rect {
92 left: llimphi_compositor::llimphi_layout::taffy::prelude::auto(),
93 right: length(16.0_f32),
94 top: llimphi_compositor::llimphi_layout::taffy::prelude::auto(),
95 bottom: length(16.0_f32),
96 },
97 ..Default::default()
98 })
99 .children(vec![f]),
100 );
101 }
102 let body_layer = View::new(Style {
103 size: Size {
104 width: percent(1.0_f32),
105 height: llimphi_compositor::llimphi_layout::taffy::prelude::Dimension::auto(),
106 },
107 flex_grow: 1.0,
108 ..Default::default()
109 })
110 .children(body_layer_children);
111 col_children.push(body_layer);
112
113 if let Some(bar) = spec.bottom_bar {
114 col_children.push(
115 View::new(Style {
116 size: Size { width: percent(1.0_f32), height: length(56.0_f32) },
117 flex_shrink: 0.0,
118 ..Default::default()
119 })
120 .children(vec![bar]),
121 );
122 }
123
124 View::new(Style {
125 flex_direction: FlexDirection::Column,
126 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
127 ..Default::default()
128 })
129 .fill(palette.bg)
130 .children(col_children)
131}
132
133/// App bar estándar — barra superior 48 px con título a la izquierda y
134/// slot de acciones a la derecha. El caller pasa el `View` de las
135/// acciones (botones de ícono típicamente).
136pub fn app_bar_view<Msg: Clone + 'static>(
137 title: impl Into<String>,
138 actions: Vec<View<Msg>>,
139 palette: &ScaffoldPalette,
140 theme: &Theme,
141) -> View<Msg> {
142 use llimphi_compositor::llimphi_text::Alignment;
143 let _ = palette;
144 let title_view = View::new(Style {
145 size: Size {
146 width: llimphi_compositor::llimphi_layout::taffy::prelude::auto(),
147 height: percent(1.0_f32),
148 },
149 flex_grow: 1.0,
150 align_items: Some(AlignItems::CENTER),
151 justify_content: Some(JustifyContent::FLEX_START),
152 padding: llimphi_compositor::llimphi_layout::taffy::Rect {
153 left: length(16.0_f32),
154 right: length(0.0_f32),
155 top: length(0.0_f32),
156 bottom: length(0.0_f32),
157 },
158 ..Default::default()
159 })
160 .text_aligned(title.into(), 16.0, theme.fg_text, Alignment::Start)
161 .bold();
162 let actions_view = View::new(Style {
163 flex_direction: FlexDirection::Row,
164 size: Size {
165 width: llimphi_compositor::llimphi_layout::taffy::prelude::auto(),
166 height: percent(1.0_f32),
167 },
168 align_items: Some(AlignItems::CENTER),
169 gap: Size { width: length(4.0_f32), height: length(0.0_f32) },
170 padding: llimphi_compositor::llimphi_layout::taffy::Rect {
171 left: length(0.0_f32),
172 right: length(8.0_f32),
173 top: length(0.0_f32),
174 bottom: length(0.0_f32),
175 },
176 ..Default::default()
177 })
178 .children(actions);
179 View::new(Style {
180 flex_direction: FlexDirection::Row,
181 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
182 align_items: Some(AlignItems::CENTER),
183 ..Default::default()
184 })
185 .fill(theme.bg_panel)
186 .children(vec![title_view, actions_view])
187}