Three things the measurements asked for, all about how much a box that came from an answer costs. **A part in the box's own coordinates.** Saying "less eleven pixels at the end" in frame lengths from the box's start means reading how long the box is, and a container whose box is its own answer then depends on its own answer: `Pad` drew sixty-four times in one resize frame at seed 13, chasing its own width. `Part::Of` says the same thing as a part of the box, which composes without a length -- pixels are pixels wherever the box lands -- and what a child under it holds for maps back through that part onto the container's own box rather than onto the frame. **One axis of the box at a time.** `extent_len` pinned both axes, so a span dividing one of them held for one length of the other as well, and a resize broke every span whose cross-axis answer moved. **No lazy placement.** Leaving a child's answer to be placed at the end of the parent's draw, rather than as the child answers, was meant to save a recomposition. It costs one instead: the drawing is put in the part first and in the answer's box after, and where it does not hold for both that is two drawings rather than one. Seed 1 at depth 8 went from 391 widget draws on a resize to 29 with it gone. The test that pinned three draws for a numeric leaf in a span goes with it. Seed 1 at depth 8, widget draws / distinct widgets / update, against #18's head and against the commit this branch started from: | phase |e44dea3|34cafb6| here | | --- | --- | --- | --- | | cold | 369/261/10.6 | 463/274/13.3 | 516/288/12.0 | | repaint | 1 | 1 | 1 | | many | 157/95/0.33 | 263/108/0.59 | 187/119/0.52 | | size | 16/12/0.018 | 3/3 | 3/3/0.010 | | scroll | 2/0.002 | 1 | 1/0.004 | | resize | 13/13/0.019 | 22/15/0.032 | 24/76/0.090 | Seed 13 at depth 8 is where the protocol still costs: `many` 1091 draws against #18's 524, and `resize` 2215 against a frame #18 does not draw at all. Both are the same shape -- an answer measured in one box and drawn in another -- and the handoff says where that comes from. Checked: fmt, clippy with -D warnings, 108 suite tests, 20 core tests, the 11 generated cases, and the shrinker at 400 trees of depth 5, which fails seeds 2 (repaint) and 108 (reorder).
231 lines
8.7 KiB
Rust
231 lines
8.7 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;
|
|
// The row: this span's own box, as a length of the frame its children
|
|
// are laid out against. Its start is nothing's business -- a slot is
|
|
// a length from it -- so what this reads is the length alone.
|
|
let far = painter.extent_len(axis);
|
|
let along = |from: Len, to: Len| match self.dir.sign {
|
|
Sign::Pos => UiSpan::new(from, to),
|
|
Sign::Neg => UiSpan::new(far - to, far - from),
|
|
};
|
|
// Across itself the child sits where its own alignment says, in the
|
|
// whole of the row: a span is what contains its children there, and
|
|
// nothing divides that axis.
|
|
let across = Place::Within(Part::All);
|
|
// A length for every child before their final slots are chosen. The
|
|
// frame passes through unchanged, so `rel(0.5)` is half the area this
|
|
// span was given whatever else is in it and wherever this child sits
|
|
// among them; what it is drawn in is the room left from the cursor,
|
|
// because a text has to wrap at the width actually there.
|
|
let mut cursor = Len::rel_min();
|
|
let mut lens = Vec::with_capacity(self.children.len());
|
|
for child in &self.children {
|
|
let room = Place::Within(Part::From(along(cursor, far)));
|
|
let len = painter
|
|
.widget_at(child, UiRegion::FULL, axis.pair(room, across))
|
|
.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 row less everything
|
|
// fixed, as a length of the frame rather than a number of pixels.
|
|
let room = far - 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.frame_px_len(axis)) > Px::ZERO;
|
|
let holds = match shares {
|
|
true => Holds::from(Px::STEP..=Px::MAX),
|
|
false => Holds::from(Px::MIN..=Px::ZERO),
|
|
};
|
|
painter.frame_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 from = 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);
|
|
// Along the row the span says where the child goes, and that slot
|
|
// is the drawing's box outright rather than something to place an
|
|
// answer inside again.
|
|
let slot = Place::Fill(Part::From(along(from, start)));
|
|
let placed = painter.widget_at(child, UiRegion::FULL, axis.pair(slot, across));
|
|
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
|
|
}
|
|
}
|