Compare commits

..
Author SHA1 Message Date
iris-ai 5b181bc8af Experiment: say the room from the cursor as an inset, and read the row's length only where a slot depends on it
Measured identical to its parent at every phase of the cost rig; kept as
evidence, not proposed for landing.
2026-09-19 02:19:14 -04:00
iris-ai f6242aa33c Take a span child's length from its hint, and ask it once in its slot
A share child was drawn in the measuring room and again in its slot, and
one record holding two questions made every local change under it defer
to the span. Where a rule or a hint gives the length along the span, the
first ask answers nothing the rule does not, so the child is asked once,
in its slot; the widgets that always report the whole of their box now
say so. A hint with a fraction resolves against the frame and pins it.

The dump rig prints every cold layout so a change to it shows in a diff.
2026-09-19 02:09:41 -04:00
iris-ai a888717ee9 Say window where these comments still say frame
Lengths became lengths of the window when the frame did, and `Part::From`'s
own documentation still described its spans as frame lengths -- which is
what the scroll above read them as.
2026-09-19 01:23:34 -04:00
iris-ai e8a5792dcb Place a scroll's fitting content in the viewport, not in the window
A scroll that has not been scrolled and whose content fits asked for its
content box as `Part::From(UiSpan::FULL)`. A `Part::From` span is in window
lengths, so `rel(1.0)` in one is the whole window rather than the whole box,
and the content landed in a window-tall box anchored at the viewport's
start -- 50 px low for a 300 px viewport in a 400 px window.

Saying the whole of the box as `Part::All` is the one expression that cannot
mean anything else, and it is also the place the child was already asked in,
so the placement becomes a no-op.
2026-09-19 01:23:34 -04:00
iris-aiandClaude Opus 5 a30971e4c5 Call the record's boxes what they are
The offer names are from the protocol before this one, where a widget was
drawn twice and the record had to say which drawing was the question. It
is asked once now, so offer_part is the part it was asked in, offer_place
the place it was asked at, and place where its drawing was put: part,
asked and placed. LayoutHolds::frame is a range on the window since the
frame became a length of one, and the frame's own entry is the frame_len
pin beside it, so it is window; Painter::frame_own goes with it.
answers_at had one caller and said less than the line that replaces it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 00:40:23 -04:00
iris-aiandClaude Opus 5 adbedaf264 Say what the fuzzer's branching widget branched on
Branch reads a measurement in pixels and draws a different subtree either
side of a threshold, and it left that read as a pin on the window, so
every one of them redrew on every resize: at depth 8 that was seed 1's
resize going from 40 widget draws to 131 and seed 13's from nothing to
828. It now states the range it actually branched on, the way Span states
the one that decides whether its shares have room. A fixture that redraws
everything on a resize cannot tell a change that reuses well from one
that does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 00:24:10 -04:00
iris-aiandClaude Opus 5 23523eea29 Take a window read where a length is resolved against it
A widget that resolves a window length in pixels depends on that window
wherever the length is a fraction of it, and nothing was recording that:
Painter::to_px replaces window_px_len and pins the window it read, while
a length that is only pixels is that many pixels in any window and pins
nothing. Span still states the range it actually branched on, which
replaces the pin with something wider.

Scroll is where it showed: its content's answer is a window length now,
so a viewport whose own box does not change with the window -- 40 px of
a branch's box -- kept an end-snapped offset from the window before.
Seed 942 at depth 6 under resize, pinned as
unsettled::resizing_under_a_short_scroll_snaps_its_window_tall_content_again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 00:21:11 -04:00
iris-aiandClaude Opus 5 1512d8418b Make the frame a length of the window and the box a region
There is one coordinate unit, the window. Every box in the tree is a
region in window units and a widget's frame is a length in the same
units, which is only what fractions resolve against, so the box need not
be the frame and padding can take from both without either becoming the
other. A region node's entry is a translation -- a rel 1 region anchored
where its box starts -- rather than a box, so nothing composes a frame
back up a chain and a node that moves is one entry write.

Padding is then an inset of both: its pixels come off the frame, so
rel(1.0) under it fills the padded widget rather than overflowing it,
and off the box, so what is drawn sits inside. A length a container
decides for a child's frame is a length of the window like everything
else here -- a row's slot, padding's frame less its pixels, or the box a
stack's sizing child decided, which arrives as Part::Sized -- because a
slot of a row is not a fraction of anything the row can name, the same
reason a node entry is a translation. A declaration is a fraction of
whichever of those reached it, and is the only one that also places the
box.

Frame validity is a pin beside the box's, not a range: a range of window
pixels cannot say which frame an answer is a fraction of, since two
frames are different lengths at the same window size. A widget pins its
frame by reading it or by being answered with it under a fractional
rule, and the pin composes up wherever a length of this frame is what
reached the child.

Also here, because the diagnosis needed them: the shrinker reports the
shrunk tree's own divergence with each level's frame, ask, box and size
warm against cold, and there is a size-resize case -- a change and then a
resize, the order that shows an answer kept as a fraction of the wrong
length, which every other case compares at the window it was made at.

Three defects the reports found, each pinned: a rule changed over two
pads relocated the column under them instead of dividing it again (seed
59, depth 5, resize-size), a share inside padding had the padding taken
off twice, and a root resolved its own rule twice.

fmt and clippy clean with and without layout-diagnostics, 121 suite, 20
core, 11 generated, the 400-seed depth-5 shrinker over all sixteen cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 00:14:46 -04:00
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
iris-aiandClaude Opus 5 34cafb6edc Read the marks rather than the queue to decide the walk is done
Review of the two commits above. The queue was the walk's only record of
what was left, so a mark that reached `needs_redraw` without going through
`mark` -- an `on_undraw` handler is the reachable one -- would have waited
for the next frame. The set is read again once the queue drains, which is
what the scan it replaced did for free. `pop_last` takes the deepest entry
in one step rather than reading and then removing it.

The rest is comments: nine lines shorter, and the arm that takes an
ordinary ask said only what it does for a declared length.

Unchanged by all of it: 109 suite and 20 core tests, the four fuzzer runs
(100 seeds, 400 trees at depth 5, 1000 at depth 6, 2000 at depth 4), the
five reference renders and the resized `tabs`, and every counter on the
diagnostics rig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:31:29 -04:00
iris-aiandClaude Opus 5 3bf22935ce Take the deepest dirty widget from an ordered queue, not by scanning
The walk found the next widget to settle with `max_by_key` over the whole
`needs_redraw` set, and `depth` is a hash lookup, so a frame did a lookup
per marked widget per pop -- 131 depth reads for nine marks at seed 1
depth 8, 1,314 for 34, and 14,611 for 145. The set is scanned once now and
kept in a `BTreeSet` keyed by depth, and every mark made while the walk
runs goes through `mark`, which puts itself in place. The same three
counts become 57, 160 and 436.

Two things the scan gave for free are paid for explicitly: a widget that
was settled inside an ancestor's draw, or deferred to one, is dropped when
its entry comes up, and an entry whose widget has since changed depth --
a subtree that moved under a new parent -- is re-queued at the depth it now
has. What is drawn does not change: widget draws are identical at every
load measured.

Median frame at seed 1, depth 8: 0.955 -> 0.843 ms with 145 marks, 0.668 ->
0.666 with 34, and seed 13's default load 5.19 -> 4.86 ms. Ties between
equal depths now break by widget id rather than by hash order, which makes
the walk deterministic; nothing in the order within one depth was ever
relied on, since a widget at the same depth as another cannot contain it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:16:40 -04:00
iris-aiandClaude Opus 5 e6ba570d07 Give a child a part of the container's extent rather than its raw box
`Pad` and `Stack` read `Painter::placement` to put their children inside
their own drawing, and reading it is what says the drawing holds for that
placement alone. So a pad or a stack anywhere in a row was drawn again --
with its whole subtree -- the moment an earlier sibling changed length,
however little else had moved.

`widget_within` now takes a `DrawRegion`, and `DrawRegion::Extent(part)`
gives the child a part of the extent without reading it. What is retained
is the part rather than the box it resolved to, so moving the extent
re-places the child through the same rule instead of redrawing the parent:
`inherited_children` becomes `extent_children`, carrying `Inherit` for the
wrapper case `Painter::widget` already had and `Within(part)` for the new
one.

The dependency that goes up is a range on the container's extent rather
than on its frame, since only the part's *length* reaches the child and
where the part sits is re-placed. A declared length is unchanged: it is a
length of the frame wherever the box it sits in came from. What still pins
the placement is a report with a fraction in it -- the same fraction of a
different extent is a different length -- and that pin is on the answer,
which `extent_frames_keep_fractional_reports_and_numeric_dependencies_valid`
fails without.

Three tests from the first attempt at this come with it, and the
diagnostics rig now says which of the three contracts refused a reuse,
which is what found the above.

Measured, seed 1 at depth 8, median frame: `many` 0.667 -> 0.613 ms and
`resize` 48 -> 32 us; seed 13's `many` 6.35 -> 5.15 ms. Green: fmt, clippy,
109 suite and 20 core tests, the oracle at 100 seeds, the shrinker at 400
trees of depth 5, 1000 seeds at depth 6, and 2000 seeds at depth 4 over all
fifteen cases. The five reference renders are byte-identical to `0e107f0`
on Venus, as are `tabs` resized to 900x1200 and `random` to 1280x800
against cold renders there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:15:17 -04:00
iris-ai 0e107f0e89 Keep valid layout guarantees when a redraw widens their range 2026-09-17 17:24:53 -04:00
iris-ai f860f716e6 Separate measured-answer dependencies from retained drawing validity 2026-09-17 17:10:55 -04:00
iris-ai c44bd198ee Retain child frames relative to the container extent 2026-09-17 16:40:54 -04:00
iris-ai a7307d95fd Resolve a text draw's glyph origin once 2026-09-17 16:17:47 -04:00
26 changed files with 2381 additions and 1091 deletions

No files matched your search

+7 -5
View File
@@ -32,7 +32,6 @@ pub(crate) enum Counter {
SizeReads, SizeReads,
HintHits, HintHits,
HintMisses, HintMisses,
RetainedSizeHits,
ReuseAttempts, ReuseAttempts,
ReuseExact, ReuseExact,
ReuseMoved, ReuseMoved,
@@ -42,7 +41,6 @@ pub(crate) enum Counter {
ReuseOutside, ReuseOutside,
ReuseWrongLayer, ReuseWrongLayer,
ReuseWrongNode, ReuseWrongNode,
PlaceRedraws,
QueuePops, QueuePops,
DepthReads, DepthReads,
LocalRedraws, LocalRedraws,
@@ -54,10 +52,13 @@ pub(crate) enum Counter {
TextShapes, TextShapes,
TextBreaks, TextBreaks,
GlyphPlacements, GlyphPlacements,
OutsidePinnedLen,
OutsideFrame,
OutsideExtent,
} }
impl Counter { impl Counter {
const COUNT: usize = Self::GlyphPlacements as usize + 1; const COUNT: usize = Self::OutsideExtent as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [ const NAMES: [&'static str; Self::COUNT] = [
"updates", "updates",
@@ -67,7 +68,6 @@ impl Counter {
"draw-result size reads", "draw-result size reads",
"hint hits", "hint hits",
"hint misses", "hint misses",
"retained size hits",
"reuse attempts", "reuse attempts",
"reuse exact", "reuse exact",
"reuse moved", "reuse moved",
@@ -77,7 +77,6 @@ impl Counter {
"reuse: outside what it holds for", "reuse: outside what it holds for",
"reuse: another layer", "reuse: another layer",
"reuse: region-node choice changed", "reuse: region-node choice changed",
"placed by redrawing",
"redraw queue pops", "redraw queue pops",
"depth reads", "depth reads",
"local redraws", "local redraws",
@@ -89,6 +88,9 @@ impl Counter {
"text shapes", "text shapes",
"text line breaks", "text line breaks",
"glyph placements", "glyph placements",
"reuse outside: the length it was pinned to",
"reuse outside: a frame length",
"reuse outside: an extent length",
]; ];
} }
+41 -39
View File
@@ -1,5 +1,5 @@
use crate::{ use crate::{
DrawRegion, LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, RegionAlign, RetainedPrimitive, LayerId, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Place, RegionAlign, RetainedPrimitive,
Size, TextureHandle, UiRegion, UiVec2, WidgetId, Size, TextureHandle, UiRegion, UiVec2, WidgetId,
}; };
@@ -9,28 +9,39 @@ use crate::{
#[derive(Debug)] #[derive(Debug)]
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
/// The box its parent gave it, in `parent_move`'s coordinates: what it /// Where its drawing goes, in its region node's coordinates.
/// was asked about, and what a fraction under it is a fraction of. A pub extent: UiRegion,
/// local redraw asks here. /// What a fraction declared or reported under this widget is a fraction
pub region: UiRegion, /// of, as a length of the window.
/// Where its drawing sits inside that box, in the box's own coordinates. pub frame: UiVec2,
pub placement: UiRegion, /// A frame its parent decided for it on each axis -- a row's slot, or
/// The original frame in its parent widget's coordinates. Recomposition /// padding's frame less its pixels -- as a length of the window. `None`
/// and pixel-length evaluation both follow this chain. /// forwards the parent's frame. What it declared is kept separately in
pub given_region: UiRegion, /// `declared` and is a fraction of whichever of the two reached it.
/// The lengths of the box its parent first asked about it in, as pub narrow: [Option<Len>; 2],
/// lengths of the box the parent was itself offered. Any later box it /// Where its drawing was put, and where it was asked, each as a part of
/// was given was decided knowing its answer, so this is the question /// its parent's box. The two differ where a container asks in one place
/// asked again -- and a chain of fractions has no frame in it, which is /// and puts the answer in another -- a row measures from its cursor and
/// why a region node between two widgets cannot break it. /// puts the child in its slot. A part is a length from the box's start,
pub offer_len: UiVec2, /// so a box that moved re-places every child by re-adding that start.
pub offer_placement: [Option<crate::UiSpan>; 2], pub placed: [Place; 2],
pub asked: [Place; 2],
/// The box it was asked in, in the parent's region-node 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.
pub part: UiRegion,
/// The measured answer and its dependencies. A hint-only dependency or /// The measured answer and its dependencies. A hint-only dependency or
/// a widget first encountered during placement has no measurement yet. /// a widget first encountered during placement has no measurement yet.
pub answer: Option<(Size, LayoutHolds)>, 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 reported, in window-unit lengths.
pub size: Size, pub size: Size,
/// The frame, extent and explicit placement reads that this drawing holds for. /// The window and extent reads that this drawing holds for, and the
/// frame and box it pinned.
pub holds: LayoutHolds, pub holds: LayoutHolds,
pub drawn: bool, pub drawn: bool,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
@@ -39,26 +50,25 @@ pub struct ActiveData {
/// widget a frame visits and cannot drift while one is being drawn. /// widget a frame visits and cannot drift while one is being drawn.
pub depth: usize, pub depth: usize,
pub textures: Vec<TextureHandle>, 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 primitives: Vec<RetainedPrimitive>,
pub mask_region: Option<DrawRegion>, pub mask_region: Option<UiRegion>,
pub inherited_children: Vec<WidgetId>,
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>, pub size_deps: Vec<WidgetId>,
/// The movable region its primitives are positioned through: its own when /// The movable region its primitives are positioned through: its own when
/// opted in, otherwise the nearest ancestor's. /// opted in, otherwise the nearest ancestor's.
pub move_idx: MoveIdx, 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, /// A change to one moves a box this widget cannot fix by drawing again,
/// and comparing them is what says so. /// and comparing them is what says so.
pub declared: [Option<LayoutLen>; 2], 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 /// Its alignment when it was last drawn, which a change to the property
/// is found against. /// is found against.
pub own_align: RegionAlign, pub own_align: RegionAlign,
/// The movable region whose coordinates `region` uses. /// The movable region whose coordinates `extent` uses when this widget
/// does not own a region node.
pub parent_move: MoveIdx, pub parent_move: MoveIdx,
/// The mask its drawing is clipped to: one it set itself, or the one it /// The mask its drawing is clipped to: one it set itself, or the one it
/// inherited from whoever drew it. /// inherited from whoever drew it.
@@ -72,18 +82,10 @@ pub struct ActiveData {
} }
impl ActiveData { impl ActiveData {
/// Whether what it answered still stands for a box of these pixel /// What it answered when its parent asked, where it has been asked at
/// lengths -- the box it was asked in, where `holds` is about the box its /// all. Not `size`, which is what its last drawing reported: a drawing
/// answer then chose. /// re-expressed in the box that answer chose is not a second answer.
pub fn answers_at(&self, px: crate::PxVec2) -> bool { pub fn measured(&self) -> Option<Size> {
self.answer.is_some_and(|(_, holds)| { self.answer.map(|(size, _)| size)
holds.contains(
px,
UiRegion {
x: self.offer_placement[0].unwrap_or(crate::UiSpan::FULL),
y: self.offer_placement[1].unwrap_or(crate::UiSpan::FULL),
},
)
})
} }
} }
-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,
}
+15
View File
@@ -73,6 +73,21 @@ impl Holds {
} }
} }
/// What a box has to be for a part of it, this many pixels shorter, to
/// stay in this range: the range moved by that much, an end that was
/// unbounded staying so.
pub const fn longer_by(self, px: Px) -> Self {
let lo = match self.lo.raw() == Px::MIN.raw() {
true => self.lo,
false => self.lo.add(px),
};
let hi = match self.hi.raw() == Px::MAX.raw() {
true => self.hi,
false => self.hi.add(px),
};
Self { lo, hi }
}
const fn raws(lo: i64, hi: i64) -> Self { const fn raws(lo: i64, hi: i64) -> Self {
Self { Self {
lo: Px::from_raw(narrow(lo)), lo: Px::from_raw(narrow(lo)),
+62 -16
View File
@@ -1,33 +1,79 @@
use crate::{Axis, Holds, PxVec2, UiRegion}; use crate::{Axis, Holds, Len, PxVec2, UiRegion, UiVec2};
/// 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 window lengths its reads
/// hold for, the pixel lengths of its own box, and the symbolic lengths of
/// that box and of its frame where either one is what it was expressed in.
///
/// The symbolic lengths are pins rather than ranges: 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. A box pin 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.
///
/// A frame pin says the answer or the drawing is a fraction of the frame,
/// which is a different length wherever the frame is a different one -- at
/// the same window size, so no range of window pixels can say it. A length
/// of the frame that is only pixels is not one: it is that many pixels
/// whatever the frame turns out to be.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHolds { pub struct LayoutHolds {
pub frame: [Holds; 2], pub window: [Holds; 2],
pub frame_len: [Option<Len>; 2],
pub extent: [Holds; 2], pub extent: [Holds; 2],
pub placement: Option<UiRegion>, pub extent_len: [Option<Len>; 2],
} }
impl LayoutHolds { impl LayoutHolds {
pub const ANY: Self = Self { pub const ANY: Self = Self {
frame: [Holds::ANY; 2], window: [Holds::ANY; 2],
frame_len: [None; 2],
extent: [Holds::ANY; 2], extent: [Holds::ANY; 2],
placement: None, extent_len: [None; 2],
}; };
pub fn contains(self, px: PxVec2, placement: UiRegion) -> bool { pub fn and(self, other: Self) -> Self {
self.placement.is_none_or(|old| old == placement) let mut result = Self::ANY;
&& [Axis::X, Axis::Y].into_iter().all(|axis| { for n in 0..2 {
self.frame[axis as usize].contains(px.axis(axis)) result.window[n] = self.window[n].and(other.window[n]);
&& self.extent[axis as usize] result.extent[n] = self.extent[n].and(other.extent[n]);
.contains(placement.axis(axis).len().to_px(px.axis(axis))) debug_assert!(
self.extent_len[n].is_none()
|| other.extent_len[n].is_none()
|| self.extent_len[n] == other.extent_len[n]
);
debug_assert!(
self.frame_len[n].is_none()
|| other.frame_len[n].is_none()
|| self.frame_len[n] == other.frame_len[n]
);
result.extent_len[n] = self.extent_len[n].or(other.extent_len[n]);
result.frame_len[n] = self.frame_len[n].or(other.frame_len[n]);
}
result
}
pub fn covers(self, other: Self) -> bool {
(0..2).all(|n| {
self.window[n].lo <= other.window[n].lo
&& self.window[n].hi >= other.window[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))
&& self.frame_len[n].is_none_or(|len| other.frame_len[n] == Some(len))
}) })
} }
pub fn in_frame(self, placement: UiRegion) -> [Holds; 2] { pub fn contains(self, window: PxVec2, frame: UiVec2, extent: UiRegion) -> bool {
[Axis::X, Axis::Y].map(|axis| { AXES.into_iter().all(|axis| {
self.frame[axis as usize] let n = axis as usize;
.and(self.extent[axis as usize].through(placement.axis(axis).len())) let len = extent.axis(axis).len();
self.window[n].contains(window.axis(axis))
&& self.frame_len[n].is_none_or(|pinned| pinned == frame.axis(axis))
&& self.extent[n].contains(len.to_px(window.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; pub const CHAIN_LIMIT: u32 = 64;
mod active; mod active;
mod draw_region;
mod holds; mod holds;
mod layout_holds; mod layout_holds;
mod painter; mod painter;
mod place;
mod render_state; mod render_state;
pub use active::*; pub use active::*;
pub use draw_region::*;
pub use holds::*; pub use holds::*;
pub use layout_holds::*; pub use layout_holds::*;
pub use painter::{Painter, PrimitiveLike}; pub use painter::{Painter, PrimitiveLike};
pub use place::*;
pub use render_state::*; pub use render_state::*;
#[derive(Default)] #[derive(Default)]
+362 -353
View File
@@ -1,14 +1,14 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter}; use crate::layout_diagnostics::{self as diag, Counter};
use crate::{ use crate::{
Axis, DrawRegion, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Axis, Holds, LayoutHolds, LayoutLen, Len, Part, Place, Px, PxVec2, RegionAlign, Rel,
RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind, GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
TexturePrimitive, TexturePrimitive,
}, },
ui::render_state::DrawInfo, ui::render_state::{DrawInfo, Placing},
}; };
const AXES: [Axis; 2] = [Axis::X, Axis::Y]; const AXES: [Axis; 2] = [Axis::X, Axis::Y];
@@ -17,50 +17,40 @@ pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState, pub(super) state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc, pub(super) rsc: &'a mut dyn UiRsc,
/// The box its parent gave it, in the coordinates of `move_idx`: what a /// This widget's frame, per axis: a length of the window, and what a
/// fraction of this widget's area is a fraction of, and what every region /// fraction it or anything under it declares or reports is a fraction
/// it writes composes within. The same box on the ask that measures and /// of. A length rather than a box, so padding can take from both the
/// the ask that places, which is what keeps a fraction under it from /// frame and the box without either becoming the other.
/// being resolved twice. pub(super) frame: UiVec2,
pub(super) region: UiRegion, /// Where this widget's drawing goes, in its region node's coordinates.
/// Where this widget's drawing sits inside that box, in the box's own pub(super) extent: UiRegion,
/// coordinates: `FULL` while its answer is not yet known, and the box /// The extent's symbolic length where this draw read it, which makes the
/// its answer or its parent chose once one of them has. /// drawing one that holds for that length alone -- the way reading a
pub(super) placement: UiRegion, /// length in pixels makes it hold for that number of pixels.
/// Whether this draw read its placement, which makes the drawing one pub(super) extent_len: [Option<Len>; 2],
/// that holds for that placement alone -- the way reading a length in /// The window in pixels. Frames and boxes become pixels against this one
/// pixels makes it hold for that length. /// unit, regardless of region-node boundaries.
pub(super) reads_placement: bool, pub(super) window: PxVec2,
/// 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.
pub(super) px: PxVec2,
pub(super) mask: MaskIdx, pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<RetainedPrimitive>, pub(super) primitives: Vec<RetainedPrimitive>,
pub(super) mask_region: Option<DrawRegion>, pub(super) mask_region: Option<UiRegion>,
pub(super) inherited_children: Vec<WidgetId>, /// Only children whose answers were read constrain this widget's answer.
pub(super) extent_own: [Holds; 2], pub(super) answer_under: LayoutHolds,
pub(super) extent_under: [Holds; 2],
pub(super) children: Vec<WidgetId>, 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. /// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>, 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 the window in pixels, per axis: every
/// length until it reads one, then that one, unless it says otherwise. /// window until it reads one, then that one, unless it says otherwise.
pub(super) own: [Holds; 2], pub(super) window_own: [Holds; 2],
/// What the children it asked about and drew keep it to. /// Its frame's symbolic length where this draw read it, which makes the
pub(super) under: [Holds; 2], /// drawing one that holds for that frame alone.
pub(super) frame_own_len: [Option<Len>; 2],
/// The window reads' equivalent 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: /// The movable region this widget's primitives are positioned through:
/// its own when opted in, otherwise the nearest ancestor's. /// its own when opted in, otherwise the nearest ancestor's.
pub(super) move_idx: MoveIdx, pub(super) move_idx: MoveIdx,
@@ -73,13 +63,29 @@ pub struct Painter<'a> {
} }
impl<'a> 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>(); let kind = self.rsc.ui_mut().primitives.kind::<P>();
self.write(kind, primitive, region); self.write(kind, primitive, region);
} }
/// Takes the kind, for a caller writing many of one primitive. /// Takes the kind, for a caller writing many of one primitive.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: DrawRegion) { 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 its region
/// node's coordinates.
fn resolve(&self, region: UiRegion) -> UiRegion {
region.within(&self.extent)
}
fn write_resolved<P: Primitive>(
&mut self,
kind: PrimitiveKind<P>,
primitive: P,
region: UiRegion,
resolved: UiRegion,
) {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PrimitiveWrites); diag::bump(Counter::PrimitiveWrites);
let h = self.state.layers.write( let h = self.state.layers.write(
@@ -88,7 +94,7 @@ impl<'a> Painter<'a> {
kind, kind,
id: self.id, id: self.id,
primitive, primitive,
region: region.resolve(self.region, self.placement), region: resolved,
mask_idx: self.mask, mask_idx: self.mask,
move_idx: self.move_idx, move_idx: self.move_idx,
}, },
@@ -106,152 +112,77 @@ impl<'a> Painter<'a> {
/// Writes a primitive over the whole of this widget's own box. /// Writes a primitive over the whole of this widget's own box.
pub fn primitive(&mut self, primitive: impl PrimitiveLike) { pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
let at = DrawRegion::Extent(UiRegion::FULL);
let primitive = primitive.into_primitive(self); 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 /// Writes a primitive in a part of this widget's own box, in that box's
/// geometry attached to this widget's box without reading its placement. /// coordinates.
pub fn primitive_within( pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
&mut self,
primitive: impl PrimitiveLike,
region: impl Into<DrawRegion>,
) {
let primitive = primitive.into_primitive(self); 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. /// Sets a mask, in this widget's own box's coordinates.
pub fn set_mask(&mut self, region: impl Into<DrawRegion>) { pub fn set_mask(&mut self, region: UiRegion) {
let region = region.into();
self.mask_region = Some(region); self.mask_region = Some(region);
assert!(self.mask == MaskIdx::NONE); 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 { self.mask = self.rsc.ui_mut().masks.push(Mask {
region: region.resolve(self.region, self.placement), region: resolved,
move_idx: self.move_idx, move_idx,
}); });
} }
/// Draws a widget in the whole of this widget's own box: it gets the /// Draws a widget in the whole of this widget's own box, with the frame
/// same region -- the same area for its fractions to be of -- and is put /// forwarded unchanged: what a container that is only a wrapper around
/// where this widget was put. What a container that is only a wrapper /// one child wants, and what every transparent container passes for the
/// around one child wants, since its box is the child's. /// frame.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> { pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
let own = self.placement; self.widget_at(id, [None; 2], [Place::Within(Part::All); 2])
self.widget_at_inner(id, UiRegion::FULL, [Some(own.x), Some(own.y)], true, false)
} }
/// What a widget's rules declare its lengths to be, which whoever draws /// Asks a child, saying what its fractions are of and where it is asked.
/// 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.inherited_children.retain(|child| *child != id.id());
self.state.undraw_rec(id.id(), self.rsc);
}
/// Draws a widget somewhere within this one. `region` is in this widget's
/// own region, and is the child's own region: what its declared lengths
/// and its report are fractions of. Where its drawing sits inside that is
/// its own answer placed by its alignment.
pub fn widget_within<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, [None; 2])
}
/// Draws a widget in `region`, saying where in it the drawing goes.
/// ///
/// `region` is the child's own area: what a fraction it declares or /// `narrow` is a length this widget decided for the child's frame, per
/// reports is a fraction of, and the coordinates the regions it writes /// axis, as a length of this widget's own frame: a resolved share, or a
/// compose within. It is the same box on the ask that measures and the /// box a sibling's answer decided. `None` forwards this widget's frame,
/// ask that places, which is what stops a fraction under it being /// which is what a container that only divides room passes, so a
/// resolved twice. /// 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.
/// ///
/// `placement` is what of that region the child's drawing takes, per /// `place` is where the child is asked, per axis, as a part of this
/// axis, wherever this widget is choosing. `None` leaves the axis to the /// widget's box: see [`Place`]. The child draws once, in that box, and
/// child's own answer and alignment, which is what /// its answer is placed inside it by re-expressing the drawing. Nothing
/// [`Self::widget_within`] passes. A span passes the whole row as the /// is drawn again in a box an answer chose; a container that puts the
/// region, so `rel(0.5)` is half the row wherever the child sits in it, /// answer somewhere else says so with [`Self::place_at`].
/// and places the child by passing the slot along its axis.
pub fn widget_at<'s, W: ?Sized>( pub fn widget_at<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, narrow: [Option<Len>; 2],
placement: [Option<UiSpan>; 2], place: [Place; 2],
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
self.widget_at_inner(id, region, placement, false, false)
}
fn widget_at_inner<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
placement: [Option<UiSpan>; 2],
inherited: bool,
measuring: bool,
) -> DrawResult<'s, 'a, W> {
if inherited {
if !self.inherited_children.contains(&id.id()) {
self.inherited_children.push(id.id());
}
} else {
self.inherited_children.retain(|child| *child != id.id());
}
let region_node = self.rsc.widgets().is_region_node(id.id()); let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id); let declared = self.declared_lens(id);
let align = self.rsc.widgets().alignment(id.id()); let align = self.rsc.widgets().alignment(id.id());
let (local, placement) = ask_box(region, declared, align, placement); let (frame, extent) =
let within = match local == UiRegion::FULL { frame_and_extent(self.extent, self.frame, place, narrow, declared, align);
true => self.region,
false => local.within(&self.region),
};
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
if region_node { if region_node {
diag::bump(Counter::RegionNodeDraws); diag::bump(Counter::RegionNodeDraws);
diag::region_node(id.id(), self.id, within); diag::region_node(id.id(), self.id, extent);
} }
// A child listed twice would be moved twice. // 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()); self.children.push(id.id());
} }
let first_ask = self.offer(id.id()); let px = frame.to_px(self.window);
let given_len = local.size(); let (size, answer_holds, holds) = self.state.draw_inner(
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 (size, holds) = self.state.draw_inner(
id.id(), id.id(),
within,
DrawInfo { DrawInfo {
layer: self.layer, layer: self.layer,
parent: Some(self.id), parent: Some(self.id),
@@ -259,53 +190,93 @@ impl<'a> Painter<'a> {
parent_move: self.move_idx, parent_move: self.move_idx,
region_node, region_node,
mask: self.mask, mask: self.mask,
given_region: local, frame,
offer_len, part: extent,
offer_placement, placed: place,
asked: place,
narrow,
re_asked,
px, px,
offered_px,
placement,
}, },
None, None,
measuring,
self.rsc, self.rsc,
); );
// Whatever the child's answer holds for keeps this one to the boxes let holds = self.in_parent(holds, extent, place, narrow, declared);
// that give the child a length inside it. let answer_holds = self.in_parent(answer_holds, extent, place, narrow, declared);
for axis in AXES { match self.under.iter_mut().find(|(child, _)| *child == id.id()) {
let n = axis as usize; Some((_, kept)) => *kept = holds,
let frame = holds.frame[n].through(local.axis(axis).len()); None => self.under.push((id.id(), holds)),
self.under[n] = self.under[n].and(frame);
if inherited && declared[n].is_none() {
self.extent_under[n] = self.extent_under[n].and(holds.extent[n]);
self.reads_placement |= holds.placement.is_some();
} else {
let extent = placement[n].unwrap_or(UiSpan::FULL).len();
self.under[n] = self.under[n].and(
holds.extent[n]
.through(extent)
.through(local.axis(axis).len()),
);
}
} }
DrawResult { DrawResult {
child: id, child: id,
painter: self, painter: self,
size: in_parent_frame(size, local.size(), declared), size,
answer_holds,
} }
} }
/// What a child says its length is without being drawn, if it can say. /// Takes back a child that was drawn only to find out how long it is.
/// Asking counts as reading its size. /// 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,
frame: self.frame,
window: self.window,
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,
/// as the length its draw would report: a fraction in it is resolved
/// against this widget's frame, which is the frame a child asked with
/// nothing narrowed gets. Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> { pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
let widgets = self.rsc.widgets(); let widgets = self.rsc.widgets();
// A rule is the answer where there is one: it wins over whatever the // A rule is the answer where there is one: it wins over whatever the
// widget would draw, so it has to win over what the widget says too. // widget would draw, so it has to win over what the widget says too.
let hint = widgets.size_rules(id.id()).axis(axis).exact().or_else(|| { let hint = widgets
.size_rules(id.id())
.axis(axis)
.exact()
.or_else(|| {
widgets widgets
.get_dyn(id.id()) .get_dyn(id.id())
.and_then(|widget| widget.size_hint(axis)) .and_then(|widget| widget.size_hint(axis))
}); })
.map(|hint| hint.within_len(self.frame.axis(axis)));
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::hint_read(id.id(), self.id, axis, hint); diag::hint_read(id.id(), self.id, axis, hint);
match hint { match hint {
@@ -313,6 +284,11 @@ impl<'a> Painter<'a> {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::HintHits); diag::bump(Counter::HintHits);
self.depend_on(id); self.depend_on(id);
// A fraction was just resolved against this frame, so what
// this draw does with it is a function of the frame's length.
if hint.rel != Rel::ZERO {
self.frame_own_len[axis as usize] = Some(self.frame.axis(axis));
}
Some(hint) Some(hint)
} }
None => { None => {
@@ -323,65 +299,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, false, 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.under.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>) { fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
if !self.size_deps.contains(&child.id()) { if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id()); self.size_deps.push(child.id());
@@ -402,11 +319,13 @@ impl<'a> Painter<'a> {
/// Writes glyphs in the selected frame or extent coordinates. /// Writes glyphs in the selected frame or extent coordinates.
// TODO: merge the text methods into the primitive ones. // TODO: merge the text methods into the primitive ones.
pub fn glyphs(&mut self, text: &RenderedText, origin: impl Into<DrawRegion>) { pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
let origin = origin.into(); // Glyph offsets and sizes are pixels, which compose additively.
// Only the shared origin needs composing through the extent.
let resolved = self.resolve(origin);
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>(); let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() { for glyph in text.glyphs.iter() {
let region = origin.map(|mut region| { let place = |mut region: UiRegion| {
region.x.end = region.x.start; region.x.end = region.x.start;
region.y.end = region.y.start; region.y.end = region.y.start;
let mut region = region.offset(UiVec2::from_px(glyph.offset)); let mut region = region.offset(UiVec2::from_px(glyph.offset));
@@ -417,8 +336,8 @@ impl<'a> Painter<'a> {
region.x.end = region.x.start.offset(size.x); region.x.end = region.x.start.offset(size.x);
region.y.end = region.y.start.offset(size.y); region.y.end = region.y.start.offset(size.y);
region region
}); };
self.write( self.write_resolved(
kind, kind,
GlyphPrimitive { GlyphPrimitive {
uv_min: glyph.entry.uv_min, uv_min: glyph.entry.uv_min,
@@ -427,29 +346,33 @@ impl<'a> Painter<'a> {
color: text.color, color: text.color,
flags: glyph.entry.flags(), flags: glyph.entry.flags(),
}, },
region, place(origin),
place(resolved),
); );
} }
} }
/// The box this widget's parent gave it, in the coordinates its own /// The symbolic length of this widget's own box along one axis, in the
/// primitives are written in -- so a region composed `within` it may be /// lengths of its frame that it places its children in. Reading it pins
/// drawn directly. Its own box is [`Self::placement`] of this one. /// the drawing to that length -- and to nothing about where the box
pub fn region(&self) -> UiRegion { /// starts, which is what lets a container move without being drawn
self.region /// 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's drawing goes inside the box it was given, in that /// The symbolic length of this widget's frame along one axis: what a
/// box's coordinates: what its own answer took of it, or what its parent /// fraction it or anything under it declares is a fraction of. A
/// chose for it. `FULL` on the ask that measures, since nothing has been /// container reads it to hand a length of it down -- padding, which
/// placed yet. /// takes its pixels off. Reading it pins the drawing to that frame, the
/// /// way [`Self::extent_len`] pins it to the box.
/// Reading it is what says the drawing depends on it, so a widget that pub fn frame_len(&mut self, axis: Axis) -> Len {
/// positions its own content reads it and is drawn again once its box is let len = self.frame.axis(axis);
/// known, and one that fills whatever it is given never is. self.frame_own_len[axis as usize] = Some(len);
pub fn placement(&mut self) -> UiRegion { len
self.reads_placement = true;
self.placement
} }
/// Where this widget sits in a box longer than the length it takes. A /// Where this widget sits in a box longer than the length it takes. A
@@ -477,14 +400,6 @@ impl<'a> Painter<'a> {
.is_some() .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 /// 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. /// that holds for this box only, until `holds` says how far it goes.
pub fn px_size(&mut self) -> PxVec2 { pub fn px_size(&mut self) -> PxVec2 {
@@ -494,8 +409,8 @@ impl<'a> Painter<'a> {
/// One axis of this widget's own box in pixels. Prefer this to /// One axis of this widget's own box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing. /// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px { pub fn px_len(&mut self, axis: Axis) -> Px {
let part = self.placement.axis(axis).len(); let part = self.extent.axis(axis).len();
let len = part.to_px(self.px.axis(axis)); let len = part.to_px(self.window.axis(axis));
let own = &mut self.extent_own[axis as usize]; let own = &mut self.extent_own[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(len);
@@ -508,10 +423,10 @@ impl<'a> Painter<'a> {
/// of the box, and the same reported size. A widget that read its length /// of the box, and the same reported size. A widget that read its length
/// in pixels holds for that one alone until it says otherwise. /// in pixels holds for that one alone until it says otherwise.
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) { 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(); let holds = holds.into();
debug_assert!( debug_assert!(
holds.contains(part.to_px(self.px.axis(axis))), holds.contains(part.to_px(self.window.axis(axis))),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box", "'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
self.label(), self.label(),
self.id self.id
@@ -519,30 +434,34 @@ impl<'a> Painter<'a> {
self.extent_own[axis as usize] = holds; self.extent_own[axis as usize] = holds;
} }
/// One axis of the box this widget's parent gave it, in pixels -- what a /// A window length in pixels, which is what every length in layout is
/// fraction of its area resolves against, and so what a container divides /// measured in. Reading one pins the drawing to this window wherever the
/// among its children. Its own box is a part of this one. /// length is a fraction of it; one that is only pixels is that many
pub fn region_px_len(&mut self, axis: Axis) -> Px { /// pixels in any window and pins nothing.
let len = self.px.axis(axis); pub fn to_px(&mut self, len: Len, axis: Axis) -> Px {
let own = &mut self.own[axis as usize]; let window = self.window.axis(axis);
if len.rel != Rel::ZERO {
let own = &mut self.window_own[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(window);
} }
len }
len.to_px(window)
} }
/// [`Self::holds`] stated about the region rather than about this /// The windows this drawing holds for, stated rather than taken: a
/// widget's own box, for a container whose drawing turns on the box it /// container that branched on a length in pixels says which side of the
/// was given rather than on the part of it it took. /// boundary it was on, which is wider than the one window reading that
pub fn region_holds(&mut self, axis: Axis, holds: impl Into<Holds>) { /// length pins, and replaces it.
pub fn window_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into(); let holds = holds.into();
debug_assert!( debug_assert!(
holds.contains(self.px.axis(axis)), holds.contains(self.window.axis(axis)),
"'{}' ({:?}) says its drawing holds for lengths that leave out its region", "'{}' ({:?}) says its drawing holds for windows that leave out this one",
self.label(), self.label(),
self.id self.id
); );
self.own[axis as usize] = holds; self.window_own[axis as usize] = holds;
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
@@ -585,6 +504,7 @@ pub struct DrawResult<'p, 'a, W: ?Sized> {
painter: &'p mut Painter<'a>, painter: &'p mut Painter<'a>,
child: &'p StrongWidget<W>, child: &'p StrongWidget<W>,
size: Size, size: Size,
answer_holds: LayoutHolds,
} }
impl<W: ?Sized> DrawResult<'_, '_, W> { impl<W: ?Sized> DrawResult<'_, '_, W> {
@@ -595,6 +515,7 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
diag::size_read(self.child.id(), self.painter.id, self.size); diag::size_read(self.child.id(), self.painter.id, self.size);
} }
self.painter.depend_on(self.child); self.painter.depend_on(self.child);
self.painter.answer_under = self.painter.answer_under.and(self.answer_holds);
self.size self.size
} }
@@ -628,19 +549,86 @@ impl PrimitiveLike for &TextureHandle {
} }
} }
/// A child's answer as lengths of the parent's own region. A widget reports /// Moves what a child depends on into this widget's own terms: this
/// a fraction of its own region, and `of` is that region as a length of this /// method's `impl` block is where a `Painter`'s own boxes are, so it takes
/// one. Pixels come through untouched, being that many pixels wherever they /// only what the child was asked with.
/// end up. A declared axis is already the parent's: it resolved the rule in impl Painter<'_> {
/// its own region, and the rule is what the report says. /// Window ranges are already about the one unit and combine directly.
fn in_parent_frame(size: Size, of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size { /// A frame pin becomes this widget's own frame wherever a length of it
let mut size = size; /// is what reached the child; where only pixels did, no length of this
for (axis, declared) in AXES.into_iter().zip(declared) { /// frame can change the child's and the pin stops here.
if declared.is_none() { ///
*size.axis_mut(axis) = size.axis(axis).within_len(of.axis(axis)); /// Extent validity maps back through the part of this widget's box,
/// where the box the child was asked in is that part; a declared length
/// places the box inside the part instead, and then only that length
/// reaches the child. A narrowed frame is not one of these: it decides
/// what fractions under the child mean and leaves the box the part it
/// was given.
fn in_parent(
&self,
holds: LayoutHolds,
extent: UiRegion,
place: [Place; 2],
narrow: [Option<Len>; 2],
declared: [Option<LayoutLen>; 2],
) -> LayoutHolds {
let mut result = LayoutHolds::ANY;
for axis in AXES {
let n = axis as usize;
// Every read became pixels against the window, so a range on
// it is already in this widget's terms.
result.window[n] = holds.window[n];
let reaches = narrow[n].is_none()
&& !matches!(place[n].part(), Part::Sized(_))
&& declared[n].is_none_or(|len| len.rel != Rel::ZERO);
result.frame_len[n] = holds.frame_len[n].and(reaches.then(|| self.frame.axis(axis)));
match (place[n].part(), declared[n].is_some()) {
// 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, false) => {
result.extent[n] = holds.extent[n];
result.extent_len[n] = holds.extent_len[n];
}
// Its box is this widget's own less the inset. Where that is
// pixels, its box is exactly that many shorter in any window,
// so what it holds for is a range on this widget's box moved
// by them, and a length it pinned is this widget's length
// less them. An inset with a fraction in it is a different
// number of pixels in each window, and taking it off a length
// rounds once more than taking it off pixels does: there the
// child's box is a fixed expression of this one, so this
// widget's length is pinned and the range goes on the window
// through the child's box, the way a slot's does.
(Part::Inset { lead, trail }, false) => {
let inset = lead + trail;
match inset.rel == Rel::ZERO {
true => {
result.extent[n] = holds.extent[n].longer_by(inset.px);
result.extent_len[n] = holds.extent_len[n].map(|pinned| pinned + inset);
}
false => {
result.window[n] = result.window[n]
.and(holds.extent[n].through(extent.axis(axis).len()));
result.extent_len[n] = Some(self.extent.axis(axis).len());
} }
} }
size }
// Its box is a length this widget decided, from its own
// frame or from a sibling's answer: no length of this
// widget's box reaches it, so what it holds for is a range
// on the window and none of it on that box.
_ => {
result.window[n] =
result.window[n].and(holds.extent[n].through(extent.axis(axis).len()));
}
}
}
result
}
} }
/// What a widget declares a length of its box to be. `leftover` is not one: a /// What a widget declares a length of its box to be. `leftover` is not one: a
@@ -674,68 +662,89 @@ pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: b
reported.leftover != Weight::ZERO || declared.is_some() || decided reported.leftover != Weight::ZERO || declared.is_some() || decided
} }
/// What of the box it was given a widget's drawing occupies, as lengths of /// Where a widget's drawing goes inside the part its parent gave it: what
/// that box: the size it reported wherever that is a part to be placed, and /// it reported, on the side of the part its alignment says, and the whole
/// the whole of the box wherever the answer fills it. /// part wherever the answer fills it.
/// ///
/// A reported fraction is a fraction of the box the widget drew in, where a /// The length it reported is a length of its frame, and the part is one too,
/// declared one is a fraction of the box its parent handed down -- a span /// so this takes one from the other rather than composing it into the part.
/// reporting `rel(1.0)` means all of what it was given, whatever that was a /// That is what makes a fraction the same fraction wherever the part it is
/// fraction of. So this is a length of the box rather than a length composed /// placed in sits and however long it is -- the fraction is resolved once,
/// into it, and a box in pixels is this step from the given box's pixels. /// here, against the frame it was reported of.
pub(crate) fn placed_lens( pub(crate) fn placed_extent(
part: UiRegion,
size: Size, size: Size,
declared: [Option<LayoutLen>; 2], declared: [Option<LayoutLen>; 2],
decided: [bool; 2], fill: [bool; 2],
) -> UiVec2 { align: RegionAlign,
let mut lens = UiVec2::FULL_SIZE; ) -> UiRegion {
for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) { let mut placed = part;
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;
for axis in AXES { for axis in AXES {
// The whole of the box is already where it sits, and the arithmetic let n = axis as usize;
// below is the identity for it. let reported = size.axis(axis);
if lens.axis(axis) == Len::FULL { if fills(reported, declared[n], fill[n]) {
continue; continue;
} }
let len = Len::from_parts(reported.rel, reported.px);
let span = placed.axis_mut(axis); 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.start += (span.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len; span.end = span.start + len;
} }
placed placed
} }
/// A declared axis gets a frame of that length, aligned within the parent's /// The frame length and the box a child is asked in, in the coordinates the
/// slot (or the offer). Undeclared axes keep the offered frame and chosen /// widget asking draws in.
/// placement, so their reported fractions retain that reference. ///
pub(crate) fn ask_box( /// `own` is that widget's own box, and `place` what of it the child is
mut region: UiRegion, /// given. `narrow` is a frame the container decided for the child -- a row's
/// slot, or padding's frame less its pixels -- and [`Part::Sized`] one a
/// sibling's answer decided; both are window lengths, like every other
/// length here, since a slot of a row is not a fraction of anything the row
/// can name. The child's declaration is a fraction of whichever reached it,
/// and is the only one of the three that also places the box: a box the
/// caller decided is what `place` names.
pub(crate) fn frame_and_extent(
own: UiRegion,
parent_frame: UiVec2,
place: [Place; 2],
narrow: [Option<Len>; 2],
declared: [Option<LayoutLen>; 2], declared: [Option<LayoutLen>; 2],
align: RegionAlign, align: RegionAlign,
placement: [Option<UiSpan>; 2], ) -> (UiVec2, UiRegion) {
) -> (UiRegion, [Option<UiSpan>; 2]) { let part = part_of(own, place, align);
let mut placed = [None; 2]; let mut frame = parent_frame;
for (axis, (len, chosen)) in AXES.into_iter().zip(declared.into_iter().zip(placement)) { let mut extent = part;
let Some(len) = len else { for axis in AXES {
placed[axis as usize] = chosen; let n = axis as usize;
continue; let sized = match place[n].part() {
Part::Sized(len) => Some(len),
_ => None,
}; };
let span = region.axis_mut(axis); let base = sized
let len = Len::from_parts(len.rel, len.px); .or(narrow[n])
let slot = chosen.unwrap_or(*span); .unwrap_or_else(|| parent_frame.axis(axis));
span.start = slot.start + (slot.len() - len).scale(align.axis(axis).rel()); let len = declared[n]
span.end = span.start + len; .map(|len| Len::from_parts(len.rel, len.px).within_len(base))
.unwrap_or(base);
*frame.axis_mut(axis) = len;
if declared[n].is_some() {
let slot = part.axis(axis);
let start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
*extent.axis_mut(axis) = UiSpan::new(start, start + len);
} }
(region, placed) }
(frame, extent)
}
/// The part of a widget's own box a `place` names, in the coordinates that
/// box is in.
fn part_of(extent: UiRegion, place: [Place; 2], align: RegionAlign) -> UiRegion {
let mut part = extent;
for axis in AXES {
*part.axis_mut(axis) = place[axis as usize]
.part()
.of(*extent.axis(axis), align.axis(axis));
}
part
} }
+74
View File
@@ -0,0 +1,74 @@
use crate::{AxisAlign, Len, 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,
/// Window lengths from where the box starts, which is what a container
/// dividing room among its children speaks: a child's report is a window
/// length, so the cursor that sums those reports is one too. A moved box
/// re-places every child by re-adding its start, exactly. A fraction
/// here is a fraction of the window and not of the box -- the whole of a
/// box is [`Self::All`], not a `rel(1.0)` span.
From(UiSpan),
/// The box less a window length at each end, which is what a container
/// that insets one speaks -- padding, or a row asking a child in the
/// room left from its cursor. Neither end names the box's length, so a
/// container can say "from here to my end" without reading how long it
/// is, and a box chosen from its own answer does not feed back into the
/// answer.
Inset { lead: Len, trail: Len },
/// A box of this length, wherever in the parent's box the child's own
/// alignment puts it, and that same length as its frame. Unlike `From`,
/// it is a length decided from above rather than a place along a
/// container's cursor -- what a stack's sizing child decides for the
/// rest.
Sized(Len),
}
impl Part {
/// Where it lands in the coordinates `extent` is in.
pub(crate) fn of(self, extent: UiSpan, align: AxisAlign) -> UiSpan {
match self {
Self::All => extent,
Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end),
Self::Inset { lead, trail } => UiSpan::new(extent.start + lead, extent.end - trail),
Self::Sized(len) => {
let start = extent.start + (extent.len() - len).scale(align.rel());
UiSpan::new(start, start + len)
}
}
}
}
/// 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
+1 -1
View File
@@ -1,4 +1,4 @@
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SlotId { pub struct SlotId {
idx: u32, idx: u32,
genr: u32, genr: u32,
+25 -9
View File
@@ -117,19 +117,35 @@ pub struct Branch {
impl Widget for Branch { impl Widget for Branch {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let mut top = UiRegion::FULL; let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
top.y.end = top.y.start.offset(Px::from_int(40)); let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut)));
let measured = painter.widget_within(&self.probe, top).len(Axis::X); let measured = painter
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); .widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top])
.len(Axis::X);
let len = measured.apply_leftover();
let px = painter.to_px(len, Axis::X);
// The range it actually branched on, said the way a container says
// one: pinning the window instead would redraw this widget on every
// resize, which is a fixture that never exercises reuse.
let threshold = Px::from_f32(self.threshold);
let holds = match px > threshold {
true => Holds::from(threshold + Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=threshold),
};
painter.window_holds(Axis::X, holds.through(len));
let mut below = UiRegion::FULL; let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y))));
below.y.start = below.y.start.offset(Px::from_int(40)); let place = [Place::Within(Part::All), below];
match px > Px::from_f32(self.threshold) { match px > threshold {
true => painter.widget_within(&self.wide, below), true => painter.widget_at(&self.wide, [None; 2], place),
false => painter.widget_within(&self.narrow, below), false => painter.widget_at(&self.narrow, [None; 2], place),
}; };
Size::LEFTOVER Size::LEFTOVER
} }
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
} }
pub struct Spanned { pub struct Spanned {
+5 -1
View File
@@ -6,7 +6,7 @@ pub struct Masked {
impl Widget for Masked { impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(DrawRegion::Extent(UiRegion::FULL)); painter.set_mask(UiRegion::FULL);
painter.widget(&self.inner); painter.widget(&self.inner);
// What it occupies is its box, on both axes, for the reason `Scroll` // 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 // reports the same: it clips what is inside to that box, so it can
@@ -15,4 +15,8 @@ impl Widget for Masked {
// draw, and the framework would place the drawing it clipped away. // draw, and the framework would place the drawing it clipped away.
Size::LEFTOVER Size::LEFTOVER
} }
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
} }
+9 -2
View File
@@ -7,7 +7,14 @@ pub struct Offset {
impl Widget for Offset { impl Widget for Offset {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let region = UiRegion::FULL.offset(self.amt); // The whole of this widget's box, moved: the frame passes through, so
painter.widget_within(&self.inner, region).size() // 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()
} }
} }
+26 -2
View File
@@ -13,8 +13,32 @@ impl Widget for Pad {
// it; where the box is bigger -- a share of a row, a rule over this // 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 // 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. // edge pinned it to a corner it had not asked for.
let inside = self.padding.region_of(painter.placement()); //
let inner = painter.widget_within(&self.inner, inside).size(); // Padding is an inset of both: it comes off the frame, so `rel(1)`
// under it fills this widget rather than overflowing it by the
// padding, and it comes off the box, so what is drawn sits inside.
// The two stay distinct -- the box can be narrower still, where a row
// asked this widget in the room left, and a text wraps at that.
let inset = |lead: Px, trail: Px| {
Place::Within(Part::Inset {
lead: Len::from_parts(Rel::ZERO, lead),
trail: Len::from_parts(Rel::ZERO, trail),
})
};
let place = [
inset(self.padding.left, self.padding.right),
inset(self.padding.top, self.padding.bottom),
];
// Read from this widget's own frame rather than written as a
// fraction of it: a frame is a length of the window like everything
// else here, and taking the padding off is the whole of what this
// widget does to it.
let narrow = [
(Axis::X, self.padding.left + self.padding.right),
(Axis::Y, self.padding.top + self.padding.bottom),
]
.map(|(axis, pixels)| Some(painter.frame_len(axis) - Len::from_parts(Rel::ZERO, pixels)));
let inner = painter.widget_at(&self.inner, narrow, place).size();
Size { Size {
x: LayoutLen { x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right, px: inner.x.px + self.padding.left + self.padding.right,
+25 -23
View File
@@ -12,15 +12,13 @@ pub struct Scroll {
impl Widget for Scroll { impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let container_len = painter.px_len(self.axis); let container_len = painter.px_len(self.axis);
// Draw in the whole container only when its scrolling-axis length is // Asked in the whole viewport, then put at the scrolled offset.
// not already known, then draw it at the scrolled offset. let answer_len = painter
let whole = UiRegion::FULL; .widget_at(&self.inner, [None; 2], [Place::Fill(Part::All); 2])
let own = painter.placement(); .len(self.axis);
let answer_len = let fixed = painter.to_px(Len::from_parts(answer_len.rel, answer_len.px), self.axis);
painter.measure_len(&self.inner, self.axis, whole, [Some(own.x), Some(own.y)]);
let content = answer_len.apply_leftover();
self.container_len = container_len; self.container_len = container_len;
self.content_len = content.to_px(container_len); self.content_len = fixed.max(container_len);
if self.snap_end { if self.snap_end {
self.amt = self.content_len - self.container_len; self.amt = self.content_len - self.container_len;
@@ -33,9 +31,9 @@ impl Widget for Scroll {
// the drawing holds for that length alone. One scrolled part way sits // the drawing holds for that length alone. One scrolled part way sits
// where it is until the box shrinks past what is left of it. Kept to // where it is until the box shrinks past what is left of it. Kept to
// the end, it moves with every length. // the end, it moves with every length.
let fixed_len = content.rel == Rel::ZERO; let fixed_len = answer_len.rel == Rel::ZERO && answer_len.leftover == Weight::ZERO;
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG { if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG {
painter.holds(self.axis, self.content_len..=Px::MAX); painter.holds(self.axis, fixed..=Px::MAX);
} else if fixed_len && !self.snap_end { } else if fixed_len && !self.snap_end {
let left = self.content_len - self.amt; let left = self.content_len - self.amt;
painter.holds(self.axis, Px::MIN..=left); painter.holds(self.axis, Px::MIN..=left);
@@ -46,7 +44,6 @@ impl Widget for Scroll {
// have placed the whole scroll in a box longer than it. // have placed the whole scroll in a box longer than it.
let slack = (self.container_len - self.content_len).max(Px::ZERO); let slack = (self.container_len - self.content_len).max(Px::ZERO);
let anchor = slack.mul(align.rel()); let anchor = slack.mul(align.rel());
let mut region = UiRegion::FULL;
// Content that fills the viewport and has not been scrolled is the // 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 // 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 // its own length in pixels is the same box in another form, and the
@@ -54,26 +51,31 @@ impl Widget for Scroll {
// one centred in `px 900`, since halving a difference is not halving // one centred in `px 900`, since halving a difference is not halving
// each part of it. // each part of it.
let moved = anchor != Px::ZERO || self.amt != Px::ZERO; let moved = anchor != Px::ZERO || self.amt != Px::ZERO;
if moved || self.content_len != self.container_len { let content = match moved || self.content_len != self.container_len {
let offset = UiVec2::from_axis( true => {
self.axis, let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
Len::from_parts(Rel::ZERO, anchor - self.amt), Part::From(UiSpan::new(start, start.offset(self.content_len)))
Len::ZERO,
);
region = region.offset(offset);
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
} }
// The viewport is the inner's region, so a fraction it declares or false => Part::All,
};
// 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 // 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 // content box its own answer decided. Where it goes is the content
// box, scrolled. // box, scrolled: its drawing moved there, not made again there.
painter.widget_at(&self.inner, whole, [Some(region.x), Some(region.y)]); painter.place_at(
&self.inner,
self.axis.pair(Place::Fill(content), Place::Fill(Part::All)),
);
// What it occupies is its box, on both axes: it clips its content to // What it occupies is its box, on both axes: it clips its content to
// that box, so it can neither take less of one nor honestly ask for // that box, so it can neither take less of one nor honestly ask for
// more. The content's length is what it scrolls through, not what it // more. The content's length is what it scrolls through, not what it
// is. // is.
Size::LEFTOVER Size::LEFTOVER
} }
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
} }
impl Scroll { impl Scroll {
+83 -36
View File
@@ -10,34 +10,51 @@ pub struct Span {
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let axis = self.dir.axis; let axis = self.dir.axis;
// The row: this span's own box, as a span of the region it was given. // The room left from the cursor to the row's end, said without the
// Its children are laid out along it, and what they declare or report // row's length: a child measured in it does not make this drawing
// is a fraction of the region -- the area this span was told it has, // depend on how long the row is.
// which it passes on unchanged. let room_from = |cursor: Len| match self.dir.sign {
let own = painter.placement(); Sign::Pos => Part::Inset {
let row = *own.axis(axis); lead: cursor,
// Across itself the span's own box is the child's region: a span is trail: Len::ZERO,
// what contains its children there, and nothing divides that axis. },
// Along it the whole region is, so a fraction means the same thing Sign::Neg => Part::Inset {
// for every child however much of the row is left when it is asked. lead: Len::ZERO,
let region = UiRegion::from_axis(axis, UiSpan::FULL, *own.axis(!axis)); trail: cursor,
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),
}; };
let far = row.len(); // Across itself the child sits where its own alignment says, in the
// A length for every child before their final boxes are chosen: from // whole of the row: a span is what contains its children there, and
// a hint where one exists, and from drawing otherwise. // nothing divides that axis.
let across = Place::Within(Part::All);
// A length for every child before their final slots are chosen: from
// a hint where one says, and from drawing otherwise. 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 a drawn child 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 drawn fixed child gets: its slot is its
// answer, and the drawing is moved there once the shares are known.
// A hinted child is asked once, in its slot.
let mut cursor = Len::rel_min(); let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len()); let mut lens = Vec::with_capacity(self.children.len());
let mut measured = Vec::with_capacity(self.children.len());
for child in &self.children { for child in &self.children {
// The whole region is the child's, so `rel(0.5)` is half the area let size = match painter.size_hint(child, axis) {
// this span was given whatever else is in it and wherever this Some(len) => {
// child sits among them. What it is placed in is the room left measured.push(None);
// from the cursor, because a text has to wrap at the width len
// actually there. }
let room = axis.pair(Some(along(cursor, far)), None); None => {
let len = painter.measure_len(child, axis, region, room); let room = Place::Within(room_from(cursor));
let size = painter
.widget_at(child, [None; 2], axis.pair(room, across))
.size();
measured.push(Some(size));
size.axis(axis)
}
};
let len = size;
cursor.px += len.px + self.gap; cursor.px += len.px + self.gap;
cursor.rel += len.rel; cursor.rel += len.rel;
lens.push(len); lens.push(len);
@@ -54,9 +71,25 @@ impl Widget for Span {
|sum, len| sum + *len, |sum, len| sum + *len,
); );
// The row: this span's own box as a length of the window, read only
// where a slot depends on it -- shares divide what is left of it,
// and a negative row counts from its end. Reading it pins the
// drawing to this length; a positive row of fixed children is not
// pinned and holds for any length its children do. Its start is
// nothing's business: a slot is a length from it.
let far = (total.leftover > Weight::ZERO || self.dir.sign == Sign::Neg)
.then(|| painter.extent_len(axis));
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => {
let far = far.expect("a negative row reads its length");
UiSpan::new(far - to, far - from)
}
};
// What is left for the shares to divide: the row less everything // What is left for the shares to divide: the row less everything
// fixed, as a length of the 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); // Nothing where there are no shares, and nothing reads it there.
let room = far.map_or(Len::ZERO, |far| far - Len::from_parts(total.rel, total.px));
// Whether anything is left over is a question in pixels: `rel(0.5)` // Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. Asked of `room` // beside 300 px is full at 600 and overfull at 400. Asked of `room`
// itself, and answered back through the same expression, so the // itself, and answered back through the same expression, so the
@@ -68,12 +101,12 @@ impl Widget for Span {
// exist at all turns on this. // exist at all turns on this.
let mut shares = false; let mut shares = false;
if total.leftover > Weight::ZERO { if total.leftover > Weight::ZERO {
shares = room.to_px(painter.region_px_len(axis)) > Px::ZERO; shares = painter.to_px(room, axis) > Px::ZERO;
let holds = match shares { let holds = match shares {
true => Holds::from(Px::STEP..=Px::MAX), true => Holds::from(Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=Px::ZERO), false => Holds::from(Px::MIN..=Px::ZERO),
}; };
painter.region_holds(axis, holds.through(room)); painter.window_holds(axis, holds.through(room));
} }
// Across itself a span is as long as its longest child -- unless a // Across itself a span is as long as its longest child -- unless a
@@ -92,7 +125,8 @@ impl Widget for Span {
let mut taken = Weight::ZERO; let mut taken = Weight::ZERO;
let mut start = Len::rel_min(); let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO; let mut ortho = LayoutLen::ZERO;
for (child, len) in self.children.iter().zip(&lens) { for ((child, len), measured) in self.children.iter().zip(&lens).zip(&measured) {
let len = *len;
// A child asking for nothing but a part of what is left over, // 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 // when nothing is, is not drawn at all. One that also asked for
// pixels or a fraction keeps those and overflows. // pixels or a fraction keeps those and overflows.
@@ -109,14 +143,27 @@ impl Widget for Span {
fixed.px += len.px; fixed.px += len.px;
fixed.rel += len.rel; fixed.rel += len.rel;
start = shared(fixed, taken, total.leftover, room); start = shared(fixed, taken, total.leftover, room);
// Along the row the span says where the child goes; across it the // Along the row the span says where the child goes, and that slot
// child sits where its own alignment says. Its region is the // is the child's box outright rather than something to place an
// whole of what this span was given either way, which is what its // answer inside again. A share is decided here and nowhere
// fractions are of. // else: its slot narrows its frame, and the child is asked in
let placed = // it, since a text wraps at the width it is actually given. A
painter.widget_at(child, region, axis.pair(Some(along(from, start)), None)); // fixed child's slot is its own answer, so a drawing made in the
// room is put there as it is, and one not made yet is made here.
let slot = along(from, start);
let place = axis.pair(Place::Fill(Part::From(slot)), across);
let mut narrow = [None; 2];
if len.leftover > Weight::ZERO && shares {
narrow[axis as usize] = Some(slot.len());
}
let used = match (measured, narrow[axis as usize]) {
(Some(size), None) => {
painter.place_at(child, place);
size.axis(!axis)
}
_ => painter.widget_at(child, narrow, place).len(!axis),
};
if shrinks { if shrinks {
let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the // Choosing between a fixed and a relative length from the
// span's own eventual width admits multiple fixed points. // span's own eventual width admits multiple fixed points.
// A scalable child therefore makes Children scalable too; // A scalable child therefore makes Children scalable too;
+26 -15
View File
@@ -13,12 +13,9 @@ impl Widget for Stack {
StackSize::Default => None, StackSize::Default => None,
StackSize::Child(i) => Some(i), StackSize::Child(i) => Some(i),
}; };
// This stack's own box, which is `FULL` until its answer is known. // Whichever child sizes the stack is given the stack's whole box --
let placement = painter.placement(); // the stack is the length that child asked for, so placing that
// Whichever child sizes the stack keeps the stack's whole region as // answer inside the box it decided would apply it twice.
// 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.
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) { 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 // 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 // rather than a second drawing of it somewhere else: a retained
@@ -26,27 +23,41 @@ impl Widget for Stack {
Some((i, child)) => { Some((i, child)) => {
painter.child_layer_at(i); painter.child_layer_at(i);
painter painter
.widget_at( .widget_at(child, [None; 2], [Place::Fill(Part::All); 2])
child,
UiRegion::FULL,
[Some(placement.x), Some(placement.y)],
)
.size() .size()
} }
None => Size::LEFTOVER, 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 place = [Axis::X, Axis::Y].map(|axis| {
let len = size.axis(axis);
match len.leftover == Weight::ZERO {
true => Place::Fill(Part::Sized(Len::from_parts(len.rel, len.px))),
false => Place::Within(Part::All),
}
});
for (i, child) in self.children.iter().enumerate() { for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) { if sizing == Some(i) {
continue; continue;
} }
painter.child_layer_at(i); painter.child_layer_at(i);
// Every other child has the stack's own box for its region, since painter.widget_at(child, [None; 2], place);
// the stack is what contains it, and where it sits in one bigger
// than itself is its own business.
painter.widget_within(child, placement);
} }
size size
} }
/// Without a sizing child a stack is whatever box it is given, which it
/// can say without drawing anything.
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
match self.size {
StackSize::Default => Some(LayoutLen::LEFTOVER),
StackSize::Child(_) => None,
}
}
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
+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 // hair under that line, and the break made in it is not the break a
// cold layout makes there. // cold layout makes there.
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size)); let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
painter.glyphs(tex, DrawRegion::Extent(region)); painter.glyphs(tex, region);
(region, size) (region, size)
} }
+10 -8
View File
@@ -21,16 +21,18 @@ struct BranchesOnMeasurement {
impl Widget for BranchesOnMeasurement { impl Widget for BranchesOnMeasurement {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let mut top = UiRegion::FULL; let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
top.y.end = top.y.start.offset(Px::from_int(40)); let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut)));
let measured = painter.widget_within(&self.probe, top).len(Axis::X); let measured = painter
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); .widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top])
.len(Axis::X);
let px = painter.to_px(measured.apply_leftover(), Axis::X);
let mut below = UiRegion::FULL; let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y))));
below.y.start = below.y.start.offset(Px::from_int(40)); let place = [Place::Within(Part::All), below];
match px > Px::from_f32(self.threshold) { match px > Px::from_f32(self.threshold) {
true => painter.widget_within(&self.wide, below), true => painter.widget_at(&self.wide, [None; 2], place),
false => painter.widget_within(&self.narrow, below), false => painter.widget_at(&self.narrow, [None; 2], place),
}; };
Size::LEFTOVER Size::LEFTOVER
} }
+89 -5
View File
@@ -83,11 +83,10 @@ fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
assert!(crowded > whole_row, "{crowded} against {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 is an inset: it narrows the frame a fraction resolves against and
/// padding, so half of the inset plus the padding is half the box plus one /// adds itself back to the padded widget's reported length.
/// padding, not two.
#[test] #[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 mut h = Harness::new((400, 100));
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc); 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); let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
@@ -96,10 +95,85 @@ 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. // 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))); h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, inner, (10, 10), (200, 90));
assert_corners!(h, padded, (0, 0), (210, 100)); assert_corners!(h, padded, (0, 0), (210, 100));
assert_corners!(h, tail, (210, 0), (310, 100)); assert_corners!(h, tail, (210, 0), (310, 100));
} }
const PARAGRAPH: &str = "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.";
/// The worked example of what padding insets: in a 900 px row after a 24 px
/// icon, a `rel(1.0)` inside `pad(16)` is 900 - 32 and overflows the row by
/// the icon's width, while a wrapping text beside it is asked in the room
/// left, 900 - 24 - 32, and wraps there.
#[test]
fn padding_keeps_the_frame_distinct_from_the_room_left_in_a_row() {
let mut h = Harness::new((900, 200));
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let padded = fill.pad(16).add(&mut h.rsc);
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let fill_width = h.region(&fill).unwrap().size().x;
assert_eq!(fill_width, Px::from_int(868));
let mut h = Harness::new((900, 200));
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let padded = text.pad(16).add(&mut h.rsc);
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()];
let window = h.render.output_size().x;
let asked = active.part.x.len().to_px(window);
assert_eq!(active.frame.x.to_px(window), Px::from_int(868));
assert_eq!(asked, Px::from_int(844));
}
/// The other way round: a share inside padding. A slot is a length of the
/// row, which is already the padded width, so what the span decided reaches
/// the child as it stands -- taking the padding off a second time would make
/// `rel(1.0)` in the slot shorter than the slot.
#[test]
fn a_share_inside_padding_fills_the_slot_it_was_given() {
let mut h = Harness::new((900, 200));
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let first = Span {
children: vec![fill.add_strong(&mut h.rsc)],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.width(leftover(1))
.add(&mut h.rsc);
let second = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
let row = (first, second).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(row.pad(16));
assert_eq!(h.region(&first).unwrap().size().x, Px::from_int(434));
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(434));
}
/// The same padding in a share instead: the slot is 450, so both the
/// fraction and the wrap are the slot less the padding, and the two agree.
#[test]
fn padding_narrows_both_frame_and_box_inside_a_share() {
let mut h = Harness::new((900, 200));
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let padded = fill.pad(16).width(leftover(1)).add(&mut h.rsc);
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(418));
let mut h = Harness::new((900, 200));
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let padded = text.pad(16).width(leftover(1)).add(&mut h.rsc);
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()];
let window = h.render.output_size().x;
assert_eq!(active.frame.x.to_px(window), Px::from_int(418));
assert_eq!(active.part.x.len().to_px(window), Px::from_int(418));
}
#[test] #[test]
fn a_span_ruled_across_itself_does_not_measure_its_children_there() { fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
@@ -426,7 +500,7 @@ fn a_row_of_equal_shares_fills_it_exactly() {
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`. /// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) { fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
let active = &h.render.active[&id]; let active = &h.render.active[&id];
let region = h.render.moves.resolve(active.parent_move, active.region); let region = h.render.moves.resolve(active.move_idx, active.extent);
let dim = h.size().axis(axis); let dim = h.size().axis(axis);
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor(); 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()); let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
@@ -737,3 +811,13 @@ fn a_fixed_child_is_centered_in_its_wrappers_share() {
assert_corners!(h, wrapper, (200, 0), (900, 400)); assert_corners!(h, wrapper, (200, 0), (900, 400));
assert_corners!(h, leaf, (500, 150), (600, 250)); assert_corners!(h, leaf, (500, 150), (600, 250));
} }
/// The root's frame is the window and its rule is a fraction of that, which
/// is one resolution and not two: nothing above it narrowed anything.
#[test]
fn a_root_with_a_fraction_rule_is_that_fraction_of_the_window() {
let mut h = Harness::new((900, 200));
let root = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
h.set_root(root);
assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450));
}
+457 -66
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)); h.set_root((hinted, asked).span(Dir::RIGHT));
assert_eq!(told_draws.get(), 1); assert_eq!(told_draws.get(), 1);
// Only the available length changes: positioning the final slot does // Asked once, from the cursor; its slot is its answer and the drawing is
// not invalidate a numeric size read. // moved there.
assert_eq!(asked_draws.get(), 2); assert_eq!(asked_draws.get(), 1);
} }
#[test] #[test]
@@ -213,9 +213,12 @@ struct FromHint {
impl Widget for FromHint { impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap(); let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let mut region = UiRegion::FULL; let top = UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, len.px));
region.y.end = region.y.start.offset(len.px); painter.widget_at(
painter.widget_within(&self.inner, region); &self.inner,
[None; 2],
[Place::Within(Part::All), Place::Within(Part::From(top))],
);
Size::LEFTOVER Size::LEFTOVER
} }
} }
@@ -252,10 +255,9 @@ impl Widget for ReadsBox {
/// Reads its box across one axis only, so its drawing holds for a taller /// Reads its box across one axis only, so its drawing holds for a taller
/// box on its own and only a wider one is worth a draw. /// box on its own and only a wider one is worth a draw.
/// ///
/// Both of these report a quarter of what they read, without saying that the /// Both of these report a quarter of what they read. The quarter-sized box
/// drawing holds there too, so each length they are asked at costs two draws: /// the answer places them in is not a question: the drawing is moved there,
/// one to answer, and one in the quarter-sized box that answer places them /// so each length they are asked at costs one draw.
/// in. The counts below are in those pairs.
struct ReadsWidth { struct ReadsWidth {
draws: Rc<Cell<usize>>, 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)); 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 /// 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 /// 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. /// 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.resize((800, 100));
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 2); assert_eq!(draws.get(), settled + 1);
} }
#[test] #[test]
@@ -356,7 +403,7 @@ fn a_resize_only_redraws_read_axes() {
h.resize((800, 300)); h.resize((800, 300));
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 2, "width changes its answer"); assert_eq!(draws.get(), settled + 1, "width changes its answer");
} }
/// A window is measured onto the grid like everything else, so a resize too /// A window is measured onto the grid like everything else, so a resize too
@@ -383,7 +430,7 @@ fn a_resize_within_one_step_is_not_a_resize() {
h.resize((400.0 + step, 200.0)); h.resize((400.0 + step, 200.0));
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 2); assert_eq!(draws.get(), settled + 1);
} }
/// The same for a box that changes because a sibling did: what is compared /// The same for a box that changes because a sibling did: what is compared
@@ -749,36 +796,6 @@ fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
.collect() .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] #[test]
fn changing_an_inherited_extent_keeps_the_original_measurement_offer() { fn changing_an_inherited_extent_keeps_the_original_measurement_offer() {
fn build(h: &mut Harness, width: i32, text: &str) -> (WeakWidget<Text>, WeakWidget<Rect>) { 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 { impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size { 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 Size::LEFTOVER
} }
} }
@@ -866,7 +890,7 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
impl Widget for Painted { impl Widget for Painted {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1); 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)); painter.primitive(RectPrimitive::color(Color::BLUE));
Size::LEFTOVER Size::LEFTOVER
} }
@@ -916,38 +940,405 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
} }
#[test] #[test]
fn a_span_does_not_place_its_measurement_before_assigning_the_childs_slot() { fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
struct MeasuredBox(Rc<Cell<usize>>); struct Glyphs {
buffer: TextBuffer,
impl Widget for MeasuredBox { draws: Rc<Cell<usize>>,
}
impl Widget for Glyphs {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1); self.draws.set(self.draws.get() + 1);
painter.px_size(); let text = painter.render_text(&mut self.buffer, &TextAttrs::default(), None);
painter.primitive(RectPrimitive::color(Color::BLUE)); let origin = UiRegion::new(
Size::from((100, 50)) 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, origin);
Size::LEFTOVER
} }
} }
struct Frame {
child: StrongWidget,
region: UiRegion,
extent: UiRegion,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
[Some(self.region.x.len()), None],
[
Place::Fill(Part::From(self.extent.x)),
Place::Fill(Part::From(self.extent.y)),
],
);
Size::LEFTOVER
}
}
for node in [false, true] {
let mut h = Harness::new((403, 211));
let draws = Rc::new(Cell::new(0));
let text = Glyphs {
buffer: TextBuffer::new("Glyphs: gj AV\nsecond line"),
draws: draws.clone(),
}
.add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(text, node);
let root = Frame {
child: text.add_strong(&mut h.rsc),
region: UiRegion::FULL,
extent: UiRegion::FULL,
}
.add(&mut h.rsc);
h.set_root(root);
for (start, end) in [(0.13, 0.83), (-0.17, 1.23), (0.31, 0.67)] {
let before = draws.get();
h.rsc[root].region.x = UiSpan::new(Len::px(13.125), Len::px(287.375));
h.rsc[root].extent = UiRegion::new(
UiSpan::new(Len::rel(start), Len::rel(end)),
UiSpan::new(Len::px(7.25), Len::rel(end)),
);
h.frame();
assert_eq!(draws.get(), before);
let retained = primitive_bounds(&h, text.id());
assert!(!retained.is_empty());
let _ = h.rsc.widgets_mut().get_dyn_mut(text.id());
h.frame();
assert!(draws.get() > before);
assert_eq!(retained, primitive_bounds(&h, text.id()));
}
}
}
#[test]
fn resizing_does_not_remeasure_a_fixed_stack_for_its_unmeasured_overlay() {
let mut h = Harness::new((400, 200));
let (sizing, _) = counted(&mut h, Size::from((100, 80)), false);
let (overlay, draws) = counted(&mut h, Size::LEFTOVER, true);
h.set_root((sizing, overlay).stack().size(StackSize::Child(0)));
let settled = draws.get();
h.resize((800, 300));
h.frame();
assert_eq!(draws.get(), settled);
assert_corners!(h, overlay, (350, 110), (450, 190));
}
struct Unmeasured {
child: StrongWidget,
draws: Rc<Cell<usize>>,
}
impl Widget for Unmeasured {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.widget(&self.child);
Size::LEFTOVER
}
}
#[test]
fn a_declared_size_change_stops_at_an_independent_parent() {
let mut h = Harness::new((400, 200));
let leaf = rect(Color::RED).width(100).add(&mut h.rsc);
let parent = Unmeasured {
child: leaf.add_strong(&mut h.rsc),
draws: Rc::new(Cell::new(0)),
}
.add_strong(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
h.set_root(Unmeasured {
child: parent,
draws: draws.clone(),
});
let settled = draws.get();
h.set_len(leaf, Axis::X, 150);
h.frame();
assert_corners!(h, leaf, (125, 0), (275, 200));
assert_eq!(draws.get(), settled);
}
#[test]
fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0)); let draws = Rc::new(Cell::new(0));
let leaf = MeasuredBox(draws.clone()).add(&mut h.rsc); let leaf = ReadsWidth {
h.set_root((leaf,).span(Dir::RIGHT).width(rel(1.0)).height(rel(1.0))); draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root((leaf,).stack());
let settled = draws.get();
assert_eq!(draws.get(), 3); h.resize((800, 200));
assert_corners!(h, leaf, (0, 75), (100, 125)); h.frame();
assert_eq!(
primitive_bounds(&h, leaf.id()), assert!(draws.get() > settled);
vec![h.region(&leaf.id()).unwrap()] assert_corners!(h, leaf, (300, 90), (500, 110));
}
#[test]
fn changed_drawing_dependencies_reach_ancestors_without_a_size_change() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
h.set_root(((leaf,).stack(),).stack());
h.rsc[leaf].reads_box = true;
h.frame();
let settled = draws.get();
h.resize((800, 200));
h.frame();
assert_eq!(draws.get(), settled + 1);
assert_corners!(h, leaf, (0, 0), (800, 200));
}
#[test]
fn widening_and_restoring_a_contract_does_not_invalidate_its_reader() {
let mut h = Harness::new((400, 200));
let (leaf, leaf_draws) = counted(&mut h, Size::LEFTOVER, true);
let draws = Rc::new(Cell::new(0));
let child = leaf.add_strong(&mut h.rsc);
h.set_root(Unmeasured {
child,
draws: draws.clone(),
});
let settled = draws.get();
for reads_box in [false, true, false, true] {
h.rsc[leaf].reads_box = reads_box;
h.frame();
assert_eq!(draws.get(), settled);
}
let settled = leaf_draws.get();
h.resize((800, 200));
h.frame();
assert_eq!(leaf_draws.get(), settled + 1);
}
#[test]
fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
struct Observed<W> {
widget: W,
draws: Rc<Cell<usize>>,
}
impl<W: Widget> Widget for Observed<W> {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
self.widget.draw(painter)
}
}
struct Frame {
child: StrongWidget,
extent: UiRegion,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
[None; 2],
[
Place::Fill(Part::From(self.extent.x)),
Place::Fill(Part::From(self.extent.y)),
],
); );
h.frame(); Size::LEFTOVER
assert_eq!(draws.get(), 3); }
}
for node in [false, true] {
let plant = |h: &mut Harness, extent| {
let draws = Rc::new(Cell::new(0));
let leaf = rect(Color::BLUE).masked().add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(leaf, node);
let fixed = rect(Color::RED).width(31).height(19).add(&mut h.rsc);
let stack = Observed {
widget: Stack {
children: vec![leaf.add_strong(&mut h.rsc), fixed.add_strong(&mut h.rsc)],
size: StackSize::Default,
},
draws: draws.clone(),
}
.add_strong(&mut h.rsc);
let pad = Observed {
widget: Pad {
inner: stack,
padding: Padding::uniform(7).with_left(13),
},
draws: draws.clone(),
}
.add_strong(&mut h.rsc);
let root = Frame { child: pad, extent }.add(&mut h.rsc);
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, 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();
assert_eq!(draws.get(), before);
let mut cold = Harness::new((403, 211));
let (_, other, other_fixed, _) = plant(&mut cold, extent);
for (a, b) in [(leaf.id(), other.id()), (fixed.id(), other_fixed.id())] {
assert_eq!(warm.region(&a), cold.region(&b));
assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b));
}
let mask = |h: &Harness, id: WidgetId| {
let active = &h.render.active[&id];
let mask = &h.rsc.ui().masks[active.mask.idx()];
h.render
.moves
.resolve(mask.move_idx, mask.region)
.to_px(h.render.output_size())
};
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
}
}
}
h.resize((600, 300)); #[test]
fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
struct Measured;
impl Widget for Measured {
fn draw(&mut self, painter: &mut Painter) -> Size {
let width = painter.px_len(Axis::X);
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::from((80, if width > Px::from_int(100) { 40 } else { 60 }))
}
}
struct Frame {
child: StrongWidget,
start: f32,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
[None; 2],
[
Place::Fill(Part::From(UiSpan::new(
Len::px(self.start),
Len::px(self.start + 200.0),
))),
Place::Fill(Part::From(UiSpan::FULL)),
],
);
Size::LEFTOVER
}
}
let mut h = Harness::new((400, 200));
let leaf = Measured.add(&mut h.rsc);
let stack = (leaf,).stack().add_strong(&mut h.rsc);
let root = Frame {
child: stack,
start: 0.0,
}
.add(&mut h.rsc);
h.set_root(root);
assert_corners!(h, leaf, (60, 80), (140, 120));
h.rsc[root].start = 30.0;
h.frame(); h.frame();
assert_eq!(draws.get(), 6); assert_corners!(h, leaf, (90, 80), (170, 120));
assert_corners!(h, leaf, (0, 125), (100, 175));
assert_eq!( assert_eq!(
primitive_bounds(&h, leaf.id()), primitive_bounds(&h, leaf.id()),
vec![h.region(&leaf.id()).unwrap()] vec![h.region(&leaf).unwrap()]
); );
} }
#[test]
fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
struct Container {
child: StrongWidget,
region: UiRegion,
}
impl Widget for Container {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter
.widget_at(
&self.child,
[None; 2],
[
Place::Within(Part::From(self.region.x)),
Place::Within(Part::From(self.region.y)),
],
)
.size()
}
}
struct Frame {
child: StrongWidget,
extent: UiRegion,
answer: Rc<Cell<Size>>,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.answer.set(
painter
.widget_at(
&self.child,
[None; 2],
[
Place::Fill(Part::From(self.extent.x)),
Place::Fill(Part::From(self.extent.y)),
],
)
.size(),
);
Size::LEFTOVER
}
}
for fractional in [false, true] {
for region in [
UiRegion::FULL,
UiRegion::new(UiSpan::new(Len::rel(0.13), Len::rel(0.79)), UiSpan::FULL),
] {
let plant = |h: &mut Harness, extent| {
let size = if fractional {
Size {
x: rel(0.5),
y: LayoutLen::px(27),
}
} else {
Size::from((80, 27))
};
let (leaf, _) = counted(h, size, !fractional);
let child = Container {
child: leaf.add_strong(&mut h.rsc),
region,
}
.add_strong(&mut h.rsc);
let answer = Rc::new(Cell::new(Size::ZERO));
let root = Frame {
child,
extent,
answer: answer.clone(),
}
.add(&mut h.rsc);
h.set_root(root);
(root, leaf, answer)
};
let mut warm = Harness::new((403, 211));
let (root, leaf, answer) = plant(&mut warm, UiRegion::FULL);
for width in [191.125, 297.25, 83.75] {
let extent =
UiRegion::new(UiSpan::new(Len::px(13.125), Len::px(width)), UiSpan::FULL);
warm.rsc[root].extent = extent;
warm.frame();
let mut cold = Harness::new((403, 211));
let (_, other, other_answer) = plant(&mut cold, extent);
assert_eq!(answer.get(), other_answer.get());
assert_eq!(warm.region(&leaf), cold.region(&other));
}
}
}
}
+80 -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)); assert_corners!(h, top, (0, 0), (400, 200));
} }
#[test]
fn fixed_content_and_a_share_fill_one_viewport() {
let mut h = Harness::new((900, 100));
let content = rect(Color::RED)
.width(LayoutLen {
px: Px::from_int(600),
rel: Rel::ZERO,
leftover: Weight::ONE,
})
.add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
h.set_root(scroll);
assert_corners!(h, content, (0, 0), (900, 100));
}
#[test]
fn fixed_content_wider_than_the_viewport_still_scrolls() {
let mut h = Harness::new((900, 100));
let content = rect(Color::RED).width(1200).add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
h.set_root(scroll);
assert_corners!(h, content, (-300, 0), (900, 100));
}
#[test]
fn a_lone_share_fills_without_scrolling() {
let mut h = Harness::new((900, 100));
let content = rect(Color::RED).width(LayoutLen::LEFTOVER).add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
h.set_root(scroll);
assert_corners!(h, content, (0, 0), (900, 100));
}
#[test]
fn wrapping_content_beside_a_fixed_length_is_stable_warm_and_cold() {
fn plant(h: &mut Harness) -> (WidgetId, WidgetId) {
let fixed = rect(Color::RED).width(600).add(&mut h.rsc);
let text = wtext("Wrapping shapes one source into as many lines as the box leaves room for, so a paragraph's height is an answer and not a setting.")
.size(16)
.wrap(true)
.width(LayoutLen::LEFTOVER)
.add(&mut h.rsc);
let content = (fixed, text).span(Dir::RIGHT).add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
h.set_root(scroll);
(text.id(), content.id())
}
let mut warm = Harness::new((900, 300));
let (text, content) = plant(&mut warm);
warm.rsc.widgets_mut().get_dyn_mut(text);
warm.frame();
let mut cold = Harness::new((900, 300));
let (cold_text, cold_content) = plant(&mut cold);
assert_eq!(warm.region(&text), cold.region(&cold_text));
assert_eq!(warm.region(&content), cold.region(&cold_content));
}
/// A widget that clips to its box may not report more than the box: its /// A widget that clips to its box may not report more than the box: its
/// parent would place the part it cut off, and the framework would put a /// parent would place the part it cut off, and the framework would put a
/// drawing longer than its box somewhere. `Masked` is the second of these /// drawing longer than its box somewhere. `Masked` is the second of these
@@ -71,7 +134,7 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
impl Widget for Clipper { impl Widget for Clipper {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region()); painter.set_mask(UiRegion::FULL);
painter.widget(&self.0).size() painter.widget(&self.0).size()
} }
} }
@@ -82,3 +145,19 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
h.set_root(clipper); h.set_root(clipper);
h.frame(); h.frame();
} }
/// Content that fits sits in the viewport, not in a box of the window's
/// length anchored at the viewport's start. `Part::From` takes window
/// lengths, so a `rel(1.0)` span in one is the window, and only a scroll
/// filling the window would land right.
#[test]
fn content_that_fits_is_placed_in_the_viewport_and_not_in_the_window() {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).height(100).add(&mut h.rsc);
let inner = rect(Color::BLUE).height(50).add(&mut h.rsc);
let scroll = Scroll::new(inner.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
h.set_root((head, scroll).span(Dir::DOWN));
assert_corners!(h, scroll, (0, 100), (400, 400));
assert_corners!(h, inner, (0, 225), (400, 275));
}
+395
View File
@@ -14,6 +14,180 @@ use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::Branch; use iris::random::Branch;
fn assert_same_regions(
warm: &Harness,
warm_ids: &[WidgetId],
cold: &Harness,
cold_ids: &[WidgetId],
) {
let mut wrong = Vec::new();
for (i, (&w, &c)) in warm_ids.iter().zip(cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
/// Ten widgets, shrunk from seed 2 at depth 5. The stack is as tall as its
/// first child, so its other children belong in that one-line box. A cold
/// layout used to keep the span's answer from the larger measuring box while
/// a repaint asked it in the stack's final box.
fn plant_stack_in_its_sizing_childs_box(h: &mut Harness) -> Vec<WidgetId> {
let sizing = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add(&mut h.rsc);
let filler = rect(Color::CYAN.alpha(252)).add(&mut h.rsc);
let plain = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add(&mut h.rsc);
let span = (filler, plain).span(Dir::DOWN).add(&mut h.rsc);
let pad = Pad {
padding: Padding::ZERO,
inner: span.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
let probe = rect(Color::RED).add(&mut h.rsc);
let wide = rect(Color::YELLOW.alpha(252)).add(&mut h.rsc);
let narrow = rect(Color::RED).add(&mut h.rsc);
let branch = Branch {
probe: probe.add_strong(&mut h.rsc),
wide: wide.add_strong(&mut h.rsc),
narrow: narrow.add_strong(&mut h.rsc),
threshold: 55.0,
}
.add(&mut h.rsc);
let stack = Stack {
children: vec![
sizing.add_strong(&mut h.rsc),
pad.add_strong(&mut h.rsc),
branch.add_strong(&mut h.rsc),
],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
h.rsc
.widgets_mut()
.set_size_rules(stack.id(), Some(LayoutLen::LEFTOVER), None);
h.set_root(stack);
vec![
sizing.id(),
filler.id(),
plain.id(),
span.id(),
pad.id(),
probe.id(),
wide.id(),
narrow.id(),
branch.id(),
stack.id(),
]
}
#[test]
fn repainting_a_stack_uses_the_box_its_sizing_child_decided() {
let mut warm = Harness::new((900, 1200));
let ids = plant_stack_in_its_sizing_childs_box(&mut warm);
for &id in &ids {
warm.rsc.widgets_mut().get_dyn_mut(id);
}
warm.frame();
let mut cold = Harness::new((900, 1200));
let cold_ids = plant_stack_in_its_sizing_childs_box(&mut cold);
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
/// Ten widgets, shrunk from seed 108 at depth 5. The nested reverse spans
/// evaluate the branch in successively narrower boxes. The answer from the
/// final, decided box must be the one retained after every span is reordered.
fn plant_branch_in_nested_reverse_spans(
h: &mut Harness,
reordered: bool,
) -> (Vec<WidgetId>, [WeakWidget<Span>; 3]) {
let pair = |first: StrongWidget, second: StrongWidget| match reordered {
true => vec![second, first],
false => vec![first, second],
};
let probe = rect(Color::RED.alpha(63)).add(&mut h.rsc);
let wide = rect(Color::RED).add(&mut h.rsc);
let narrow = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let branch = Branch {
probe: probe.add_strong(&mut h.rsc),
wide: wide.add_strong(&mut h.rsc),
narrow: narrow.add_strong(&mut h.rsc),
threshold: 483.0,
}
.add(&mut h.rsc);
let wrapped = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let down = Span {
children: pair(
branch.add_strong(&mut h.rsc),
wrapped.add_strong(&mut h.rsc),
),
dir: Dir::DOWN,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let inner_filler = rect(Color::CYAN.alpha(63)).add(&mut h.rsc);
let inner = Span {
children: pair(
down.add_strong(&mut h.rsc),
inner_filler.add_strong(&mut h.rsc),
),
dir: Dir::LEFT,
gap: Px::ZERO,
}
.height(LayoutLen::rel(1.0))
.add(&mut h.rsc);
let outer_filler = rect(Color::GREEN.alpha(63)).add(&mut h.rsc);
let outer = Span {
children: pair(
inner.add_strong(&mut h.rsc),
outer_filler.add_strong(&mut h.rsc),
),
dir: Dir::LEFT,
gap: Px::ZERO,
}
.height(LayoutLen::rel(1.0))
.add(&mut h.rsc);
h.set_root(outer);
(
vec![
probe.id(),
wide.id(),
narrow.id(),
branch.id(),
wrapped.id(),
down.id(),
inner_filler.id(),
inner.id(),
outer_filler.id(),
outer.id(),
],
[down, inner, outer],
)
}
#[test]
fn reordering_nested_spans_keeps_the_answer_from_the_decided_box() {
let mut warm = Harness::new((900, 1200));
let (ids, spans) = plant_branch_in_nested_reverse_spans(&mut warm, false);
for span in spans {
warm.rsc[span].children.rotate_left(1);
}
warm.frame();
let mut cold = Harness::new((900, 1200));
let (cold_ids, _) = plant_branch_in_nested_reverse_spans(&mut cold, true);
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about /// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
/// the tree changes -- every widget is marked for redraw and the frame is /// the tree changes -- every widget is marked for redraw and the frame is
/// taken again -- so no box may move, and a warm frame has to land where a /// taken again -- so no box may move, and a warm frame has to land where a
@@ -559,6 +733,51 @@ fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \ const PARAGRAPH: &str = "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."; box leaves room for, so a paragraph's height is an answer and not a setting.";
fn plant_stack_resized_from_free(h: &mut Harness, fixed: bool) -> (Vec<WidgetId>, WidgetId) {
let sizing = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
h.rsc.widgets_mut().set_size_rules(sizing.id(), None, None);
if fixed {
h.rsc.widgets_mut().set_size_rules(
sizing.id(),
Some(LayoutLen::px(112)),
Some(LayoutLen::px(101)),
);
}
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let pad = Pad {
padding: Padding::ZERO,
inner: text.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
let stack = Stack {
children: vec![sizing.add_strong(&mut h.rsc), pad.add_strong(&mut h.rsc)],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
h.set_root(stack);
(
vec![sizing.id(), text.id(), pad.id(), stack.id()],
sizing.id(),
)
}
#[test]
fn fixing_a_stacks_sizing_child_repositions_its_overlay() {
let mut warm = Harness::new((900, 1200));
let (ids, sizing) = plant_stack_resized_from_free(&mut warm, false);
warm.frame();
warm.rsc.widgets_mut().set_size_rules(
sizing,
Some(LayoutLen::px(112)),
Some(LayoutLen::px(101)),
);
warm.frame();
let mut cold = Harness::new((900, 1200));
let (cold_ids, _) = plant_stack_resized_from_free(&mut cold, true);
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
/// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4, /// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4,
/// `shuffle-swap-for-three`). The stack takes its size from the span above, /// `shuffle-swap-for-three`). The stack takes its size from the span above,
/// the span takes its width from the longest line of the texts in it, and /// the span takes its width from the longest line of the texts in it, and
@@ -672,3 +891,179 @@ fn adding_text_to_a_reverse_row_keeps_its_shared_height() {
let (_, other, _) = build(&mut cold, true); let (_, other, _) = build(&mut cold, true);
assert_eq!(warm.region(&shared), cold.region(&other)); 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);
}
/// Six widgets, shrunk from seed 59 at depth 5 (`resize-size`). The column
/// divides the box it is given between two shares, so its drawing holds for
/// that box's length alone, and the pads above it pass that dependency up:
/// each one's box is a part of the box it was asked in. Padding narrowing
/// the frame it hands down does not change that, and while it was taken to,
/// changing the rule over the pads relocated the column's drawing into the
/// new box instead of dividing it again.
fn plant_two_shares_under_two_pads(h: &mut Harness, height: f32) -> Vec<WidgetId> {
let top = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
let bottom = rect(Color::RED).add(&mut h.rsc);
let column = (top, bottom).span(Dir::DOWN).add(&mut h.rsc);
let inner = Pad {
padding: Padding::ZERO,
inner: column.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
let outer = Pad {
padding: Padding::ZERO,
inner: inner.add_strong(&mut h.rsc),
}
.height(height)
.add(&mut h.rsc);
let beside = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((outer, beside).span(Dir::RIGHT));
vec![
top.id(),
bottom.id(),
column.id(),
inner.id(),
outer.id(),
beside.id(),
]
}
#[test]
fn changing_a_rule_over_two_pads_divides_the_column_again() {
let mut warm = Harness::new((900, 1200));
let ids = plant_two_shares_under_two_pads(&mut warm, 88.0);
warm.frame();
warm.rsc
.widgets_mut()
.set_size_rules(ids[4], None, Some(LayoutLen::px(105)));
warm.frame();
let mut cold = Harness::new((900, 1200));
let cold_ids = plant_two_shares_under_two_pads(&mut cold, 105.0);
cold.frame();
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
/// Six widgets, shrunk from seed 942 at depth 6 (`resize`). A `Branch` asks
/// its probe in the top 40 px of its box and forwards the frame, so the
/// scroll's own box is 40 px tall whatever the window is -- but its content
/// is as tall as the frame, which is the window, and a scroll kept to its
/// end has to be told when that changes. Resolving a length against the
/// window is what reads it, so that is where the dependency is taken.
fn plant_a_window_tall_column_in_a_short_scroll(h: &mut Harness) -> Vec<WidgetId> {
let leaf = rect(Color::RED).add(&mut h.rsc);
let column = Span {
children: vec![leaf.add_strong(&mut h.rsc)],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.height(rel(1.0))
.add(&mut h.rsc);
let scroll = Scroll::new(column.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
let wide = rect(Color::BLUE).add(&mut h.rsc);
let narrow = rect(Color::GREEN).add(&mut h.rsc);
let root = Branch {
probe: scroll.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);
h.set_root(root);
vec![leaf.id(), column.id(), scroll.id(), root.id()]
}
#[test]
fn resizing_under_a_short_scroll_snaps_its_window_tall_content_again() {
let mut warm = Harness::new((1920, 1200));
let ids = plant_a_window_tall_column_in_a_short_scroll(&mut warm);
warm.frame();
warm.resize((640, 900));
warm.frame();
let mut cold = Harness::new((640, 900));
let cold_ids = plant_a_window_tall_column_in_a_short_scroll(&mut cold);
cold.frame();
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
+42
View File
@@ -0,0 +1,42 @@
//! Prints where a cold layout puts every widget of many grown trees, so two
//! commits can be compared on cold layout alone. The warm/cold oracle cannot
//! see a change that moves cold layout, since both of its sides move; this
//! can, by diffing its output across the change:
//!
//! IRIS_DUMP_SEEDS=400 IRIS_DUMP_DEPTH=5 cargo test --release \
//! --test layout_dump -- --ignored --nocapture > /tmp/before.txt
//!
//! then the same after, and `diff` the two. A line is one widget: the seed,
//! its index in creation order, and its box in window pixels, or `-` where
//! it is not drawn.
use iris::harness::Harness;
use iris::random::{Edits, grow};
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(fallback)
}
#[test]
#[ignore = "a dump to diff across commits, not a check"]
fn every_cold_layout_is_printed() {
let seeds = env("IRIS_DUMP_SEEDS", 400_u64);
let depth = env("IRIS_DUMP_DEPTH", 5_usize);
let mut out = String::new();
for seed in 1..=seeds {
let mut harness = Harness::new((1920.0, 1200.0));
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
harness.state.root = Some(root);
harness.frame();
for (index, id) in tree.ids.iter().enumerate() {
match harness.region(id) {
Some(region) => out.push_str(&format!("{seed} {index} {region:?}\n")),
None => out.push_str(&format!("{seed} {index} -\n")),
}
}
}
print!("{out}");
}
+60 -4
View File
@@ -103,6 +103,11 @@ pub enum Case {
/// A resize and then a size change, so a retained answer is asked to /// A resize and then a size change, so a retained answer is asked to
/// survive two different kinds of invalidation in a row. /// survive two different kinds of invalidation in a row.
ResizeSize, ResizeSize,
/// A size change and then a resize, which is the other order and not the
/// same test: a length answered as a fraction of one box and kept as a
/// fraction of another agrees at the size it was changed at and parts
/// from it at every other one.
SizeResize,
/// A few declared sizes. /// A few declared sizes.
Size, Size,
/// Every declared size at once, so every reader of a size has a changed /// Every declared size at once, so every reader of a size has a changed
@@ -119,12 +124,13 @@ pub enum Case {
Shuffle(Shuffle), Shuffle(Shuffle),
} }
pub const ALL: [Case; 15] = [ pub const ALL: [Case; 16] = [
Case::Repaint, Case::Repaint,
Case::RepaintSome, Case::RepaintSome,
Case::Resize, Case::Resize,
Case::ResizeRepaint, Case::ResizeRepaint,
Case::ResizeSize, Case::ResizeSize,
Case::SizeResize,
Case::Size, Case::Size,
Case::EverySize, Case::EverySize,
Case::Align, Case::Align,
@@ -146,6 +152,7 @@ impl Case {
Self::Resize => "resize", Self::Resize => "resize",
Self::ResizeRepaint => "resize-repaint", Self::ResizeRepaint => "resize-repaint",
Self::ResizeSize => "resize-size", Self::ResizeSize => "resize-size",
Self::SizeResize => "size-resize",
Self::Size => "size", Self::Size => "size",
Self::EverySize => "every-size", Self::EverySize => "every-size",
Self::Align => "align", Self::Align => "align",
@@ -170,6 +177,15 @@ impl Case {
_ => (STILL, STILL), _ => (STILL, STILL),
} }
} }
/// The window the warm tree is taken to after the change, where the case
/// is about what the change left behind rather than about the change.
fn then_resize(self) -> Option<(f32, f32)> {
match self {
Self::SizeResize => Some(INNER),
_ => None,
}
}
} }
fn mark(warm: &mut Harness, tree: &Tree, step: usize) { fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
@@ -284,7 +300,7 @@ fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mu
warm.frame(); warm.frame();
return out; return out;
} }
Case::Size | Case::ResizeSize => Edits { Case::Size | Case::ResizeSize | Case::SizeResize => Edits {
sizes: some_sizes(warm, tree, rng), sizes: some_sizes(warm, tree, rng),
..Default::default() ..Default::default()
}, },
@@ -384,6 +400,17 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
label label
} }
/// One widget's layout as it stands: the frame its fractions resolved
/// against, the box it was asked in, the box its drawing went in, and what
/// it reported. In window units, which is what both trees are in.
fn record(id: WidgetId, h: &Harness) -> String {
let active = &h.render.active[&id];
format!(
"frame {} ask {} box {} size {}",
active.frame, active.part, active.extent, active.size,
)
}
/// Runs `case` on the tree `plan` describes, warm and cold, and says where /// Runs `case` on the tree `plan` describes, warm and cold, and says where
/// the two disagree. `seed` chooses only the values a case picks at random, /// the two disagree. `seed` chooses only the values a case picks at random,
/// so one plan under one case is one comparison however it was reached. /// so one plan under one case is one comparison however it was reached.
@@ -400,12 +427,29 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
warm.frame(); warm.frame();
} }
let cold_plan = change(case, &mut warm, &mut tree, plan, &mut Rng::new(seed)); let cold_plan = change(case, &mut warm, &mut tree, plan, &mut Rng::new(seed));
// Whatever the change left, seen at another window: an answer kept as a
// fraction of the wrong length is the same number of pixels where it was
// made and a different one everywhere else.
let end = match case.then_resize() {
Some(after) => {
warm.resize(after);
warm.frame();
after
}
None => end,
};
let mut cold = Harness::new(end); let mut cold = Harness::new(end);
let (root, cold_tree) = build(&mut cold.rsc, &cold_plan); let (root, cold_tree) = build(&mut cold.rsc, &cold_plan);
cold.state.root = Some(root); cold.state.root = Some(root);
cold.frame(); cold.frame();
let places: HashMap<WidgetId, usize> = tree
.ids
.iter()
.enumerate()
.map(|(i, &id)| (id, i))
.collect();
let mut drawn = 0; let mut drawn = 0;
for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() { for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c)); let (got, want) = (warm.region(&w), cold.region(&c));
@@ -416,6 +460,7 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
// Where two trees disagree is rarely where the cause is, so the // Where two trees disagree is rarely where the cause is, so the
// ancestry comes with it, marking the widgets that own a region. // ancestry comes with it, marking the widgets that own a region.
let mut chain = Vec::new(); let mut chain = Vec::new();
let mut records = Vec::new();
let mut at = Some(w); let mut at = Some(w);
while let Some(id) = at { while let Some(id) = at {
let active = &warm.render.active[&id]; let active = &warm.render.active[&id];
@@ -424,11 +469,22 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
false => "*", false => "*",
}; };
chain.push(format!("{}{node}", describe(id, &warm))); chain.push(format!("{}{node}", describe(id, &warm)));
// What each level was asked in on both sides, since the level
// where the two stop agreeing is the one to look at rather than
// the leaf that reported the difference.
let cold_id = places.get(&id).and_then(|&i| cold_tree.ids.get(i));
records.push(format!(
" {}\n warm {}\n cold {}",
describe(id, &warm),
record(id, &warm),
cold_id.map_or("-".into(), |&id| record(id, &cold)),
));
at = active.parent; at = active.parent;
} }
return Some(format!( return Some(format!(
"widget {i}\n warm {got:?}\n cold {want:?}\n {}", "widget {i}\n warm {got:?}\n cold {want:?}\n {}\n{}",
chain.join(" < ") chain.join(" < "),
records.join("\n"),
)); ));
} }
match drawn { match drawn {
+6 -2
View File
@@ -70,10 +70,14 @@ fn no_grown_tree_lays_out_differently_warm_than_cold() {
over_seeds(seeds, |seed| { over_seeds(seeds, |seed| {
let grown = plan(seed, depth, &Edits::default()); let grown = plan(seed, depth, &Edits::default());
for &case in &cases { for &case in &cases {
let Some(how) = diverges(&grown, case, seed) else { if diverges(&grown, case, seed).is_none() {
continue; continue;
}; }
let small = shrink(grown.clone(), case, seed); let small = shrink(grown.clone(), case, seed);
// Described from the shrunk tree: the grown tree's chain names
// widgets that are no longer there, and the ancestry of the
// failure is what a test is written from.
let how = diverges(&small, case, seed).unwrap_or_default();
println!( println!(
"seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}", "seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
case.name(), case.name(),