use crate::compat::Vec;
use crate::core::{ObjectId, Rect, Size};
use crate::layout::{
max_preferred, total_bounds, total_minimum, AxisHints, ChildInfo, Hints, Layout, LayoutParams,
};
use crate::style::EdgeOffsets;
use crate::widget::metrics::ControlMetrics;
use crate::widget::{Widget, WidgetFactory};
struct Child {
id: ObjectId,
hints: Hints,
params: LayoutParams,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlexibleAxis {
Width,
Height,
}
pub struct CompositeBuilder {
layout: Box<dyn Layout>,
children: Vec<Child>,
padding: EdgeOffsets,
floor: Size,
dirty: core::cell::Cell<bool>,
}
impl CompositeBuilder {
pub fn new(layout: Box<dyn Layout>, padding: EdgeOffsets, floor: Size) -> Self {
Self {
layout,
children: Vec::new(),
padding,
floor,
dirty: core::cell::Cell::new(true),
}
}
pub fn invalidate(&self) {
self.dirty.set(true);
}
pub fn is_dirty(&self) -> bool {
self.dirty.get()
}
pub fn take_dirty(&self) -> bool {
self.dirty.replace(false)
}
pub fn set_child_hints(&mut self, id: ObjectId, hints: Hints) -> bool {
match self.children.iter_mut().find(|child| child.id == id) {
Some(child) => {
if child.hints != hints {
child.hints = hints;
self.dirty.set(true);
}
true
}
None => false,
}
}
pub fn set_child_params(&mut self, id: ObjectId, params: LayoutParams) -> bool {
match self.children.iter_mut().find(|child| child.id == id) {
Some(child) => {
if child.params == params {
return true;
}
child.params = params;
self.dirty.set(true);
self.sync_layout_weights();
true
}
None => false,
}
}
fn sync_layout_weights(&mut self) {
for child in self.children.iter() {
self.layout.remove_widget(child.id);
}
for child in self.children.iter() {
let weight =
if child.params.fill { child.params.stretch.max(1) } else { child.params.stretch };
self.layout.add_widget(child.id, weight);
}
}
pub fn add(
&mut self,
factory: &WidgetFactory,
kind_or_name: &str,
text: &str,
geometry: Rect,
params: LayoutParams,
) -> Option<Box<dyn Widget>> {
let widget = factory.create(kind_or_name, geometry, text)?;
let id = widget.id();
let hints = widget.hints();
self.register(id, hints, params);
Some(widget)
}
pub fn add_flexible(
&mut self,
factory: &WidgetFactory,
kind_or_name: &str,
text: &str,
geometry: Rect,
params: LayoutParams,
axis: FlexibleAxis,
) -> Option<Box<dyn Widget>> {
let widget = factory.create(kind_or_name, geometry, text)?;
let id = widget.id();
let mut hints = widget.hints();
match axis {
FlexibleAxis::Width => {
let pref = hints.width.pref;
hints.width =
AxisHints::new(hints.width.min.min(pref), pref, hints.width.max.max(pref));
}
FlexibleAxis::Height => {
let pref = hints.height.pref;
hints.height =
AxisHints::new(hints.height.min.min(pref), pref, hints.height.max.max(pref));
}
}
self.register(id, hints, params);
Some(widget)
}
pub fn add_sized(
&mut self,
factory: &WidgetFactory,
kind_or_name: &str,
text: &str,
size: Size,
params: LayoutParams,
) -> Option<Box<dyn Widget>> {
let widget =
factory.create(kind_or_name, Rect::new(0, 0, size.width, size.height), text)?;
let id = widget.id();
self.register(id, Hints::fixed(size.width, size.height), params);
Some(widget)
}
pub fn add_with_hints(
&mut self,
factory: &WidgetFactory,
kind_or_name: &str,
text: &str,
hints: Hints,
params: LayoutParams,
) -> Option<Box<dyn Widget>> {
let pref = hints.preferred();
let widget =
factory.create(kind_or_name, Rect::new(0, 0, pref.width, pref.height), text)?;
let id = widget.id();
self.register(id, hints, params);
Some(widget)
}
fn register(&mut self, id: ObjectId, hints: Hints, params: LayoutParams) {
let grow = if params.fill { params.stretch.max(1) } else { params.stretch };
self.layout.add_widget(id, grow);
self.children.push(Child { id, hints, params });
self.dirty.set(true);
}
#[allow(dead_code)]
pub(crate) fn add_at_hint(
&mut self,
factory: &WidgetFactory,
kind_or_name: &str,
text: &str,
params: LayoutParams,
) -> Option<Box<dyn Widget>> {
let widget = factory.create(kind_or_name, Rect::new(0, 0, 0, 0), text)?;
let id = widget.id();
let hints = widget.hints();
self.register(id, hints, params);
Some(widget)
}
pub fn hints(&self, major_is_horizontal: bool) -> Hints {
let vertical = !major_is_horizontal;
let infos = self.child_infos();
let major_min = total_minimum(&infos, vertical);
let major_occupied = total_bounds(&infos, vertical);
let cross_pref = max_preferred(&infos, vertical);
let content = if major_is_horizontal {
Size::new(major_occupied, cross_pref)
} else {
Size::new(cross_pref, major_occupied)
};
let size = ControlMetrics::implicit_size(content, self.padding, self.floor);
if major_is_horizontal {
Hints {
width: AxisHints::new(
major_min.saturating_add(self.padding.horizontal_total()).max(self.floor.width),
size.width,
u32::MAX,
),
height: AxisHints::new(size.height, size.height, size.height),
}
} else {
Hints {
width: AxisHints::new(size.width, size.width, size.width),
height: AxisHints::new(
major_min.saturating_add(self.padding.vertical_total()).max(self.floor.height),
size.height,
u32::MAX,
),
}
}
}
fn child_infos(&self) -> Vec<ChildInfo> {
self.children
.iter()
.map(|child| ChildInfo { id: child.id, hints: child.hints, params: child.params })
.collect()
}
pub fn arrange(&self, rect: Rect, out: &mut dyn FnMut(ObjectId, Rect)) {
let content = ControlMetrics::content_box(rect, self.padding);
let infos = self.child_infos();
self.layout.arrange(content, &infos, out);
}
pub fn child_ids(&self) -> Vec<ObjectId> {
self.children.iter().map(|child| child.id).collect()
}
pub fn len(&self) -> usize {
self.children.len()
}
pub fn is_empty(&self) -> bool {
self.children.is_empty()
}
}
pub struct ActionRow {
builder: CompositeBuilder,
labels: Vec<crate::compat::String>,
gap: u32,
}
impl ActionRow {
pub fn new(gap: u32) -> Self {
let layout: Box<dyn Layout> = Box::new(crate::layout::FlexLayout::with_params(
crate::layout::FlexDirection::Row,
crate::layout::FlexWrap::NoWrap,
crate::layout::JustifyContent::FlexEnd,
crate::layout::AlignItems::Stretch,
0,
0,
));
Self {
builder: CompositeBuilder::new(layout, EdgeOffsets::all(0), Size::new(0, 0)),
labels: Vec::new(),
gap,
}
}
pub fn add(
&mut self,
factory: &WidgetFactory,
label: &str,
size: Size,
) -> Option<Box<dyn Widget>> {
let leading = if self.labels.is_empty() { 0 } else { self.gap };
let widget = self.builder.add(
factory,
"button",
label,
Rect::new(0, 0, size.width, size.height),
LayoutParams::new().with_margins(EdgeOffsets::new(0, 0, 0, leading)),
);
if widget.is_some() {
self.labels.push(label.to_string());
}
widget
}
pub fn len(&self) -> usize {
self.builder.len()
}
pub fn is_empty(&self) -> bool {
self.builder.is_empty()
}
pub fn preferred_width(&self) -> u32 {
self.builder.hints(true).width.pref
}
pub fn arrange(&self, band: Rect) -> (Rect, Vec<Rect>) {
let mut placed: Vec<Rect> = Vec::with_capacity(self.builder.len());
self.builder.arrange(band, &mut |_, rect| placed.push(rect));
let row = match (placed.first(), placed.last()) {
(Some(first), Some(last)) => {
let left = first.x;
let right = last.x.saturating_add(last.width as i32);
Rect::new(left, band.y, (right - left).max(0) as u32, band.height)
}
_ => Rect::new(band.x + band.width as i32, band.y, 0, band.height),
};
(row, placed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Font;
use crate::layout::{AlignItems, FlexDirection, FlexLayout, FlexWrap, JustifyContent};
use crate::widget::metrics::{dimensions, estimate_text_width};
use crate::widget::WidgetFactory;
fn row(gap: i32) -> Box<dyn Layout> {
Box::new(FlexLayout::with_params(
FlexDirection::Row,
FlexWrap::NoWrap,
JustifyContent::FlexStart,
AlignItems::Stretch,
gap,
0,
))
}
fn builder() -> (WidgetFactory, CompositeBuilder) {
let factory = WidgetFactory::new_with_defaults();
let builder = CompositeBuilder::new(row(8), EdgeOffsets::all(0), Size::new(64, 40));
(factory, builder)
}
#[test]
fn a_childs_wish_reaches_the_composites_hints() {
let (factory, mut builder) = builder();
let narrow = builder
.add(&factory, "label", "A", Rect::new(0, 0, 8, 14), LayoutParams::new())
.expect("label is published");
let before = builder.hints(true);
let wide = builder
.add(
&factory,
"label",
"A much longer label",
Rect::new(0, 0, 120, 14),
LayoutParams::new(),
)
.expect("label is published");
let after = builder.hints(true);
assert!(
wide.hints().width.pref > narrow.hints().width.pref,
"the fixture's two labels must actually differ in their own size wish"
);
assert!(
after.width.pref > before.width.pref,
"adding a wider child must widen the composite: {} -> {}",
before.width.pref,
after.width.pref
);
assert!(
after.width.min > before.width.min,
"the composite's floor must include the new child's floor"
);
let children = narrow.hints().width.pref.saturating_add(wide.hints().width.pref);
assert_eq!(
after.width.pref, children,
"the composite's preference is the children's own sum"
);
assert_eq!(wide.id(), builder.child_ids()[1]);
}
#[test]
fn the_layout_owns_placement_and_padding_is_removed_first() {
let factory = WidgetFactory::new_with_defaults();
let padding = EdgeOffsets { left: 10, right: 4, top: 6, bottom: 2 };
let mut builder = CompositeBuilder::new(row(8), padding, Size::new(0, 0));
let a = builder
.add(&factory, "label", "A", Rect::new(0, 0, 40, 20), LayoutParams::new())
.expect("label is published");
let b = builder
.add(&factory, "label", "B", Rect::new(0, 0, 30, 20), LayoutParams::new())
.expect("label is published");
let (id_a, id_b) = (a.id(), b.id());
let mut placed: Vec<(ObjectId, Rect)> = Vec::new();
builder.arrange(Rect::new(0, 0, 200, 60), &mut |id, rect| placed.push((id, rect)));
assert_eq!(placed.len(), 2);
assert_eq!(placed[0].0, id_a);
assert_eq!(placed[1].0, id_b);
assert_eq!(
placed[0].1.x, padding.left as i32,
"the first child starts at the content edge"
);
assert_eq!(placed[0].1.y, padding.top as i32);
let right_limit = 200 - padding.right as i32;
for (_, rect) in &placed {
assert!(
rect.x + rect.width as i32 <= right_limit,
"a child must not be placed outside the content box: {rect:?}"
);
}
}
#[test]
fn a_dirty_flag_is_taken_not_read() {
let factory = WidgetFactory::new_with_defaults();
let mut builder = CompositeBuilder::new(row(0), EdgeOffsets::all(0), Size::new(0, 0));
assert!(builder.is_dirty(), "a fresh composite has never been arranged");
assert!(builder.take_dirty(), "the first consume reports the owed layout");
assert!(!builder.is_dirty());
assert!(
!builder.take_dirty(),
"a second consume must report nothing, or the host would relayout every frame"
);
builder
.add(&factory, "label", "A", Rect::new(0, 0, 40, 20), LayoutParams::new())
.expect("label is published");
assert!(builder.is_dirty(), "adding a child changes what the composite needs");
assert!(builder.take_dirty());
assert!(!builder.take_dirty());
}
#[test]
fn a_changed_child_wish_notifies_the_composite_once() {
let factory = WidgetFactory::new_with_defaults();
let mut builder = CompositeBuilder::new(row(0), EdgeOffsets::all(0), Size::new(0, 0));
let child = builder
.add(&factory, "label", "A", Rect::new(0, 0, 40, 20), LayoutParams::new())
.expect("label is published");
let id = child.id();
let _ = builder.take_dirty();
assert!(builder.set_child_hints(id, Hints::fixed(80, 20)));
assert!(builder.set_child_hints(id, Hints::fixed(90, 20)));
assert!(builder.set_child_params(id, LayoutParams::filled()));
assert!(builder.set_child_params(id, LayoutParams::stretched(2)));
assert!(builder.take_dirty(), "the batched writes owe exactly one relayout");
assert!(!builder.take_dirty(), "and only one");
assert!(builder.set_child_hints(id, Hints::fixed(90, 20)));
assert!(!builder.is_dirty(), "an idempotent write owes no work");
assert!(!builder.set_child_hints(999_999, Hints::fixed(1, 1)));
assert!(!builder.set_child_params(999_999, LayoutParams::new()));
}
#[test]
fn reweighting_a_child_does_not_move_it() {
let factory = WidgetFactory::new_with_defaults();
let mut builder = CompositeBuilder::new(row(0), EdgeOffsets::all(0), Size::new(0, 0));
let first = builder
.add(&factory, "label", "A", Rect::new(0, 0, 10, 20), LayoutParams::new())
.expect("label is published");
let second = builder
.add(&factory, "label", "B", Rect::new(0, 0, 10, 20), LayoutParams::new())
.expect("label is published");
let third = builder
.add(&factory, "label", "C", Rect::new(0, 0, 10, 20), LayoutParams::new())
.expect("label is published");
let ids = [first.id(), second.id(), third.id()];
assert!(builder.set_child_params(ids[0], LayoutParams::filled()));
let mut order: Vec<ObjectId> = Vec::new();
builder.arrange(Rect::new(0, 0, 300, 40), &mut |id, _| order.push(id));
assert_eq!(order, ids, "the row order is the child list's, not the setters'");
let mut widths: Vec<(ObjectId, u32)> = Vec::new();
builder.arrange(Rect::new(0, 0, 300, 40), &mut |id, rect| widths.push((id, rect.width)));
let filling = widths.iter().find(|(id, _)| *id == ids[0]).expect("first is placed").1;
let fixed = widths.iter().find(|(id, _)| *id == ids[1]).expect("second is placed").1;
assert!(filling > fixed, "the filling child absorbs the room: {widths:?}");
}
#[test]
fn fill_is_separate_from_the_size_wish() {
let factory = WidgetFactory::new_with_defaults();
let mut builder = CompositeBuilder::new(row(0), EdgeOffsets::all(0), Size::new(0, 0));
builder
.add(&factory, "label", "fixed", Rect::new(0, 0, 40, 20), LayoutParams::new())
.expect("label is published");
builder
.add(&factory, "label", "grows", Rect::new(0, 0, 40, 20), LayoutParams::filled())
.expect("label is published");
let mut widths = Vec::new();
builder.arrange(Rect::new(0, 0, 300, 40), &mut |_, rect| widths.push(rect.width));
assert_eq!(widths.len(), 2);
assert!(
widths[1] > widths[0],
"the `fill` child must be wider than the fixed one: {widths:?}"
);
}
#[test]
fn an_unknown_child_is_reported_not_fatal() {
let (factory, mut builder) = builder();
assert!(builder
.add(
&factory,
"no_such_control_at_all",
"",
Rect::new(0, 0, 10, 10),
LayoutParams::new()
)
.is_none());
assert!(builder.is_empty());
}
#[test]
fn the_action_row_is_right_anchored() {
let factory = WidgetFactory::new_with_defaults();
let mut row = ActionRow::new(6);
for label in ["Cancel", "Back", "Finish"] {
row.add(&factory, label, Size::new(72, 36)).expect("button is published");
}
assert_eq!(row.len(), 3);
let expected: u32 = ["Cancel", "Back", "Finish"]
.iter()
.map(|label| {
(estimate_text_width(label, &Font::default(), 1.0) + 24)
.max(dimensions::BUTTON_MIN.width)
})
.sum();
assert_eq!(row.preferred_width(), expected + 2 * 6);
let band = Rect::new(0, 0, 240, 40);
let (span, buttons) = row.arrange(band);
assert_eq!(buttons.len(), 3);
assert_eq!(
span.x + span.width as i32,
band.x + band.width as i32,
"the row's trailing edge must be the band's trailing edge"
);
for pair in buttons.windows(2) {
assert_eq!(
pair[1].x - (pair[0].x + pair[0].width as i32),
6,
"the gap between two buttons must be the row's own gap"
);
}
assert_eq!(buttons[0].x, span.x, "the span begins at the first button");
}
#[test]
fn a_row_too_narrow_for_its_buttons_keeps_them_inside_the_band() {
let factory = WidgetFactory::new_with_defaults();
let mut row = ActionRow::new(6);
row.add(&factory, "One", Size::new(100, 36)).expect("button is published");
row.add(&factory, "Two", Size::new(100, 36)).expect("button is published");
let band = Rect::new(0, 0, 120, 40);
let (span, buttons) = row.arrange(band);
assert_eq!(buttons.len(), 2);
for button in &buttons {
assert!(
button.x >= band.x && button.x + button.width as i32 <= band.x + band.width as i32,
"every button must stay inside the band: {button:?} in {band:?}"
);
assert!(button.width > 0, "and none is dropped: {button:?}");
}
assert!(
span.x + span.width as i32 <= band.x + band.width as i32,
"the span the row reports must be inside the band: {span:?}"
);
assert!(
row.preferred_width() > 120,
"the row's own preference is still the unsqueezed requirement: {}",
row.preferred_width()
);
}
#[test]
fn an_empty_action_row_is_empty() {
let row = ActionRow::new(6);
assert!(row.is_empty());
assert_eq!(row.preferred_width(), 0);
let band = Rect::new(0, 0, 240, 40);
let (span, buttons) = row.arrange(band);
assert!(buttons.is_empty());
assert_eq!(span.width, 0, "an empty row spans no room");
}
#[test]
fn a_missing_control_does_not_break_the_row() {
let factory = WidgetFactory::new_with_defaults();
let mut row = ActionRow::new(6);
assert!(row.add(&factory, "Ok", Size::new(72, 36)).is_some());
assert_eq!(row.len(), 1);
}
#[test]
fn the_hints_are_always_normalised() {
let (factory, mut builder) = builder();
builder
.add(&factory, "label", "x", Rect::new(0, 0, 10, 10), LayoutParams::new())
.expect("label is published");
for horizontal in [true, false] {
let hints = builder.hints(horizontal);
for axis in [hints.width, hints.height] {
assert!(
axis.min <= axis.pref && axis.pref <= axis.max,
"min <= pref <= max must hold by construction: {axis:?}"
);
}
}
}
}