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