Compare commits

...
Author SHA1 Message Date
iris-aiandClaude Fable 5.1 0ef87ebfcf Defer a twice-asked widget's local redraw to its parent
A span asks a share child twice in one draw: in the room, whose answer
its slots rest on, and in the decided slot, whose cross-axis answer it
reads. The record keeps only the second question, so a local redraw that
found that answer unchanged never told the row that the first had --
seed 946 at depth 6, where emptying a fixed-height column turns it from
a share into a fixed width as wide as the row. A widget its parent asked
more than once in one draw now defers to that parent, like one whose
declared length changed. Pinned as
unsettled::emptying_a_column_the_row_asked_twice_asks_the_row_again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 18:29:58 -04:00
iris-aiandClaude Fable 5.1 3091fb86df Ask each child once and place its answer by re-expression
A widget draws in the box it is asked in and its answer is placed inside
that box by re-expressing the drawing; nothing is drawn again in a box an
answer chose. The offer machinery, whose job was to tell a measuring draw
from a placing one, goes with the placing draw. A span measures each child
from its cursor and moves fixed children to their slots with place_at; a
share child is asked once more in its decided slot with its frame narrowed
to it. A stack asks non-sizing children in the box its sizing child
decided. A scroll asks its content once and moves it to the scrolled
offset. A local redraw asks the retained question again and puts the
answer back where the parent placed it.

A symbolic length a child pinned composes through Part::Of exactly where
the part is the whole box less pixels, and pins the parent's own length
otherwise; dropping it let a pad reuse a drawing across a narrowed frame
of the same pixel length (shrinker seeds 60, 248 and 384 at depth 5).

Suite 114/114 including the two decided-box pins, fast oracle 11/11,
shrinker 400 seeds at depth 5 over all fifteen cases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 18:20:57 -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
iris-ai 0954770ceb Say what a child gets of a container's box in that box's own lengths
Three things the measurements asked for, all about how much a box that came
from an answer costs.

**A part in the box's own coordinates.** Saying "less eleven pixels at the
end" in frame lengths from the box's start means reading how long the box is,
and a container whose box is its own answer then depends on its own answer:
`Pad` drew sixty-four times in one resize frame at seed 13, chasing its own
width. `Part::Of` says the same thing as a part of the box, which composes
without a length -- pixels are pixels wherever the box lands -- and what a
child under it holds for maps back through that part onto the container's
own box rather than onto the frame.

**One axis of the box at a time.** `extent_len` pinned both axes, so a span
dividing one of them held for one length of the other as well, and a resize
broke every span whose cross-axis answer moved.

**No lazy placement.** Leaving a child's answer to be placed at the end of
the parent's draw, rather than as the child answers, was meant to save a
recomposition. It costs one instead: the drawing is put in the part first
and in the answer's box after, and where it does not hold for both that is
two drawings rather than one. Seed 1 at depth 8 went from 391 widget draws
on a resize to 29 with it gone. The test that pinned three draws for a
numeric leaf in a span goes with it.

Seed 1 at depth 8, widget draws / distinct widgets / update, against #18's
head and against the commit this branch started from:

| phase   | e44dea3      | 34cafb6      | here          |
| ---     | ---          | ---          | ---           |
| cold    | 369/261/10.6 | 463/274/13.3 | 516/288/12.0  |
| repaint | 1            | 1            | 1             |
| many    | 157/95/0.33  | 263/108/0.59 | 187/119/0.52  |
| size    | 16/12/0.018  | 3/3          | 3/3/0.010     |
| scroll  | 2/0.002      | 1            | 1/0.004       |
| resize  | 13/13/0.019  | 22/15/0.032  | 24/76/0.090   |

Seed 13 at depth 8 is where the protocol still costs: `many` 1091 draws
against #18's 524, and `resize` 2215 against a frame #18 does not draw at
all. Both are the same shape -- an answer measured in one box and drawn in
another -- and the handoff says where that comes from.

Checked: fmt, clippy with -D warnings, 108 suite tests, 20 core tests, the
11 generated cases, and the shrinker at 400 trees of depth 5, which fails
seeds 2 (repaint) and 108 (reorder).
2026-09-18 01:06:05 -04:00
iris-ai 1956be3f3d Lay out in a frame that passes through and a box placed in it
A widget is asked in two boxes rather than one. Its frame is what a fraction
it declares or reports is a fraction of, and it passes through a span, a
stack and a scroll unchanged, so `rel(0.5)` is half the same area however
many containers sit between: a frame is narrowed only by what is decided
above the widget -- a declared length, the root. Its extent is where the
drawing goes, given as a `Place` per axis: a part of the parent's own box,
measured in frame lengths from where that box starts, which the child either
fills or has its answer placed inside.

What that buys is that nothing under a container depends on where the
container sits. A container reads `extent_len` for the length it divides and
nothing about the start, so moving it re-places its children by re-adding
that start and draws nobody again; and a fraction is resolved once, against
the frame, rather than once per box it is composed through -- a stack sized
by a child that reports `rel(0.5)` no longer takes half of half.

`Place` replaces `DrawRegion`, `ExtentPlacement`, `widget_within`,
`measure_len`, `region()`, `placement()` and `box_of`. Primitives and masks
are written in the widget's own box's coordinates alone, so the drawing has
one reference rather than two. The placement pin goes with them: reading the
extent's length pins that length symbolically, and pins compose only where a
child's box is its parent's own.

Placing an answer waits for the end of the parent's draw or for the next ask
of that child in it, so a span child is one drawing and one move rather than
two moves.

`Pad` is transparent: its padding goes around what it pads and its child
keeps the outer frame, which is where `Outset` was going anyway. A fraction
under a pad is now a fraction of the frame rather than of the inset box.

Checked: fmt, clippy with -D warnings, 109 suite tests and 20 core tests in
debug, the 11 generated cases, and the shrinker at 400 trees of depth 5 over
all fifteen cases -- which still finds seed 108 under `reorder`, where a
wrapping text measured in one box and drawn in another settles differently
warm than cold. `redraw` therefore keeps the baseline's deferral for a box
that is not as long as the one the widget was measured in; the plan's step
6 is not done, and the next commit message or the handoff says why.
2026-09-18 00:40:59 -04:00
21 changed files with 1385 additions and 1083 deletions

No files matched your search

+2 -6
View File
@@ -32,7 +32,6 @@ pub(crate) enum Counter {
SizeReads,
HintHits,
HintMisses,
RetainedSizeHits,
ReuseAttempts,
ReuseExact,
ReuseMoved,
@@ -42,7 +41,6 @@ pub(crate) enum Counter {
ReuseOutside,
ReuseWrongLayer,
ReuseWrongNode,
PlaceRedraws,
QueuePops,
DepthReads,
LocalRedraws,
@@ -54,7 +52,7 @@ pub(crate) enum Counter {
TextShapes,
TextBreaks,
GlyphPlacements,
OutsidePlacement,
OutsidePinnedLen,
OutsideFrame,
OutsideExtent,
}
@@ -70,7 +68,6 @@ impl Counter {
"draw-result size reads",
"hint hits",
"hint misses",
"retained size hits",
"reuse attempts",
"reuse exact",
"reuse moved",
@@ -80,7 +77,6 @@ impl Counter {
"reuse: outside what it holds for",
"reuse: another layer",
"reuse: region-node choice changed",
"placed by redrawing",
"redraw queue pops",
"depth reads",
"local redraws",
@@ -92,7 +88,7 @@ impl Counter {
"text shapes",
"text line breaks",
"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: an extent length",
];
+55 -61
View File
@@ -1,6 +1,6 @@
use crate::{
DrawRegion, LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, RegionAlign, RetainedPrimitive,
Size, TextureHandle, UiRegion, UiVec2, WidgetId,
LayerId, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Place, RegionAlign, RetainedPrimitive,
Size, TextureHandle, UiRegion, WidgetId,
};
/// What is kept of a widget its parent has asked about. `drawn` says whether
@@ -9,28 +9,45 @@ use crate::{
#[derive(Debug)]
pub struct ActiveData {
pub id: WidgetId,
/// The box its parent gave it, in `parent_move`'s coordinates: what it
/// was asked about, and what a fraction under it is a fraction of. A
/// local redraw asks here.
pub region: UiRegion,
/// Where its drawing sits inside that box, in the box's own coordinates.
pub placement: UiRegion,
/// The original frame in its parent widget's coordinates. Recomposition
/// and pixel-length evaluation both follow this chain.
pub given_region: UiRegion,
/// The lengths of the box its parent first asked about it in, as
/// lengths of the box the parent was itself offered. Any later box it
/// was given was decided knowing its answer, so this is the question
/// asked again -- and a chain of fractions has no frame in it, which is
/// why a region node between two widgets cannot break it.
pub offer_len: UiVec2,
pub offer_placement: [Option<crate::UiSpan>; 2],
/// Its frame in `parent_move`'s coordinates: what a fraction it declares
/// or reports is a fraction of, composed. Everything it draws sits inside
/// this by way of `extent`.
pub frame_abs: UiRegion,
/// Where its drawing goes, in the frame's own coordinates.
pub extent: UiRegion,
/// That frame in its parent's frame coordinates, before composition:
/// forwarded whole by a transparent container, narrowed by a declared
/// 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.
pub frame: UiRegion,
/// The length its frame was narrowed to on each axis, as a length of
/// its parent's frame: a declared rule, or a box its parent decided for
/// it. `None` forwards the parent's frame whole. Kept as a length rather
/// than a position so that every placement puts the frame back in the
/// part it is given.
pub narrow: [Option<Len>; 2],
/// Where its drawing was put, as a part of its parent's box, and where
/// it was asked. The two differ where a container asks in one place and
/// places the answer in another -- a row measures from its cursor and
/// puts the child in its slot. A part is a length from the box's start,
/// so a box that moved re-places every child by re-adding that start.
pub place: [Place; 2],
pub offer_place: [Place; 2],
/// The box it was asked in, in its frame's coordinates: the box its
/// drawing was made in and the one its contract is about. Its drawing
/// is placed elsewhere by re-expression, never by asking again there.
pub offer_part: UiRegion,
/// The measured answer and its dependencies. A hint-only dependency or
/// a widget first encountered during placement has no measurement yet.
pub answer: Option<(Size, LayoutHolds)>,
/// What the widget said it used of its box, the last time it drew.
/// Asked more than once in its parent's last draw -- measured in one box
/// and then asked in the one the parent decided. The parent's layout
/// rests on the first answer and its drawing on the last, so only the
/// parent can ask either again.
pub re_asked: bool,
/// What the widget said it used of its frame, the last time it drew.
pub size: Size,
/// The frame, extent and explicit placement reads that this drawing holds for.
/// The frame and extent reads that this drawing holds for.
pub holds: LayoutHolds,
pub drawn: bool,
pub parent: Option<WidgetId>,
@@ -39,30 +56,24 @@ pub struct ActiveData {
/// widget a frame visits and cannot drift while one is being drawn.
pub depth: usize,
pub textures: Vec<TextureHandle>,
/// Its primitives, each keeping the box it was written in -- in this
/// widget's extent coordinates, which is what a move recomposes from.
pub primitives: Vec<RetainedPrimitive>,
pub mask_region: Option<DrawRegion>,
/// The children whose box is a part of this widget's extent rather than
/// of its frame, and which part each was given. Moving the extent
/// re-places them through that part, so the drawing need not depend on
/// where it sits.
pub(crate) extent_children: Vec<(WidgetId, ExtentPlacement)>,
pub mask_region: Option<UiRegion>,
pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>,
/// The movable region its primitives are positioned through: its own when
/// opted in, otherwise the nearest ancestor's.
pub move_idx: MoveIdx,
/// The declared lengths whoever drew this widget resolved into its box.
/// The declared lengths whoever drew this widget resolved into its frame.
/// A change to one moves a box this widget cannot fix by drawing again,
/// and comparing them is what says so.
pub declared: [Option<LayoutLen>; 2],
/// The axes along which its parent chose its box from its own answer,
/// so a local redraw asks the question its parent asked.
pub decided: [bool; 2],
/// Its alignment when it was last drawn, which a change to the property
/// is found against.
pub own_align: RegionAlign,
/// The movable region whose coordinates `region` uses.
/// The movable region whose coordinates `frame_abs` uses.
pub parent_move: MoveIdx,
/// The mask its drawing is clipped to: one it set itself, or the one it
/// inherited from whoever drew it.
@@ -76,37 +87,20 @@ pub struct ActiveData {
}
impl ActiveData {
/// Whether what it answered still stands for a box of these pixel
/// lengths -- the box it was asked in, where `holds` is about the box its
/// answer then chose.
pub fn answers_at(&self, px: crate::PxVec2) -> bool {
self.answer.is_some_and(|(_, holds)| {
holds.contains(
px,
UiRegion {
x: self.offer_placement[0].unwrap_or(crate::UiSpan::FULL),
y: self.offer_placement[1].unwrap_or(crate::UiSpan::FULL),
},
)
})
/// 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)
}
}
/// What of a container's extent a child was given: the whole of it, for a
/// wrapper whose box is its child's, or a part of it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum ExtentPlacement {
Inherit,
Within(UiRegion),
}
impl ExtentPlacement {
/// The child's frame in the container's frame coordinates, and the slot
/// the container chose within it.
pub fn resolve(self, extent: UiRegion) -> (UiRegion, [Option<crate::UiSpan>; 2]) {
match self {
Self::Inherit => (UiRegion::FULL, [Some(extent.x), Some(extent.y)]),
Self::Within(part) => (part.within(&extent), [None; 2]),
}
/// Whether what it answered still stands for a frame of these pixel
/// lengths. The answer was given in the box its parent first asked
/// about, which is what it is checked against -- `holds` on the record
/// is about the box the answer then chose.
pub fn answers_at(&self, px: crate::PxVec2, part: UiRegion) -> bool {
self.answer
.is_some_and(|(_, holds)| holds.contains(px, part))
}
}
-36
View File
@@ -1,36 +0,0 @@
use crate::{PrimitiveHandle, UiRegion};
/// Retains which box geometry follows when only the extent changes.
#[derive(Clone, Copy, Debug)]
pub enum DrawRegion {
Frame(UiRegion),
Extent(UiRegion),
}
impl DrawRegion {
pub(crate) fn resolve(self, frame: UiRegion, extent: UiRegion) -> UiRegion {
match self {
Self::Frame(local) => local.within(&frame),
Self::Extent(local) => local.within(&extent).within(&frame),
}
}
pub(crate) fn map(self, f: impl FnOnce(UiRegion) -> UiRegion) -> Self {
match self {
Self::Frame(local) => Self::Frame(f(local)),
Self::Extent(local) => Self::Extent(f(local)),
}
}
}
impl From<UiRegion> for DrawRegion {
fn from(region: UiRegion) -> Self {
Self::Frame(region)
}
}
#[derive(Debug)]
pub struct RetainedPrimitive {
pub handle: PrimitiveHandle,
pub region: DrawRegion,
}
+40 -40
View File
@@ -1,63 +1,63 @@
use crate::{Axis, Holds, PxVec2, UiRegion};
use crate::{Axis, Holds, Len, PxVec2, UiRegion};
/// Dependencies of one evaluation, before the frame and extent are composed.
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// What one evaluation of a widget depends on: the pixel lengths of its
/// frame and of its own box that its drawing and its answer hold for, and
/// the symbolic length of its own box where it read one.
///
/// 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,
/// so what it draws turns on that box's length and on nothing about where it
/// is. It reaches the parent only where the box it pinned is the parent's
/// 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)]
pub struct LayoutHolds {
pub frame: [Holds; 2],
pub extent: [Holds; 2],
pub placement: Option<UiRegion>,
pub extent_len: [Option<Len>; 2],
}
impl LayoutHolds {
pub const ANY: Self = Self {
frame: [Holds::ANY; 2],
extent: [Holds::ANY; 2],
placement: None,
extent_len: [None; 2],
};
pub fn and(self, other: Self) -> Self {
debug_assert!(
self.placement.is_none()
|| other.placement.is_none()
|| self.placement == other.placement
);
Self {
frame: [
self.frame[0].and(other.frame[0]),
self.frame[1].and(other.frame[1]),
],
extent: [
self.extent[0].and(other.extent[0]),
self.extent[1].and(other.extent[1]),
],
placement: self.placement.or(other.placement),
let mut result = Self::ANY;
for n in 0..2 {
result.frame[n] = self.frame[n].and(other.frame[n]);
result.extent[n] = self.extent[n].and(other.extent[n]);
debug_assert!(
self.extent_len[n].is_none()
|| other.extent_len[n].is_none()
|| self.extent_len[n] == other.extent_len[n]
);
result.extent_len[n] = self.extent_len[n].or(other.extent_len[n]);
}
result
}
pub fn covers(self, other: Self) -> bool {
self.placement
.is_none_or(|placement| other.placement == Some(placement))
&& [0, 1].into_iter().all(|n| {
self.frame[n].lo <= other.frame[n].lo
&& self.frame[n].hi >= other.frame[n].hi
&& self.extent[n].lo <= other.extent[n].lo
&& self.extent[n].hi >= other.extent[n].hi
})
(0..2).all(|n| {
self.frame[n].lo <= other.frame[n].lo
&& self.frame[n].hi >= other.frame[n].hi
&& self.extent[n].lo <= other.extent[n].lo
&& self.extent[n].hi >= other.extent[n].hi
&& self.extent_len[n].is_none_or(|len| other.extent_len[n] == Some(len))
})
}
pub fn contains(self, px: PxVec2, placement: UiRegion) -> bool {
self.placement.is_none_or(|old| old == placement)
&& [Axis::X, Axis::Y].into_iter().all(|axis| {
self.frame[axis as usize].contains(px.axis(axis))
&& self.extent[axis as usize]
.contains(placement.axis(axis).len().to_px(px.axis(axis)))
})
}
pub fn in_frame(self, placement: UiRegion) -> [Holds; 2] {
[Axis::X, Axis::Y].map(|axis| {
self.frame[axis as usize]
.and(self.extent[axis as usize].through(placement.axis(axis).len()))
pub fn contains(self, px: PxVec2, extent: UiRegion) -> bool {
AXES.into_iter().all(|axis| {
let n = axis as usize;
let len = extent.axis(axis).len();
self.frame[n].contains(px.axis(axis))
&& self.extent[n].contains(len.to_px(px.axis(axis)))
&& self.extent_len[n].is_none_or(|pinned| pinned == len)
})
}
}
+2 -2
View File
@@ -10,17 +10,17 @@ use crate::{
pub const CHAIN_LIMIT: u32 = 64;
mod active;
mod draw_region;
mod holds;
mod layout_holds;
mod painter;
mod place;
mod render_state;
pub use active::*;
pub use draw_region::*;
pub use holds::*;
pub use layout_holds::*;
pub use painter::{Painter, PrimitiveLike};
pub use place::*;
pub use render_state::*;
#[derive(Default)]
+293 -385
View File
@@ -1,14 +1,14 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter};
use crate::{
Axis, DrawRegion, ExtentPlacement, Holds, LayoutHolds, LayoutLen, Len, Px, PxVec2, RegionAlign,
Axis, Holds, LayoutHolds, LayoutLen, Len, Part, Place, Px, PxVec2, RegionAlign, Rel,
RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
TexturePrimitive,
},
ui::render_state::DrawInfo,
ui::render_state::{DrawInfo, Placing},
};
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
@@ -17,51 +17,43 @@ pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc,
/// The box its parent gave it, in the coordinates of `move_idx`: what a
/// fraction of this widget's area is a fraction of, and what every region
/// it writes composes within. The same box on the ask that measures and
/// the ask that places, which is what keeps a fraction under it from
/// being resolved twice.
pub(super) region: UiRegion,
/// Where this widget's drawing sits inside that box, in the box's own
/// coordinates: `FULL` while its answer is not yet known, and the box
/// its answer or its parent chose once one of them has.
pub(super) placement: UiRegion,
/// Whether this draw read its placement, which makes the drawing one
/// that holds for that placement alone -- the way reading a length in
/// pixels makes it hold for that length.
pub(super) reads_placement: bool,
/// That box in pixels, which its children's are a length of: threaded
/// down from the box this widget was given rather than composed back up
/// the chain, so every length in layout is one multiply from its
/// parent's and [`Holds::through`] inverts exactly that.
/// What a fraction this widget declares or reports is a fraction of, in
/// the coordinates of `move_idx`: forwarded from its parent unchanged
/// through a span, a stack or a scroll, and narrowed only by what was
/// 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
/// under it from being resolved twice.
pub(super) frame: UiRegion,
/// Where this widget's drawing goes, in the frame's own coordinates.
/// Everything it writes is in these coordinates, and its children are
/// placed as parts of it.
pub(super) extent: UiRegion,
/// The extent's symbolic length where this draw read it, which makes the
/// drawing one that holds for that length alone -- the way reading a
/// length in pixels makes it hold for that number of pixels.
pub(super) extent_len: [Option<Len>; 2],
/// 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
/// in layout is one multiply from its parent's and [`Holds::through`]
/// inverts exactly that.
pub(super) px: PxVec2,
pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<RetainedPrimitive>,
pub(super) mask_region: Option<DrawRegion>,
pub(super) extent_children: Vec<(WidgetId, ExtentPlacement)>,
pub(super) extent_own: [Holds; 2],
pub(super) mask_region: Option<UiRegion>,
/// Only children whose answers were read constrain this widget's answer.
pub(super) answer_under: LayoutHolds,
pub(super) children: Vec<WidgetId>,
/// The children asked about so far, so the first box each was asked in
/// is the one recorded as its offer.
pub(super) offered: Vec<WidgetId>,
/// The lengths of the box this widget was first asked about in, in
/// pixels. Its children's offers are a fraction of it.
pub(super) offered_px: PxVec2,
/// Whether this draw is in a box of those lengths, which makes the
/// questions it asks the ones a cold layout asks and their answers the
/// ones to keep.
pub(super) at_offer: bool,
/// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>,
/// What this draw itself read of its box 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.
pub(super) own: [Holds; 2],
/// Dependencies of every child drawing, including unmeasured overlays.
pub(super) under: LayoutHolds,
pub(super) frame_own: [Holds; 2],
/// The same for its own box.
pub(super) extent_own: [Holds; 2],
/// What each child's drawing depends on. Asking a child again replaces
/// its drawing, so it replaces this too rather than narrowing it.
pub(super) under: Vec<(WidgetId, LayoutHolds)>,
/// The movable region this widget's primitives are positioned through:
/// its own when opted in, otherwise the nearest ancestor's.
pub(super) move_idx: MoveIdx,
@@ -74,26 +66,29 @@ pub struct Painter<'a> {
}
impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: DrawRegion) {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<P>();
self.write(kind, primitive, region);
}
/// Takes the kind, for a caller writing many of one primitive.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: DrawRegion) {
self.write_resolved(
kind,
primitive,
region,
region.resolve(self.region, self.placement),
);
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
self.write_resolved(kind, primitive, region, self.resolve(region));
}
/// A box in this widget's extent coordinates, composed into the
/// coordinates its move slot is in: through the extent, then through the
/// frame the extent is a part of. The same two steps a recomposition
/// replays, so a moved drawing lands where a cold one does.
fn resolve(&self, region: UiRegion) -> UiRegion {
region.within(&self.extent).within(&self.frame)
}
fn write_resolved<P: Primitive>(
&mut self,
kind: PrimitiveKind<P>,
primitive: P,
region: DrawRegion,
region: UiRegion,
resolved: UiRegion,
) {
#[cfg(feature = "layout-diagnostics")]
@@ -122,135 +117,71 @@ impl<'a> Painter<'a> {
/// Writes a primitive over the whole of this widget's own box.
pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
let at = DrawRegion::Extent(UiRegion::FULL);
let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, at)
self.primitive_at(primitive, UiRegion::FULL)
}
/// Writes in the frame by default. `DrawRegion::Extent` keeps the local
/// geometry attached to this widget's box without reading its placement.
pub fn primitive_within(
&mut self,
primitive: impl PrimitiveLike,
region: impl Into<DrawRegion>,
) {
/// Writes a primitive in a part of this widget's own box, in that box's
/// coordinates.
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, region.into());
self.primitive_at(primitive, region);
}
/// Sets a mask in the selected frame or extent coordinates.
pub fn set_mask(&mut self, region: impl Into<DrawRegion>) {
let region = region.into();
/// Sets a mask, in this widget's own box's coordinates.
pub fn set_mask(&mut self, region: UiRegion) {
self.mask_region = Some(region);
assert!(self.mask == MaskIdx::NONE);
let resolved = self.resolve(region);
let move_idx = self.move_idx;
self.mask = self.rsc.ui_mut().masks.push(Mask {
region: region.resolve(self.region, self.placement),
move_idx: self.move_idx,
region: resolved,
move_idx,
});
}
/// Draws a widget in the whole of this widget's own box: it gets the
/// same region -- the same area for its fractions to be of -- and is put
/// where this widget was put. What a container that is only a wrapper
/// around one child wants, since its box is the child's.
/// Draws a widget in the whole of this widget's own box, with the frame
/// forwarded unchanged: what a container that is only a wrapper around
/// one child wants, and what every transparent container passes for the
/// frame.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
let own = self.placement;
self.widget_at_inner(
id,
UiRegion::FULL,
[Some(own.x), Some(own.y)],
Some(ExtentPlacement::Inherit),
false,
)
self.widget_at(id, [None; 2], [Place::Within(Part::All); 2])
}
/// What a widget's rules declare its lengths to be, which whoever draws
/// it resolves into its box. Reading them depends on nothing -- the box
/// that comes of them is kept on the child, and `redraw` compares it
/// there.
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] {
declared_lens(self.rsc.widgets(), id.id())
}
/// Takes back a child that was drawn only to find out how long it is.
/// Its drawing is dropped and it is not one of this widget's children
/// this frame; what it answered is still something this widget asked.
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.children.retain(|child| *child != id.id());
self.extent_children.retain(|(child, _)| *child != id.id());
self.state.undraw_rec(id.id(), self.rsc);
}
/// Draws a child in `region`, relative to this widget's frame. The child
/// resolves declared lengths and reports against that frame, then places
/// its drawing by its own alignment.
/// Asks a child, saying what its fractions are of and where it is asked.
///
/// `DrawRegion::Extent` gives a part of where this widget's drawing sits
/// instead, for a container whose children belong inside that rather than
/// inside the box it was offered. The part is what is kept, so moving the
/// extent re-places the child rather than drawing this widget again.
pub fn widget_within<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: impl Into<DrawRegion>,
) -> DrawResult<'s, 'a, W> {
match region.into() {
DrawRegion::Frame(region) => self.widget_at(id, region, [None; 2]),
DrawRegion::Extent(part) => {
let within = part.within(&self.placement);
self.widget_at_inner(
id,
within,
[None; 2],
Some(ExtentPlacement::Within(part)),
false,
)
}
}
}
/// Draws a widget in `region`, saying where in it the drawing goes.
/// `narrow` is a length this widget decided for the child's frame, per
/// axis, as a length of this widget's own frame: a resolved share, or a
/// box a sibling's answer decided. `None` forwards this widget's frame,
/// which is what a container that only divides room passes, so a
/// fraction under it means the same wherever it sits and however deeply
/// it is nested. A declared length narrows the frame here whatever the
/// caller says. A narrowed frame is placed in the part by the child's
/// alignment and is the box the child is asked in.
///
/// `region` is the child's own area: what a fraction it declares or
/// reports is a fraction of, and the coordinates the regions it writes
/// compose within. It is the same box on the ask that measures and the
/// ask that places, which is what stops a fraction under it being
/// resolved twice.
///
/// `placement` is what of that region the child's drawing takes, per
/// axis, wherever this widget is choosing. `None` leaves the axis to the
/// child's own answer and alignment, which is what
/// [`Self::widget_within`] passes. A span passes the whole row as the
/// region, so `rel(0.5)` is half the row wherever the child sits in it,
/// and places the child by passing the slot along its axis.
/// `place` is where the child is asked, per axis, as a part of this
/// widget's box: see [`Place`]. The child draws once, in that box, and
/// its answer is placed inside it by re-expressing the drawing. Nothing
/// is drawn again in a box an answer chose; a container that puts the
/// answer somewhere else says so with [`Self::place_at`].
pub fn widget_at<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
placement: [Option<UiSpan>; 2],
narrow: [Option<Len>; 2],
place: [Place; 2],
) -> DrawResult<'s, 'a, W> {
self.widget_at_inner(id, region, placement, None, false)
}
fn widget_at_inner<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
placement: [Option<UiSpan>; 2],
extent: Option<ExtentPlacement>,
measuring: bool,
) -> DrawResult<'s, 'a, W> {
self.extent_children.retain(|(child, _)| *child != id.id());
if let Some(extent) = extent {
self.extent_children.push((id.id(), extent));
}
let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id);
let align = self.rsc.widgets().alignment(id.id());
let (local, placement) = ask_box(region, declared, align, placement);
let declared_narrow = narrowed_by(declared);
let narrow = [
declared_narrow[0].or(narrow[0]),
declared_narrow[1].or(narrow[1]),
];
let (local, extent) = frame_and_extent(part_of(self.extent, place), narrow, align);
let within = match local == UiRegion::FULL {
true => self.region,
false => local.within(&self.region),
true => self.frame,
false => local.within(&self.frame),
};
#[cfg(feature = "layout-diagnostics")]
if region_node {
@@ -258,35 +189,13 @@ impl<'a> Painter<'a> {
diag::region_node(id.id(), self.id, within);
}
// A child listed twice would be moved twice.
if !self.children.contains(&id.id()) {
let re_asked = self.children.contains(&id.id());
if !re_asked {
self.children.push(id.id());
}
let first_ask = self.offer(id.id());
let given_len = local.size();
let offer_len = match first_ask {
true => given_len,
false => self
.state
.active
.get(&id.id())
.map_or(given_len, |a| a.offer_len),
};
let offer_placement = if first_ask {
placement
} else {
self.state
.active
.get(&id.id())
.map_or(placement, |a| a.offer_placement)
};
let px = given_len.to_px(self.px);
let offered_px = offer_len.to_px(self.offered_px);
// The answer and what it holds for, both about the box asked in. The
// child's record may say something else once its drawing has been
// placed: a drawing made again in its placed box holds for that box.
let px = local.size().to_px(self.px);
let (size, answer_holds, holds) = self.state.draw_inner(
id.id(),
within,
DrawInfo {
layer: self.layer,
parent: Some(self.id),
@@ -294,76 +203,77 @@ impl<'a> Painter<'a> {
parent_move: self.move_idx,
region_node,
mask: self.mask,
given_region: local,
offer_len,
offer_placement,
frame: local,
frame_abs: within,
part: extent,
place,
offer_place: place,
narrow,
re_asked,
px,
offered_px,
placement,
},
None,
measuring,
self.rsc,
);
let in_parent = |holds: LayoutHolds| {
let mut result = LayoutHolds::ANY;
for axis in AXES {
let n = axis as usize;
let chosen = placement[n].unwrap_or(UiSpan::FULL).len();
match extent {
// Its box is this widget's own, so what its drawing holds
// for is what this widget's extent holds for.
Some(ExtentPlacement::Inherit) if declared[n].is_none() => {
result.frame[n] = holds.frame[n].through(local.axis(axis).len());
result.extent[n] = holds.extent[n];
if holds.placement.is_some() {
result.placement = Some(self.placement);
}
}
// Its box is a part of this widget's extent, so what it
// holds for is a range on that extent and none of it a
// range on the frame. Only the part's length reaches it,
// which is what lets the extent move without a redraw.
Some(ExtentPlacement::Within(part)) if declared[n].is_none() => {
result.extent[n] = holds.frame[n]
.and(holds.extent[n].through(chosen))
.through(part.axis(axis).len());
}
// Its box is a length of this widget's frame: an
// ordinary ask, or a declared length, which is that
// length wherever the box it sits in came from.
_ => {
result.frame[n] = holds.frame[n].through(local.axis(axis).len()).and(
holds.extent[n]
.through(chosen)
.through(local.axis(axis).len()),
);
}
}
}
result
};
self.under = self.under.and(in_parent(holds));
let mut answer_holds = in_parent(answer_holds);
// What it reports is a fraction of the box it was given, which is a
// part of this widget's extent -- so the same fraction is a different
// length once that extent is, and pixels are not. The answer only:
// the drawing this holds is re-placed rather than made again.
if matches!(extent, Some(ExtentPlacement::Within(_)))
&& AXES.into_iter().any(|axis| {
declared[axis as usize].is_none() && size.axis(axis).rel != crate::Rel::ZERO
})
{
answer_holds.placement = Some(self.placement);
let own = self.extent;
let compose = |holds| in_parent(holds, local, extent, place, narrow, own);
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 {
child: id,
painter: self,
size: in_parent_frame(size, local.size(), declared),
answer_holds,
answer_holds: compose(answer_holds),
}
}
/// Takes back a child that was drawn only to find out how long it is.
/// Its drawing is dropped and it is not one of this widget's children
/// this frame; what it answered is still something this widget asked.
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.children.retain(|child| *child != id.id());
self.under.retain(|(child, _)| *child != id.id());
self.state.undraw_rec(id.id(), self.rsc);
}
/// Puts a child asked about in this draw somewhere else in this
/// widget's box: its answer, placed in this part instead. The drawing
/// is re-expressed there rather than made again -- what a row does once
/// it knows every slot, having measured each child from its cursor.
pub fn place_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, place: [Place; 2]) {
debug_assert!(
self.children.contains(&id.id()),
"'{}' placed a child it did not ask about in this draw",
self.label()
);
let at = self.placing();
self.state.place_in(id.id(), &at, place, self.rsc);
}
/// This widget as the thing its children are placed within.
fn placing(&self) -> Placing {
Placing {
id: self.id,
extent: self.extent,
local: self.frame,
px: self.px,
depth: self.depth,
move_idx: self.move_idx,
mask: self.mask,
}
}
/// What a widget's rules declare its lengths to be, which whoever draws
/// it resolves into its frame. Reading them depends on nothing -- the box
/// that comes of them is kept on the child, and `redraw` compares it
/// there.
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] {
declared_lens(self.rsc.widgets(), id.id())
}
/// What a child says its length is without being drawn, if it can say.
/// Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
@@ -392,65 +302,6 @@ impl<'a> Painter<'a> {
}
}
/// Measures a child's length from its hint, a retained answer, or `draw`.
/// A fresh draw evaluates the offer without placing its answer. The caller
/// must later place or undraw the child.
pub fn measure_len<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
region: UiRegion,
placement: [Option<UiSpan>; 2],
) -> LayoutLen {
let offered = placement;
let declared = self.declared_lens(child);
let align = self.rsc.widgets().alignment(child.id());
let (local, placement) = ask_box(region, declared, align, placement);
let first_ask = self.at_offer && !self.offered.contains(&child.id());
if let Some(hint) = self.size_hint(child, axis) {
return hint;
}
let px = local.size().to_px(self.px);
let retained =
self.state
.retained_size(child.id(), px, placement, self.move_idx, self.rsc.widgets());
let Some((size, holds)) = retained else {
return self
.widget_at_inner(child, region, offered, None, true)
.len(axis);
};
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::RetainedSizeHits);
self.depend_on(child);
if first_ask {
self.offered.push(child.id());
let active = self.state.active.get_mut(&child.id()).unwrap();
active.offer_len = local.size();
active.offer_placement = placement;
}
let placement = UiRegion {
x: placement[0].unwrap_or(UiSpan::FULL),
y: placement[1].unwrap_or(UiSpan::FULL),
};
let holds = holds.in_frame(placement);
for (axis, under) in AXES.into_iter().zip(self.answer_under.frame.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
}
in_parent_frame(size, local.size(), declared).axis(axis)
}
/// Whether this is the first box a child is asked about in during a draw
/// that is itself in the box it was asked in -- the question a cold
/// layout asks, whose answer is the one to keep.
fn offer(&mut self, child: WidgetId) -> bool {
if !self.at_offer || self.offered.contains(&child) {
return false;
}
self.offered.push(child);
true
}
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id());
@@ -471,11 +322,10 @@ impl<'a> Painter<'a> {
/// Writes glyphs in the selected frame or extent coordinates.
// TODO: merge the text methods into the primitive ones.
pub fn glyphs(&mut self, text: &RenderedText, origin: impl Into<DrawRegion>) {
let origin = origin.into();
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
// Glyph offsets and sizes are pixels, which compose additively.
// Only the shared origin needs the frame/extent composition.
let resolved = origin.resolve(self.region, self.placement);
// Only the shared origin needs composing through the extent.
let resolved = self.resolve(origin);
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() {
let place = |mut region: UiRegion| {
@@ -499,30 +349,22 @@ impl<'a> Painter<'a> {
color: text.color,
flags: glyph.entry.flags(),
},
origin.map(place),
place(origin),
place(resolved),
);
}
}
/// The box this widget's parent gave it, in the coordinates its own
/// primitives are written in -- so a region composed `within` it may be
/// drawn directly. Its own box is [`Self::placement`] of this one.
pub fn region(&self) -> UiRegion {
self.region
}
/// Where this widget's drawing goes inside the box it was given, in that
/// box's coordinates: what its own answer took of it, or what its parent
/// chose for it. `FULL` on the ask that measures, since nothing has been
/// placed yet.
///
/// Reading it is what says the drawing depends on it, so a widget that
/// positions its own content reads it and is drawn again once its box is
/// known, and one that fills whatever it is given never is.
pub fn placement(&mut self) -> UiRegion {
self.reads_placement = true;
self.placement
/// The symbolic length of this widget's own box along one axis, in the
/// lengths of its frame that it places its children in. Reading it pins
/// the drawing to that length -- and to nothing about where the box
/// starts, which is what lets a container move without being drawn
/// again. One axis at a time, because a container that divides one axis
/// holds for any length of the other.
pub fn extent_len(&mut self, axis: Axis) -> Len {
let len = self.extent.axis(axis).len();
self.extent_len[axis as usize] = Some(len);
len
}
/// Where this widget sits in a box longer than the length it takes. A
@@ -550,14 +392,6 @@ impl<'a> Painter<'a> {
.is_some()
}
/// The part of this widget's box that something of `size` takes, at the
/// near edge. A container that reports one child's size gives every child
/// this, so what it draws is inside what it says it occupies.
pub fn box_of(&self, size: Size) -> UiRegion {
let lens = placed_lens(size, [None; 2], [false; 2]);
placed_box(UiRegion::FULL, lens, RegionAlign::NEAR)
}
/// This widget's own box in pixels. Reading it makes the drawing one
/// that holds for this box only, until `holds` says how far it goes.
pub fn px_size(&mut self) -> PxVec2 {
@@ -567,7 +401,7 @@ impl<'a> Painter<'a> {
/// One axis of this widget's own box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px {
let part = self.placement.axis(axis).len();
let part = self.extent.axis(axis).len();
let len = part.to_px(self.px.axis(axis));
let own = &mut self.extent_own[axis as usize];
if *own == Holds::ANY {
@@ -581,7 +415,7 @@ impl<'a> Painter<'a> {
/// of the box, and the same reported size. A widget that read its length
/// in pixels holds for that one alone until it says otherwise.
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let part = self.placement.axis(axis).len();
let part = self.extent.axis(axis).len();
let holds = holds.into();
debug_assert!(
holds.contains(part.to_px(self.px.axis(axis))),
@@ -592,30 +426,30 @@ impl<'a> Painter<'a> {
self.extent_own[axis as usize] = holds;
}
/// One axis of the box this widget's parent gave it, in pixels -- what a
/// fraction of its area resolves against, and so what a container divides
/// among its children. Its own box is a part of this one.
pub fn region_px_len(&mut self, axis: Axis) -> Px {
/// One axis of this widget's frame in pixels -- what a fraction of its
/// area resolves against, and so what a container divides among its
/// children. Its own box is a part of this one.
pub fn frame_px_len(&mut self, axis: Axis) -> Px {
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 {
*own = Holds::at(len);
}
len
}
/// [`Self::holds`] stated about the region rather than about this
/// widget's own box, for a container whose drawing turns on the box it
/// was given rather than on the part of it it took.
pub fn region_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
/// [`Self::holds`] stated about the frame rather than about this
/// widget's own box, for a container whose drawing turns on what its
/// fractions are of rather than on the part of it it took.
pub fn frame_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into();
debug_assert!(
holds.contains(self.px.axis(axis)),
"'{}' ({:?}) says its drawing holds for lengths that leave out its region",
"'{}' ({:?}) says its drawing holds for lengths that leave out its frame",
self.label(),
self.id
);
self.own[axis as usize] = holds;
self.frame_own[axis as usize] = holds;
}
pub fn text_data(&mut self) -> &mut TextData {
@@ -703,6 +537,68 @@ impl PrimitiveLike for &TextureHandle {
}
}
/// What a child depends on, said about the boxes the widget that drew it
/// has rather than the ones the child was given.
///
/// `frame` is the child's frame in this widget's frame coordinates and
/// `extent` the box it was given, in the child's own frame coordinates. Both
/// reach it as one length, so what it holds for maps back through that
/// length exactly -- and where the box it was given is this widget's own,
/// what it says about that box is what this widget can say about its own.
/// `own` is this widget's own box, for a pin that cannot be said exactly.
pub(crate) fn in_parent(
holds: LayoutHolds,
frame: UiRegion,
extent: UiRegion,
place: [Place; 2],
narrow: [Option<Len>; 2],
own: UiRegion,
) -> LayoutHolds {
let mut result = LayoutHolds::ANY;
for axis in AXES {
let n = axis as usize;
let frame_len = frame.axis(axis).len();
result.frame[n] = holds.frame[n].through(frame_len);
match (place[n].part(), narrow[n]) {
// Its box is this widget's own, or a part of it in that box's
// own lengths: so what it holds for is a range on this widget's
// 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
// the whole of it, and pins the same way.
(Part::All, None) => {
result.extent[n] = holds.extent[n];
result.extent_len[n] = holds.extent_len[n];
}
// Its box is a part of this widget's own box, in that box's own
// lengths, so what it holds for maps back through that part into
// a range on this widget's box. A length it pinned is this
// widget's length less the part's pixels where the part is the
// whole of the box less pixels, which is the one shape that
// inverts exactly; any other part pins this widget's own length.
(Part::Of(span), None) => {
let part_len = span.len();
result.extent[n] = holds.extent[n].through(part_len);
result.extent_len[n] = holds.extent_len[n].map(|pinned| match part_len.rel {
Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px),
_ => own.axis(axis).len(),
});
}
// Its box is a part of this widget's frame, or a length of it
// decided here: a length of the frame is all that reaches it,
// so what it holds for is a range on the frame and none of it
// on this widget's own box.
_ => {
result.frame[n] = result.frame[n].and(
holds.extent[n]
.through(extent.axis(axis).len())
.through(frame_len),
);
}
}
}
result
}
/// A child's answer as lengths of the parent's own region. A widget reports
/// a fraction of its own region, and `of` is that region as a length of this
/// one. Pixels come through untouched, being that many pixels wherever they
@@ -749,68 +645,80 @@ pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: b
reported.leftover != Weight::ZERO || declared.is_some() || decided
}
/// What of the box it was given a widget's drawing occupies, as lengths of
/// that box: the size it reported wherever that is a part to be placed, and
/// the whole of the box wherever the answer fills it.
/// Where a widget's drawing goes inside the part its parent gave it: what
/// it reported, on the side of the part its alignment says, and the whole
/// part wherever the answer fills it.
///
/// A reported fraction is a fraction of the box the widget drew in, where a
/// declared one is a fraction of the box its parent handed down -- a span
/// reporting `rel(1.0)` means all of what it was given, whatever that was a
/// fraction of. So this is a length of the box rather than a length composed
/// into it, and a box in pixels is this step from the given box's pixels.
pub(crate) fn placed_lens(
/// The length it reported is a length of its frame, and the part is one too,
/// so this takes one from the other rather than composing it into the part.
/// That is what makes a fraction the same fraction wherever the part it is
/// placed in sits and however long it is -- the fraction is resolved once,
/// here, against the frame it was reported of.
pub(crate) fn placed_extent(
part: UiRegion,
size: Size,
declared: [Option<LayoutLen>; 2],
decided: [bool; 2],
) -> UiVec2 {
let mut lens = UiVec2::FULL_SIZE;
for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) {
let reported = size.axis(axis);
if !fills(reported, declared, decided) {
*lens.axis_mut(axis) = Len::from_parts(reported.rel, reported.px);
}
}
lens
}
/// Where that drawing sits: those lengths taken of the box the widget was
/// asked in, on the side of it that the widget's alignment says.
pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) -> UiRegion {
let mut placed = region;
fill: [bool; 2],
align: RegionAlign,
) -> UiRegion {
let mut placed = part;
for axis in AXES {
// The whole of the box is already where it sits, and the arithmetic
// below is the identity for it.
if lens.axis(axis) == Len::FULL {
let n = axis as usize;
let reported = size.axis(axis);
if fills(reported, declared[n], fill[n]) {
continue;
}
let len = Len::from_parts(reported.rel, reported.px);
let span = placed.axis_mut(axis);
let len = lens.axis(axis).within_len(span.len());
span.start += (span.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len;
}
placed
}
/// A declared axis gets a frame of that length, aligned within the parent's
/// slot (or the offer). Undeclared axes keep the offered frame and chosen
/// placement, so their reported fractions retain that reference.
pub(crate) fn ask_box(
mut region: UiRegion,
declared: [Option<LayoutLen>; 2],
align: RegionAlign,
placement: [Option<UiSpan>; 2],
) -> (UiRegion, [Option<UiSpan>; 2]) {
let mut placed = [None; 2];
for (axis, (len, chosen)) in AXES.into_iter().zip(declared.into_iter().zip(placement)) {
let Some(len) = len else {
placed[axis as usize] = chosen;
continue;
};
let span = region.axis_mut(axis);
let len = Len::from_parts(len.rel, len.px);
let slot = chosen.unwrap_or(*span);
span.start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len;
/// The part of a widget's own box a `place` names, in the coordinates that
/// box is in.
pub(crate) fn part_of(extent: UiRegion, place: [Place; 2]) -> UiRegion {
let mut part = extent;
for axis in AXES {
*part.axis_mut(axis) = place[axis as usize].part().of(*extent.axis(axis));
}
(region, placed)
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]) -> [Option<Len>; 2] {
declared.map(|declared| declared.map(|len| Len::from_parts(len.rel, len.px)))
}
/// The frame a child is asked in and the box it is asked in, both in the
/// coordinates of the widget asking.
///
/// `part` is what of the caller's own box the child is given. `narrow` is a
/// length decided for the child's frame -- a rule, a share, a box a sibling
/// decided -- which makes the frame the box the child is asked in: that
/// length is what decided where it goes, placed in the part by the child's
/// alignment. Where nothing narrowed it, the frame is the caller's own and
/// the part is the box.
///
/// A length rather than a position, so that a child placed again is put back
/// in whatever part it is given rather than where it first was.
pub(crate) fn frame_and_extent(
part: UiRegion,
narrow: [Option<Len>; 2],
align: RegionAlign,
) -> (UiRegion, UiRegion) {
let mut frame = UiRegion::FULL;
let mut extent = part;
for (axis, narrow) in AXES.into_iter().zip(narrow) {
if let Some(len) = narrow {
let slot = part.axis(axis);
let start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
*frame.axis_mut(axis) = UiSpan::new(start, start + len);
*extent.axis_mut(axis) = UiSpan::FULL;
}
}
(frame, extent)
}
+61
View File
@@ -0,0 +1,61 @@
use crate::{PrimitiveHandle, UiRegion, UiSpan};
/// What of a widget's own box a child is given, along one axis.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Part {
/// The whole of it.
All,
/// Frame lengths from where the box starts, which is what a container
/// dividing room among its children speaks: a child's report is a length
/// of the frame, so the cursor that sums those reports is one too. A
/// moved box re-places every child by re-adding its start, exactly.
From(UiSpan),
/// A part of the box in its own coordinates, which is what a container
/// that insets one speaks: taking eleven pixels off the end needs no
/// length, where saying the same thing in frame lengths would make the
/// container read its own box -- and a box chosen from its own answer
/// then feeds back into the answer.
Of(UiSpan),
}
impl Part {
/// Where it lands in the coordinates `extent` is in.
pub(crate) fn of(self, extent: UiSpan) -> UiSpan {
match self {
Self::All => extent,
Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end),
Self::Of(span) => span.within(&extent),
}
}
}
/// Where a child goes along one axis, as a part of this widget's box.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Place {
/// The child's answer, aligned inside the part by the child's alignment.
Within(Part),
/// Exactly the part; the answer is not placed inside it again.
Fill(Part),
}
impl Place {
pub(crate) fn part(self) -> Part {
match self {
Self::Within(part) | Self::Fill(part) => part,
}
}
/// Whether the part is the drawing's box outright, rather than the box
/// the answer is placed inside.
pub(crate) fn fills(self) -> bool {
matches!(self, Self::Fill(_))
}
}
/// A primitive as it was written: its box in the widget's own box's
/// coordinates, which is what a move of that box re-composes from.
#[derive(Debug)]
pub struct RetainedPrimitive {
pub handle: PrimitiveHandle,
pub region: UiRegion,
}
File diff suppressed because it is too large. Load diff
+9 -7
View File
@@ -117,16 +117,18 @@ pub struct Branch {
impl Widget for Branch {
fn draw(&mut self, painter: &mut Painter) -> Size {
let mut top = UiRegion::FULL;
top.y.end = top.y.start.offset(Px::from_int(40));
let measured = painter.widget_within(&self.probe, top).len(Axis::X);
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut)));
let measured = painter
.widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top])
.len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
let mut below = UiRegion::FULL;
below.y.start = below.y.start.offset(Px::from_int(40));
let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y))));
let place = [Place::Within(Part::All), below];
match px > Px::from_f32(self.threshold) {
true => painter.widget_within(&self.wide, below),
false => painter.widget_within(&self.narrow, below),
true => painter.widget_at(&self.wide, [None; 2], place),
false => painter.widget_at(&self.narrow, [None; 2], place),
};
Size::LEFTOVER
}
+1 -1
View File
@@ -6,7 +6,7 @@ pub struct Masked {
impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(DrawRegion::Extent(UiRegion::FULL));
painter.set_mask(UiRegion::FULL);
painter.widget(&self.inner);
// What it occupies is its box, on both axes, for the reason `Scroll`
// reports the same: it clips what is inside to that box, so it can
+9 -2
View File
@@ -7,7 +7,14 @@ pub struct Offset {
impl Widget for Offset {
fn draw(&mut self, painter: &mut Painter) -> Size {
let region = UiRegion::FULL.offset(self.amt);
painter.widget_within(&self.inner, region).size()
// The whole of this widget's box, moved: the frame passes through, so
// what the child declares or reports means the same as it would
// without the offset.
let moved = |len: Len, amt: Len| Place::Within(Part::From(UiSpan::new(amt, len + amt)));
let place = [
moved(painter.extent_len(Axis::X), self.amt.x),
moved(painter.extent_len(Axis::Y), self.amt.y),
];
painter.widget_at(&self.inner, [None; 2], place).size()
}
}
+18 -2
View File
@@ -13,8 +13,24 @@ impl Widget for Pad {
// it; where the box is bigger -- a share of a row, a rule over this
// widget -- the slack is the inner's to sit in, and forcing the near
// edge pinned it to a corner it had not asked for.
let inside = DrawRegion::Extent(self.padding.region());
let inner = painter.widget_within(&self.inner, inside).size();
//
// 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 box it draws in is moved in by the pixels. Said as a part of
// this widget's own box in that box's own lengths, so nothing here
// reads how long the box is -- and a box chosen from this widget's
// own answer therefore does not feed back into that answer.
let inset = |lead: Px, trail: Px| {
Place::Within(Part::Of(UiSpan::new(
Len::from_parts(Rel::ZERO, lead),
Len::from_parts(Rel::ONE, -trail),
)))
};
let place = [
inset(self.padding.left, self.padding.right),
inset(self.padding.top, self.padding.bottom),
];
let inner = painter.widget_at(&self.inner, [None; 2], place).size();
Size {
x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right,
+19 -22
View File
@@ -12,15 +12,13 @@ pub struct Scroll {
impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) -> Size {
let container_len = painter.px_len(self.axis);
// Draw in the whole container only when its scrolling-axis length is
// not already known, then draw it at the scrolled offset.
let whole = UiRegion::FULL;
let own = painter.placement();
let answer_len =
painter.measure_len(&self.inner, self.axis, whole, [Some(own.x), Some(own.y)]);
let content = answer_len.apply_leftover();
// Asked in the whole viewport, then put at the scrolled offset.
let answer_len = painter
.widget_at(&self.inner, [None; 2], [Place::Fill(Part::All); 2])
.len(self.axis);
let fixed = Len::from_parts(answer_len.rel, answer_len.px).to_px(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 {
self.amt = self.content_len - self.container_len;
@@ -33,9 +31,9 @@ impl Widget for Scroll {
// 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
// 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 {
painter.holds(self.axis, self.content_len..=Px::MAX);
painter.holds(self.axis, fixed..=Px::MAX);
} else if fixed_len && !self.snap_end {
let left = self.content_len - self.amt;
painter.holds(self.axis, Px::MIN..=left);
@@ -46,7 +44,7 @@ impl Widget for Scroll {
// have placed the whole scroll in a box longer than it.
let slack = (self.container_len - self.content_len).max(Px::ZERO);
let anchor = slack.mul(align.rel());
let mut region = UiRegion::FULL;
let mut content = UiSpan::FULL;
// Content that fills the viewport and has not been scrolled is the
// viewport, and is handed back as it came. Writing the same box as
// its own length in pixels is the same box in another form, and the
@@ -55,19 +53,18 @@ impl Widget for Scroll {
// 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,
Len::from_parts(Rel::ZERO, anchor - self.amt),
Len::ZERO,
);
region = region.offset(offset);
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
content = UiSpan::new(start, start.offset(self.content_len));
}
// The viewport is the inner's region, so a fraction it declares or
// The viewport is the inner's frame, so a fraction it declares or
// reports is a fraction of what is on screen rather than of the
// content box its own answer decided. Where it is put is the content
// box, scrolled.
painter.widget_at(&self.inner, whole, [Some(region.x), Some(region.y)]);
// content box its own answer decided. Where it goes is the content
// box, scrolled: its drawing moved there, not made again there.
painter.place_at(
&self.inner,
self.axis
.pair(Place::Fill(Part::From(content)), Place::Fill(Part::All)),
);
// 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
// more. The content's length is what it scrolls through, not what it
+50 -36
View File
@@ -10,38 +10,38 @@ pub struct Span {
impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size {
let axis = self.dir.axis;
// The row: this span's own box, as a span of the region it was given.
// Its children are laid out along it, and what they declare or report
// is a fraction of the region -- the area this span was told it has,
// which it passes on unchanged.
let own = painter.placement();
let row = *own.axis(axis);
// Across itself the span's own box is the child's region: a span is
// what contains its children there, and nothing divides that axis.
// Along it the whole region is, so a fraction means the same thing
// for every child however much of the row is left when it is asked.
let region = UiRegion::from_axis(axis, UiSpan::FULL, *own.axis(!axis));
// 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
// a length from it -- so what this reads is the length alone.
let far = painter.extent_len(axis);
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(row.start + from, row.start + to),
Sign::Neg => UiSpan::new(row.end - to, row.end - from),
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => UiSpan::new(far - to, far - from),
};
let far = row.len();
// A length for every child before their final boxes are chosen: from
// a hint where one exists, and from drawing otherwise.
// Across itself the child sits where its own alignment says, in the
// whole of the row: a span is what contains its children there, and
// nothing divides that axis.
let across = Place::Within(Part::All);
// A length for every child before their final slots are chosen. The
// frame passes through unchanged, so `rel(0.5)` is half the area this
// span was given whatever else is in it and wherever this child sits
// among them; what it is asked in is the room left from the cursor,
// because a text has to wrap at the width actually there. This is
// the one ask a fixed child gets: its slot is its answer, and the
// drawing is moved there once the shares are known.
let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len());
let mut sizes = Vec::with_capacity(self.children.len());
for child in &self.children {
// The whole region is the child's, so `rel(0.5)` is half the area
// this span was given whatever else is in it and wherever this
// child sits among them. What it is placed in is the room left
// from the cursor, because a text has to wrap at the width
// actually there.
let room = axis.pair(Some(along(cursor, far)), None);
let len = painter.measure_len(child, axis, region, room);
let room = Place::Within(Part::From(along(cursor, far)));
let size = painter
.widget_at(child, [None; 2], axis.pair(room, across))
.size();
let len = size.axis(axis);
cursor.px += len.px + self.gap;
cursor.rel += len.rel;
lens.push(len);
sizes.push(size);
}
let lens: Vec<LayoutLen> = sizes.iter().map(|size| size.axis(axis)).collect();
let gaps = self
.gap
@@ -55,7 +55,7 @@ impl Widget for Span {
);
// What is left for the shares to divide: the row less everything
// fixed, as a length of the region 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);
// 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`
@@ -68,12 +68,12 @@ impl Widget for Span {
// exist at all turns on this.
let mut shares = false;
if total.leftover > Weight::ZERO {
shares = room.to_px(painter.region_px_len(axis)) > Px::ZERO;
shares = room.to_px(painter.frame_px_len(axis)) > Px::ZERO;
let holds = match shares {
true => Holds::from(Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=Px::ZERO),
};
painter.region_holds(axis, holds.through(room));
painter.frame_holds(axis, holds.through(room));
}
// Across itself a span is as long as its longest child -- unless a
@@ -92,7 +92,8 @@ impl Widget for Span {
let mut taken = Weight::ZERO;
let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO;
for (child, len) in self.children.iter().zip(&lens) {
for (child, size) in self.children.iter().zip(&sizes) {
let len = size.axis(axis);
// 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.
@@ -109,14 +110,27 @@ impl Widget for Span {
fixed.px += len.px;
fixed.rel += len.rel;
start = shared(fixed, taken, total.leftover, room);
// Along the row the span says where the child goes; across it the
// child sits where its own alignment says. Its region is the
// whole of what this span was given either way, which is what its
// fractions are of.
let placed =
painter.widget_at(child, region, axis.pair(Some(along(from, start)), None));
// Along the row the span says where the child goes, and that slot
// is the child's box outright rather than something to place an
// answer inside again. A share is decided here and nowhere
// else: its slot narrows its frame, and the child is asked in
// it, since a text wraps at the width it is actually given. A
// fixed child's slot is its own answer, so its drawing is put
// there as it is.
let slot = along(from, start);
let place = axis.pair(Place::Fill(Part::From(slot)), across);
let used = match len.leftover > Weight::ZERO && shares {
true => {
let mut narrow = [None; 2];
narrow[axis as usize] = Some(slot.len());
painter.widget_at(child, narrow, place).len(!axis)
}
false => {
painter.place_at(child, place);
size.axis(!axis)
}
};
if shrinks {
let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the
// span's own eventual width admits multiple fixed points.
// A scalable child therefore makes Children scalable too;
+16 -9
View File
@@ -13,29 +13,36 @@ impl Widget for Stack {
StackSize::Default => None,
StackSize::Child(i) => Some(i),
};
// Whichever child sizes the stack keeps the stack's whole region as
// its own -- the stack is the length that child asked for, so taking
// the fraction of the stack's box again would take it twice -- and is
// put where the stack itself is put.
// Whichever child sizes the stack is given the stack's whole box --
// the stack is the length that child asked for, so placing that
// answer inside the box it decided would apply it twice.
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) {
// On the layer that child ends up on, so the ask below is a reuse
// rather than a second drawing of it somewhere else: a retained
// drawing belongs to the layer it was made on.
Some((i, child)) => {
painter.child_layer_at(i);
painter.widget(child).size()
painter
.widget_at(child, [None; 2], [Place::Fill(Part::All); 2])
.size()
}
None => Size::LEFTOVER,
};
// Every other child gets the box the sizing child decided: the
// stack is that length, so that is the box they are asked in, and a
// fraction under them is a fraction of it. A share leaves the axis
// to whoever gave the stack its box. Where a child sits in a box
// bigger than itself is its own business.
let narrow = [Axis::X, Axis::Y].map(|axis| {
let len = size.axis(axis);
(len.leftover == Weight::ZERO).then(|| Len::from_parts(len.rel, len.px))
});
for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) {
continue;
}
painter.child_layer_at(i);
// Every other child has the stack's own box for its region, since
// the stack is what contains it, and where it sits in one bigger
// than itself is its own business.
painter.widget_within(child, DrawRegion::Extent(UiRegion::FULL));
painter.widget_at(child, narrow, [Place::Within(Part::All); 2]);
}
size
}
+1 -1
View File
@@ -80,7 +80,7 @@ impl TextView {
// hair under that line, and the break made in it is not the break a
// cold layout makes there.
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
painter.glyphs(tex, DrawRegion::Extent(region));
painter.glyphs(tex, region);
(region, size)
}
+9 -7
View File
@@ -21,16 +21,18 @@ struct BranchesOnMeasurement {
impl Widget for BranchesOnMeasurement {
fn draw(&mut self, painter: &mut Painter) -> Size {
let mut top = UiRegion::FULL;
top.y.end = top.y.start.offset(Px::from_int(40));
let measured = painter.widget_within(&self.probe, top).len(Axis::X);
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut)));
let measured = painter
.widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top])
.len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
let mut below = UiRegion::FULL;
below.y.start = below.y.start.offset(Px::from_int(40));
let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y))));
let place = [Place::Within(Part::All), below];
match px > Px::from_f32(self.threshold) {
true => painter.widget_within(&self.wide, below),
false => painter.widget_within(&self.narrow, below),
true => painter.widget_at(&self.wide, [None; 2], place),
false => painter.widget_at(&self.narrow, [None; 2], place),
};
Size::LEFTOVER
}
+10 -7
View File
@@ -83,11 +83,12 @@ fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
assert!(crowded > whole_row, "{crowded} against {whole_row}");
}
/// The same reading through a pad: its inset is the whole box less the
/// padding, so half of the inset plus the padding is half the box plus one
/// padding, not two.
/// The same reading through a pad: padding goes around what it pads and
/// does not narrow what a fraction under it is a fraction of, so half of the
/// window plus the padding is what the pad takes and where the next child
/// starts.
#[test]
fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() {
let mut h = Harness::new((400, 100));
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
@@ -96,8 +97,9 @@ fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
// placed inside it by its own alignment, which is not what is under test.
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, padded, (0, 0), (210, 100));
assert_corners!(h, tail, (210, 0), (310, 100));
assert_corners!(h, inner, (10, 10), (210, 90));
assert_corners!(h, padded, (0, 0), (220, 100));
assert_corners!(h, tail, (220, 0), (320, 100));
}
#[test]
@@ -426,7 +428,8 @@ fn a_row_of_equal_shares_fills_it_exactly() {
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
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 drawn = active.extent.within(&active.frame_abs);
let region = h.render.moves.resolve(active.parent_move, drawn);
let dim = h.size().axis(axis);
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
+109 -102
View File
@@ -156,9 +156,9 @@ fn a_span_child_that_declares_its_length_is_drawn_once() {
h.set_root((hinted, asked).span(Dir::RIGHT));
assert_eq!(told_draws.get(), 1);
// Only the available length changes: positioning the final slot does
// not invalidate a numeric size read.
assert_eq!(asked_draws.get(), 2);
// Asked once, from the cursor; its slot is its answer and the drawing is
// moved there.
assert_eq!(asked_draws.get(), 1);
}
#[test]
@@ -213,9 +213,12 @@ struct FromHint {
impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let mut region = UiRegion::FULL;
region.y.end = region.y.start.offset(len.px);
painter.widget_within(&self.inner, region);
let top = UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, len.px));
painter.widget_at(
&self.inner,
[None; 2],
[Place::Within(Part::All), Place::Within(Part::From(top))],
);
Size::LEFTOVER
}
}
@@ -252,10 +255,9 @@ impl Widget for ReadsBox {
/// 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.
///
/// Both of these report a quarter of what they read, without saying that the
/// drawing holds there too, so each length they are asked at costs two draws:
/// one to answer, and one in the quarter-sized box that answer places them
/// in. The counts below are in those pairs.
/// Both of these report a quarter of what they read. The quarter-sized box
/// the answer places them in is not a question: the drawing is moved there,
/// so each length they are asked at costs one draw.
struct ReadsWidth {
draws: Rc<Cell<usize>>,
}
@@ -305,6 +307,51 @@ fn a_span_ruled_across_itself_moves_its_child_without_redrawing_it() {
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0));
}
/// A row places its children as lengths from where its own box starts, so a
/// child that grew moves the ones after it and nothing else: each of them is
/// the same box in a new place, which the retained drawing follows without
/// being made again. Both kinds of length: one the row resolves from a rule,
/// and one it takes from what the child reported.
#[test]
fn a_row_moves_what_follows_a_child_that_grew_rather_than_drawing_it() {
for declared in [false, true] {
let mut h = Harness::new((400, 200));
let first = rect(Color::RED).width(50).add(&mut h.rsc);
let ruled = Rc::new(Cell::new(0));
let second = Counted {
draws: ruled.clone(),
size: Size::LEFTOVER,
reads_box: false,
};
let second = match declared {
true => second.width(rel(0.25)).add(&mut h.rsc),
false => second.width(60).add(&mut h.rsc),
};
let (third, reported) = counted(&mut h, Size::from((70, 20)), false);
h.set_root((first, second, third).span(Dir::RIGHT).width(rel(1.0)));
let (was_ruled, was_reported) = (ruled.get(), reported.get());
// A quarter of the row is a quarter of the row, wherever it sits in
// it and whatever the first child takes.
let width = match declared {
true => 100,
false => 60,
};
assert_corners!(h, second, (50, 0), (50 + width, 200));
h.set_len(first, Axis::X, 80);
h.frame();
assert_eq!(ruled.get(), was_ruled, "the ruled child was drawn again");
assert_eq!(
reported.get(),
was_reported,
"the reported child was drawn again"
);
assert_corners!(h, second, (80, 0), (80 + width, 200));
assert_corners!(h, third, (80 + width, 90), (150 + width, 110));
}
}
/// The output is the root of the box chain, so a resize is a box that changed
/// length like any other -- there is not a second rule for the window. A
/// drawing that holds for one length is drawn again whichever box moved.
@@ -336,7 +383,7 @@ fn a_resize_redraws_what_read_its_box() {
h.resize((800, 100));
h.frame();
assert_eq!(draws.get(), settled + 2);
assert_eq!(draws.get(), settled + 1);
}
#[test]
@@ -356,7 +403,7 @@ fn a_resize_only_redraws_read_axes() {
h.resize((800, 300));
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
@@ -383,7 +430,7 @@ fn a_resize_within_one_step_is_not_a_resize() {
h.resize((400.0 + step, 200.0));
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
@@ -749,36 +796,6 @@ fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
.collect()
}
#[test]
fn frame_geometry_and_extent_geometry_keep_their_references() {
struct Both(Rc<Cell<usize>>);
impl Widget for Both {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.primitive_within(RectPrimitive::color(Color::RED), UiRegion::FULL);
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::LEFTOVER
}
}
for node in [false, true] {
let mut h = Harness::new((400, 200));
let first = rect(Color::GREEN).width(100).add(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
let both = Both(draws.clone()).add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(both, node);
h.set_root((first, both).span(Dir::RIGHT));
let count = draws.get();
h.set_len(first, Axis::X, 200);
h.frame();
assert_eq!(draws.get(), count);
let bounds = primitive_bounds(&h, both.id());
assert_eq!(bounds[0].top_left.x, Px::ZERO);
assert_eq!(bounds[0].bot_right.x, Px::from_int(400));
assert_eq!(bounds[1].top_left.x, Px::from_int(200));
assert_eq!(bounds[1].bot_right.x, Px::from_int(400));
}
}
#[test]
fn changing_an_inherited_extent_keeps_the_original_measurement_offer() {
fn build(h: &mut Harness, width: i32, text: &str) -> (WeakWidget<Text>, WeakWidget<Rect>) {
@@ -858,7 +875,14 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_within(&self.child, self.region);
painter.widget_at(
&self.child,
[None; 2],
[
Place::Within(Part::From(self.region.x)),
Place::Within(Part::From(self.region.y)),
],
);
Size::LEFTOVER
}
}
@@ -866,7 +890,7 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
impl Widget for Painted {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.set_mask(DrawRegion::Extent(UiRegion::FULL));
painter.set_mask(UiRegion::FULL);
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::LEFTOVER
}
@@ -915,43 +939,6 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
}
}
#[test]
fn a_span_does_not_place_its_measurement_before_assigning_the_childs_slot() {
struct MeasuredBox(Rc<Cell<usize>>);
impl Widget for MeasuredBox {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.px_size();
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::from((100, 50))
}
}
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = MeasuredBox(draws.clone()).add(&mut h.rsc);
h.set_root((leaf,).span(Dir::RIGHT).width(rel(1.0)).height(rel(1.0)));
assert_eq!(draws.get(), 3);
assert_corners!(h, leaf, (0, 75), (100, 125));
assert_eq!(
primitive_bounds(&h, leaf.id()),
vec![h.region(&leaf.id()).unwrap()]
);
h.frame();
assert_eq!(draws.get(), 3);
h.resize((600, 300));
h.frame();
assert_eq!(draws.get(), 6);
assert_corners!(h, leaf, (0, 125), (100, 175));
assert_eq!(
primitive_bounds(&h, leaf.id()),
vec![h.region(&leaf.id()).unwrap()]
);
}
#[test]
fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
struct Glyphs {
@@ -966,8 +953,7 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
UiSpan::new(Len::rel(0.23) + Len::px(-7.125), Len::FULL),
UiSpan::new(Len::rel(0.37) + Len::px(3.25), Len::FULL),
);
painter.glyphs(text, DrawRegion::Frame(origin));
painter.glyphs(text, DrawRegion::Extent(origin));
painter.glyphs(text, origin);
Size::LEFTOVER
}
}
@@ -980,8 +966,11 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
self.region,
[Some(self.extent.x), Some(self.extent.y)],
[Some(self.region.x.len()), None],
[
Place::Fill(Part::From(self.extent.x)),
Place::Fill(Part::From(self.extent.y)),
],
);
Size::LEFTOVER
}
@@ -1128,7 +1117,7 @@ fn widening_and_restoring_a_contract_does_not_invalidate_its_reader() {
assert_eq!(leaf_draws.get(), settled + 1);
}
#[test]
fn padding_and_stack_frames_follow_the_extent_without_drawing_again() {
fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
struct Observed<W> {
widget: W,
draws: Rc<Cell<usize>>,
@@ -1147,8 +1136,11 @@ fn padding_and_stack_frames_follow_the_extent_without_drawing_again() {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
UiRegion::FULL,
[Some(self.extent.x), Some(self.extent.y)],
[None; 2],
[
Place::Fill(Part::From(self.extent.x)),
Place::Fill(Part::From(self.extent.y)),
],
);
Size::LEFTOVER
}
@@ -1179,13 +1171,18 @@ fn padding_and_stack_frames_follow_the_extent_without_drawing_again() {
h.set_root(root);
(root, leaf, fixed, draws)
};
// The same box in three places. A pad places its child as lengths of
// its own box measured from where that box starts, so moving it is
// nothing to the pad -- where changing its length is a different
// question, and does draw it again.
let at = |start: f32| {
let span = |start: Len| UiSpan::new(start, start + Len::rel(0.4));
UiRegion::new(span(Len::rel(start) + Len::px(3.125)), span(Len::px(11.25)))
};
let mut warm = Harness::new((403, 211));
let (root, leaf, fixed, draws) = plant(&mut warm, UiRegion::FULL);
for (start, end) in [(0.13, 0.83), (-0.17, 1.23), (0.31, 0.67)] {
let extent = UiRegion::new(
UiSpan::new(Len::rel(start) + Len::px(3.125), Len::rel(end)),
UiSpan::new(Len::px(11.25), Len::rel(end)),
);
let (root, leaf, fixed, draws) = plant(&mut warm, at(0.13));
for start in [0.13, -0.17, 0.31] {
let extent = at(start);
let before = draws.get();
warm.rsc[root].extent = extent;
warm.frame();
@@ -1227,13 +1224,13 @@ fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
UiRegion::FULL,
[None; 2],
[
Some(UiSpan::new(
Place::Fill(Part::From(UiSpan::new(
Len::px(self.start),
Len::px(self.start + 200.0),
)),
Some(UiSpan::FULL),
))),
Place::Fill(Part::From(UiSpan::FULL)),
],
);
Size::LEFTOVER
@@ -1267,7 +1264,14 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
impl Widget for Container {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter
.widget_within(&self.child, DrawRegion::Extent(self.region))
.widget_at(
&self.child,
[None; 2],
[
Place::Within(Part::From(self.region.x)),
Place::Within(Part::From(self.region.y)),
],
)
.size()
}
}
@@ -1282,8 +1286,11 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
painter
.widget_at(
&self.child,
UiRegion::FULL,
[Some(self.extent.x), Some(self.extent.y)],
[None; 2],
[
Place::Fill(Part::From(self.extent.x)),
Place::Fill(Part::From(self.extent.y)),
],
)
.size(),
);
+64 -1
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));
}
#[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
/// 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
@@ -71,7 +134,7 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
impl Widget for Clipper {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region());
painter.set_mask(UiRegion::FULL);
painter.widget(&self.0).size()
}
}
+255
View File
@@ -14,6 +14,180 @@ use iris::harness::Harness;
use iris::prelude::*;
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
/// 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
@@ -672,3 +846,84 @@ fn adding_text_to_a_reverse_row_keeps_its_shared_height() {
let (_, other, _) = build(&mut cold, true);
assert_eq!(warm.region(&shared), cold.region(&other));
}
/// Nine widgets, shrunk from seed 946 at depth 6. The column is a share of
/// the row while its rect has room to draw and a fixed width once it has
/// not, so the row asks it twice: in the room, where it answers a share,
/// and in its slot, where it answers its text's width. Emptying the column
/// changes only the first answer. A local redraw that asked only the second
/// question kept the row as it was; the column has to defer to the row.
fn plant_column_that_is_a_share_only_while_its_rect_fits(
h: &mut Harness,
emptied: bool,
) -> (Vec<WidgetId>, WeakWidget<Span>, Vec<StrongWidget>) {
let first = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let filler = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
let second = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let mut spare: Vec<StrongWidget> =
vec![filler.add_strong(&mut h.rsc), second.add_strong(&mut h.rsc)];
let mut children: Vec<StrongWidget> = vec![first.add_strong(&mut h.rsc)];
if !emptied {
children.append(&mut spare);
}
let column = Span {
children,
dir: Dir::DOWN,
gap: Px::ZERO,
}
.height(159)
.add(&mut h.rsc);
let left = rect(Color::MAGENTA.alpha(189)).add(&mut h.rsc);
let right = rect(Color::BLUE.alpha(0)).add(&mut h.rsc);
let row = Span {
children: vec![
left.add_strong(&mut h.rsc),
column.add_strong(&mut h.rsc),
right.add_strong(&mut h.rsc),
],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let end = rect(Color::MAGENTA.alpha(189)).add(&mut h.rsc);
let root = Span {
children: vec![end.add_strong(&mut h.rsc), row.add_strong(&mut h.rsc)],
dir: Dir::LEFT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
h.set_root(root);
(
vec![
first.id(),
filler.id(),
second.id(),
column.id(),
left.id(),
right.id(),
row.id(),
end.id(),
root.id(),
],
column,
spare,
)
}
#[test]
fn emptying_a_column_the_row_asked_twice_asks_the_row_again() {
let mut warm = Harness::new((900, 1200));
let (ids, column, _spare) =
plant_column_that_is_a_share_only_while_its_rect_fits(&mut warm, false);
warm.frame();
// Kept alive: dropping the last share of a widget frees its id.
let _removed: Vec<StrongWidget> = warm.rsc[column].children.drain(1..).collect();
warm.frame();
let mut cold = Harness::new((900, 1200));
let (cold_ids, _, _spare) =
plant_column_that_is_a_share_only_while_its_rect_fits(&mut cold, true);
cold.frame();
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}