The decision used a rounded division, `total.px.div(fixed)`, where the room the children get is a floored multiply, so the boundary and the drawing it guards were two expressions for one length and disagreed at the edge of it. `room` is that length as a `Len`, `room.to_px` is the multiply, and `Holds::through` is its exact preimage -- so ask `room` whether anything is left and hand the answer back through the same expression. The three branches go with the division. They were the sign of `1 - rel`: the fixed parts growing slower than the box, faster, or exactly with it, and `through` reads that sign already. Forty lines become twelve, one `div` leaves layout, and the boundary is the drawing's own. Green on the suite, the shrinker at 400 seeds of depth 5, the oracle at 1000 seeds of depth 6 and 120 in debug, and 2000 seeds at depth 4 over all fifteen cases. `tabs`, `view`, `minimal` and `random` byte-identical.
237 lines
8.6 KiB
Rust
237 lines
8.6 KiB
Rust
use crate::prelude::*;
|
|
use std::marker::PhantomData;
|
|
|
|
pub struct Span {
|
|
pub children: Vec<StrongWidget>,
|
|
pub dir: Dir,
|
|
pub gap: Px,
|
|
}
|
|
|
|
impl Widget for Span {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
let axis = self.dir.axis;
|
|
// A length for every child before their final boxes are chosen: from
|
|
// a hint where one exists, and from drawing otherwise.
|
|
let mut cursor = Len::rel_min();
|
|
let mut lens = Vec::with_capacity(self.children.len());
|
|
for child in &self.children {
|
|
let mut span = UiSpan::new(cursor, Len::rel_max());
|
|
if self.dir.sign == Sign::Neg {
|
|
span.flip();
|
|
}
|
|
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
|
// Offered the room left from the cursor, because a text has to
|
|
// wrap at the width actually there, but reporting a fraction of
|
|
// the whole row: `rel(0.5)` is half the span whatever else is in
|
|
// it and wherever this child sits among them.
|
|
let len = match painter.known_len(child, axis, region, UiVec2::FULL_SIZE) {
|
|
Some(len) => len,
|
|
None => painter
|
|
.widget_at(child, region, UiVec2::FULL_SIZE, [false; 2])
|
|
.len(axis),
|
|
};
|
|
cursor.px += len.px + self.gap;
|
|
cursor.rel += len.rel;
|
|
lens.push(len);
|
|
}
|
|
|
|
let gaps = self
|
|
.gap
|
|
.mul_int(self.children.len().saturating_sub(1) as i32);
|
|
let total = lens.iter().fold(
|
|
LayoutLen {
|
|
px: gaps,
|
|
..LayoutLen::ZERO
|
|
},
|
|
|sum, len| sum + *len,
|
|
);
|
|
|
|
// What is left for the shares to divide: the box less everything
|
|
// fixed, as a length of the box rather than a number of pixels.
|
|
let room = Len::rel_max() - Len::from_parts(total.rel, total.px);
|
|
// Whether anything is left over is a question in pixels: `rel(0.5)`
|
|
// beside 300 px is full at 600 and overfull at 400. Asked of `room`
|
|
// itself, and answered back through the same expression, so the
|
|
// boundary is the drawing's own and not a second way of finding it:
|
|
// the three cases a rounded division needed -- the fixed parts
|
|
// growing slower than the box, faster, or exactly with it -- are the
|
|
// sign of `room.rel`, which `through` already reads. What the
|
|
// generated oracle checks is the consequence, since which children
|
|
// exist at all turns on this.
|
|
let mut shares = false;
|
|
if total.leftover > Weight::ZERO {
|
|
shares = room.to_px(painter.px_len(axis)) > Px::ZERO;
|
|
let holds = match shares {
|
|
true => Holds::from(Px::STEP..=Px::MAX),
|
|
false => Holds::from(Px::MIN..=Px::ZERO),
|
|
};
|
|
painter.holds(axis, holds.through(room));
|
|
}
|
|
|
|
// Across itself a span is as long as its longest child -- unless a
|
|
// rule beside it gives that length outright, and then reading them
|
|
// answers nothing and makes its size depend on theirs for it. A rule
|
|
// that only bounds the length does not count: the answer is still
|
|
// this span's to give.
|
|
let shrinks = !painter.has_exact_size(!axis);
|
|
// What the fixed parts and the gaps before here take, which is a sum
|
|
// of lengths and exact, and how much of the leftover weight is
|
|
// spoken for. A position is one from the other rather than a step
|
|
// from the last child: the share of the room is rounded, and taking
|
|
// each from the one before it would carry every rounding along the
|
|
// row.
|
|
let mut fixed = Len::rel_min();
|
|
let mut taken = Weight::ZERO;
|
|
let mut start = Len::rel_min();
|
|
let mut ortho = LayoutLen::ZERO;
|
|
for (child, len) in self.children.iter().zip(&lens) {
|
|
// A child asking for nothing but a part of what is left over,
|
|
// when nothing is, is not drawn at all. One that also asked for
|
|
// pixels or a fraction keeps those and overflows.
|
|
if len.leftover > Weight::ZERO && len.px == Px::ZERO && len.rel == Rel::ZERO && !shares
|
|
{
|
|
painter.undraw(child);
|
|
fixed.px += self.gap;
|
|
continue;
|
|
}
|
|
let mut span = UiSpan::FULL;
|
|
span.start = start;
|
|
if len.leftover > Weight::ZERO && shares {
|
|
taken += len.leftover;
|
|
}
|
|
fixed.px += len.px;
|
|
fixed.rel += len.rel;
|
|
start = shared(fixed, taken, total.leftover, room);
|
|
span.end = start;
|
|
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
|
if self.dir.sign == Sign::Neg {
|
|
region.flip(axis);
|
|
}
|
|
// Along the row this box is the child's own answer, so the answer
|
|
// is not placed in it again; across it the child sits where its
|
|
// alignment says.
|
|
let placed = painter.widget_at(
|
|
child,
|
|
region,
|
|
UiVec2::FULL_SIZE,
|
|
[axis == Axis::X, axis == Axis::Y],
|
|
);
|
|
if shrinks {
|
|
let used = placed.len(!axis);
|
|
// Choosing between a fixed and a relative length from the
|
|
// span's own eventual width admits multiple fixed points.
|
|
// A scalable child therefore makes Children scalable too;
|
|
// only fixed children are compared with one another.
|
|
if used.rel != Rel::ZERO || used.leftover != Weight::ZERO {
|
|
ortho = LayoutLen::LEFTOVER;
|
|
} else if ortho.leftover == Weight::ZERO {
|
|
ortho.px = ortho.px.max(used.px);
|
|
}
|
|
}
|
|
fixed.px += self.gap;
|
|
start = shared(fixed, taken, total.leftover, room);
|
|
}
|
|
|
|
// Carried whole rather than collapsed to one share: a span that sizes
|
|
// from its children does not resolve `leftover`, it passes the weight up,
|
|
// so nesting spans divides the same space rather than re-dividing a
|
|
// share of it. Four `leftover(1)` children under two spans under one span
|
|
// get a quarter each, which collapsing to `leftover(1)` per level does
|
|
// not give. Resolution happens at the nearest ancestor with a length,
|
|
// and the root always has one.
|
|
let along = total;
|
|
let ortho = match shrinks {
|
|
true => ortho,
|
|
false => LayoutLen::rel(1.0),
|
|
};
|
|
Size::from_axis(axis, along, ortho)
|
|
}
|
|
}
|
|
|
|
/// Where a row has reached: everything fixed before this point, which is a
|
|
/// sum and exact, plus the share of the room the weights so far are worth,
|
|
/// which is one rounding wherever it is asked for.
|
|
fn shared(fixed: Len, taken: Weight, weight: Weight, room: Len) -> Len {
|
|
if taken == Weight::ZERO {
|
|
return fixed;
|
|
}
|
|
fixed + room.scale(Rel::ratio(taken, weight))
|
|
}
|
|
|
|
impl Span {
|
|
pub fn empty(dir: Dir) -> Self {
|
|
Self {
|
|
children: Vec::new(),
|
|
dir,
|
|
gap: Px::ZERO,
|
|
}
|
|
}
|
|
|
|
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
|
self.gap = Px::from_num(gap);
|
|
self
|
|
}
|
|
|
|
pub fn push(&mut self, w: StrongWidget) {
|
|
self.children.push(w);
|
|
}
|
|
|
|
pub fn pop(&mut self) -> Option<StrongWidget> {
|
|
self.children.pop()
|
|
}
|
|
}
|
|
|
|
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
|
|
pub children: Wa,
|
|
pub dir: Dir,
|
|
pub gap: Px,
|
|
_pd: PhantomData<(State, Tag)>,
|
|
}
|
|
|
|
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc>
|
|
for SpanBuilder<Rsc, LEN, Wa, Tag>
|
|
{
|
|
type Widget = Span;
|
|
|
|
#[track_caller]
|
|
fn run(self, rsc: &mut Rsc) -> Self::Widget {
|
|
Span {
|
|
children: self.children.add(rsc).arr.into_iter().collect(),
|
|
dir: self.dir,
|
|
gap: self.gap,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
|
|
SpanBuilder<State, LEN, Wa, Tag>
|
|
{
|
|
pub fn new(children: Wa, dir: Dir) -> Self {
|
|
Self {
|
|
children,
|
|
dir,
|
|
gap: Px::ZERO,
|
|
_pd: PhantomData,
|
|
}
|
|
}
|
|
|
|
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
|
self.gap = Px::from_num(gap);
|
|
self
|
|
}
|
|
}
|
|
|
|
impl std::ops::Deref for Span {
|
|
type Target = Vec<StrongWidget>;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.children
|
|
}
|
|
}
|
|
|
|
impl std::ops::DerefMut for Span {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.children
|
|
}
|
|
}
|