A widget's region is now held in the coordinates of the slot it draws in rather than the window's, and `Painter::place` is how a container asks for a slot: it draws a child it decides the box of and may decide again. Everything under that slot is a fraction of its box, so placing the child a second time is one entry to write whether it moved or changed length. A child drawn any other way has no slot and shares its nearest ancestor's. That is what keeps the chain short. `chain_cost` measured depth as the cost -- free to 8, +42.6% at 16 -- and a slot per widget put a transcript's glyphs past that for nothing, since almost every slot was zero. `Span`, `Aligned` and `Scroll` are the containers that re-place a child after drawing it, and `tests/layout.rs` pins that four widgets between a span and a leaf leave the leaf's chain one deep. `UiRegion::stretch`, `UiRegion::stretchable` and `UiScalar::stretch` are gone. Nothing is inverted any more: a box that changed length is written to its slot, and the descendants recompose against it in the shader. That also retires the case the guard existed for, where a fixed length has no fraction to recover -- `tests/layout.rs` now stretches a 40-tall row on its other axis, which `stretchable` refused outright. What still walks the CPU is deciding who must draw again, which no chain can answer: `mark_resized` descends from the widget whose box changed and marks anything whose own box changed length and whose drawing reads it. A part of a box with no relative extent on an axis is a fixed length, and composing into it leaves none either, so the walk stops where a length did not change -- an 80-wide child in a widened row is not redrawn though it says `Redraw`. `Span`, `Pad`, `Stack`, `Offset`, `Aligned`, `SetSize` and `LayerOffset` say `Scale`: each places in fractions and offsets of its own box and none reads the box's pixel length. `Scroll` and `MaxSize` do read pixels and stay `Redraw`. 45 tests pass, five of them new. Render verification comes after the CPU side, per the owner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
145 lines
4.0 KiB
Rust
145 lines
4.0 KiB
Rust
use crate::prelude::*;
|
|
use std::marker::PhantomData;
|
|
|
|
pub struct Span {
|
|
pub children: Vec<StrongWidget>,
|
|
pub dir: Dir,
|
|
pub gap: f32,
|
|
}
|
|
|
|
impl Widget for Span {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
let axis = self.dir.axis;
|
|
// A length for every child before any is placed: from its own hint
|
|
// where it has one, and from drawing it where it does not.
|
|
let lens: Vec<Len> = self
|
|
.children
|
|
.iter()
|
|
.map(|child| match painter.size_hint(child, axis) {
|
|
Some(len) => len,
|
|
None => painter.place(child, UiRegion::FULL).len(axis),
|
|
})
|
|
.collect();
|
|
|
|
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
|
|
let total = lens.iter().fold(Len::abs(gap), |sum, len| sum + *len);
|
|
|
|
let mut start = UiScalar::rel_min();
|
|
let mut ortho = Len::ZERO;
|
|
for (child, len) in self.children.iter().zip(&lens) {
|
|
let mut span = UiSpan::FULL;
|
|
span.start = start;
|
|
if len.rest > 0.0 {
|
|
let offset = UiScalar::new(total.rel, total.abs);
|
|
let rel_end = UiScalar::rel(len.rest / total.rest);
|
|
let end = (UiScalar::rel_max() + start) - offset;
|
|
start = rel_end.within(&start.to(end));
|
|
}
|
|
start.abs += len.abs;
|
|
start.rel += len.rel;
|
|
span.end = start;
|
|
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
|
if self.dir.sign == Sign::Neg {
|
|
region.flip(axis);
|
|
}
|
|
let used = painter.place(child, region).size().axis(!axis);
|
|
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
|
|
if used.rel > 0.0 || used.rest > 0.0 {
|
|
ortho = Len::REST;
|
|
} else if ortho.rest == 0.0 {
|
|
ortho.abs = ortho.abs.max(used.abs);
|
|
}
|
|
start.abs += self.gap;
|
|
}
|
|
|
|
let along = match total.rest == 0.0 && total.rel == 0.0 {
|
|
true => total,
|
|
false => Len::default(),
|
|
};
|
|
Size::from_axis(axis, along, ortho)
|
|
}
|
|
|
|
/// Every child is placed in fractions and offsets of the span's own box,
|
|
/// so a longer box holds the same layout and the children follow it.
|
|
fn on_resize(&self, _: Axis) -> OnResize {
|
|
OnResize::Scale
|
|
}
|
|
}
|
|
|
|
impl Span {
|
|
pub fn empty(dir: Dir) -> Self {
|
|
Self {
|
|
children: Vec::new(),
|
|
dir,
|
|
gap: 0.0,
|
|
}
|
|
}
|
|
|
|
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
|
self.gap = gap.to_f32();
|
|
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: f32,
|
|
_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: 0.0,
|
|
_pd: PhantomData,
|
|
}
|
|
}
|
|
|
|
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
|
self.gap = gap.to_f32();
|
|
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
|
|
}
|
|
}
|