1#![allow(non_snake_case)]
4
5use std::rc::Rc;
6
7use cranpose_core::{NodeId, SlotId};
8use cranpose_ui_graphics::{Color, EdgeInsets};
9use cranpose_ui_layout::{Constraints, Placement};
10
11use super::SubcomposeLayout;
12use crate::{
13 composable,
14 layout_direction::{layout_direction, LayoutDirection},
15 subcompose_layout::{SubcomposeLayoutScope, SubcomposeMeasureScope},
16 Modifier,
17};
18
19#[derive(Clone, Copy, Debug, Default, PartialEq)]
24pub struct PaddingValues {
25 pub start: f32,
26 pub top: f32,
27 pub end: f32,
28 pub bottom: f32,
29}
30
31impl PaddingValues {
32 pub const fn new(start: f32, top: f32, end: f32, bottom: f32) -> Self {
33 Self {
34 start,
35 top,
36 end,
37 bottom,
38 }
39 }
40
41 pub const fn all(value: f32) -> Self {
43 Self::new(value, value, value, value)
44 }
45
46 pub fn apply_to(self, modifier: Modifier) -> Modifier {
48 self.apply_to_in(modifier, layout_direction())
49 }
50
51 pub fn apply_to_in(self, modifier: Modifier, direction: LayoutDirection) -> Modifier {
53 modifier.padding_relative_in(direction, self.start, self.top, self.end, self.bottom)
54 }
55
56 pub fn physical(self, direction: LayoutDirection) -> EdgeInsets {
58 let (left, right) = direction.resolve(self.start, self.end);
59 EdgeInsets::from_components(left, self.top, right, self.bottom)
60 }
61
62 pub fn max(self, other: Self) -> Self {
64 Self::new(
65 self.start.max(other.start),
66 self.top.max(other.top),
67 self.end.max(other.end),
68 self.bottom.max(other.bottom),
69 )
70 }
71}
72
73#[derive(Clone, Copy, Debug, Default, PartialEq)]
78pub struct ScaffoldColors {
79 pub background: Option<Color>,
81 pub top_bar: Option<Color>,
83 pub bottom_bar: Option<Color>,
85}
86
87impl ScaffoldColors {
88 pub fn uniform(background: Color) -> Self {
91 Self {
92 background: Some(background),
93 top_bar: Some(background),
94 bottom_bar: Some(background),
95 }
96 }
97
98 pub fn with_background(mut self, color: Color) -> Self {
100 self.background = Some(color);
101 self
102 }
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub struct ScaffoldContentInsets {
112 pub top: bool,
114 pub bottom: bool,
116 pub start: bool,
118 pub end: bool,
120}
121
122impl Default for ScaffoldContentInsets {
123 fn default() -> Self {
124 Self {
125 top: true,
126 bottom: true,
127 start: true,
128 end: true,
129 }
130 }
131}
132
133impl ScaffoldContentInsets {
134 pub const fn none() -> Self {
136 Self {
137 top: false,
138 bottom: false,
139 start: false,
140 end: false,
141 }
142 }
143
144 fn filter(self, insets: PaddingValues) -> PaddingValues {
146 PaddingValues::new(
147 if self.start { insets.start } else { 0.0 },
148 if self.top { insets.top } else { 0.0 },
149 if self.end { insets.end } else { 0.0 },
150 if self.bottom { insets.bottom } else { 0.0 },
151 )
152 }
153}
154
155#[derive(Clone)]
157struct ScaffoldSlots {
158 top_bar: Rc<dyn Fn()>,
159 bottom_bar: Rc<dyn Fn()>,
160 floating_action: Rc<dyn Fn()>,
161 content: Rc<dyn Fn(PaddingValues)>,
162}
163
164impl PartialEq for ScaffoldSlots {
165 fn eq(&self, other: &Self) -> bool {
166 Rc::ptr_eq(&self.top_bar, &other.top_bar)
167 && Rc::ptr_eq(&self.bottom_bar, &other.bottom_bar)
168 && Rc::ptr_eq(&self.floating_action, &other.floating_action)
169 && Rc::ptr_eq(&self.content, &other.content)
170 }
171}
172
173#[derive(Clone, Copy, Debug, Default, PartialEq)]
175pub struct ScaffoldSpec {
176 pub colors: ScaffoldColors,
178 pub content_insets: ScaffoldContentInsets,
180}
181
182impl ScaffoldSpec {
183 pub fn with_colors(mut self, colors: ScaffoldColors) -> Self {
185 self.colors = colors;
186 self
187 }
188}
189
190pub fn Scaffold<T, B, C>(modifier: Modifier, top_bar: T, bottom_bar: B, content: C) -> NodeId
197where
198 T: Fn() + 'static,
199 B: Fn() + 'static,
200 C: Fn(PaddingValues) + 'static,
201{
202 ScaffoldWith(
203 modifier,
204 ScaffoldSpec::default(),
205 top_bar,
206 bottom_bar,
207 || {},
208 content,
209 )
210}
211
212pub fn ScaffoldWith<T, B, F, C>(
214 modifier: Modifier,
215 spec: ScaffoldSpec,
216 top_bar: T,
217 bottom_bar: B,
218 floating_action: F,
219 content: C,
220) -> NodeId
221where
222 T: Fn() + 'static,
223 B: Fn() + 'static,
224 F: Fn() + 'static,
225 C: Fn(PaddingValues) + 'static,
226{
227 ScaffoldImpl(
228 modifier,
229 spec,
230 ScaffoldSlots {
231 top_bar: Rc::new(top_bar),
232 bottom_bar: Rc::new(bottom_bar),
233 floating_action: Rc::new(floating_action),
234 content: Rc::new(content),
235 },
236 )
237}
238
239const FLOATING_ACTION_MARGIN: f32 = 16.0;
241
242#[composable]
243fn ScaffoldImpl(modifier: Modifier, spec: ScaffoldSpec, slots: ScaffoldSlots) -> NodeId {
244 let direction = layout_direction();
245 let insets = crate::safe_area::window_insets().combined();
246 let (start_inset, end_inset) = match direction {
247 LayoutDirection::Ltr => (insets.left, insets.right),
248 LayoutDirection::Rtl => (insets.right, insets.left),
249 };
250 let window_padding = spec.content_insets.filter(PaddingValues::new(
251 start_inset,
252 insets.top,
253 end_inset,
254 insets.bottom,
255 ));
256
257 let colors = spec.colors;
258 let modifier = match colors.background {
259 Some(background) => modifier.background(background),
260 None => modifier,
261 };
262
263 let top_bar = slots.top_bar;
264 let bottom_bar = slots.bottom_bar;
265 let floating_action = slots.floating_action;
266 let content = slots.content;
267
268 SubcomposeLayout(modifier, move |scope, constraints| {
269 let width = constraints.max_width.max(constraints.min_width);
270 let height = constraints.max_height.max(constraints.min_height);
271 let loose = Constraints::loose(width, height);
272
273 let top_content = Rc::clone(&top_bar);
274 let top_nodes = scope.subcompose(SlotId::new(0), move || top_content());
275 let mut top_height = 0.0_f32;
276 let mut top_placements = Vec::with_capacity(top_nodes.len());
277 for node in top_nodes {
278 let placeable = scope.measure(node, loose);
279 top_height = top_height.max(placeable.height());
280 top_placements.push(Placement::new(placeable.node_id(), 0.0, 0.0, 1));
281 }
282
283 let bottom_content = Rc::clone(&bottom_bar);
284 let bottom_nodes = scope.subcompose(SlotId::new(1), move || bottom_content());
285 let mut bottom_height = 0.0_f32;
286 let mut bottom_placeables = Vec::with_capacity(bottom_nodes.len());
287 for node in bottom_nodes {
288 let placeable = scope.measure(node, loose);
289 bottom_height = bottom_height.max(placeable.height());
290 bottom_placeables.push(placeable);
291 }
292
293 let padding = window_padding.max(PaddingValues::new(0.0, top_height, 0.0, bottom_height));
296 let content_slot = Rc::clone(&content);
297 let content_nodes = scope.subcompose(SlotId::new(2), move || content_slot(padding));
298
299 let mut placements = Vec::with_capacity(
300 top_placements.len() + bottom_placeables.len() + content_nodes.len() + 1,
301 );
302 for node in content_nodes {
303 let placeable = scope.measure(node, Constraints::tight(width, height));
304 placements.push(Placement::new(placeable.node_id(), 0.0, 0.0, 0));
305 }
306 placements.extend(top_placements);
307 for placeable in bottom_placeables {
308 placements.push(Placement::new(
309 placeable.node_id(),
310 0.0,
311 (height - placeable.height()).max(0.0),
312 1,
313 ));
314 }
315
316 let floating_content = Rc::clone(&floating_action);
319 let floating_nodes = scope.subcompose(SlotId::new(3), move || floating_content());
320 for node in floating_nodes {
321 let placeable = scope.measure(node, loose);
322 let bottom_gap = padding.bottom + FLOATING_ACTION_MARGIN;
323 let end_gap = padding.end + FLOATING_ACTION_MARGIN;
324 let x = match direction {
325 LayoutDirection::Ltr => (width - placeable.width() - end_gap).max(0.0),
326 LayoutDirection::Rtl => end_gap.min((width - placeable.width()).max(0.0)),
327 };
328 let y = (height - placeable.height() - bottom_gap).max(0.0);
329 placements.push(Placement::new(placeable.node_id(), x, y, 2));
330 }
331
332 scope.layout(width, height, placements)
333 })
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn padding_values_preserve_each_edge() {
342 assert_eq!(
343 PaddingValues::new(1.0, 2.0, 3.0, 4.0),
344 PaddingValues {
345 start: 1.0,
346 top: 2.0,
347 end: 3.0,
348 bottom: 4.0,
349 }
350 );
351 assert_eq!(
352 PaddingValues::all(6.0),
353 PaddingValues::new(6.0, 6.0, 6.0, 6.0)
354 );
355 }
356
357 #[test]
358 fn reading_order_padding_swaps_sides_in_a_right_to_left_layout() {
359 let padding = PaddingValues::new(24.0, 2.0, 8.0, 4.0);
360 assert_eq!(
361 padding.physical(LayoutDirection::Ltr),
362 EdgeInsets::from_components(24.0, 2.0, 8.0, 4.0)
363 );
364 assert_eq!(
365 padding.physical(LayoutDirection::Rtl),
366 EdgeInsets::from_components(8.0, 2.0, 24.0, 4.0)
367 );
368 }
369
370 #[test]
371 fn merging_takes_the_larger_of_each_edge() {
372 let bars = PaddingValues::new(0.0, 56.0, 0.0, 0.0);
373 let insets = PaddingValues::new(0.0, 24.0, 0.0, 48.0);
374 assert_eq!(insets.max(bars), PaddingValues::new(0.0, 56.0, 0.0, 48.0));
375 }
376
377 #[test]
378 fn a_screen_can_keep_the_insets_it_draws_under() {
379 let insets = PaddingValues::new(4.0, 24.0, 4.0, 48.0);
380 let consumed = ScaffoldContentInsets {
381 top: false,
382 ..ScaffoldContentInsets::default()
383 }
384 .filter(insets);
385 assert_eq!(consumed, PaddingValues::new(4.0, 0.0, 4.0, 48.0));
386 assert_eq!(
387 ScaffoldContentInsets::none().filter(insets),
388 PaddingValues::default()
389 );
390 }
391
392 #[test]
393 fn a_scaffold_states_no_surfaces_by_default() {
394 assert_eq!(ScaffoldSpec::default().colors, ScaffoldColors::default());
395 let colors = ScaffoldColors::uniform(Color(0.1, 0.1, 0.1, 1.0));
396 assert_eq!(colors.background, colors.top_bar);
397 assert_eq!(colors.background, colors.bottom_bar);
398 }
399}