Take two roundings out of where a box comes from
Traced what was left of the warm-against-cold difference after fixed point. It is not accumulation and not one place: it is the same box reached two ways, and each way rounds where the other does not. `Scroll` was writing a box it had been given back out as its own length in pixels. That is the same box in another form, and centring a part in `rel 1` lands a step from centring it in `px 900`, because halving a difference is not halving each part of it. Content that fills the viewport and has not been scrolled is now handed back as it came, which makes the shrinker's `repaint` and `resize-repaint` cases agree exactly rather than within a step. `Span` placed each child a step from where the last one ended, so the rounding of every share was carried along the row. A position is now the fixed parts before it -- a sum, exact -- plus one rounded share of the room. Measured: two hundred equal shares of a 1000 px row ended at 999.999 and now end at 1000, and `tests/cases/layout.rs` pins it at 2, 3, 7, 64 and 200. What is left is a step per level of nesting between the two ways, which is what the fuzzers now allow: four of the five shrinker cases pass at one step and the fifth is five spans deep. Closing it needs one way of asking where a box is, which is a bigger change than this. Checked: fmt, clippy, 103 tests, all five shrinker cases at 300 seeds, 100 generated seeds, five examples byte-identical at 1920x1200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9d8415d65f
commit
bdab55824f
6 files changed
+90
-25
No files matched your search
@@ -186,6 +186,8 @@ impl UiScalar {
|
|||||||
|
|
||||||
/// Both channels by the same factor, which is what a fraction of a
|
/// Both channels by the same factor, which is what a fraction of a
|
||||||
/// length means when the length is part pixels and part a share.
|
/// length means when the length is part pixels and part a share.
|
||||||
|
/// Both channels by the same fraction, which is what a part of a length
|
||||||
|
/// means when the length is part pixels and part a share.
|
||||||
pub const fn scale(&self, by: Rel) -> Self {
|
pub const fn scale(&self, by: Rel) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rel: self.rel.mul(by),
|
rel: self.rel.mul(by),
|
||||||
|
|||||||
@@ -46,13 +46,23 @@ impl Widget for Scroll {
|
|||||||
// have placed the whole scroll in a box longer than it.
|
// have placed the whole scroll in a box longer than it.
|
||||||
let slack = (self.container_len - self.content_len).max(Px::ZERO);
|
let slack = (self.container_len - self.content_len).max(Px::ZERO);
|
||||||
let anchor = slack.mul(align.rel());
|
let anchor = slack.mul(align.rel());
|
||||||
let offset = UiVec2::from_axis(
|
let mut region = UiRegion::FULL;
|
||||||
self.axis,
|
// Content that fills the viewport and has not been scrolled is the
|
||||||
UiScalar::from_parts(Rel::ZERO, anchor - self.amt),
|
// viewport, and is handed back as it came. Writing the same box as
|
||||||
UiScalar::ZERO,
|
// its own length in pixels is the same box in another form, and the
|
||||||
);
|
// two do not round alike: a part centred in `rel 1` lands a step from
|
||||||
let mut region = UiRegion::FULL.offset(offset);
|
// one centred in `px 900`, since halving a difference is not halving
|
||||||
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
// each part of it.
|
||||||
|
let moved = anchor != Px::ZERO || self.amt != Px::ZERO;
|
||||||
|
if moved || self.content_len != self.container_len {
|
||||||
|
let offset = UiVec2::from_axis(
|
||||||
|
self.axis,
|
||||||
|
UiScalar::from_parts(Rel::ZERO, anchor - self.amt),
|
||||||
|
UiScalar::ZERO,
|
||||||
|
);
|
||||||
|
region = region.offset(offset);
|
||||||
|
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
||||||
|
}
|
||||||
painter.widget_aligned(&self.inner, region, RegionAlign::NEAR);
|
painter.widget_aligned(&self.inner, region, RegionAlign::NEAR);
|
||||||
// What it occupies is its box, on both axes: it clips its content to
|
// What it occupies is its box, on both axes: it clips its content to
|
||||||
// that box, so it can neither take less of one nor honestly ask for
|
// that box, so it can neither take less of one nor honestly ask for
|
||||||
|
|||||||
@@ -83,6 +83,15 @@ impl Widget for Span {
|
|||||||
// rule beside it already says how long it is, and then reading them
|
// rule beside it already says how long it is, and then reading them
|
||||||
// answers nothing and makes its size depend on theirs for it.
|
// answers nothing and makes its size depend on theirs for it.
|
||||||
let shrinks = !painter.ruled(!axis);
|
let shrinks = !painter.ruled(!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 = UiScalar::rel_min();
|
||||||
|
let mut taken = Weight::ZERO;
|
||||||
|
let room = UiScalar::rel_max() - UiScalar::from_parts(total.rel, total.px);
|
||||||
let mut start = UiScalar::rel_min();
|
let mut start = UiScalar::rel_min();
|
||||||
let mut ortho = Len::ZERO;
|
let mut ortho = Len::ZERO;
|
||||||
for (child, len) in self.children.iter().zip(&lens) {
|
for (child, len) in self.children.iter().zip(&lens) {
|
||||||
@@ -92,20 +101,17 @@ impl Widget for Span {
|
|||||||
if len.leftover > Weight::ZERO && len.px == Px::ZERO && len.rel == Rel::ZERO && !shares
|
if len.leftover > Weight::ZERO && len.px == Px::ZERO && len.rel == Rel::ZERO && !shares
|
||||||
{
|
{
|
||||||
painter.undraw(child);
|
painter.undraw(child);
|
||||||
start.px += self.gap;
|
fixed.px += self.gap;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut span = UiSpan::FULL;
|
let mut span = UiSpan::FULL;
|
||||||
span.start = start;
|
span.start = start;
|
||||||
if len.leftover > Weight::ZERO && shares {
|
if len.leftover > Weight::ZERO && shares {
|
||||||
let offset = UiScalar::from_parts(total.rel, total.px);
|
taken += len.leftover;
|
||||||
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 += len.px;
|
fixed.px += len.px;
|
||||||
start.rel += len.rel;
|
fixed.rel += len.rel;
|
||||||
|
start = shared(fixed, taken, total.leftover, room);
|
||||||
span.end = start;
|
span.end = start;
|
||||||
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
||||||
if self.dir.sign == Sign::Neg {
|
if self.dir.sign == Sign::Neg {
|
||||||
@@ -124,7 +130,8 @@ impl Widget for Span {
|
|||||||
ortho.px = ortho.px.max(used.px);
|
ortho.px = ortho.px.max(used.px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
start.px += self.gap;
|
fixed.px += self.gap;
|
||||||
|
start = shared(fixed, taken, total.leftover, room);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carried whole rather than collapsed to one share: a span that sizes
|
// Carried whole rather than collapsed to one share: a span that sizes
|
||||||
@@ -143,6 +150,16 @@ impl Widget for Span {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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: UiScalar, taken: Weight, weight: Weight, room: UiScalar) -> UiScalar {
|
||||||
|
if taken == Weight::ZERO {
|
||||||
|
return fixed;
|
||||||
|
}
|
||||||
|
fixed + room.scale(Rel::ratio(taken, weight))
|
||||||
|
}
|
||||||
|
|
||||||
impl Span {
|
impl Span {
|
||||||
pub fn empty(dir: Dir) -> Self {
|
pub fn empty(dir: Dir) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -285,6 +285,43 @@ fn an_uneven_nesting_still_gives_every_share_the_same_length() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// However many ways a row is divided, the shares add up to the row: each
|
||||||
|
/// one is the fixed parts before it plus a share of the room, rather than a
|
||||||
|
/// step from where the last one ended, so the roundings do not accumulate
|
||||||
|
/// along it. Chained, two hundred of them ended a step short of the edge.
|
||||||
|
#[test]
|
||||||
|
fn a_row_of_equal_shares_fills_it_exactly() {
|
||||||
|
for n in [2usize, 3, 7, 64, 200] {
|
||||||
|
let mut h = Harness::new((1000, 100));
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
let mut kids: Vec<StrongWidget> = Vec::new();
|
||||||
|
for _ in 0..n {
|
||||||
|
let kid = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
ids.push(kid.id());
|
||||||
|
kids.push(kid.add_strong(&mut h.rsc));
|
||||||
|
}
|
||||||
|
let span = Span {
|
||||||
|
children: kids,
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(span);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
for (i, id) in ids.iter().enumerate() {
|
||||||
|
let at = h.region(id).expect("a share drew nothing").top_left.x;
|
||||||
|
let want = Px::from_f32(1000.0 * (i as f32) / (n as f32));
|
||||||
|
assert!(
|
||||||
|
(at - want).abs() <= Px::STEP,
|
||||||
|
"{n} shares: the {i}th starts at {at:?}, not {want:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let end = h.region(ids.last().unwrap()).unwrap().bot_right.x;
|
||||||
|
assert_eq!(end, Px::from_int(1000), "{n} shares do not reach the edge");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Where the shader puts an edge: the two parts of a scalar are floored
|
/// Where the shader puts an edge: the two parts of a scalar are floored
|
||||||
/// apart, so a fraction and a pixel offset snap independently, and each is
|
/// apart, so a fraction and a pixel offset snap independently, and each is
|
||||||
/// taken to the boundary it composes to within half a step of. Kept in step
|
/// taken to the boundary it composes to within half a step of. Kept in step
|
||||||
|
|||||||
+5
-6
@@ -31,12 +31,11 @@ fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
|||||||
}
|
}
|
||||||
const SEEDS: [u64; 9] = [1, 2, 3, 5, 8, 10, 13, 86, 98];
|
const SEEDS: [u64; 9] = [1, 2, 3, 5, 8, 10, 13, 86, 98];
|
||||||
|
|
||||||
/// The same box, to a step of the grid per operation. Warm and cold reach a
|
/// The same box, to a step of the grid per level of nesting between the two
|
||||||
/// coordinate by different arithmetic: a move lands on the same number now,
|
/// ways of reaching it. A move, a repaint and a row of shares land on the
|
||||||
/// and a length composed one way against the same length measured another can
|
/// same number now; what is left is a box centred in a fraction of its parent
|
||||||
/// land one step out. These cases apply two operations in turn, so they allow
|
/// against the same box centred in its own pixels. A step is a thousandth of
|
||||||
/// two steps -- a thousandth of a pixel each, where this was a twentieth of
|
/// a pixel, where this was a twentieth of one before any of it was on a grid.
|
||||||
/// one before any of it was on a grid.
|
|
||||||
const AGREE_STEPS: i32 = 2;
|
const AGREE_STEPS: i32 = 2;
|
||||||
|
|
||||||
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
|
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
|
||||||
|
|||||||
+3
-3
@@ -51,9 +51,9 @@ const WORDS: &[&str] = &[
|
|||||||
const ONE_LINE: &str = "one line, overflowing whatever it is given";
|
const ONE_LINE: &str = "one line, overflowing whatever it is given";
|
||||||
|
|
||||||
const OUTER: (f32, f32) = (1920.0, 1200.0);
|
const OUTER: (f32, f32) = (1920.0, 1200.0);
|
||||||
/// Steps of the grid two ways of reaching a box may differ by. See
|
/// Steps of the grid two ways of reaching a box may differ by: one per level
|
||||||
/// `docs/HANDOFF.md`'s "Fixed point" in `ai-app-2` for where the last of
|
/// of nesting between them, and these trees are five deep. See
|
||||||
/// them is.
|
/// `docs/HANDOFF.md`'s "Fixed point" in `ai-app-2` for what is left.
|
||||||
const AGREE_STEPS: i32 = 2;
|
const AGREE_STEPS: i32 = 2;
|
||||||
const INNER: (f32, f32) = (640.0, 900.0);
|
const INNER: (f32, f32) = (640.0, 900.0);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user