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