#![allow(non_snake_case)]
use std::rc::Rc;
use cranpose_core::{NodeId, SlotId};
use cranpose_ui_graphics::{Color, EdgeInsets};
use cranpose_ui_layout::{Constraints, Placement};
use super::SubcomposeLayout;
use crate::{
Modifier, composable,
layout_direction::{LayoutDirection, layout_direction},
subcompose_layout::{SubcomposeLayoutScope, SubcomposeMeasureScope},
};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct PaddingValues {
pub start: f32,
pub top: f32,
pub end: f32,
pub bottom: f32,
}
impl PaddingValues {
pub const fn new(start: f32, top: f32, end: f32, bottom: f32) -> Self {
Self {
start,
top,
end,
bottom,
}
}
pub const fn all(value: f32) -> Self {
Self::new(value, value, value, value)
}
pub fn apply_to(self, modifier: Modifier) -> Modifier {
self.apply_to_in(modifier, layout_direction())
}
pub fn apply_to_in(self, modifier: Modifier, direction: LayoutDirection) -> Modifier {
modifier.padding_relative_in(direction, self.start, self.top, self.end, self.bottom)
}
pub fn physical(self, direction: LayoutDirection) -> EdgeInsets {
let (left, right) = direction.resolve(self.start, self.end);
EdgeInsets::from_components(left, self.top, right, self.bottom)
}
pub fn max(self, other: Self) -> Self {
Self::new(
self.start.max(other.start),
self.top.max(other.top),
self.end.max(other.end),
self.bottom.max(other.bottom),
)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ScaffoldColors {
pub background: Option<Color>,
pub top_bar: Option<Color>,
pub bottom_bar: Option<Color>,
}
impl ScaffoldColors {
pub fn uniform(background: Color) -> Self {
Self {
background: Some(background),
top_bar: Some(background),
bottom_bar: Some(background),
}
}
pub fn with_background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ScaffoldContentInsets {
pub top: bool,
pub bottom: bool,
pub start: bool,
pub end: bool,
}
impl Default for ScaffoldContentInsets {
fn default() -> Self {
Self {
top: true,
bottom: true,
start: true,
end: true,
}
}
}
impl ScaffoldContentInsets {
pub const fn none() -> Self {
Self {
top: false,
bottom: false,
start: false,
end: false,
}
}
fn filter(self, insets: PaddingValues) -> PaddingValues {
PaddingValues::new(
if self.start { insets.start } else { 0.0 },
if self.top { insets.top } else { 0.0 },
if self.end { insets.end } else { 0.0 },
if self.bottom { insets.bottom } else { 0.0 },
)
}
}
#[derive(Clone)]
struct ScaffoldSlots {
top_bar: Rc<dyn Fn()>,
bottom_bar: Rc<dyn Fn()>,
floating_action: Rc<dyn Fn()>,
content: Rc<dyn Fn(PaddingValues)>,
}
impl PartialEq for ScaffoldSlots {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.top_bar, &other.top_bar)
&& Rc::ptr_eq(&self.bottom_bar, &other.bottom_bar)
&& Rc::ptr_eq(&self.floating_action, &other.floating_action)
&& Rc::ptr_eq(&self.content, &other.content)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ScaffoldSpec {
pub colors: ScaffoldColors,
pub content_insets: ScaffoldContentInsets,
}
impl ScaffoldSpec {
pub fn with_colors(mut self, colors: ScaffoldColors) -> Self {
self.colors = colors;
self
}
}
pub fn Scaffold<T, B, C>(modifier: Modifier, top_bar: T, bottom_bar: B, content: C) -> NodeId
where
T: Fn() + 'static,
B: Fn() + 'static,
C: Fn(PaddingValues) + 'static,
{
ScaffoldWith(
modifier,
ScaffoldSpec::default(),
top_bar,
bottom_bar,
|| {},
content,
)
}
pub fn ScaffoldWith<T, B, F, C>(
modifier: Modifier,
spec: ScaffoldSpec,
top_bar: T,
bottom_bar: B,
floating_action: F,
content: C,
) -> NodeId
where
T: Fn() + 'static,
B: Fn() + 'static,
F: Fn() + 'static,
C: Fn(PaddingValues) + 'static,
{
ScaffoldImpl(
modifier,
spec,
ScaffoldSlots {
top_bar: Rc::new(top_bar),
bottom_bar: Rc::new(bottom_bar),
floating_action: Rc::new(floating_action),
content: Rc::new(content),
},
)
}
const FLOATING_ACTION_MARGIN: f32 = 16.0;
#[composable]
fn ScaffoldImpl(modifier: Modifier, spec: ScaffoldSpec, slots: ScaffoldSlots) -> NodeId {
let direction = layout_direction();
let insets = crate::safe_area::window_insets().combined();
let (start_inset, end_inset) = match direction {
LayoutDirection::Ltr => (insets.left, insets.right),
LayoutDirection::Rtl => (insets.right, insets.left),
};
let window_padding = spec.content_insets.filter(PaddingValues::new(
start_inset,
insets.top,
end_inset,
insets.bottom,
));
let colors = spec.colors;
let modifier = match colors.background {
Some(background) => modifier.background(background),
None => modifier,
};
let top_bar = slots.top_bar;
let bottom_bar = slots.bottom_bar;
let floating_action = slots.floating_action;
let content = slots.content;
SubcomposeLayout(modifier, move |scope, constraints| {
let width = constraints.max_width.max(constraints.min_width);
let height = constraints.max_height.max(constraints.min_height);
let loose = Constraints::loose(width, height);
let top_content = Rc::clone(&top_bar);
let top_nodes = scope.subcompose(SlotId::new(0), (), move || top_content());
let mut top_height = 0.0_f32;
let mut top_placements = Vec::with_capacity(top_nodes.len());
for node in top_nodes {
let placeable = scope.measure(node, loose);
top_height = top_height.max(placeable.height());
top_placements.push(Placement::new(placeable.node_id(), 0.0, 0.0, 1));
}
let bottom_content = Rc::clone(&bottom_bar);
let bottom_nodes = scope.subcompose(SlotId::new(1), (), move || bottom_content());
let mut bottom_height = 0.0_f32;
let mut bottom_placeables = Vec::with_capacity(bottom_nodes.len());
for node in bottom_nodes {
let placeable = scope.measure(node, loose);
bottom_height = bottom_height.max(placeable.height());
bottom_placeables.push(placeable);
}
let padding = window_padding.max(PaddingValues::new(0.0, top_height, 0.0, bottom_height));
let content_slot = Rc::clone(&content);
let content_nodes =
scope.subcompose(SlotId::new(2), padding, move || content_slot(padding));
let mut placements = Vec::with_capacity(
top_placements.len() + bottom_placeables.len() + content_nodes.len() + 1,
);
for node in content_nodes {
let placeable = scope.measure(node, Constraints::tight(width, height));
placements.push(Placement::new(placeable.node_id(), 0.0, 0.0, 0));
}
placements.extend(top_placements);
for placeable in bottom_placeables {
placements.push(Placement::new(
placeable.node_id(),
0.0,
(height - placeable.height()).max(0.0),
1,
));
}
let floating_content = Rc::clone(&floating_action);
let floating_nodes = scope.subcompose(SlotId::new(3), (), move || floating_content());
for node in floating_nodes {
let placeable = scope.measure(node, loose);
let bottom_gap = padding.bottom + FLOATING_ACTION_MARGIN;
let end_gap = padding.end + FLOATING_ACTION_MARGIN;
let x = match direction {
LayoutDirection::Ltr => (width - placeable.width() - end_gap).max(0.0),
LayoutDirection::Rtl => end_gap.min((width - placeable.width()).max(0.0)),
};
let y = (height - placeable.height() - bottom_gap).max(0.0);
placements.push(Placement::new(placeable.node_id(), x, y, 2));
}
scope.layout(width, height, placements)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn padding_values_preserve_each_edge() {
assert_eq!(
PaddingValues::new(1.0, 2.0, 3.0, 4.0),
PaddingValues {
start: 1.0,
top: 2.0,
end: 3.0,
bottom: 4.0,
}
);
assert_eq!(
PaddingValues::all(6.0),
PaddingValues::new(6.0, 6.0, 6.0, 6.0)
);
}
#[test]
fn reading_order_padding_swaps_sides_in_a_right_to_left_layout() {
let padding = PaddingValues::new(24.0, 2.0, 8.0, 4.0);
assert_eq!(
padding.physical(LayoutDirection::Ltr),
EdgeInsets::from_components(24.0, 2.0, 8.0, 4.0)
);
assert_eq!(
padding.physical(LayoutDirection::Rtl),
EdgeInsets::from_components(8.0, 2.0, 24.0, 4.0)
);
}
#[test]
fn merging_takes_the_larger_of_each_edge() {
let bars = PaddingValues::new(0.0, 56.0, 0.0, 0.0);
let insets = PaddingValues::new(0.0, 24.0, 0.0, 48.0);
assert_eq!(insets.max(bars), PaddingValues::new(0.0, 56.0, 0.0, 48.0));
}
#[test]
fn a_screen_can_keep_the_insets_it_draws_under() {
let insets = PaddingValues::new(4.0, 24.0, 4.0, 48.0);
let consumed = ScaffoldContentInsets {
top: false,
..ScaffoldContentInsets::default()
}
.filter(insets);
assert_eq!(consumed, PaddingValues::new(4.0, 0.0, 4.0, 48.0));
assert_eq!(
ScaffoldContentInsets::none().filter(insets),
PaddingValues::default()
);
}
#[test]
fn a_scaffold_states_no_surfaces_by_default() {
assert_eq!(ScaffoldSpec::default().colors, ScaffoldColors::default());
let colors = ScaffoldColors::uniform(Color(0.1, 0.1, 0.1, 1.0));
assert_eq!(colors.background, colors.top_bar);
assert_eq!(colors.background, colors.bottom_bar);
}
}