Compare commits

...
Author SHA1 Message Date
iris-aiandClaude Fable 5.1 aaba7dbfee Keep the step 3/4 experiment as evidence
The worker's uncommitted attempt at evaluating children in parent-decided
boxes, preserved as it stood when it stopped: separate answer-only pixel
reads, per-child drawing contracts, provisional Fill asks in Span, and an
assertion that a placed drawing hold for its answer box. The suite passes
and every generated case stops on that assertion at Text. Not the
protocol; wip/one-ask is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 18:21:24 -04:00
iris-ai 4328eac756 Keep leftover shares inside scroll viewports 2026-09-18 14:11:02 -04:00
iris-ai b842e4f474 Pin decided-box warm/cold failures 2026-09-18 14:09:00 -04:00
iris-ai 49cec82c1b Say which box a pin reaches the parent through, and derive the offer
The review pass over the two commits before it:

- `LayoutHolds`'s comment said a pin does not compose into the parent. It
  does, where the box it pinned is the parent's own box, which is the one
  case where the parent's own length is what was pinned.
- `DrawInfo::offer` was stored beside the two fields it is computed from.
  It is a method now, with the open question written where it is asked
  rather than only in the handoff.
- `Painter::own` is `frame_own` beside `extent_own`, since a widget's own
  box is the extent and the frame is what it is a part of.
- One expression for the length a rule gives a frame (`narrowed_by`) and
  one for what a widget answered (`ActiveData::measured`), each of which
  had two.
- The counter said "the placement it was pinned to" for what is now a
  length; the deferral in `redraw` named the seeds that made it necessary
  before the last commit rather than the ones that do now; three comments
  claimed an inset that does not exist yet.

No behaviour change: 108 suite tests, 20 core, the 11 generated cases, the
same two shrinker seeds failing at 400/5, and `view` and `tabs` byte-identical
to the renders taken before it.
2026-09-18 01:21:03 -04:00
13 changed files with 488 additions and 171 deletions

No files matched your search

+2 -2
View File
@@ -54,7 +54,7 @@ pub(crate) enum Counter {
TextShapes, TextShapes,
TextBreaks, TextBreaks,
GlyphPlacements, GlyphPlacements,
OutsidePlacement, OutsidePinnedLen,
OutsideFrame, OutsideFrame,
OutsideExtent, OutsideExtent,
} }
@@ -92,7 +92,7 @@ impl Counter {
"text shapes", "text shapes",
"text line breaks", "text line breaks",
"glyph placements", "glyph placements",
"reuse outside: the placement it was pinned to", "reuse outside: the length it was pinned to",
"reuse outside: a frame length", "reuse outside: a frame length",
"reuse outside: an extent length", "reuse outside: an extent length",
]; ];
+9 -1
View File
@@ -17,7 +17,7 @@ pub struct ActiveData {
pub extent: UiRegion, pub extent: UiRegion,
/// That frame in its parent's frame coordinates, before composition: /// That frame in its parent's frame coordinates, before composition:
/// forwarded whole by a transparent container, narrowed by a declared /// forwarded whole by a transparent container, narrowed by a declared
/// length or an inset. Its length is the same on every ask, which is what /// length. Its length is the same on every ask, which is what
/// a local redraw relies on to ask its parent's own question again. /// a local redraw relies on to ask its parent's own question again.
pub frame: UiRegion, pub frame: UiRegion,
/// What of its parent's extent the drawing was given, and what it was /// What of its parent's extent the drawing was given, and what it was
@@ -77,6 +77,14 @@ pub struct ActiveData {
} }
impl ActiveData { impl ActiveData {
/// What it answered when its parent measured it, where it has been
/// measured at all. Not `size`, which is what its last drawing reported:
/// a drawing made in the box that answer chose is answering a different
/// question.
pub fn measured(&self) -> Option<Size> {
self.answer.map(|(size, _)| size)
}
/// Whether what it answered still stands for a frame of these pixel /// Whether what it answered still stands for a frame of these pixel
/// lengths. The answer was given in the box its parent first asked /// lengths. The answer was given in the box its parent first asked
/// about, which is what it is checked against -- `holds` on the record /// about, which is what it is checked against -- `holds` on the record
+3 -2
View File
@@ -9,8 +9,9 @@ const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// The symbolic length is a pin rather than a range: a container places its /// The symbolic length is a pin rather than a range: a container places its
/// children as lengths of its frame measured from where its own box starts, /// children as lengths of its frame measured from where its own box starts,
/// so what it draws turns on that box's length and on nothing about where it /// so what it draws turns on that box's length and on nothing about where it
/// is. It does not compose into the parent -- a widget pinned this way is /// is. It reaches the parent only where the box it pinned is the parent's
/// checked when it is re-placed. /// own; anywhere else the parent chose that length itself, and a widget
/// pinned this way is checked when it is re-placed.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHolds { pub struct LayoutHolds {
pub frame: [Holds; 2], pub frame: [Holds; 2],
+83 -27
View File
@@ -20,7 +20,7 @@ pub struct Painter<'a> {
/// What a fraction this widget declares or reports is a fraction of, in /// What a fraction this widget declares or reports is a fraction of, in
/// the coordinates of `move_idx`: forwarded from its parent unchanged /// the coordinates of `move_idx`: forwarded from its parent unchanged
/// through a span, a stack or a scroll, and narrowed only by what was /// through a span, a stack or a scroll, and narrowed only by what was
/// decided above it -- a declared length, an inset, the root. Its length /// decided above it -- a declared length, or the root. Its length
/// is the same on every ask of the widget, which is what keeps a fraction /// is the same on every ask of the widget, which is what keeps a fraction
/// under it from being resolved twice. /// under it from being resolved twice.
pub(super) frame: UiRegion, pub(super) frame: UiRegion,
@@ -32,6 +32,10 @@ pub struct Painter<'a> {
/// drawing one that holds for that length alone -- the way reading a /// drawing one that holds for that length alone -- the way reading a
/// length in pixels makes it hold for that number of pixels. /// length in pixels makes it hold for that number of pixels.
pub(super) extent_len: [Option<Len>; 2], pub(super) extent_len: [Option<Len>; 2],
/// Symbolic box lengths read only to compute the answer. A container can
/// replace the provisional drawings used for that answer with drawings
/// in decided boxes, so this contract is independent of the final one.
pub(super) answer_extent_len: [Option<Len>; 2],
/// The frame in pixels, which its children's frames are a length of: /// The frame in pixels, which its children's frames are a length of:
/// threaded down rather than composed back up the chain, so every length /// threaded down rather than composed back up the chain, so every length
/// in layout is one multiply from its parent's and [`Holds::through`] /// in layout is one multiply from its parent's and [`Holds::through`]
@@ -55,11 +59,15 @@ pub struct Painter<'a> {
pub(super) size_deps: Vec<WidgetId>, pub(super) size_deps: Vec<WidgetId>,
/// What this draw itself read of its frame in pixels, per axis: every /// What this draw itself read of its frame in pixels, per axis: every
/// length until it reads one, then that one, unless it says otherwise. /// length until it reads one, then that one, unless it says otherwise.
pub(super) own: [Holds; 2], pub(super) frame_own: [Holds; 2],
/// The same for its extent. /// The same for its own box.
pub(super) extent_own: [Holds; 2], pub(super) extent_own: [Holds; 2],
/// Dependencies of every child drawing, including unmeasured overlays. /// Pixel-box dependencies used only to compute the answer. These do not
pub(super) under: LayoutHolds, /// constrain a retained drawing placed inside that answer.
pub(super) answer_extent_own: [Holds; 2],
/// The final drawing kept for each child. Asking one child again replaces
/// its provisional drawing and therefore replaces this contract too.
pub(super) under: Vec<(WidgetId, LayoutHolds)>,
/// The movable region this widget's primitives are positioned through: /// The movable region this widget's primitives are positioned through:
/// its own when opted in, otherwise the nearest ancestor's. /// its own when opted in, otherwise the nearest ancestor's.
pub(super) move_idx: MoveIdx, pub(super) move_idx: MoveIdx,
@@ -161,8 +169,8 @@ impl<'a> Painter<'a> {
/// [`UiRegion::FULL`] forwards this widget's frame, which is what a /// [`UiRegion::FULL`] forwards this widget's frame, which is what a
/// container that only divides room passes, so a fraction under it means /// container that only divides room passes, so a fraction under it means
/// the same wherever it sits and however deeply it is nested. Narrowing /// the same wherever it sits and however deeply it is nested. Narrowing
/// it is for what is decided from above -- an inset's margins -- and a /// it is for what is decided from above, and a declared length narrows
/// declared length narrows it here. /// it here.
/// ///
/// `place` is where the drawing goes, per axis, as a part of this /// `place` is where the drawing goes, per axis, as a part of this
/// widget's extent: see [`Place`]. A narrowed frame is its own extent, /// widget's extent: see [`Place`]. A narrowed frame is its own extent,
@@ -176,14 +184,12 @@ impl<'a> Painter<'a> {
let region_node = self.rsc.widgets().is_region_node(id.id()); let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id); let declared = self.declared_lens(id);
let align = self.rsc.widgets().alignment(id.id()); let align = self.rsc.widgets().alignment(id.id());
let narrow = AXES.map(|axis| { let (local, extent) = frame_and_extent(
let n = axis as usize; frame,
// A rule's fraction is a fraction of the frame the child was part_of(self.extent, place),
// given, which is the one length the rule can mean. narrowed_by(declared, frame),
declared[n] align,
.map(|len| Len::from_parts(len.rel, len.px).within_len(frame.axis(axis).len())) );
});
let (local, extent) = frame_and_extent(frame, part_of(self.extent, place), narrow, align);
let within = match local == UiRegion::FULL { let within = match local == UiRegion::FULL {
true => self.frame, true => self.frame,
false => local.within(&self.frame), false => local.within(&self.frame),
@@ -224,17 +230,17 @@ impl<'a> Painter<'a> {
part: extent, part: extent,
place, place,
offer_place, offer_place,
// The question its parent measured it by, asked again: the
// same widget in the same place, however this draw came
// about.
offer: place == offer_place,
px, px,
}, },
None, None,
self.rsc, self.rsc,
); );
let compose = |holds| in_parent(holds, local, extent, place, declared); let compose = |holds| in_parent(holds, local, extent, place, declared);
self.under = self.under.and(compose(holds)); let holds = compose(holds);
match self.under.iter_mut().find(|(child, _)| *child == id.id()) {
Some((_, kept)) => *kept = holds,
None => self.under.push((id.id(), holds)),
}
DrawResult { DrawResult {
child: id, child: id,
painter: self, painter: self,
@@ -248,6 +254,7 @@ impl<'a> Painter<'a> {
/// this frame; what it answered is still something this widget asked. /// this frame; what it answered is still something this widget asked.
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) { pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.children.retain(|child| *child != id.id()); self.children.retain(|child| *child != id.id());
self.under.retain(|(child, _)| *child != id.id());
self.state.undraw_rec(id.id(), self.rsc); self.state.undraw_rec(id.id(), self.rsc);
} }
@@ -362,9 +369,31 @@ impl<'a> Painter<'a> {
pub fn extent_len(&mut self, axis: Axis) -> Len { pub fn extent_len(&mut self, axis: Axis) -> Len {
let len = self.extent.axis(axis).len(); let len = self.extent.axis(axis).len();
self.extent_len[axis as usize] = Some(len); self.extent_len[axis as usize] = Some(len);
self.answer_extent_len[axis as usize] = Some(len);
len len
} }
/// The symbolic length used to compute this widget's answer, where the
/// final drawing itself is rebuilt without depending on that length.
pub fn answer_extent_len(&mut self, axis: Axis) -> Len {
let len = self.extent.axis(axis).len();
self.answer_extent_len[axis as usize] = Some(len);
len
}
/// Says that the final drawing uses a symbolic length already read for
/// the answer.
pub fn drawing_uses_extent_len(&mut self, axis: Axis, len: Len) {
debug_assert_eq!(self.extent.axis(axis).len(), len);
self.extent_len[axis as usize] = Some(len);
}
/// A part of this widget's box, expressed in its frame coordinates so it
/// can be used as a child frame decided here.
pub fn extent_part(&self, axis: Axis, part: Part) -> UiSpan {
part.of(*self.extent.axis(axis))
}
/// Where this widget sits in a box longer than the length it takes. A /// Where this widget sits in a box longer than the length it takes. A
/// widget that positions its own content reads it to place that content /// widget that positions its own content reads it to place that content
/// the way the box around it would have placed the widget. /// the way the box around it would have placed the widget.
@@ -396,6 +425,14 @@ impl<'a> Painter<'a> {
PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y)) PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y))
} }
/// This widget's own box in pixels, used only to compute its answer.
pub fn answer_px_size(&mut self) -> PxVec2 {
PxVec2::new(
self.answer_px_len(Axis::X),
self.answer_px_len(Axis::Y),
)
}
/// One axis of this widget's own box in pixels. Prefer this to /// One axis of this widget's own box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing. /// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px { pub fn px_len(&mut self, axis: Axis) -> Px {
@@ -408,6 +445,17 @@ impl<'a> Painter<'a> {
len len
} }
/// One pixel length used only to compute this widget's answer. The final
/// drawing may be retained when that answer is placed in another box.
pub fn answer_px_len(&mut self, axis: Axis) -> Px {
let len = self.extent.axis(axis).len().to_px(self.px.axis(axis));
let own = &mut self.answer_extent_own[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(len);
}
len
}
/// The lengths of this widget's own box on `axis` that what it is drawing /// The lengths of this widget's own box on `axis` that what it is drawing
/// holds for -- the same primitives, in the same fractions and offsets /// holds for -- the same primitives, in the same fractions and offsets
/// of the box, and the same reported size. A widget that read its length /// of the box, and the same reported size. A widget that read its length
@@ -429,7 +477,7 @@ impl<'a> Painter<'a> {
/// children. Its own box is a part of this one. /// children. Its own box is a part of this one.
pub fn frame_px_len(&mut self, axis: Axis) -> Px { pub fn frame_px_len(&mut self, axis: Axis) -> Px {
let len = self.px.axis(axis); let len = self.px.axis(axis);
let own = &mut self.own[axis as usize]; let own = &mut self.frame_own[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(len);
} }
@@ -447,7 +495,7 @@ impl<'a> Painter<'a> {
self.label(), self.label(),
self.id self.id
); );
self.own[axis as usize] = holds; self.frame_own[axis as usize] = holds;
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
@@ -561,7 +609,7 @@ pub(crate) fn in_parent(
// own box, which is what lets that box move without a redraw. A // own box, which is what lets that box move without a redraw. A
// length it pinned is this widget's length wherever the part is // length it pinned is this widget's length wherever the part is
// the whole of it, and pins the same way. // the whole of it, and pins the same way.
(Part::All, None) => { (Part::All, None) if *frame.axis(axis) == UiSpan::FULL => {
result.extent[n] = holds.extent[n]; result.extent[n] = holds.extent[n];
result.extent_len[n] = holds.extent_len[n]; result.extent_len[n] = holds.extent_len[n];
} }
@@ -663,10 +711,8 @@ pub(crate) fn placed_extent(
placed placed
} }
/// The part of a widget's extent a `place` names, in the coordinates its /// The part of a widget's own box a `place` names, in the coordinates that
/// extent is in: a span is measured in frame lengths from where the extent /// box is in.
/// starts, so nothing under it depends on where that is, and an extent that
/// moved re-places every child by re-adding its start.
pub(crate) fn part_of(extent: UiRegion, place: [Place; 2]) -> UiRegion { pub(crate) fn part_of(extent: UiRegion, place: [Place; 2]) -> UiRegion {
let mut part = extent; let mut part = extent;
for axis in AXES { for axis in AXES {
@@ -675,6 +721,16 @@ pub(crate) fn part_of(extent: UiRegion, place: [Place; 2]) -> UiRegion {
part part
} }
/// The length a rule gives a child's frame, per axis: a fraction in it is a
/// fraction of the frame the child was given, which is the one length the
/// rule can mean.
pub(crate) fn narrowed_by(declared: [Option<LayoutLen>; 2], frame: UiRegion) -> [Option<Len>; 2] {
AXES.map(|axis| {
declared[axis as usize]
.map(|len| Len::from_parts(len.rel, len.px).within_len(frame.axis(axis).len()))
})
}
/// The frame a child is asked in and the box its drawing goes in, both in /// The frame a child is asked in and the box its drawing goes in, both in
/// the coordinates of the widget asking. /// the coordinates of the widget asking.
/// ///
+65 -102
View File
@@ -1,9 +1,9 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::ui::painter::{declared_lens, frame_and_extent, part_of, placed_extent}; use crate::ui::painter::{declared_lens, frame_and_extent, narrowed_by, part_of, placed_extent};
use crate::{ use crate::{
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, Moves,
Moves, Painter, Part, PixelRegion, Place, PxVec2, Size, StrongWidget, UiRegion, UiRsc, Weight, Painter, Part, PixelRegion, Place, PxVec2, Size, StrongWidget, UiRegion, UiRsc, Weight,
WidgetId, Widgets, WidgetId, Widgets,
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
@@ -34,9 +34,6 @@ pub(super) struct DrawInfo {
/// given at the parent's first ask of it. See [`Place`]. /// given at the parent's first ask of it. See [`Place`].
pub place: [Place; 2], pub place: [Place; 2],
pub offer_place: [Place; 2], pub offer_place: [Place; 2],
/// Whether this ask is the one the widget's answer is kept from: the
/// first box its parent asked about, in the parent's own measuring draw.
pub offer: bool,
/// The frame in pixels: one multiply from the parent's own, which is /// The frame in pixels: one multiply from the parent's own, which is
/// where every pixel length in layout comes from. /// where every pixel length in layout comes from.
pub px: PxVec2, pub px: PxVec2,
@@ -48,6 +45,20 @@ impl DrawInfo {
fn fill(&self) -> [bool; 2] { fn fill(&self) -> [bool; 2] {
self.place.map(Place::fills) self.place.map(Place::fills)
} }
/// Whether this ask is the one the widget's answer is kept from: the
/// same widget in the place its parent measured it by, however this draw
/// came about.
///
/// **Open.** A place is a length from where the asking widget's own box
/// starts, so two drawings of that widget -- one in the box its parent
/// measured it in, one in the box its own answer chose -- ask their
/// children in the same places and different boxes, and this cannot tell
/// them apart. Shrinker seeds 2 (`repaint`) and 108 (`reorder`) at depth
/// 5 are where that shows.
fn offer(&self) -> bool {
self.place == self.offer_place
}
} }
/// What a widget's children are placed in: its own box, the coordinates its /// What a widget's children are placed in: its own box, the coordinates its
@@ -153,7 +164,6 @@ impl UiRenderState {
part: UiRegion::FULL, part: UiRegion::FULL,
place: [Place::Within(Part::All); 2], place: [Place::Within(Part::All); 2],
offer_place: [Place::Within(Part::All); 2], offer_place: [Place::Within(Part::All); 2],
offer: true,
px, px,
} }
} }
@@ -209,9 +219,7 @@ impl UiRenderState {
/// The root's frame: the window, narrowed by the root's own rules. Its /// The root's frame: the window, narrowed by the root's own rules. Its
/// extent is that frame, since nothing above it chose anything else. /// extent is that frame, since nothing above it chose anything else.
fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion { fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion {
let declared = declared_lens(widgets, id); let narrow = narrowed_by(declared_lens(widgets, id), UiRegion::FULL);
let narrow =
AXES.map(|axis| declared[axis as usize].map(|len| Len::from_parts(len.rel, len.px)));
frame_and_extent( frame_and_extent(
UiRegion::FULL, UiRegion::FULL,
UiRegion::FULL, UiRegion::FULL,
@@ -243,7 +251,7 @@ impl UiRenderState {
true => None, true => None,
false => self false => self
.retained_answer(id, part, info) .retained_answer(id, part, info)
.or_else(|| self.try_reuse(id, frame, part, info, rsc)), .and_then(|answer| self.try_reuse(id, frame, part, info, rsc).map(|_| answer)),
}; };
let answer = retained.unwrap_or_else(|| { let answer = retained.unwrap_or_else(|| {
if old.is_none() { if old.is_none() {
@@ -257,49 +265,25 @@ impl UiRenderState {
// axis the parent left open. The frame itself does not change, so // axis the parent left open. The frame itself does not change, so
// nothing under it resolves a fraction a second time. // nothing under it resolves a fraction a second time.
// //
// From the answer it gave when its parent first asked, and not from
// what a placing evaluation reported: a drawing made in the box that
// answer chose is answering a different question, and placing it by
// that would move the box out from under itself.
let measured = match info.offer {
true => answer.0,
false => self.active[&id].answer.map_or(answer.0, |(size, _)| size),
};
let extent = placed_extent( let extent = placed_extent(
part, part,
measured, answer.0,
declared_lens(rsc.widgets(), id), declared_lens(rsc.widgets(), id),
info.fill(), info.fill(),
align, align,
); );
self.place(id, extent, info, rsc); self.place(id, extent, info, rsc);
// On axes the parent filled, measurement and drawing share an extent.
// Otherwise the answer fixes the final extent as a function of the
// frame, so pull that drawing's validity back through it.
let drawing_holds = self.active[&id].holds; let drawing_holds = self.active[&id].holds;
let mut settled = answer;
for axis in AXES {
let n = axis as usize;
settled.1.frame[n] = settled.1.frame[n].and(drawing_holds.frame[n]);
if info.fill()[n] {
settled.1.extent[n] = settled.1.extent[n].and(drawing_holds.extent[n]);
} else {
settled.1.frame[n] = settled.1.frame[n]
.and(drawing_holds.extent[n].through(extent.axis(axis).len()));
}
}
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
// Whoever asked owns how the boxes were reached: the frame it stated, // Whoever asked owns how the boxes were reached: the frame it stated,
// and what of its own box it gave the drawing. A local redraw asks // and what of its own box it gave the drawing. A local redraw asks
// the same question again from these. // the same question again from these.
active.frame_abs = frame; active.frame_abs = frame;
active.frame = info.frame; active.frame = info.frame;
if info.offer { active.answer = Some(answer);
active.answer = Some(answer); active.offer_place = info.offer_place;
active.offer_place = info.offer_place; active.offer_part = part;
active.offer_part = part;
}
active.place = info.place; active.place = info.place;
active.own_align = align; active.own_align = align;
// A subtree can be reused whole under a different parent -- same box, // A subtree can be reused whole under a different parent -- same box,
@@ -314,7 +298,7 @@ impl UiRenderState {
{ {
old_parent.children.retain(|child| *child != id); old_parent.children.retain(|child| *child != id);
} }
(answer.0, answer.1, settled.1) (answer.0, answer.1, drawing_holds)
} }
/// Recompose retained geometry when the evaluation still holds at this extent. /// Recompose retained geometry when the evaluation still holds at this extent.
@@ -325,6 +309,15 @@ impl UiRenderState {
{ {
return; return;
} }
assert_eq!(
extent,
info.part,
"'{}' ({id:?}) does not hold for the box its answer chose: part {:?}, wanted {extent:?}, retained {:?} with {:?}",
rsc.widgets().label(id),
info.part,
self.active.get(&id).map(|active| active.extent),
self.active.get(&id).map(|active| active.holds),
);
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceRedraws); diag::bump(Counter::PlaceRedraws);
let old = self.remove(id, false, rsc); let old = self.remove(id, false, rsc);
@@ -361,13 +354,14 @@ impl UiRenderState {
// Only evaluation at the original offer establishes the children's // Only evaluation at the original offer establishes the children's
// offers. A placing evaluation must not overwrite that question. // offers. A placing evaluation must not overwrite that question.
let px = info.px; let px = info.px;
let at_offer = info.offer; let at_offer = info.offer();
let mut painter = Painter { let mut painter = Painter {
state: self, state: self,
frame: local, frame: local,
extent, extent,
extent_len: [None; 2], extent_len: [None; 2],
answer_extent_len: [None; 2],
px, px,
mask: info.mask, mask: info.mask,
layer: info.layer, layer: info.layer,
@@ -380,9 +374,10 @@ impl UiRenderState {
offered: Vec::new(), offered: Vec::new(),
at_offer, at_offer,
size_deps: Vec::new(), size_deps: Vec::new(),
own: [Holds::ANY; 2], frame_own: [Holds::ANY; 2],
under: LayoutHolds::ANY, under: Vec::new(),
extent_own: [Holds::ANY; 2], extent_own: [Holds::ANY; 2],
answer_extent_own: [Holds::ANY; 2],
answer_under: LayoutHolds::ANY, answer_under: LayoutHolds::ANY,
depth: info.depth, depth: info.depth,
move_idx, move_idx,
@@ -411,13 +406,15 @@ impl UiRenderState {
primitives, primitives,
mask_region, mask_region,
extent_own, extent_own,
answer_extent_own,
extent_len, extent_len,
answer_extent_len,
answer_under, answer_under,
children, children,
offered: _, offered: _,
at_offer: _, at_offer: _,
size_deps, size_deps,
own, frame_own,
under, under,
move_idx, move_idx,
layer, layer,
@@ -457,13 +454,23 @@ impl UiRenderState {
if let Some(idx) = retired_move { if let Some(idx) = retired_move {
self.moves.remove(idx); self.moves.remove(idx);
} }
let own_holds = LayoutHolds { let drawing_own = LayoutHolds {
frame: own, frame: frame_own,
extent: extent_own, extent: extent_own,
extent_len, extent_len,
}; };
let answer_holds = own_holds.and(answer_under); let answer_own = LayoutHolds {
let holds = answer_holds.and(under); extent: [
drawing_own.extent[0].and(answer_extent_own[0]),
drawing_own.extent[1].and(answer_extent_own[1]),
],
extent_len: answer_extent_len,
..drawing_own
};
let answer_holds = answer_own.and(answer_under);
let holds = under
.into_iter()
.fold(drawing_own, |holds, (_, child)| holds.and(child));
debug_assert!( debug_assert!(
holds.contains(px, extent), holds.contains(px, extent),
"'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}", "'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}",
@@ -488,7 +495,6 @@ impl UiRenderState {
part: UiRegion::FULL, part: UiRegion::FULL,
place: [Place::Within(Part::All); 2], place: [Place::Within(Part::All); 2],
offer_place: [Place::Within(Part::All); 2], offer_place: [Place::Within(Part::All); 2],
offer: false,
px, px,
}, },
rsc, rsc,
@@ -659,7 +665,7 @@ impl UiRenderState {
for axis in AXES { for axis in AXES {
let n = axis as usize; let n = axis as usize;
if holds.extent_len[n].is_some_and(|pinned| pinned != extent.axis(axis).len()) { if holds.extent_len[n].is_some_and(|pinned| pinned != extent.axis(axis).len()) {
diag::bump(Counter::OutsidePlacement); diag::bump(Counter::OutsidePinnedLen);
} }
if !holds.frame[n].contains(info.px.axis(axis)) { if !holds.frame[n].contains(info.px.axis(axis)) {
diag::bump(Counter::OutsideFrame); diag::bump(Counter::OutsideFrame);
@@ -721,13 +727,9 @@ impl UiRenderState {
fn place_child(&mut self, child: WidgetId, at: &Placing, rsc: &mut dyn UiRsc) { fn place_child(&mut self, child: WidgetId, at: &Placing, rsc: &mut dyn UiRsc) {
let active = &self.active[&child]; let active = &self.active[&child];
let (frame, part) = Self::re_ask(active, at.extent, active.place); let (frame, part) = Self::re_ask(active, at.extent, active.place);
// The answer it gave, and not what its last drawing reported: a
// drawing made in the box that answer chose is answering a different
// question.
let answer = active.answer.map_or(active.size, |(size, _)| size);
let extent = placed_extent( let extent = placed_extent(
part, part,
answer, active.measured().unwrap_or(active.size),
active.declared, active.declared,
active.place.map(Place::fills), active.place.map(Place::fills),
active.own_align, active.own_align,
@@ -744,10 +746,6 @@ impl UiRenderState {
part, part,
place: active.place, place: active.place,
offer_place: active.offer_place, offer_place: active.offer_place,
// Putting it back where it was asked about is that ask again, so
// what it answers there is the answer -- and putting it anywhere
// else is not, however the box was arrived at.
offer: active.place == active.offer_place,
px: frame.size().to_px(at.px), px: frame.size().to_px(at.px),
}; };
self.place(child, extent, info, rsc); self.place(child, extent, info, rsc);
@@ -1180,10 +1178,10 @@ impl UiRenderState {
}; };
let px = self.asked_px(id); let px = self.asked_px(id);
let (was_answer, was_holds) = (active.answer, active.holds); let (was_answer, was_holds) = (active.answer, active.holds);
// The boxes its parent gave it, then and now: its frame is the same let needs_replacement = active.place != active.offer_place;
// on every ask, so the question its parent asked is the one this // Re-ask the question its answer came from, not a later placement of
// asks again -- there is no box here that could be its parent's to // that answer. A parent may move the retained drawing into a box the
// choose instead. // answer chose without making that box a new question.
let parent_extent = self.active[&parent].extent; let parent_extent = self.active[&parent].extent;
let info = DrawInfo { let info = DrawInfo {
layer: active.layer, layer: active.layer,
@@ -1194,51 +1192,16 @@ impl UiRenderState {
mask: active.parent_mask, mask: active.parent_mask,
frame: active.frame, frame: active.frame,
frame_abs: active.frame_abs, frame_abs: active.frame_abs,
part: Self::re_ask(active, parent_extent, active.place).1, part: Self::re_ask(active, parent_extent, active.offer_place).1,
place: active.place, place: active.offer_place,
offer_place: active.offer_place, offer_place: active.offer_place,
offer: false,
px, px,
}; };
// The ask that measured it, asked again: the box it was measured in
// as its parent left it, rather than where that ask's place resolves
// to now -- a parent drawn again in the box its own answer chose
// gives its children boxes it never measured anything in.
let offered = DrawInfo {
place: info.offer_place,
part: active.offer_part,
offer: true,
..info
};
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws); diag::bump(Counter::LocalRedraws);
// Asked again in the box its parent gave it, which is the question
// its parent asked only while that box is as long as the one it was
// measured in. Any other box is a different question, so the parent
// asks it, with the mark left on. Lengths and not whole boxes: what
// a drawing depends on is its lengths, so the same lengths elsewhere
// is one question.
//
// The frame is the same on every ask now, so this is about the box
// the drawing goes in alone. Removing it -- asking the measuring
// question here and placing the answer afterwards -- is what the
// transparent-frames plan asks for next, and it does not hold yet:
// seeds 104 (align) and 210 (reorder) at depth 5 settle differently
// warm and cold without it.
if info.part.size() != offered.part.size() {
self.mark(id, rsc.widgets_mut());
self.mark(parent, rsc.widgets_mut());
return false;
}
let old = self.remove(id, false, rsc); let old = self.remove(id, false, rsc);
// The original measurement is refreshed before the assigned slot is let answer = self.draw_inner(id, info, old, rsc);
// restored: its lengths may differ even though the frame is
// unchanged.
let answer = self.draw_inner(id, offered, old, rsc);
if info.place != info.offer_place || info.part != offered.part {
self.draw_inner(id, info, None, rsc);
}
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
// A wider contract does not invalidate the guarantee the parent kept. // A wider contract does not invalidate the guarantee the parent kept.
// Retain that guarantee so widening and narrowing back do not churn it. // Retain that guarantee so widening and narrowing back do not churn it.
@@ -1251,7 +1214,7 @@ impl UiRenderState {
if active.holds.covers(was_holds) && was_holds.contains(px, active.extent) { if active.holds.covers(was_holds) && was_holds.contains(px, active.extent) {
active.holds = was_holds; active.holds = was_holds;
} }
if active.answer != was_answer || active.holds != was_holds { if needs_replacement || active.answer != was_answer || active.holds != was_holds {
// The parent retains both the answer and the drawing's validity; // The parent retains both the answer and the drawing's validity;
// even an unchanged size can narrow the range safe for a resize. // even an unchanged size can narrow the range safe for a resize.
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
+1 -1
View File
@@ -124,7 +124,7 @@ impl Widget for Branch {
.len(Axis::X); .len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y)))); let below = Place::Within(Part::Of(UiSpan::new(cut, Len::FULL)));
let place = [Place::Within(Part::All), below]; let place = [Place::Within(Part::All), below];
match px > Px::from_f32(self.threshold) { match px > Px::from_f32(self.threshold) {
true => painter.widget_at(&self.wide, UiRegion::FULL, place), true => painter.widget_at(&self.wide, UiRegion::FULL, place),
+4 -6
View File
@@ -16,12 +16,10 @@ impl Widget for Pad {
// //
// The padding goes around what it pads: the frame passes through, so // The padding goes around what it pads: the frame passes through, so
// the inner's fractions mean what they would without it, and only // the inner's fractions mean what they would without it, and only
// the box it draws in is moved in by the pixels. // the box it draws in is moved in by the pixels. Said as a part of
// A part of this widget's own box, in that box's own lengths: the // this widget's own box in that box's own lengths, so nothing here
// padding is pixels, which are the same pixels wherever the box // reads how long the box is -- and a box chosen from this widget's
// lands, so nothing here reads how long the box is -- and a box // own answer therefore does not feed back into that answer.
// chosen from this widget's own answer therefore does not feed back
// into that answer.
let inset = |lead: Px, trail: Px| { let inset = |lead: Px, trail: Px| {
Place::Within(Part::Of(UiSpan::new( Place::Within(Part::Of(UiSpan::new(
Len::from_parts(Rel::ZERO, lead), Len::from_parts(Rel::ZERO, lead),
+4 -4
View File
@@ -17,9 +17,9 @@ impl Widget for Scroll {
let answer_len = painter let answer_len = painter
.widget_at(&self.inner, whole, [Place::Fill(Part::All); 2]) .widget_at(&self.inner, whole, [Place::Fill(Part::All); 2])
.len(self.axis); .len(self.axis);
let content = answer_len.apply_leftover(); let fixed = Len::from_parts(answer_len.rel, answer_len.px).to_px(container_len);
self.container_len = container_len; self.container_len = container_len;
self.content_len = content.to_px(container_len); self.content_len = fixed.max(container_len);
if self.snap_end { if self.snap_end {
self.amt = self.content_len - self.container_len; self.amt = self.content_len - self.container_len;
@@ -32,9 +32,9 @@ impl Widget for Scroll {
// the drawing holds for that length alone. One scrolled part way sits // the drawing holds for that length alone. One scrolled part way sits
// where it is until the box shrinks past what is left of it. Kept to // where it is until the box shrinks past what is left of it. Kept to
// the end, it moves with every length. // the end, it moves with every length.
let fixed_len = content.rel == Rel::ZERO; let fixed_len = answer_len.rel == Rel::ZERO && answer_len.leftover == Weight::ZERO;
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG { if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG {
painter.holds(self.axis, self.content_len..=Px::MAX); painter.holds(self.axis, fixed..=Px::MAX);
} else if fixed_len && !self.snap_end { } else if fixed_len && !self.snap_end {
let left = self.content_len - self.amt; let left = self.content_len - self.amt;
painter.holds(self.axis, Px::MIN..=left); painter.holds(self.axis, Px::MIN..=left);
+30 -6
View File
@@ -13,8 +13,8 @@ impl Widget for Span {
// The row: this span's own box, as a length of the frame its children // The row: this span's own box, as a length of the frame its children
// are laid out against. Its start is nothing's business -- a slot is // are laid out against. Its start is nothing's business -- a slot is
// a length from it -- so what this reads is the length alone. // a length from it -- so what this reads is the length alone.
let far = painter.extent_len(axis); let far = painter.answer_extent_len(axis);
let along = |from: Len, to: Len| match self.dir.sign { let measure_along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(from, to), Sign::Pos => UiSpan::new(from, to),
Sign::Neg => UiSpan::new(far - to, far - from), Sign::Neg => UiSpan::new(far - to, far - from),
}; };
@@ -30,7 +30,7 @@ impl Widget for Span {
let mut cursor = Len::rel_min(); let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len()); let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children { for child in &self.children {
let room = Place::Within(Part::From(along(cursor, far))); let room = Place::Fill(Part::From(measure_along(cursor, far)));
let len = painter let len = painter
.widget_at(child, UiRegion::FULL, axis.pair(room, across)) .widget_at(child, UiRegion::FULL, axis.pair(room, across))
.len(axis); .len(axis);
@@ -50,9 +50,10 @@ impl Widget for Span {
|sum, len| sum + *len, |sum, len| sum + *len,
); );
let fixed_total = Len::from_parts(total.rel, total.px);
// What is left for the shares to divide: the row less everything // What is left for the shares to divide: the row less everything
// fixed, as a length of the frame rather than a number of pixels. // fixed, as a length of the frame rather than a number of pixels.
let room = far - Len::from_parts(total.rel, total.px); let room = far - fixed_total;
// Whether anything is left over is a question in pixels: `rel(0.5)` // Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. Asked of `room` // beside 300 px is full at 600 and overfull at 400. Asked of `room`
// itself, and answered back through the same expression, so the // itself, and answered back through the same expression, so the
@@ -72,6 +73,18 @@ impl Widget for Span {
painter.frame_holds(axis, holds.through(room)); painter.frame_holds(axis, holds.through(room));
} }
if shares {
painter.drawing_uses_extent_len(axis, far);
}
let drawing_far = match shares {
true => far,
false => fixed_total,
};
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => UiSpan::new(drawing_far - to, drawing_far - from),
};
// Across itself a span is as long as its longest child -- unless a // Across itself a span is as long as its longest child -- unless a
// rule beside it gives that length outright, and then reading them // rule beside it gives that length outright, and then reading them
// answers nothing and makes its size depend on theirs for it. A rule // answers nothing and makes its size depend on theirs for it. A rule
@@ -108,8 +121,19 @@ impl Widget for Span {
// Along the row the span says where the child goes, and that slot // Along the row the span says where the child goes, and that slot
// is the drawing's box outright rather than something to place an // is the drawing's box outright rather than something to place an
// answer inside again. // answer inside again.
let slot = Place::Fill(Part::From(along(from, start))); let span = along(from, start);
let placed = painter.widget_at(child, UiRegion::FULL, axis.pair(slot, across)); let (frame, slot) = match len.leftover > Weight::ZERO && shares {
true => (
UiRegion::from_axis(
axis,
painter.extent_part(axis, Part::From(span)),
UiSpan::FULL,
),
Place::Fill(Part::All),
),
false => (UiRegion::FULL, Place::Fill(Part::From(span))),
};
let placed = painter.widget_at(child, frame, axis.pair(slot, across));
if shrinks { if shrinks {
let used = placed.len(!axis); let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the // Choosing between a fixed and a relative length from the
+9 -3
View File
@@ -33,9 +33,15 @@ impl Widget for Stack {
continue; continue;
} }
painter.child_layer_at(i); painter.child_layer_at(i);
// Every other child is drawn in the stack's own box, and where it let place = [Axis::X, Axis::Y].map(|axis| {
// sits in one bigger than itself is its own business. let len = size.axis(axis);
painter.widget(child); let part = match len.leftover > Weight::ZERO {
true => Part::All,
false => Part::From(UiSpan::new(Len::ZERO, Len::from_parts(len.rel, len.px))),
};
Place::Within(part)
});
painter.widget_at(child, UiRegion::FULL, place);
} }
size size
} }
+41 -17
View File
@@ -12,6 +12,7 @@ struct Counted {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
size: Size, size: Size,
reads_box: bool, reads_box: bool,
reads_answer_box: bool,
} }
impl Widget for Counted { impl Widget for Counted {
@@ -19,6 +20,8 @@ impl Widget for Counted {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
if self.reads_box { if self.reads_box {
painter.px_size(); painter.px_size();
} else if self.reads_answer_box {
painter.answer_px_size();
} }
self.size self.size
} }
@@ -38,11 +41,18 @@ fn counted(h: &mut Harness, size: Size, reads_box: bool) -> (WeakWidget<Counted>
draws: draws.clone(), draws: draws.clone(),
size, size,
reads_box, reads_box,
reads_answer_box: false,
} }
.add(&mut h.rsc); .add(&mut h.rsc);
(id, Counts(draws)) (id, Counts(draws))
} }
fn answer_counted(h: &mut Harness, size: Size) -> (WeakWidget<Counted>, Counts) {
let (id, draws) = counted(h, size, false);
h.rsc[id].reads_answer_box = true;
(id, draws)
}
struct Layered { struct Layered {
children: [StrongWidget<Rect>; 2], children: [StrongWidget<Rect>; 2],
_revision: usize, _revision: usize,
@@ -156,8 +166,8 @@ fn a_span_child_that_declares_its_length_is_drawn_once() {
h.set_root((hinted, asked).span(Dir::RIGHT)); h.set_root((hinted, asked).span(Dir::RIGHT));
assert_eq!(told_draws.get(), 1); assert_eq!(told_draws.get(), 1);
// Only the available length changes: positioning the final slot does // Its final slot is a parent decision, so it is evaluated there after
// not invalidate a numeric size read. // the provisional ask established its length.
assert_eq!(asked_draws.get(), 2); assert_eq!(asked_draws.get(), 2);
} }
@@ -240,7 +250,7 @@ fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
assert_corners!(h, inner, (0, 0), (400, 120)); assert_corners!(h, inner, (0, 0), (400, 120));
} }
/// Reads its box's size, which nothing but its own draw can put right. /// Reads its box's size to compute its answer.
struct ReadsBox { struct ReadsBox {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
} }
@@ -248,26 +258,36 @@ struct ReadsBox {
impl Widget for ReadsBox { impl Widget for ReadsBox {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
Size::from_px(painter.px_size().div_int(4)) Size::from_px(painter.answer_px_size().div_int(4))
} }
} }
/// Reads its box across one axis only, so its drawing holds for a taller /// Reads its box across one axis only, so its drawing holds for a taller
/// box on its own and only a wider one is worth a draw. /// box on its own and only a wider one is worth a draw.
/// ///
/// Both of these report a quarter of what they read, without saying that the /// Both report a quarter of what they read. Their empty drawings are
/// drawing holds there too, so each length they are asked at costs two draws: /// independent of that read, so a changed question costs one draw.
/// one to answer, and one in the quarter-sized box that answer places them
/// in. The counts below are in those pairs.
struct ReadsWidth { struct ReadsWidth {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
} }
struct ReadsDrawingWidth {
draws: Rc<Cell<usize>>,
}
impl Widget for ReadsDrawingWidth {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.px_len(Axis::X);
Size::LEFTOVER
}
}
impl Widget for ReadsWidth { impl Widget for ReadsWidth {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
Size::from_px(PxVec2::new( Size::from_px(PxVec2::new(
painter.px_len(Axis::X).div_int(4), painter.answer_px_len(Axis::X).div_int(4),
Px::from_int(20), Px::from_int(20),
)) ))
} }
@@ -323,6 +343,7 @@ fn a_row_moves_what_follows_a_child_that_grew_rather_than_drawing_it() {
draws: ruled.clone(), draws: ruled.clone(),
size: Size::LEFTOVER, size: Size::LEFTOVER,
reads_box: false, reads_box: false,
reads_answer_box: false,
}; };
let second = match declared { let second = match declared {
true => second.width(rel(0.25)).add(&mut h.rsc), true => second.width(rel(0.25)).add(&mut h.rsc),
@@ -384,7 +405,7 @@ fn a_resize_redraws_what_read_its_box() {
h.resize((800, 100)); h.resize((800, 100));
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 2); assert_eq!(draws.get(), settled + 1);
} }
#[test] #[test]
@@ -404,7 +425,7 @@ fn a_resize_only_redraws_read_axes() {
h.resize((800, 300)); h.resize((800, 300));
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 2, "width changes its answer"); assert_eq!(draws.get(), settled + 1, "width changes its answer");
} }
/// A window is measured onto the grid like everything else, so a resize too /// A window is measured onto the grid like everything else, so a resize too
@@ -431,7 +452,7 @@ fn a_resize_within_one_step_is_not_a_resize() {
h.resize((400.0 + step, 200.0)); h.resize((400.0 + step, 200.0));
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 2); assert_eq!(draws.get(), settled + 1);
} }
/// The same for a box that changes because a sibling did: what is compared /// The same for a box that changes because a sibling did: what is compared
@@ -500,7 +521,7 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
// Every wrapper up to the outer pad read the size below it, so the outer // Every wrapper up to the outer pad read the size below it, so the outer
// pad is what draws again -- and the span it hands the box to is the same // pad is what draws again -- and the span it hands the box to is the same
// size as before, which is what lets a draw reuse its way past the leaf. // size as before, which is what lets a draw reuse its way past the leaf.
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), true); let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), false);
let padded = leaf.pad(10).add(&mut h.rsc); let padded = leaf.pad(10).add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc); let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((padded, below).span(Dir::DOWN).pad(12)); h.set_root((padded, below).span(Dir::DOWN).pad(12));
@@ -1066,7 +1087,7 @@ fn a_declared_size_change_stops_at_an_independent_parent() {
fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() { fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0)); let draws = Rc::new(Cell::new(0));
let leaf = ReadsWidth { let leaf = ReadsDrawingWidth {
draws: draws.clone(), draws: draws.clone(),
} }
.add(&mut h.rsc); .add(&mut h.rsc);
@@ -1077,7 +1098,7 @@ fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() {
h.frame(); h.frame();
assert!(draws.get() > settled); assert!(draws.get() > settled);
assert_corners!(h, leaf, (300, 90), (500, 110)); assert_corners!(h, leaf, (0, 0), (800, 200));
} }
#[test] #[test]
@@ -1212,7 +1233,7 @@ fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
struct Measured; struct Measured;
impl Widget for Measured { impl Widget for Measured {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let width = painter.px_len(Axis::X); let width = painter.answer_px_len(Axis::X);
painter.primitive(RectPrimitive::color(Color::BLUE)); painter.primitive(RectPrimitive::color(Color::BLUE));
Size::from((80, if width > Px::from_int(100) { 40 } else { 60 })) Size::from((80, if width > Px::from_int(100) { 40 } else { 60 }))
} }
@@ -1312,7 +1333,10 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
} else { } else {
Size::from((80, 27)) Size::from((80, 27))
}; };
let (leaf, _) = counted(h, size, !fractional); let (leaf, _) = match fractional {
true => counted(h, size, false),
false => answer_counted(h, size),
};
let child = Container { let child = Container {
child: leaf.add_strong(&mut h.rsc), child: leaf.add_strong(&mut h.rsc),
region, region,
+63
View File
@@ -60,6 +60,69 @@ fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
assert_corners!(h, top, (0, 0), (400, 200)); assert_corners!(h, top, (0, 0), (400, 200));
} }
#[test]
fn fixed_content_and_a_share_fill_one_viewport() {
let mut h = Harness::new((900, 100));
let content = rect(Color::RED)
.width(LayoutLen {
px: Px::from_int(600),
rel: Rel::ZERO,
leftover: Weight::ONE,
})
.add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
h.set_root(scroll);
assert_corners!(h, content, (0, 0), (900, 100));
}
#[test]
fn fixed_content_wider_than_the_viewport_still_scrolls() {
let mut h = Harness::new((900, 100));
let content = rect(Color::RED).width(1200).add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
h.set_root(scroll);
assert_corners!(h, content, (-300, 0), (900, 100));
}
#[test]
fn a_lone_share_fills_without_scrolling() {
let mut h = Harness::new((900, 100));
let content = rect(Color::RED).width(LayoutLen::LEFTOVER).add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
h.set_root(scroll);
assert_corners!(h, content, (0, 0), (900, 100));
}
#[test]
fn wrapping_content_beside_a_fixed_length_is_stable_warm_and_cold() {
fn plant(h: &mut Harness) -> (WidgetId, WidgetId) {
let fixed = rect(Color::RED).width(600).add(&mut h.rsc);
let text = wtext("Wrapping shapes one source into as many lines as the box leaves room for, so a paragraph's height is an answer and not a setting.")
.size(16)
.wrap(true)
.width(LayoutLen::LEFTOVER)
.add(&mut h.rsc);
let content = (fixed, text).span(Dir::RIGHT).add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
h.set_root(scroll);
(text.id(), content.id())
}
let mut warm = Harness::new((900, 300));
let (text, content) = plant(&mut warm);
warm.rsc.widgets_mut().get_dyn_mut(text);
warm.frame();
let mut cold = Harness::new((900, 300));
let (cold_text, cold_content) = plant(&mut cold);
assert_eq!(warm.region(&text), cold.region(&cold_text));
assert_eq!(warm.region(&content), cold.region(&cold_content));
}
/// A widget that clips to its box may not report more than the box: its /// A widget that clips to its box may not report more than the box: its
/// parent would place the part it cut off, and the framework would put a /// parent would place the part it cut off, and the framework would put a
/// drawing longer than its box somewhere. `Masked` is the second of these /// drawing longer than its box somewhere. `Masked` is the second of these
+174
View File
@@ -14,6 +14,180 @@ use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::Branch; use iris::random::Branch;
fn assert_same_regions(
warm: &Harness,
warm_ids: &[WidgetId],
cold: &Harness,
cold_ids: &[WidgetId],
) {
let mut wrong = Vec::new();
for (i, (&w, &c)) in warm_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"));
}
/// Ten widgets, shrunk from seed 2 at depth 5. The stack is as tall as its
/// first child, so its other children belong in that one-line box. A cold
/// layout used to keep the span's answer from the larger measuring box while
/// a repaint asked it in the stack's final box.
fn plant_stack_in_its_sizing_childs_box(h: &mut Harness) -> Vec<WidgetId> {
let sizing = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add(&mut h.rsc);
let filler = rect(Color::CYAN.alpha(252)).add(&mut h.rsc);
let plain = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add(&mut h.rsc);
let span = (filler, plain).span(Dir::DOWN).add(&mut h.rsc);
let pad = Pad {
padding: Padding::ZERO,
inner: span.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
let probe = rect(Color::RED).add(&mut h.rsc);
let wide = rect(Color::YELLOW.alpha(252)).add(&mut h.rsc);
let narrow = rect(Color::RED).add(&mut h.rsc);
let branch = Branch {
probe: probe.add_strong(&mut h.rsc),
wide: wide.add_strong(&mut h.rsc),
narrow: narrow.add_strong(&mut h.rsc),
threshold: 55.0,
}
.add(&mut h.rsc);
let stack = Stack {
children: vec![
sizing.add_strong(&mut h.rsc),
pad.add_strong(&mut h.rsc),
branch.add_strong(&mut h.rsc),
],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
h.rsc
.widgets_mut()
.set_size_rules(stack.id(), Some(LayoutLen::LEFTOVER), None);
h.set_root(stack);
vec![
sizing.id(),
filler.id(),
plain.id(),
span.id(),
pad.id(),
probe.id(),
wide.id(),
narrow.id(),
branch.id(),
stack.id(),
]
}
#[test]
fn repainting_a_stack_uses_the_box_its_sizing_child_decided() {
let mut warm = Harness::new((900, 1200));
let ids = plant_stack_in_its_sizing_childs_box(&mut warm);
for &id in &ids {
warm.rsc.widgets_mut().get_dyn_mut(id);
}
warm.frame();
let mut cold = Harness::new((900, 1200));
let cold_ids = plant_stack_in_its_sizing_childs_box(&mut cold);
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
/// Ten widgets, shrunk from seed 108 at depth 5. The nested reverse spans
/// evaluate the branch in successively narrower boxes. The answer from the
/// final, decided box must be the one retained after every span is reordered.
fn plant_branch_in_nested_reverse_spans(
h: &mut Harness,
reordered: bool,
) -> (Vec<WidgetId>, [WeakWidget<Span>; 3]) {
let pair = |first: StrongWidget, second: StrongWidget| match reordered {
true => vec![second, first],
false => vec![first, second],
};
let probe = rect(Color::RED.alpha(63)).add(&mut h.rsc);
let wide = rect(Color::RED).add(&mut h.rsc);
let narrow = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let branch = Branch {
probe: probe.add_strong(&mut h.rsc),
wide: wide.add_strong(&mut h.rsc),
narrow: narrow.add_strong(&mut h.rsc),
threshold: 483.0,
}
.add(&mut h.rsc);
let wrapped = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let down = Span {
children: pair(
branch.add_strong(&mut h.rsc),
wrapped.add_strong(&mut h.rsc),
),
dir: Dir::DOWN,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let inner_filler = rect(Color::CYAN.alpha(63)).add(&mut h.rsc);
let inner = Span {
children: pair(
down.add_strong(&mut h.rsc),
inner_filler.add_strong(&mut h.rsc),
),
dir: Dir::LEFT,
gap: Px::ZERO,
}
.height(LayoutLen::rel(1.0))
.add(&mut h.rsc);
let outer_filler = rect(Color::GREEN.alpha(63)).add(&mut h.rsc);
let outer = Span {
children: pair(
inner.add_strong(&mut h.rsc),
outer_filler.add_strong(&mut h.rsc),
),
dir: Dir::LEFT,
gap: Px::ZERO,
}
.height(LayoutLen::rel(1.0))
.add(&mut h.rsc);
h.set_root(outer);
(
vec![
probe.id(),
wide.id(),
narrow.id(),
branch.id(),
wrapped.id(),
down.id(),
inner_filler.id(),
inner.id(),
outer_filler.id(),
outer.id(),
],
[down, inner, outer],
)
}
#[test]
fn reordering_nested_spans_keeps_the_answer_from_the_decided_box() {
let mut warm = Harness::new((900, 1200));
let (ids, spans) = plant_branch_in_nested_reverse_spans(&mut warm, false);
for span in spans {
warm.rsc[span].children.rotate_left(1);
}
warm.frame();
let mut cold = Harness::new((900, 1200));
let (cold_ids, _) = plant_branch_in_nested_reverse_spans(&mut cold, true);
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about /// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
/// the tree changes -- every widget is marked for redraw and the frame is /// the tree changes -- every widget is marked for redraw and the frame is
/// taken again -- so no box may move, and a warm frame has to land where a /// taken again -- so no box may move, and a warm frame has to land where a