From 4063635f3935f0cfda8f8ff67b5174463f4899c6 Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Tue, 15 Sep 2026 12:31:31 -0400 Subject: [PATCH] Check that a one-pixel line keeps its pixel through the chain Both edges of a fixed length share their box's fraction, so composing the chain moves them together and the shader's floor can shift the pixel between them but not round it away. The second test is the case that makes the first one worth having: a span short of room takes it from its shares, which go to nothing and then past it, and never from the fixed lengths between them. Expressing the same line as a fraction of the output fails both, which is what the tests are there to keep visible. Co-Authored-By: Claude Opus 5 --- tests/layout.rs | 120 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/tests/layout.rs b/tests/layout.rs index 008c735..1bb3002 100644 --- a/tests/layout.rs +++ b/tests/layout.rs @@ -229,3 +229,123 @@ fn an_uneven_nesting_still_gives_every_share_the_same_length() { assert_corners!(h, id, (x, 0), (x + 100.0, 200)); } } + +/// Where the shader puts an edge: the two parts of a scalar are floored +/// apart, so a fraction and a pixel offset snap independently. +fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) { + let active = &h.render.active[&id]; + let region = h.render.moves.resolve(active.parent_move, active.region); + let dim = h.size().axis(axis); + let edge = |s: UiScalar| (s.rel * dim).floor() + s.px.floor(); + let span = region.axis(axis); + (edge(span.start), edge(span.end)) +} + +fn hairline(h: &mut Harness, marks: &mut Vec) -> StrongWidget { + let inner = rect(Color::RED).add_strong(&mut h.rsc); + let mark = SetSize { + inner, + x: Some(Len::px(1.0)), + y: None, + } + .add_strong(&mut h.rsc); + marks.push(mark.id()); + mark +} + +fn share(h: &mut Harness, inner: StrongWidget, ratio: f32) -> StrongWidget { + SetSize { + inner, + x: Some(Len::rest(ratio)), + y: None, + } + .add_strong(&mut h.rsc) +} + +/// Shares in weights no binary fraction lands on, a padding on one branch +/// and not the other, so an edge falls near an integer as often as it can. +fn hairlines(h: &mut Harness, depth: usize, marks: &mut Vec) -> StrongWidget { + let mut span = Span::empty(Dir::RIGHT); + if depth == 0 { + let left = rect(Color::BLUE).add_strong(&mut h.rsc); + let left = share(h, left, 3.0); + span.push(left); + let mark = hairline(h, marks); + span.push(mark); + let right = rect(Color::BLUE).add_strong(&mut h.rsc); + let right = share(h, right, 7.0); + span.push(right); + return span.add_strong(&mut h.rsc); + } + let first = hairlines(h, depth - 1, marks); + let first = share(h, first, 3.0); + span.push(first); + let second = hairlines(h, depth - 1, marks); + let second = Pad { + padding: Padding { + left: 3.0, + right: 7.0, + top: 0.0, + bottom: 0.0, + }, + inner: second, + } + .add_strong(&mut h.rsc); + let second = share(h, second, 5.0); + span.push(second); + span.add_strong(&mut h.rsc) +} + +/// A one-pixel line is a pixel wherever it is drawn. Both edges of a fixed +/// length share their box's fraction, so composing the chain moves them +/// together and the shader's `floor` cannot round the pixel between them +/// away -- only shift it. A separator that disappeared at one window size +/// would be a defect no size comparison catches. +#[test] +fn a_one_pixel_line_keeps_its_pixel_through_a_chain() { + let mut h = Harness::new((1920, 1200)); + let mut marks = Vec::new(); + let root = hairlines(&mut h, 4, &mut marks); + h.state.set_root(root); + h.frame(); + assert_eq!(marks.len(), 16); + + for size in [(1920, 1200), (1919, 1201), (997, 1003), (1367, 733)] { + h.resize(size); + h.frame(); + for mark in &marks { + let (start, end) = drawn_edges(&h, *mark, Axis::X); + assert_eq!(end - start, 1.0, "at {size:?}, mark {mark:?}"); + } + } +} + +/// A span short of room takes it from its shares, which go to nothing and +/// then to nothing wider; the fixed lengths between them keep their pixels. +/// Collapsing those to make room would delete a separator the caller asked +/// for, which is worse than overflowing. +#[test] +fn a_span_out_of_room_shrinks_its_shares_and_not_its_fixed_lengths() { + let mut h = Harness::new((400, 20)); + let mut marks = Vec::new(); + let mut span = Span::empty(Dir::RIGHT); + for _ in 0..3 { + let share_of = rect(Color::BLUE).add_strong(&mut h.rsc); + let share_of = share(&mut h, share_of, 1.0); + span.push(share_of); + let mark = hairline(&mut h, &mut marks); + span.push(mark); + } + let root = span.add_strong(&mut h.rsc); + h.state.set_root(root); + h.frame(); + + for width in [400, 10, 3, 1] { + h.resize((width, 20)); + h.frame(); + for mark in &marks { + let (start, end) = drawn_edges(&h, *mark, Axis::X); + assert_eq!(end - start, 1.0, "at {width} wide, mark {mark:?}"); + } + } +}