A span reporting `Len::default()` whenever a child had a share threw away how many shares it was holding, so each level of nesting re-divided a share rather than dividing the same space. One span of a rect beside a span of three gave 1/2 and 1/6 each, where the same four rects directly in one span get a quarter. A span that sizes from its children does not resolve `rest`, it passes the weight up; resolution belongs at the nearest ancestor with a length, and since the output became a box there is always one. The placement loop already divides by `len.rest / total.rest`, so it consumes carried weights unchanged -- only what the span reported was wrong. The uneven nesting is the case that fails without this; the even one passes either way and is here as the statement of intent. Decided by the owner, 2026-09-14. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
157 lines
4.8 KiB
Rust
157 lines
4.8 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 mut cursor = UiScalar::rel_min();
|
|
let mut lens = Vec::with_capacity(self.children.len());
|
|
for child in &self.children {
|
|
let mut span = UiSpan::new(cursor, UiScalar::rel_max());
|
|
if self.dir.sign == Sign::Neg {
|
|
span.flip();
|
|
}
|
|
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
|
let len = match painter.known_len(child, axis, region) {
|
|
Some(len) => len,
|
|
None => painter.place(child, region).len(axis),
|
|
};
|
|
cursor.px += len.px + self.gap;
|
|
cursor.rel += len.rel;
|
|
lens.push(len);
|
|
}
|
|
|
|
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
|
|
let total = lens.iter().fold(Len::px(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.px);
|
|
let rel_end = UiScalar::rel(len.rest / total.rest);
|
|
let end = (UiScalar::rel_max() + start) - offset;
|
|
start = rel_end.within(&start.to(end));
|
|
}
|
|
start.px += len.px;
|
|
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.px = ortho.px.max(used.px);
|
|
}
|
|
start.px += self.gap;
|
|
}
|
|
|
|
// Carried whole rather than collapsed to one share: a span that sizes
|
|
// from its children does not resolve `rest`, it passes the weight up,
|
|
// so nesting spans divides the same space rather than re-dividing a
|
|
// share of it. Four `rest(1)` children under two spans under one span
|
|
// get a quarter each, which collapsing to `rest(1)` per level does
|
|
// not give. Resolution happens at the nearest ancestor with a length,
|
|
// and the root always has one.
|
|
let along = total;
|
|
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
|
|
}
|
|
}
|