The split box was named `region` and `placement` on 2026-09-17; `frame` came back as a length and survived, `extent` did not. It stayed as the name for both halves, distinguished only by prose: `draw_at` bound the caller's `part` to a parameter called `extent`, and `ActiveData` held two `UiRegion`s that `draw_at` wrote `part: extent` from. The box a parent asks a widget in is now the region, and where its drawing ends up is its placement. `Painter`'s four holds accumulators become the one `LayoutHolds` they were assembled into, which also drops the name mapping between them. The cold dump of 400 depth-5 trees is byte-identical across the change.
258 lines
10 KiB
Rust
258 lines
10 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.region_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: from
|
|
// a hint where one says, and from drawing otherwise. 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 a drawn child is asked 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());
|
|
let mut drawn_across = Vec::with_capacity(self.children.len());
|
|
for child in &self.children {
|
|
let len = match painter.size_hint(child, axis) {
|
|
Some(len) => {
|
|
drawn_across.push(None);
|
|
len
|
|
}
|
|
None => {
|
|
let room = Place::Within(Part::From(along(cursor, far)));
|
|
let size = painter
|
|
.widget_at(child, [None; 2], axis.pair(room, across))
|
|
.size();
|
|
drawn_across.push(Some(size.axis(!axis)));
|
|
size.axis(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 = painter.to_px(room, axis) > Px::ZERO;
|
|
let holds = match shares {
|
|
true => Holds::from(Px::STEP..=Px::MAX),
|
|
false => Holds::from(Px::MIN..=Px::ZERO),
|
|
};
|
|
painter.window_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), &across_len) in self.children.iter().zip(&lens).zip(&drawn_across) {
|
|
// 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;
|
|
start = shared(fixed, taken, total.leftover, room);
|
|
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 child's box outright rather than something to place an
|
|
// answer inside again. A share is decided here and nowhere
|
|
// else: its slot narrows its frame, and the child is asked in
|
|
// it, since a text wraps at the width it is actually given. A
|
|
// fixed child's slot is its own answer, so a drawing made in the
|
|
// room is put there as it is, and one not made yet is made here.
|
|
let slot = along(from, start);
|
|
let place = axis.pair(Place::Fill(Part::From(slot)), across);
|
|
let mut narrow = [None; 2];
|
|
if len.leftover > Weight::ZERO && shares {
|
|
narrow[axis as usize] = Some(slot.len());
|
|
}
|
|
let used = match (across_len, narrow[axis as usize]) {
|
|
(Some(across_len), None) => {
|
|
painter.place_at(child, place);
|
|
across_len
|
|
}
|
|
_ => painter.widget_at(child, narrow, place).len(!axis),
|
|
};
|
|
if shrinks {
|
|
// 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
|
|
}
|
|
}
|