diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index 873650f..7d61233 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -44,28 +44,39 @@ impl Widget for Span { let total = lens.iter().fold(Len::px(gap), |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 answer is - // the same on either side of the length the fixed parts alone fill. + // beside 300 px is full at 600 and overfull at 400. The room to divide + // is `len * fixed - total.px`, and under `HOLDS_EPSILON_PX` of it is + // none -- because the length where the room runs out is exactly the + // box a parent sizing itself from this answer hands back, and that box + // returns through the chain a few bits either way. Without the margin + // 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 mut shares = false; if total.leftover > 0.0 { let current = painter.px_len(axis); let holds = if fixed > 0.0 { - let full = total.px / fixed; - shares = current > full; + // The box length at which the room reaches the margin. + let enough = (total.px + HOLDS_EPSILON_PX) / fixed; + shares = current > enough; match shares { - true => Holds::exact(full.next_up()..=f32::INFINITY), - false => Holds::exact(f32::NEG_INFINITY..=full), + true => Holds::exact(enough.next_up()..=f32::INFINITY), + false => Holds::exact(f32::NEG_INFINITY..=enough), } } else if fixed < 0.0 { - let full = total.px / fixed; - shares = current < full; + // 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; + shares = current < enough; match shares { - true => Holds::exact(f32::NEG_INFINITY..=full.next_down()), - false => Holds::exact(full..=f32::INFINITY), + true => Holds::exact(f32::NEG_INFINITY..=enough.next_down()), + false => Holds::exact(enough..=f32::INFINITY), } } else { - shares = total.px < 0.0; + // 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; Holds::ANY }; painter.holds(axis, holds); diff --git a/tests/unsettled.rs b/tests/unsettled.rs index 046c644..bc28049 100644 --- a/tests/unsettled.rs +++ b/tests/unsettled.rs @@ -3,7 +3,9 @@ //! frame that had not settled: a wrapping text shaped at a width it was //! measured in rather than the one it was given. The rest are a widget //! measured again in a box its own answer had decided, where the old answer -//! is a fixed point whatever the content now says. +//! is a fixed point whatever the content now says. The last is neither: one +//! box length, composed two ways, landing either side of the boundary that +//! decided whether a child was drawn at all. use iris::harness::Harness; use iris::prelude::*; @@ -311,3 +313,90 @@ fn a_scrolls_retained_answer_is_the_one_a_cold_layout_asks_for() { assert_eq!(warm.region(&scroll), cold.region(&cold_scroll)); } + +/// Six widgets, shrunk from 266. `measured`'s box is exactly the height of its +/// one fixed child, which is the box a parent sizing itself from that answer +/// hands back -- so whether its leftover-only child was drawn at all came down +/// to the 0.00003 px the composed length differs by, one way warm and the +/// other cold. +fn plant_boundary(h: &mut Harness, swapped: bool) -> (Vec, [WeakWidget; 2]) { + let filler = rect(Color::RED).add(&mut h.rsc); + let plain = wtext("one line, overflowing whatever it is given") + .size(16) + .wrap(false) + .add(&mut h.rsc); + let mut pair: Vec = + vec![filler.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)]; + if swapped { + pair.rotate_left(1); + } + let measured = Span { + children: pair, + dir: Dir::DOWN, + gap: 0.0, + ortho: OrthoSize::Children, + } + .add(&mut h.rsc); + // Takes the whole box on its own, so the span above has nothing left to + // divide and `measured` is given exactly the text's height. + let whole = rect(Color::RED).add(&mut h.rsc); + h.rsc + .widgets_mut() + .set_size_rules(whole, None, Some(Len::rel(1.0))); + let mut inner_children: Vec = vec![ + measured.add_strong(&mut h.rsc), + whole.add_strong(&mut h.rsc), + ]; + if swapped { + inner_children.rotate_left(1); + } + let inner = Span { + children: inner_children, + dir: Dir::DOWN, + gap: 0.0, + ortho: OrthoSize::Children, + } + .add(&mut h.rsc); + h.rsc + .widgets_mut() + .set_size_rules(inner, None, Some(Len::px(198.0))); + // One more span above it: without a box composed through it, both trees + // round the same way and the boundary is never crossed. + let outer = (inner,).span(Dir::DOWN).add(&mut h.rsc); + h.set_root(outer); + ( + vec![ + filler.id(), + plain.id(), + measured.id(), + whole.id(), + inner.id(), + outer.id(), + ], + [measured, inner], + ) +} + +#[test] +fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() { + let mut warm = Harness::new((640, 900)); + let (ids, spans) = plant_boundary(&mut warm, false); + warm.frame(); + for span in spans { + warm.rsc[span].children.rotate_left(1); + } + warm.frame(); + + let mut cold = Harness::new((640, 900)); + let (cold_ids, _) = plant_boundary(&mut cold, true); + cold.frame(); + + let mut wrong = Vec::new(); + for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() { + let (got, want) = (warm.region(&w), cold.region(&c)); + if got != want { + wrong.push(format!("widget {i}: warm {got:?} cold {want:?}")); + } + } + assert!(wrong.is_empty(), "{}", wrong.join("\n")); +}