use frus_core::{Alignment, Rect, Scene, Size};
use frus_layout::{Dimension, Style};
use crate::interaction::Status;
use crate::theme::Theme;
use crate::widget::Widget;
pub struct SizedBox<Msg = crate::callback::Callback> {
width: Dimension,
height: Dimension,
children: Vec<Box<dyn Widget<Msg>>>,
}
impl<Msg> SizedBox<Msg> {
pub fn empty() -> Self {
Self {
width: Dimension::Auto,
height: Dimension::Auto,
children: Vec::new(),
}
}
pub fn new(child: impl Widget<Msg> + 'static) -> Self {
let mut boxed = Self::empty();
boxed.children.push(Box::new(child));
boxed
}
pub fn square(side: f32, child: impl Widget<Msg> + 'static) -> Self {
Self::new(child).width(side).height(side)
}
pub fn expand(child: impl Widget<Msg> + 'static) -> Self {
Self::new(child)
.width_dimension(Dimension::Percent(1.0))
.height_dimension(Dimension::Percent(1.0))
}
pub fn shrink(child: impl Widget<Msg> + 'static) -> Self {
Self::new(child)
}
pub fn width(self, width: f32) -> Self {
self.width_dimension(Dimension::Length(width))
}
pub fn height(self, height: f32) -> Self {
self.height_dimension(Dimension::Length(height))
}
pub fn width_fraction(self, fraction: f32) -> Self {
self.width_dimension(Dimension::Percent(fraction))
}
pub fn height_fraction(self, fraction: f32) -> Self {
self.height_dimension(Dimension::Percent(fraction))
}
fn width_dimension(mut self, width: Dimension) -> Self {
self.width = width;
self
}
fn height_dimension(mut self, height: Dimension) -> Self {
self.height = height;
self
}
pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
self.children.clear();
self.children.push(Box::new(child));
self
}
}
impl<Msg: Clone> Widget<Msg> for SizedBox<Msg> {
fn style(&self) -> Style {
Style {
width: self.width,
height: self.height,
..Default::default()
}
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
&self.children
}
fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
}
fn on_click(&self) -> Option<Msg> {
None
}
}
pub struct ConstrainedBox<Msg = crate::callback::Callback> {
min_width: Dimension,
max_width: Dimension,
min_height: Dimension,
max_height: Dimension,
children: Vec<Box<dyn Widget<Msg>>>,
}
impl<Msg> ConstrainedBox<Msg> {
pub fn new(child: impl Widget<Msg> + 'static) -> Self {
Self {
min_width: Dimension::Auto,
max_width: Dimension::Auto,
min_height: Dimension::Auto,
max_height: Dimension::Auto,
children: vec![Box::new(child)],
}
}
pub fn new_boxed(child: Box<dyn Widget<Msg>>) -> Self {
Self {
min_width: Dimension::Auto,
max_width: Dimension::Auto,
min_height: Dimension::Auto,
max_height: Dimension::Auto,
children: vec![child],
}
}
pub fn tight(width: f32, height: f32, child: impl Widget<Msg> + 'static) -> Self {
Self::new(child)
.min_width(width)
.max_width(width)
.min_height(height)
.max_height(height)
}
pub fn loose(width: f32, height: f32, child: impl Widget<Msg> + 'static) -> Self {
Self::new(child).max_width(width).max_height(height)
}
pub fn min_width(mut self, width: f32) -> Self {
self.min_width = Dimension::Length(width);
self
}
pub fn max_width(mut self, width: f32) -> Self {
self.max_width = Dimension::Length(width);
self
}
pub fn min_height(mut self, height: f32) -> Self {
self.min_height = Dimension::Length(height);
self
}
pub fn max_height(mut self, height: f32) -> Self {
self.max_height = Dimension::Length(height);
self
}
}
impl<Msg: Clone> Widget<Msg> for ConstrainedBox<Msg> {
fn style(&self) -> Style {
Style {
min_width: self.min_width,
max_width: self.max_width,
min_height: self.min_height,
max_height: self.max_height,
..Default::default()
}
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
&self.children
}
fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
fn on_click(&self) -> Option<Msg> {
None
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum IntrinsicAxis {
Width,
Height,
}
pub struct Intrinsic<Msg = crate::callback::Callback> {
axis: IntrinsicAxis,
step: Option<f32>,
children: Vec<Box<dyn Widget<Msg>>>,
}
pub type IntrinsicWidth<Msg> = Intrinsic<Msg>;
pub type IntrinsicHeight<Msg> = Intrinsic<Msg>;
impl<Msg> Intrinsic<Msg> {
pub fn width(child: impl Widget<Msg> + 'static) -> Self {
Self {
axis: IntrinsicAxis::Width,
step: None,
children: vec![Box::new(child)],
}
}
pub fn height(child: impl Widget<Msg> + 'static) -> Self {
Self {
axis: IntrinsicAxis::Height,
step: None,
children: vec![Box::new(child)],
}
}
pub fn step(mut self, step: f32) -> Self {
self.step = (step > 0.0).then_some(step);
self
}
pub fn axis(&self) -> IntrinsicAxis {
self.axis
}
pub fn quantise(&self, extent: f32) -> f32 {
match self.step {
Some(step) => (extent / step).ceil() * step,
None => extent,
}
}
}
impl<Msg: Clone> Widget<Msg> for Intrinsic<Msg> {
fn style(&self) -> Style {
Style::default()
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
&self.children
}
fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
fn on_click(&self) -> Option<Msg> {
None
}
fn intrinsic(&self) -> Option<(IntrinsicAxis, Option<f32>)> {
Some((self.axis, self.step))
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Overflow {
pub width: Option<f32>,
pub height: Option<f32>,
pub unconstrained: bool,
pub alignment: Alignment,
}
pub struct OverflowBox<Msg = crate::callback::Callback> {
overflow: Overflow,
children: Vec<Box<dyn Widget<Msg>>>,
}
impl<Msg> OverflowBox<Msg> {
pub fn new(child: impl Widget<Msg> + 'static) -> Self {
Self {
overflow: Overflow {
width: None,
height: None,
unconstrained: false,
alignment: Alignment::CENTER,
},
children: vec![Box::new(child)],
}
}
pub fn unconstrained(child: impl Widget<Msg> + 'static) -> Self {
let mut boxed = Self::new(child);
boxed.overflow.unconstrained = true;
boxed
}
pub fn width(mut self, width: f32) -> Self {
self.overflow.width = Some(width);
self
}
pub fn height(mut self, height: f32) -> Self {
self.overflow.height = Some(height);
self
}
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.overflow.alignment = alignment;
self
}
}
impl<Msg: Clone> Widget<Msg> for OverflowBox<Msg> {
fn style(&self) -> Style {
Style {
width: Dimension::Percent(1.0),
height: Dimension::Percent(1.0),
..Default::default()
}
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
&self.children
}
fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
fn on_click(&self) -> Option<Msg> {
None
}
fn overflow_box(&self) -> Option<Overflow> {
Some(self.overflow)
}
}
impl Overflow {
pub(crate) fn child_size(&self, own: Size) -> Size {
Size::new(
self.width.unwrap_or(own.width),
self.height.unwrap_or(own.height),
)
}
pub(crate) fn origin(&self, own: Rect, child: Size) -> (f32, f32) {
(
own.x + (own.width - child.width) * self.alignment.fraction_x(),
own.y + (own.height - child.height) * self.alignment.fraction_y(),
)
}
}
pub struct SizedOverflowBox<Msg = crate::callback::Callback> {
width: f32,
height: f32,
overflow: Overflow,
children: Vec<Box<dyn Widget<Msg>>>,
}
impl<Msg> SizedOverflowBox<Msg> {
pub fn new(width: f32, height: f32, child: impl Widget<Msg> + 'static) -> Self {
Self {
width,
height,
overflow: Overflow {
width: None,
height: None,
unconstrained: true,
alignment: Alignment::CENTER,
},
children: vec![Box::new(child)],
}
}
pub fn child_size(mut self, width: f32, height: f32) -> Self {
self.overflow.unconstrained = false;
self.overflow.width = Some(width);
self.overflow.height = Some(height);
self
}
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.overflow.alignment = alignment;
self
}
}
impl<Msg: Clone> Widget<Msg> for SizedOverflowBox<Msg> {
fn style(&self) -> Style {
Style {
width: Dimension::Length(self.width),
height: Dimension::Length(self.height),
..Default::default()
}
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
&self.children
}
fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
fn on_click(&self) -> Option<Msg> {
None
}
fn overflow_box(&self) -> Option<Overflow> {
Some(self.overflow)
}
fn debug_name(&self) -> &'static str {
"SizedOverflowBox"
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AxisConstraint {
AsGiven,
Unbounded,
Fixed(f32),
}
#[derive(Clone, Copy, Debug)]
pub struct ConstraintsTransform {
pub width: AxisConstraint,
pub height: AxisConstraint,
pub alignment: Alignment,
pub report: bool,
}
impl ConstraintsTransform {
pub fn new() -> Self {
Self {
width: AxisConstraint::AsGiven,
height: AxisConstraint::AsGiven,
alignment: Alignment::CENTER,
report: true,
}
}
pub fn width(mut self, width: AxisConstraint) -> Self {
self.width = width;
self
}
pub fn height(mut self, height: AxisConstraint) -> Self {
self.height = height;
self
}
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
pub fn report(mut self, report: bool) -> Self {
self.report = report;
self
}
}
impl Default for ConstraintsTransform {
fn default() -> Self {
Self::new()
}
}
impl AxisConstraint {
pub(crate) fn offered(self, offer: Option<f32>) -> (f32, bool) {
match self {
AxisConstraint::Unbounded => (0.0, true),
AxisConstraint::Fixed(extent) => (extent.max(0.0), false),
AxisConstraint::AsGiven => match offer {
Some(extent) => (extent.max(0.0), false),
None => (0.0, true),
},
}
}
pub(crate) fn at(self, own: f32) -> (f32, bool) {
match self {
AxisConstraint::Unbounded => (0.0, true),
AxisConstraint::Fixed(extent) => (extent.max(0.0), false),
AxisConstraint::AsGiven => (own.max(0.0), false),
}
}
}
pub struct ConstraintsTransformBox<Msg = crate::callback::Callback> {
transform: ConstraintsTransform,
children: Vec<Box<dyn Widget<Msg>>>,
}
impl<Msg> ConstraintsTransformBox<Msg> {
pub fn new(child: impl Widget<Msg> + 'static) -> Self {
Self {
transform: ConstraintsTransform::new(),
children: vec![Box::new(child)],
}
}
pub fn width(mut self, width: AxisConstraint) -> Self {
self.transform.width = width;
self
}
pub fn height(mut self, height: AxisConstraint) -> Self {
self.transform.height = height;
self
}
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.transform.alignment = alignment;
self
}
pub fn report(mut self, report: bool) -> Self {
self.transform.report = report;
self
}
pub fn with(child: impl Widget<Msg> + 'static, transform: ConstraintsTransform) -> Self {
Self {
transform,
children: vec![Box::new(child)],
}
}
}
impl<Msg: Clone> Widget<Msg> for ConstraintsTransformBox<Msg> {
fn style(&self) -> Style {
Style::default()
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
&self.children
}
fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
fn on_click(&self) -> Option<Msg> {
None
}
fn constraints_transform(&self) -> Option<ConstraintsTransform> {
Some(self.transform)
}
fn debug_name(&self) -> &'static str {
"ConstraintsTransformBox"
}
}
pub struct UnconstrainedBox;
impl UnconstrainedBox {
#[allow(clippy::new_ret_no_self)]
pub fn new<Msg: Clone + 'static>(
child: impl Widget<Msg> + 'static,
) -> ConstraintsTransformBox<Msg> {
ConstraintsTransformBox::new(child)
.width(AxisConstraint::Unbounded)
.height(AxisConstraint::Unbounded)
}
pub fn axis<Msg: Clone + 'static>(
child: impl Widget<Msg> + 'static,
axis: crate::scroll::Axis,
) -> ConstraintsTransformBox<Msg> {
let freed = |free: bool| match free {
true => AxisConstraint::Unbounded,
false => AxisConstraint::AsGiven,
};
ConstraintsTransformBox::new(child)
.width(freed(axis.free_x()))
.height(freed(axis.free_y()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{build_ui, Container, Flex, InspectorNode, Runtime, Text};
use frus_core::{Color, Primitive};
const RED: Color = Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
fn filler() -> Container<()> {
Container::new().flex(1.0).color(RED)
}
fn red_box<W: Widget<()> + 'static>(root: W, available: Size) -> Rect {
let runtime = Runtime::default();
let ui = build_ui(&root, available, &runtime, &Theme::dark());
ui.scene()
.primitives()
.iter()
.find_map(|p| match p {
Primitive::Rect { rect, color, .. } if color.r > 0.5 && color.g < 0.5 => {
Some(*rect)
}
_ => None,
})
.expect("the red box")
}
#[test]
fn a_ceiling_stops_a_box_that_would_fill() {
let root = Flex::<()>::column()
.width(400.0)
.child(ConstrainedBox::new(filler().height(40.0)).max_width(120.0));
let rect = red_box(root, Size::new(400.0, 200.0));
assert!((rect.width - 120.0).abs() < 0.5, "capped: {rect:?}");
}
#[test]
fn a_floor_stops_a_box_that_would_hug() {
let root = Flex::<()>::row()
.width(400.0)
.height(60.0)
.child(ConstrainedBox::new(filler()).min_width(90.0));
let rect = red_box(root, Size::new(400.0, 200.0));
assert!((rect.width - 90.0).abs() < 0.5, "floored: {rect:?}");
}
#[test]
fn a_tight_box_is_that_size_whatever_is_inside_it() {
let root = Flex::<()>::column()
.width(400.0)
.child(ConstrainedBox::tight(75.0, 33.0, filler()));
let rect = red_box(root, Size::new(400.0, 200.0));
assert!(
(rect.width - 75.0).abs() < 0.5 && (rect.height - 33.0).abs() < 0.5,
"tight: {rect:?}"
);
}
#[test]
fn a_sized_box_that_expands_takes_everything_on_offer() {
let root = Flex::<()>::column()
.width(400.0)
.height(200.0)
.child(SizedBox::expand(filler()));
let rect = red_box(root, Size::new(400.0, 200.0));
assert!(
(rect.width - 400.0).abs() < 0.5 && (rect.height - 200.0).abs() < 0.5,
"expanded: {rect:?}"
);
}
#[test]
fn a_sized_box_is_the_size_it_was_given() {
let root = Flex::<()>::column()
.width(400.0)
.child(SizedBox::square(64.0, filler()));
let rect = red_box(root, Size::new(400.0, 200.0));
assert!(
(rect.width - 64.0).abs() < 0.5 && (rect.height - 64.0).abs() < 0.5,
"square: {rect:?}"
);
}
#[test]
fn an_intrinsic_width_is_what_the_content_wants_not_what_the_column_offers() {
let root = Flex::<()>::column().width(400.0).child(Intrinsic::width(
Flex::<()>::column()
.child(Text::new("a short line"))
.child(Container::new().height(10.0).color(RED)),
));
let rect = red_box(root, Size::new(400.0, 200.0));
assert!(
rect.width > 1.0 && rect.width < 380.0,
"the content's width, not the column's: {rect:?}"
);
}
#[test]
fn an_intrinsic_width_rounds_up_to_its_step() {
let step = Intrinsic::<()>::width(Container::new()).step(20.0);
assert_eq!(step.quantise(1.0), 20.0);
assert_eq!(step.quantise(20.0), 20.0);
assert_eq!(step.quantise(21.0), 40.0);
let plain = Intrinsic::<()>::width(Container::new());
assert_eq!(plain.quantise(21.0), 21.0);
}
#[test]
fn an_overflowing_child_is_bigger_than_its_box_and_centred_on_it() {
let root = Flex::<()>::column().width(400.0).child(
SizedBox::empty()
.width(40.0)
.height(40.0)
.child(OverflowBox::new(filler()).width(100.0).height(100.0)),
);
let rect = red_box(root, Size::new(400.0, 200.0));
assert!(
(rect.width - 100.0).abs() < 0.5 && (rect.height - 100.0).abs() < 0.5,
"the child keeps its own size: {rect:?}"
);
assert!(
rect.x < -29.0 && rect.x > -31.0,
"centred, spilling: {rect:?}"
);
}
#[test]
fn an_overflow_box_can_anchor_the_spill_to_one_corner() {
let root = Flex::<()>::column().width(400.0).child(
SizedBox::empty().width(40.0).height(40.0).child(
OverflowBox::new(filler())
.width(100.0)
.height(100.0)
.alignment(Alignment::TOP_LEFT),
),
);
let rect = red_box(root, Size::new(400.0, 200.0));
assert!(
rect.x.abs() < 0.5 && rect.y.abs() < 0.5,
"anchored top-left, spilling right and down: {rect:?}"
);
}
#[test]
fn the_box_around_an_overflowing_child_does_not_grow() {
let root = Flex::<()>::column()
.width(400.0)
.child(
SizedBox::empty().width(40.0).height(40.0).child(
OverflowBox::new(Container::new().flex(1.0))
.width(100.0)
.height(100.0),
),
)
.child(Container::new().width(10.0).height(10.0).color(RED));
let rect = red_box(root, Size::new(400.0, 300.0));
assert!((rect.y - 40.0).abs() < 0.5, "the neighbour moved: {rect:?}");
}
#[test]
fn an_unconstrained_child_is_the_size_it_asks_for() {
let root = Flex::<()>::column().width(400.0).child(
SizedBox::empty()
.width(20.0)
.height(20.0)
.child(OverflowBox::unconstrained(
Container::new().width(90.0).height(30.0).color(RED),
)),
);
let rect = red_box(root, Size::new(400.0, 200.0));
assert!(
(rect.width - 90.0).abs() < 0.5 && (rect.height - 30.0).abs() < 0.5,
"unconstrained: {rect:?}"
);
}
fn inspected(root: &dyn Widget<()>, available: Size) -> (crate::Ui<()>, Vec<InspectorNode>) {
crate::build_ui_inspected(root, available, &Runtime::default(), &Theme::dark())
}
fn box_named(nodes: &[InspectorNode], name: &str) -> Rect {
nodes
.iter()
.find(|n| n.name == name)
.unwrap_or_else(|| panic!("no {name} in {}", crate::dump_tree(nodes)))
.rect
}
fn red(width: f32, height: f32) -> Container<()> {
Container::new().width(width).height(height).color(RED)
}
fn narrow_column(width: f32) -> Flex<()> {
Flex::<()>::column()
.width(width)
.align(frus_layout::Align::Start)
}
#[test]
fn a_child_that_fits_gives_the_unconstrained_box_its_own_size() {
let root = narrow_column(400.0).child(UnconstrainedBox::new(red(90.0, 30.0)));
let (ui, nodes) = inspected(&root, Size::new(400.0, 200.0));
let own = box_named(&nodes, "ConstraintsTransformBox");
assert!(
(own.width - 90.0).abs() < 0.5 && (own.height - 30.0).abs() < 0.5,
"the box is the child's size: {own:?}"
);
assert!(ui.overflows().is_empty(), "nothing ran past anything");
}
#[test]
fn an_unconstrained_child_that_does_not_fit_overflows_and_says_so() {
let root = narrow_column(100.0).child(UnconstrainedBox::new(red(300.0, 20.0)));
let (ui, nodes) = inspected(&root, Size::new(100.0, 200.0));
let child = red_box_in(&ui);
assert!(
(child.width - 300.0).abs() < 0.5,
"the child kept its size: {child:?}"
);
let own = box_named(&nodes, "ConstraintsTransformBox");
assert!(
(own.width - 100.0).abs() < 0.5,
"the box kept the room it was offered: {own:?}"
);
let spills = ui.overflows();
let mut sides: Vec<_> = spills.iter().map(|o| (o.side, o.amount)).collect();
sides.sort_by_key(|(side, _)| format!("{side:?}"));
assert_eq!(sides.len(), 2, "two edges: {spills:?}");
assert_eq!(sides[0].0, frus_layout::Side::Left);
assert_eq!(sides[1].0, frus_layout::Side::Right);
assert!(
sides.iter().all(|(_, amount)| (amount - 100.0).abs() < 0.5),
"a hundred either side: {spills:?}"
);
}
#[test]
fn freeing_one_axis_leaves_the_other_one_alone() {
let root = narrow_column(100.0).child(UnconstrainedBox::axis(
Container::new().height(200.0).color(RED),
crate::scroll::Axis::Vertical,
));
let (ui, _) = inspected(&root, Size::new(100.0, 80.0));
let child = red_box_in(&ui);
assert!(
(child.width - 100.0).abs() < 0.5,
"held to the column: {child:?}"
);
assert!(
(child.height - 200.0).abs() < 0.5,
"and as tall as it asked: {child:?}"
);
}
#[test]
fn a_transform_can_give_the_child_a_room_of_its_own() {
let root = narrow_column(200.0).child(
ConstraintsTransformBox::new(Container::new().height(20.0).color(RED))
.width(AxisConstraint::Fixed(100.0)),
);
let (ui, nodes) = inspected(&root, Size::new(200.0, 200.0));
let child = red_box_in(&ui);
assert!(
(child.width - 100.0).abs() < 0.5,
"the child was laid out in the room it was given: {child:?}"
);
assert!((box_named(&nodes, "ConstraintsTransformBox").width - 100.0).abs() < 0.5);
assert!(ui.overflows().is_empty(), "half of it fits in all of it");
}
#[test]
fn a_sized_overflow_box_reports_one_size_and_lays_its_child_out_at_another() {
let root = Flex::<()>::column()
.width(400.0)
.child(SizedOverflowBox::new(40.0, 40.0, red(100.0, 100.0)));
let (ui, nodes) = inspected(&root, Size::new(400.0, 300.0));
let own = box_named(&nodes, "SizedOverflowBox");
assert!(
(own.width - 40.0).abs() < 0.5 && (own.height - 40.0).abs() < 0.5,
"the box is the size it stated: {own:?}"
);
let child = red_box_in(&ui);
assert!(
(child.width - 100.0).abs() < 0.5,
"the child is not: {child:?}"
);
assert!(
(child.x + 30.0).abs() < 0.5 && (child.y + 30.0).abs() < 0.5,
"centred on the box it hangs out of: {child:?}"
);
}
fn red_box_in(ui: &crate::Ui<()>) -> Rect {
ui.scene()
.primitives()
.iter()
.find_map(|p| match p {
Primitive::Rect { rect, color, .. } if color.r > 0.5 && color.g < 0.5 => {
Some(*rect)
}
_ => None,
})
.expect("the red box")
}
}