Put lengths, padding, gaps and alignment on the grid too
`Len` is `Px` beside `Rel` beside `Weight`, so the seam `4e28f10` left in `Span` -- a float length added to a fixed-point cursor -- is gone, and the sum a span compares against its box is exact. `Weight` is its own scale, `Fixed<16>`, because a share of what is left over is not a fraction of anything: a list divides its room by the total of them, so the range has to hold a whole list's worth while the precision only has to tell two weights apart. `Rel::ratio` turns two weights into a share on the finer grid, which is what a span needs and what dividing them on their own grid would round away. `AxisAlign` holds a `Rel` rather than a float, which is what the layout was reading out of it anyway. `Padding` and `Span::gap` hold `Px`, converted where they are built instead of on every frame. `RegionAlign::rel` is gone; its one caller wanted a position, and now builds one. `Fixed` gains `from_num` for a number as it is written in source, `mul_int` for a length repeated a whole number of times, and `ratio`. Checked: fmt, clippy, 101 tests, 100 generated seeds in 86 s, all five shrinker cases at 300 seeds, and all five examples byte-identical at 1920x1200 against `4e28f10`. With the fuzzer comparing for equality rather than within 0.05 px, four of the five cases now pass 100 seeds -- `resize-repaint` joins the other three. `reorder` still fails one seed by one step, so the last of it is in what a box is measured *in*: `px_len` and the window are still floats. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4e28f1047e
commit
bd6de71a55
15 files changed
+199
-158
No files matched your search
+43
-33
@@ -14,7 +14,7 @@ pub enum OrthoSize {
|
||||
pub struct Span {
|
||||
pub children: Vec<StrongWidget>,
|
||||
pub dir: Dir,
|
||||
pub gap: f32,
|
||||
pub gap: Px,
|
||||
pub ortho: OrthoSize,
|
||||
}
|
||||
|
||||
@@ -35,14 +35,21 @@ impl Widget for Span {
|
||||
Some(len) => len,
|
||||
None => painter.widget_within(child, region).len(axis),
|
||||
};
|
||||
// Onto the grid at the seam: `Len` is still in floats.
|
||||
cursor.px += Px::from_f32(len.px + self.gap);
|
||||
cursor.rel += Rel::from_f32(len.rel);
|
||||
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 gaps = self
|
||||
.gap
|
||||
.mul_int(self.children.len().saturating_sub(1) as i32);
|
||||
let total = lens.iter().fold(
|
||||
Len {
|
||||
px: gaps,
|
||||
..Len::ZERO
|
||||
},
|
||||
|sum, len| sum + *len,
|
||||
);
|
||||
|
||||
// Whether anything is left over is a question in pixels: `rel(0.5)`
|
||||
// beside 300 px is full at 600 and overfull at 400. The room to divide
|
||||
@@ -53,31 +60,32 @@ impl Widget for Span {
|
||||
// 0.00003 px of rounding decides whether a leftover-only child is
|
||||
// drawn at all. The validity range is split at the moved boundary, and
|
||||
// exact there, since no box lands on it any more.
|
||||
let fixed = 1.0 - total.rel;
|
||||
let fixed = Rel::ONE - total.rel;
|
||||
let margin = Px::from_f32(HOLDS_EPSILON_PX);
|
||||
let mut shares = false;
|
||||
if total.leftover > 0.0 {
|
||||
let current = painter.px_len(axis);
|
||||
let holds = if fixed > 0.0 {
|
||||
if total.leftover > Weight::ZERO {
|
||||
let current = Px::from_f32(painter.px_len(axis));
|
||||
let holds = if fixed > Rel::ZERO {
|
||||
// The box length at which the room reaches the margin.
|
||||
let enough = (total.px + HOLDS_EPSILON_PX) / fixed;
|
||||
let enough = (total.px + margin).div(fixed);
|
||||
shares = current > enough;
|
||||
match shares {
|
||||
true => Holds::exact(enough.next_up()..=f32::INFINITY),
|
||||
false => Holds::exact(f32::NEG_INFINITY..=enough),
|
||||
true => Holds::exact(enough.to_f32().next_up()..=f32::INFINITY),
|
||||
false => Holds::exact(f32::NEG_INFINITY..=enough.to_f32()),
|
||||
}
|
||||
} else if fixed < 0.0 {
|
||||
} else if fixed < Rel::ZERO {
|
||||
// The relative parts grow faster than the box does, so here
|
||||
// a shorter box is the one that leaves room.
|
||||
let enough = (total.px + HOLDS_EPSILON_PX) / fixed;
|
||||
let enough = (total.px + margin).div(fixed);
|
||||
shares = current < enough;
|
||||
match shares {
|
||||
true => Holds::exact(f32::NEG_INFINITY..=enough.next_down()),
|
||||
false => Holds::exact(enough..=f32::INFINITY),
|
||||
true => Holds::exact(f32::NEG_INFINITY..=enough.to_f32().next_down()),
|
||||
false => Holds::exact(enough.to_f32()..=f32::INFINITY),
|
||||
}
|
||||
} else {
|
||||
// The relative parts take exactly the box, whatever it is, so
|
||||
// the only room is what negative pixels leave.
|
||||
shares = total.px < -HOLDS_EPSILON_PX;
|
||||
shares = total.px < margin.neg();
|
||||
Holds::ANY
|
||||
};
|
||||
painter.holds(axis, holds);
|
||||
@@ -89,21 +97,23 @@ impl Widget for Span {
|
||||
// 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 > 0.0 && len.px == 0.0 && len.rel == 0.0 && !shares {
|
||||
if len.leftover > Weight::ZERO && len.px == Px::ZERO && len.rel == Rel::ZERO && !shares
|
||||
{
|
||||
painter.undraw(child);
|
||||
start.px += Px::from_f32(self.gap);
|
||||
start.px += self.gap;
|
||||
continue;
|
||||
}
|
||||
let mut span = UiSpan::FULL;
|
||||
span.start = start;
|
||||
if len.leftover > 0.0 && shares {
|
||||
let offset = UiScalar::new(total.rel, total.px);
|
||||
let rel_end = UiScalar::rel(len.leftover / total.leftover);
|
||||
if len.leftover > Weight::ZERO && shares {
|
||||
let offset = UiScalar::from_parts(total.rel, total.px);
|
||||
let share = Rel::ratio(len.leftover, total.leftover);
|
||||
let rel_end = UiScalar::from_parts(share, Px::ZERO);
|
||||
let end = (UiScalar::rel_max() + start) - offset;
|
||||
start = rel_end.within(&start.to(end));
|
||||
}
|
||||
start.px += Px::from_f32(len.px);
|
||||
start.rel += Rel::from_f32(len.rel);
|
||||
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 {
|
||||
@@ -116,13 +126,13 @@ impl Widget for Span {
|
||||
// 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 != 0.0 || used.leftover != 0.0 {
|
||||
if used.rel != Rel::ZERO || used.leftover != Weight::ZERO {
|
||||
ortho = Len::LEFTOVER;
|
||||
} else if ortho.leftover == 0.0 {
|
||||
} else if ortho.leftover == Weight::ZERO {
|
||||
ortho.px = ortho.px.max(used.px);
|
||||
}
|
||||
}
|
||||
start.px += Px::from_f32(self.gap);
|
||||
start.px += self.gap;
|
||||
}
|
||||
|
||||
// Carried whole rather than collapsed to one share: a span that sizes
|
||||
@@ -146,13 +156,13 @@ impl Span {
|
||||
Self {
|
||||
children: Vec::new(),
|
||||
dir,
|
||||
gap: 0.0,
|
||||
gap: Px::ZERO,
|
||||
ortho: OrthoSize::Children,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
||||
self.gap = gap.to_f32();
|
||||
self.gap = Px::from_num(gap);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -173,7 +183,7 @@ impl Span {
|
||||
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
|
||||
pub children: Wa,
|
||||
pub dir: Dir,
|
||||
pub gap: f32,
|
||||
pub gap: Px,
|
||||
pub ortho: OrthoSize,
|
||||
_pd: PhantomData<(State, Tag)>,
|
||||
}
|
||||
@@ -201,14 +211,14 @@ impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
|
||||
Self {
|
||||
children,
|
||||
dir,
|
||||
gap: 0.0,
|
||||
gap: Px::ZERO,
|
||||
ortho: OrthoSize::Children,
|
||||
_pd: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
||||
self.gap = gap.to_f32();
|
||||
self.gap = Px::from_num(gap);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user