Compare commits

..
Author SHA1 Message Date
iris-ai 46547563c1 Take a strong widget to depend on, and use the question mark
`depend_on_size` took a bare `WidgetId`, which any id at all satisfies.
It takes a `&StrongWidget` now, so `DrawResult` carries the handle it
was drawn from rather than an id copied out of it and nothing can claim
a dependency on a widget it does not hold.

`UiSpan::outside` matched on a pair of `Option`s where `?` says it. It
was written that way while the method was still `const`, and it is not.

No call site changed. Checked: fmt, clippy and 35 tests; `tabs`, `view`,
`minimal` and `text` render unchanged, and the live sway resize round
trip still matches a cold start at each size.
2026-09-14 02:44:40 -04:00
iris-ai 53e61289f5 Say when a drawing cannot be taken out of its box, rather than guessing
`lerp_inv` returns `Option`. Inverting a lerp over a range of zero
length has no one answer, and `div_or`'s fallback picked one: the start
of the range. Measured, that is not a wrong number so much as a
plausible one -- taking a part out of a box fixed at the top of the
window hands back exactly what went in, and out of a box fixed at the
middle hands back the parent's own `rel` of 0.5 as if it were the
child's fraction. Either way the caller cannot tell that nothing was
recovered, which is the defect; the previous commit's claim that it
"returns a rel of 0" is right only for the first case.

`UiScalar::outside` and `UiSpan::outside` follow it to `Option`, and
`Remap` is the answer at the region level: `new` says whether a drawing
in one box can be put in another and `apply` then cannot fail, so
`try_reuse` asks once for a whole subtree and `reusable` goes back to
reporting only what the widget claims. That replaces the predicate the
last commit put in `reusable`, which stated the same rule in a second
place.

`DivOr` existed only for the fallback and is gone, along with
`UiRegion::outside` and `UiVec2::outside`, which had no callers once
`Remap` owned the operation. `UiRegion::axis` took `&mut self` to
return a shared reference; `Remap::new` needs it on a shared one.

This does not redraw less. `Remap::new` refuses exactly what the
predicate refused; what it buys is one statement of the rule and a
remap that cannot half-apply. Counted on a resize, with the old
wipe-everything for comparison:

  20 padded rows, 101 widgets     101 draws -> 0
  the `tabs` example               73 draws -> 0
  the `text` example               49 draws -> 48

So the saving is whole where a resize does not change any widget's
size, and nil in `text`, where both paragraphs rewrap to a different
height and the relayout that forces reaches the root. The last commit's
message oversold that case.

Checked: fmt, clippy and 35 tests. `tabs` (with the image replay),
`view` and `minimal` still byte-identical to `upstream/main`, `text`
unchanged, and the live sway resize round trip still matches a cold
start at each size.
2026-09-14 01:27:16 -04:00
iris-ai 984f482a7f Move a resized drawing instead of redrawing it
A region is a fraction of the output plus an offset and the shader
resolves it against the window every frame, so a resize already moves
the whole drawing without the CPU. Wiping the tree and drawing it again
was throwing that away.

`Painter::output_size` and `px_size` now record that a widget read
pixels, the way reading a child's size records a dependency on it, and a
resize marks only those. In the `text` example that is the two wrapping
paragraphs out of forty-odd widgets; everything else keeps its drawing
and the window uniform puts it in the right place.

Two defects the change surfaced, both of which made a resize land
somewhere a cold start would not:

`redraw` climbed to the highest reader of the changed widget and drew
from there, trusting that draw to reach back down. It does not: an
intermediate whose own box has not changed is reused as it stands and
the draw stops there. Everything between the two is now marked as well,
which is the only thing that stops the reuse. Not resize-specific --
`a_change_two_levels_under_its_reader_still_reaches_it` fails on the
mutation path too.

`mov` cannot stretch a drawing out of a box with no relative extent.
`UiScalar::within` puts a part into such a box as a plain offset from
its start, and `lerp_inv`'s divide-by-zero fallback then returns a
rel of 0 rather than saying it cannot invert, so the remap silently
leaves the drawing its old size. `OnResize::Scale` now only reuses
across a length change when the old box had a relative extent. The
underlying loss belongs to the position chain, which separates the
drawn box from the offered one; until then this is the honest
predicate.

Checked: fmt, clippy and 33 tests. `tabs` (with the image replay),
`view`, `minimal` still byte-identical to `upstream/main`, and `text`
unchanged at 1920x1200 and 900x1200. Driven live under the GPU as well:
started at 1920x1200, resized to 900x1200 and back through sway, and
each screenshot matches a cold start at that size byte for byte.
2026-09-14 01:09:03 -04:00
iris-ai 9520996623 Return the size from draw, and fold placing back into drawing
Review response.

`Painter::set_size` is gone: `Widget::draw` returns the `Size` instead, so
a widget that does not say what it used cannot compile rather than
panicking at the widget that forgot. That also settles setting it twice --
a branch that learns something late just returns a different value.

`Painter::place` and `UiRenderState::place` are gone too. `draw_inner`
already tried to reuse an active widget's drawing before redrawing it, so
`place` was `widget_within` with its own bookkeeping bolted on; drawing a
child a second time now *is* how a parent puts it where it belongs, and a
child is deduplicated in `children` because listing one twice would move
it twice. The unification also drops `place`'s use of `ActiveData::layer`,
which is the layer a widget's own `child_layer()` left the painter on
rather than the layer it was drawn into.

What `place` did unconditionally and `widget_within` did not is record the
size dependency, so `Painter::size_hint` now records one: reading a
child's length to lay out around it is reading its size, whether it came
from a draw or from a hint. `tests/retained.rs` has a parent that only
ever reads the hint, which is the case no existing widget exercises.

`()` sizes itself `Size::default()` -- rest -- rather than zero, so it is
a gap that takes an even share of a span; `WidgetPtr` with nothing in it
does the same, since it is the same situation. `Widget::on_resize`'s
default body said `Translate` while the enum's `#[default]` said `Redraw`;
it now defers to the enum.

`was` is `old` throughout, the `OnResize` variant comments are gone, and
so are two empty `impl` blocks.

Checked: fmt, clippy and 27 tests across the workspace; `tabs` on each of
its five tabs, and `tabs` with a replay that adds two images, all
byte-identical to `upstream/main`; `view` and `minimal` likewise; and the
`text` example rendered at 1920x1200 and 900x1200 to see the paragraph
reflow and its container follow.
2026-09-14 00:39:39 -04:00
iris b108645240 Say what may be done to a drawing, and default to nothing
`SizeDependence::{None, Internal, External}` becomes
`OnResize::{Scale, Translate, Redraw}`, which says what the retained path
may do rather than leaving the reader to work it out from a dependency.

`Redraw` is now the default, and that is the substance of this rather
than the naming. `Translate` was, and nothing opted into it: `SetSize`
reports one size and hands its child the whole box, so its pixels change
with the box and carrying them stretched a 100x100 rect across half the
window. A default that is only right for widgets that happen to qualify
is the same fault as an unchecked reuse flag.

`Translate` still does nothing, and now for the reason rather than the
one I gave before: `mov` translates perfectly well, but `ActiveData`'s
`region` is both the box a widget was given and the box its primitives
occupy, and `mov` remaps out of it. Keeping a drawing at its old size
while the box grows leaves those two disagreeing, and the next move
stretches it. Separating them is what the offset chain does.

Caught by rendering `tabs` against `main` rather than by a test, which is
the argument for keeping that check in the loop.
2026-09-13 23:47:12 -04:00
iris ec012c4552 Text depends on its box for where it sits, not only for shaping
Alignment was not accounted for. Glyphs anchored to the start of an axis
stay where they are when that extent changes, but the default is
`CENTER_LEFT`: a label in a box that grows taller has to re-centre, even
though its shaping is untouched. Saying `Internal` there would keep a
drawing that belongs somewhere else once the retained path can act on it.

It costs nothing today, since neither `Internal` nor `External` reuses
anything yet. It would be silent when the move chain lands, which is why
the classification wants to be right while it is being written rather
than when something starts trusting it.
2026-09-13 23:28:47 -04:00
iris f192f75b25 Size a widget while drawing it, not in a pass of its own
`desired_width`/`desired_height`, `WidgetAxisFns`, `SizeCtx` and the size
cache are gone. A widget states what it used with `Painter::set_size`
while it draws, and `Painter::widget` hands back a `DrawResult` whose
`size()` both reads the child and records that this widget's size depends
on it. Reading nothing keeps the parent independent of what the child came
to.

`Span` is what the change is for. It takes each child's `size_hint` where
there is one, draws only the children that cannot answer, allocates the
flexible space, then places everything -- which deletes `desired_ortho`,
whose own comment said it "literally copies draw ... which makes this slow
and not cool".

Invalidation follows the dependency edges the draw recorded: a widget that
needs redrawing hands off to the highest ancestor that read its size,
instead of re-running a measurement to find out whether anything changed.

`Widget::size_dependence(axis)` says how much of its box a widget's
drawing depends on -- none of it, its own extent, or the whole box -- so
the retained path can keep a drawing and write a new box into it. Asked
per axis, because wrapped text depends on the width it is offered and not
on the height. `Internal` does not yet buy more than `External`: keeping a
drawing when only the room around it changed is a translation, which waits
for the move chain.

`tests/retained.rs` covers the second frame rather than the first, which
is where the bugs were: a placed child that was not recorded as one got
pruned as departed on the next draw.

`examples/text.rs` is new, since wrapping was the one thing here with no
way to see it on its own.
2026-09-13 22:51:05 -04:00
87 changed files with 1407 additions and 10288 deletions

No files matched your search

Generated
+16 -25
View File
@@ -567,9 +567,9 @@ dependencies = [
[[package]] [[package]]
name = "dlib" name = "dlib"
version = "0.5.3" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [ dependencies = [
"libloading", "libloading",
] ]
@@ -2158,9 +2158,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.41.0" version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -2353,15 +2353,6 @@ version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
[[package]]
name = "rig-input"
version = "0.1.0"
dependencies = [
"iris",
"wayland-client",
"wayland-protocols-wlr",
]
[[package]] [[package]]
name = "roxmltree" name = "roxmltree"
version = "0.21.1" version = "0.21.1"
@@ -2918,9 +2909,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-backend" name = "wayland-backend"
version = "0.3.17" version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9"
dependencies = [ dependencies = [
"cc", "cc",
"downcast-rs", "downcast-rs",
@@ -2932,9 +2923,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-client" name = "wayland-client"
version = "0.31.15" version = "0.31.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"rustix 1.1.3", "rustix 1.1.3",
@@ -2966,9 +2957,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-protocols" name = "wayland-protocols"
version = "0.32.13" version = "0.32.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"wayland-backend", "wayland-backend",
@@ -2991,9 +2982,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-protocols-wlr" name = "wayland-protocols-wlr"
version = "0.3.12" version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"wayland-backend", "wayland-backend",
@@ -3004,9 +2995,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-scanner" name = "wayland-scanner"
version = "0.31.11" version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quick-xml", "quick-xml",
@@ -3015,9 +3006,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-sys" name = "wayland-sys"
version = "0.31.11" version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd"
dependencies = [ dependencies = [
"dlib", "dlib",
"log", "log",
+1 -12
View File
@@ -3,9 +3,6 @@ name = "iris"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
[features]
layout-diagnostics = ["iris-core/layout-diagnostics"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
@@ -23,13 +20,7 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] } tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
[workspace] [workspace]
members = ["core", "macro", "rig-input"] members = ["core", "macro"]
[profile.dev]
debug = 1
[profile.test]
debug = "line-tables-only"
[workspace.package] [workspace.package]
version = "0.1.0" version = "0.1.0"
@@ -49,5 +40,3 @@ arboard = "3.6.1"
iris-core = { path = "core" } iris-core = { path = "core" }
iris-macro = { path = "macro" } iris-macro = { path = "macro" }
tokio = "1.49.0" tokio = "1.49.0"
wayland-client = "0.31.15"
wayland-protocols-wlr = { version = "0.3.12", features = ["client"] }
-3
View File
@@ -3,9 +3,6 @@ name = "iris-core"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
[features]
layout-diagnostics = []
[dependencies] [dependencies]
wgpu = { workspace = true } wgpu = { workspace = true }
bytemuck ={ workspace = true } bytemuck ={ workspace = true }
-574
View File
@@ -1,574 +0,0 @@
use crate::{UiNum, util::Vec2};
use std::{
fmt::{Debug, Display, Formatter},
ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign},
};
/// A number held as a whole count of `1 / 2^SHIFT`.
///
/// Layout reaches one place by more than one route -- a box composed down the
/// chain, and the same box summed from what its children asked for -- and has
/// to decide whether the two are the same place. In floats they land a few
/// bits apart, which is a defect wherever the answer changes what is drawn
/// rather than where. Here adding and subtracting are exact, a multiply
/// drops to the step below, and a conversion between grids takes the nearest
/// one, so two routes to one place land on one number and everything
/// downstream compares for equality instead of for nearness.
///
/// `SHIFT` is the number of fractional bits, which is what makes the steps
/// divide a whole number: a power of two also converts to `f32` without
/// rounding while the value fits in its mantissa.
///
/// Arithmetic wraps at the ends of the range, the way the `i32` underneath
/// does. Saturating instead was measured at a twelfth of layout's
/// instructions -- five per add against one -- to keep the ordering of
/// coordinates two million pixels out, where nothing draws anyway. A value
/// off the end is a defect either way; wrapping makes it an obvious one.
/// Only [`Self::from_f32`] clamps, since a float has further to come from.
#[repr(transparent)]
#[derive(
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, bytemuck::Pod, bytemuck::Zeroable,
)]
pub struct Fixed<const SHIFT: u32>(i32);
/// A length or a coordinate in pixels, in steps of `1/1024`. Finer than
/// anything a display can show, and exact in `f32` up to 16,384 px, which is
/// what lets the same number reach the GPU.
pub type Px = Fixed<PX_SHIFT>;
/// How many bits of a pixel a [`Px`] keeps. One place, because [`PxVec2`]
/// and the shader's own decoding are the same grid or nothing lines up.
pub const PX_SHIFT: u32 = 10;
/// A share of what a box has left over, which is a weight beside its
/// siblings rather than a fraction of anything: a list divides its room by
/// the total of these, so the range has to hold a whole list's worth and the
/// precision only has to tell two weights apart.
pub type Weight = Fixed<16>;
/// A fraction of a box. Twenty-four bits of it, which matches `f32` around a
/// half and beats it above one -- where anchors actually sit -- and leaves
/// +/-128 of range, enough to sum a hundred children each asking for a whole
/// box. A `leftover` weight is not one of these: it is a share of what is
/// left rather than a fraction of anything, and it sums over a whole list.
pub type Rel = Fixed<REL_SHIFT>;
/// How many bits of a box a [`Rel`] keeps, beside [`PX_SHIFT`] and for the
/// same reason.
pub const REL_SHIFT: u32 = 24;
impl<const SHIFT: u32> Fixed<SHIFT> {
pub const ZERO: Self = Self(0);
pub const ONE: Self = Self::one();
/// The gap between neighbouring values, which is also how far apart two
/// numbers can be and still mean the same place.
pub const STEP: Self = Self(1);
/// Also what stands in for an unbounded end: compared against, never
/// added to, since arithmetic wraps past it.
pub const MIN: Self = Self(i32::MIN);
pub const MAX: Self = Self(i32::MAX);
const fn one() -> Self {
assert!(SHIFT < 31, "a Fixed needs a bit for the whole part");
Self(1 << SHIFT)
}
pub const fn from_raw(raw: i32) -> Self {
Self(raw)
}
/// The count of steps, for a caller that needs the representation rather
/// than the number.
pub const fn raw(self) -> i32 {
self.0
}
pub const fn from_int(v: i32) -> Self {
Self(v.wrapping_mul(Self::one().0))
}
/// Rounds to the nearest step, and clamps to the ends of the grid rather
/// than wrapping: this is where a number from outside arrives, and a float
/// has the range to be anywhere. A NaN has no nearest step and becomes
/// zero, which is a caller's mistake rather than a value worth carrying.
///
/// Half-away is written out rather than called through `f32::round`,
/// which is not `const`: a layout constant has to stay a constant.
pub const fn from_f32(v: f32) -> Self {
debug_assert!(!v.is_nan(), "a NaN has no place on the grid");
let scaled = v * Self::one().0 as f32;
// Above 2^23 an `f32` has no fractional part left to round, and
// adding a half there rounds the number itself up instead. The cast
// saturates at both ends and sends NaN to zero, which is the
// behaviour wanted at both.
const WHOLE: f32 = (1 << 23) as f32;
Self(match (scaled >= WHOLE, scaled <= -WHOLE, scaled < 0.0) {
(true, _, _) | (_, true, _) => scaled as i32,
(_, _, true) => (scaled - 0.5) as i32,
_ => (scaled + 0.5) as i32,
})
}
/// The first step at or above `v`, where [`Self::from_f32`] takes the
/// nearest one and is below it half the time. For a bound that has to
/// admit the value it came from: a measurement rounded down is a bound
/// that leaves out the thing it was measured from.
pub const fn ceil_from_f32(v: f32) -> Self {
let nearest = Self::from_f32(v);
match nearest.to_f32() < v {
true => nearest.next_up(),
false => nearest,
}
}
/// From a number as it is written in source -- `16`, `1.5` -- which is
/// the other place a value enters the grid.
pub fn from_num(v: impl UiNum) -> Self {
Self::from_f32(v.to_f32())
}
pub const fn to_f32(self) -> f32 {
self.0 as f32 / Self::one().0 as f32
}
/// The same value on another grid, rounded where the new one is coarser.
pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> {
Fixed(match TO >= SHIFT {
true => self.0 << (TO - SHIFT),
false => shift_round(self.0 as i64, SHIFT - TO) as i32,
})
}
pub const fn add(self, rhs: Self) -> Self {
Self(self.0.wrapping_add(rhs.0))
}
pub const fn sub(self, rhs: Self) -> Self {
Self(self.0.wrapping_sub(rhs.0))
}
pub const fn neg(self) -> Self {
Self(self.0.wrapping_neg())
}
/// Scaled by a number on any grid, which is how a length takes a fraction
/// of itself and keeps being a length: the product is measured in the
/// receiver's steps.
///
/// Dropped to the step below rather than taken to the nearest one
/// (Bryan, 2026-09-16), which costs a share a thousandth of a pixel of
/// its row -- less than an even number of pixels draws. Toward negative
/// infinity on both sides of zero, since that is a shift and nothing
/// else: a value and its negation therefore land different distances
/// from where they came, so a flipped span can sit a step from its
/// mirror image.
pub const fn mul<const BY: u32>(self, by: Fixed<BY>) -> Self {
Self(((self.0 as i64 * by.0 as i64) >> BY) as i32)
}
/// Repeated a whole number of times, which no grid rounds.
pub const fn mul_int(self, by: i32) -> Self {
Self(self.0.wrapping_mul(by))
}
/// Divided into a whole number of parts, rounded to the nearest step.
pub const fn div_int(self, by: i32) -> Self {
debug_assert!(by != 0, "no part of nothing");
if by == 0 {
return Self::ZERO;
}
Self(div_round(self.0 as i64, by as i64) as i32)
}
/// Divided by a number on any grid. A zero divisor is a caller bug -- a
/// box of no length has no fraction of itself -- and answers with the end
/// of the range so that a release build lays out something absurd rather
/// than dying.
pub const fn div<const BY: u32>(self, by: Fixed<BY>) -> Self {
debug_assert!(by.0 != 0, "dividing by a length of zero");
if by.0 == 0 {
return match self.0 < 0 {
true => Self::MIN,
false => Self::MAX,
};
}
Self(div_round((self.0 as i64) << BY, by.0 as i64) as i32)
}
/// `num / den` on *this* grid rather than on theirs, for weights coarser
/// than the share they divide.
pub const fn ratio<const OF: u32>(num: Fixed<OF>, den: Fixed<OF>) -> Self {
debug_assert!(den.0 != 0, "no part of a whole of nothing");
if den.0 == 0 {
return Self::ZERO;
}
Self(div_round((num.0 as i64) << SHIFT, den.0 as i64) as i32)
}
/// `from` and `to` a fraction of the way apart, the fraction being the
/// receiver -- the argument order [`crate::util::LerpUtil`] already uses.
pub const fn lerp<const OF: u32>(self, from: Fixed<OF>, to: Fixed<OF>) -> Fixed<OF> {
from.add(to.sub(from).mul(self))
}
pub const fn min(self, other: Self) -> Self {
match self.0 < other.0 {
true => self,
false => other,
}
}
pub const fn max(self, other: Self) -> Self {
match self.0 > other.0 {
true => self,
false => other,
}
}
pub const fn abs(self) -> Self {
Self(self.0.wrapping_abs())
}
pub const fn clamp(self, lo: Self, hi: Self) -> Self {
debug_assert!(lo.0 <= hi.0, "an empty clamp has no answer");
self.max(lo).min(hi)
}
/// The next value along, for an interval that must not admit its own
/// boundary. The step is the whole gap, so there is nothing to exclude
/// between this and the boundary itself.
pub const fn next_up(self) -> Self {
Self(self.0.wrapping_add(1))
}
pub const fn next_down(self) -> Self {
Self(self.0.wrapping_sub(1))
}
}
/// Back to a single step, rounding halves away from zero so that a value and
/// its negation round to the same distance.
const fn shift_round(v: i64, bits: u32) -> i64 {
let half = (1i64 << bits) >> 1;
match v < 0 {
true => -((-v + half) >> bits),
false => (v + half) >> bits,
}
}
const fn div_round(num: i64, den: i64) -> i64 {
let (q, rem) = (num / den, num % den);
match rem.unsigned_abs() * 2 >= den.unsigned_abs() {
true => match (num < 0) == (den < 0) {
true => q + 1,
false => q - 1,
},
false => q,
}
}
/// Toward positive infinity when `up`, toward negative infinity otherwise.
pub(crate) const fn div_toward(num: i64, den: i64, up: bool) -> i64 {
let (q, rem) = (num / den, num % den);
if rem == 0 {
return q;
}
match (rem < 0) == (den < 0) {
true => q + up as i64,
false => q - !up as i64,
}
}
/// Clamped to the ends, unlike a [`Fixed`]'s own arithmetic: a range of box
/// lengths that runs past `i32` really is unbounded.
pub(crate) const fn narrow(v: i64) -> i32 {
if v > i32::MAX as i64 {
return i32::MAX;
}
if v < i32::MIN as i64 {
return i32::MIN;
}
v as i32
}
const impl<const SHIFT: u32> Add for Fixed<SHIFT> {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Fixed::add(self, rhs)
}
}
const impl<const SHIFT: u32> Sub for Fixed<SHIFT> {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Fixed::sub(self, rhs)
}
}
const impl<const SHIFT: u32> Neg for Fixed<SHIFT> {
type Output = Self;
fn neg(self) -> Self {
Fixed::neg(self)
}
}
const impl<const SHIFT: u32> AddAssign for Fixed<SHIFT> {
fn add_assign(&mut self, rhs: Self) {
*self = Fixed::add(*self, rhs);
}
}
const impl<const SHIFT: u32> SubAssign for Fixed<SHIFT> {
fn sub_assign(&mut self, rhs: Self) {
*self = Fixed::sub(*self, rhs);
}
}
const impl<const SHIFT: u32, const BY: u32> Mul<Fixed<BY>> for Fixed<SHIFT> {
type Output = Self;
fn mul(self, rhs: Fixed<BY>) -> Self {
Fixed::mul(self, rhs)
}
}
const impl<const SHIFT: u32, const BY: u32> Div<Fixed<BY>> for Fixed<SHIFT> {
type Output = Self;
fn div(self, rhs: Fixed<BY>) -> Self {
Fixed::div(self, rhs)
}
}
impl<const SHIFT: u32> Display for Fixed<SHIFT> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.to_f32(), f)
}
}
/// Prints the number rather than the count of steps: a failing layout test
/// reports boxes, and `1126` is not a height anybody can read.
impl<const SHIFT: u32> Debug for Fixed<SHIFT> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.to_f32(), f)
}
}
/// Two of them, for the places a size or a position needs both axes: a
/// window, a box in pixels, a pointer. Held apart from [`crate::util::Vec2`]
/// because that one is what the GPU and the platform speak.
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct FixedVec2<const SHIFT: u32> {
pub x: Fixed<SHIFT>,
pub y: Fixed<SHIFT>,
}
pub type PxVec2 = FixedVec2<PX_SHIFT>;
impl<const SHIFT: u32> FixedVec2<SHIFT> {
pub const ZERO: Self = Self::splat(Fixed::ZERO);
pub const fn new(x: Fixed<SHIFT>, y: Fixed<SHIFT>) -> Self {
Self { x, y }
}
pub const fn splat(v: Fixed<SHIFT>) -> Self {
Self { x: v, y: v }
}
pub fn from_f32(v: Vec2) -> Self {
Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y))
}
/// The first step at or above each part, for a measurement reported as a
/// box: what it occupies is not less than what was measured.
pub fn ceil_from_f32(v: Vec2) -> Self {
Self::new(Fixed::ceil_from_f32(v.x), Fixed::ceil_from_f32(v.y))
}
pub fn to_f32(self) -> Vec2 {
Vec2::new(self.x.to_f32(), self.y.to_f32())
}
pub const fn div_int(self, by: i32) -> Self {
Self::new(self.x.div_int(by), self.y.div_int(by))
}
pub const fn min(self, other: Self) -> Self {
Self::new(self.x.min(other.x), self.y.min(other.y))
}
pub const fn max(self, other: Self) -> Self {
Self::new(self.x.max(other.x), self.y.max(other.y))
}
}
// `impl_op!` names one concrete type, and this one is generic.
const impl<const SHIFT: u32> Add for FixedVec2<SHIFT> {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self::new(self.x.add(rhs.x), self.y.add(rhs.y))
}
}
const impl<const SHIFT: u32> Sub for FixedVec2<SHIFT> {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self::new(self.x.sub(rhs.x), self.y.sub(rhs.y))
}
}
const impl<const SHIFT: u32> AddAssign for FixedVec2<SHIFT> {
fn add_assign(&mut self, rhs: Self) {
*self = Add::add(*self, rhs);
}
}
const impl<const SHIFT: u32> SubAssign for FixedVec2<SHIFT> {
fn sub_assign(&mut self, rhs: Self) {
*self = Sub::sub(*self, rhs);
}
}
impl<const SHIFT: u32> Debug for FixedVec2<SHIFT> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl<const SHIFT: u32> Display for FixedVec2<SHIFT> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_sum_of_steps_does_not_drift() {
let mut at = Px::ZERO;
for _ in 0..20_000 {
at += Px::from_raw(3);
}
assert_eq!(at, Px::from_raw(60_000));
for _ in 0..20_000 {
at -= Px::from_raw(3);
}
assert_eq!(at, Px::ZERO);
}
#[test]
fn a_pixel_survives_the_trip_through_f32() {
for raw in [0, 1, -1, 64, -1000, 16_777_215, -16_777_215] {
let px = Px::from_raw(raw);
assert_eq!(Px::from_f32(px.to_f32()), px);
}
}
#[test]
fn a_fraction_of_a_length_is_a_length() {
let half = Px::from_int(100) * Rel::from_f32(0.5);
assert_eq!(half, Px::from_int(50));
assert_eq!(Px::from_int(100) * Rel::ONE, Px::from_int(100));
assert_eq!(Px::from_int(100) * Rel::ZERO, Px::ZERO);
}
/// Toward negative infinity on both sides of zero, which is what makes
/// it a shift rather than a shift and a sign branch -- and what makes a
/// value and its negation land different distances from where they came,
/// so a flipped span can sit a step from its mirror image.
#[test]
fn a_multiply_drops_to_the_step_below_on_both_sides_of_zero() {
// A step and a half of one, which has no step of its own.
let step_and_a_half = Rel::from_f32(1.5).div_int(Px::ONE.raw());
assert_eq!(Px::ONE * step_and_a_half, Px::from_raw(1));
assert_eq!(Px::ONE.neg() * step_and_a_half, Px::from_raw(-2));
}
/// A division rounds to the nearest step, so it cannot put back the
/// steps a truncating multiply dropped: a round trip comes back short,
/// never long, and by the few steps the two operations gave up.
#[test]
fn dividing_by_a_fraction_cannot_undo_a_truncating_multiply() {
let third = Rel::ONE / Rel::from_int(3);
let len = Px::from_int(300);
let back = len * third / third;
assert!(back <= len, "{back:?} is longer than {len:?}");
assert!(len - back <= Px::from_raw(3), "{back:?} against {len:?}");
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
}
/// The bound a greedy line break needs: the width it was measured at is
/// not on the grid, and the narrowest box the break still holds for is
/// the step at or above it, never the one below.
#[test]
fn a_ceiling_never_lands_below_the_number_it_came_from() {
let step = 1.0 / (1 << PX_SHIFT) as f32;
for n in 0..64 {
let v = 189.0 + n as f32 * step / 3.0;
let up = Px::ceil_from_f32(v);
assert!(up.to_f32() >= v, "{up:?} is below {v}");
assert!(
up.to_f32() - v < step,
"{up:?} is more than a step above {v}"
);
}
// An exact step is its own ceiling.
assert_eq!(Px::ceil_from_f32(189.5), Px::from_f32(189.5));
}
#[test]
fn a_number_from_outside_is_clamped_to_the_grid() {
assert_eq!(Px::from_f32(1e12), Px::MAX);
assert_eq!(Px::from_f32(-1e12), Px::MIN);
}
#[test]
fn a_coarser_grid_rounds_and_a_finer_one_does_not() {
// A third, which neither grid holds exactly.
let third = Rel::ONE / Rel::from_int(3);
assert_eq!(third.to_scale::<6>(), Fixed::<6>::from_raw(21));
let coarse = Fixed::<6>::from_raw(21);
assert_eq!(coarse.to_scale::<24>().to_scale::<6>(), coarse);
}
#[test]
fn lerp_takes_the_fraction_as_the_receiver() {
let (from, to) = (Px::from_int(10), Px::from_int(20));
assert_eq!(Rel::ZERO.lerp(from, to), from);
assert_eq!(Rel::ONE.lerp(from, to), to);
assert_eq!(Rel::from_f32(0.5).lerp(from, to), Px::from_int(15));
assert_eq!(Rel::from_f32(0.5).lerp(to, from), Px::from_int(15));
}
#[test]
fn a_ratio_is_finer_than_the_weights_it_divides() {
let (one, three) = (Weight::ONE, Weight::from_int(3));
// A third, which the weights' own grid could only hold to 1/65536.
assert_eq!(Rel::ratio(one, three), Rel::from_raw(5592405));
assert_eq!(Rel::ratio(three, three), Rel::ONE);
assert_eq!(Rel::ratio(Weight::ZERO, three), Rel::ZERO);
}
#[test]
fn nothing_sits_between_a_value_and_the_next_one() {
let at = Px::from_int(3);
assert_eq!(at.next_up().next_down(), at);
assert_eq!(at.next_up().raw() - at.raw(), 1);
assert!(at.next_down() < at && at < at.next_up());
}
#[test]
fn it_prints_the_number_rather_than_the_steps() {
assert_eq!(format!("{:?}", Px::from_f32(17.59375)), "17.59375");
assert_eq!(format!("{}", Px::from_int(-2)), "-2");
}
}
-483
View File
@@ -1,483 +0,0 @@
//! Opt-in counters and coarse timers for explaining CPU layout cost.
//!
//! Enable the `layout-diagnostics` feature. With it disabled, none of the
//! instrumentation is compiled into Iris. The retained rig in
//! `tests/layout_diagnostics.rs` is the ordinary entry point.
//!
//! Timers are inclusive: `update total` contains `full layout` or
//! `incremental layout`, and `text render` contains shaping and glyph
//! placement. They locate cost within one instrumented run and must not be
//! added together. Use an uninstrumented build under `perf` for final CPU
//! totals; counting every primitive and distinct widget deliberately perturbs
//! the instrumented run.
//!
//! Call [`trace_widget`] before a frame to retain the ordered constraint,
//! reuse, size, placement, and text events for one suspicious widget. The
//! selection is a set and survives [`take`] until cleared.
use crate::{Axis, LayoutLen, PxVec2, Size, UiRegion, WidgetId};
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
fmt::Write,
time::Instant,
};
#[derive(Clone, Copy)]
pub(crate) enum Counter {
Updates,
DrawRequests,
WidgetDraws,
RegionNodeDraws,
SizeReads,
HintHits,
HintMisses,
RetainedSizeHits,
ReuseAttempts,
ReuseExact,
ReuseMoved,
ReuseDirty,
ReuseWrongParent,
ReuseRemapped,
ReuseOutside,
ReuseWrongLayer,
ReuseWrongNode,
PlaceRedraws,
QueuePops,
DepthReads,
LocalRedraws,
SizeChanges,
ReaderEdges,
PrimitiveWrites,
TextRenders,
TextShapeHits,
TextShapes,
TextBreaks,
GlyphPlacements,
}
impl Counter {
const COUNT: usize = Self::GlyphPlacements as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [
"updates",
"draw requests",
"widget draws",
"region-node draws",
"draw-result size reads",
"hint hits",
"hint misses",
"retained size hits",
"reuse attempts",
"reuse exact",
"reuse moved",
"reuse: dirty",
"reuse: wrong parent",
"reuse remapped",
"reuse: outside what it holds for",
"reuse: another layer",
"reuse: region-node choice changed",
"placed by redrawing",
"redraw queue pops",
"depth reads",
"local redraws",
"size changes",
"reader edges",
"primitive writes",
"text renders",
"text shape hits",
"text shapes",
"text line breaks",
"glyph placements",
];
}
#[derive(Clone, Copy)]
pub(crate) enum TimerKind {
Update,
FullLayout,
IncrementalLayout,
TextRender,
TextShape,
TextBreak,
GlyphPlacement,
}
impl TimerKind {
const COUNT: usize = Self::GlyphPlacement as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [
"update total",
"full layout",
"incremental layout",
"text render",
"text shape",
"text line break",
"glyph placement",
];
}
#[derive(Clone)]
pub struct Report {
counters: [u64; Counter::COUNT],
nanos: [u64; TimerKind::COUNT],
distinct_widgets: usize,
distinct_text_widgets: usize,
hot_widgets: Vec<Callsite>,
hot_text: Vec<Callsite>,
traces: Vec<TraceEvent>,
}
impl Default for Report {
fn default() -> Self {
Self {
counters: [0; Counter::COUNT],
nanos: [0; TimerKind::COUNT],
distinct_widgets: 0,
distinct_text_widgets: 0,
hot_widgets: Vec::new(),
hot_text: Vec::new(),
traces: Vec::new(),
}
}
}
impl Report {
pub fn counters(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
Counter::NAMES.into_iter().zip(self.counters)
}
/// Inclusive elapsed time accumulated for each targeted operation.
pub fn timings_ns(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
TimerKind::NAMES.into_iter().zip(self.nanos)
}
pub fn distinct_widgets(&self) -> usize {
self.distinct_widgets
}
pub fn distinct_text_widgets(&self) -> usize {
self.distinct_text_widgets
}
pub fn hot_widgets(&self) -> &[Callsite] {
&self.hot_widgets
}
pub fn hot_text(&self) -> &[Callsite] {
&self.hot_text
}
/// Ordered layout events for widgets selected with [`trace_widget`].
pub fn traces(&self) -> &[TraceEvent] {
&self.traces
}
/// Formats nonzero totals divided by `frames`.
pub fn per_frame(&self, frames: usize) -> String {
let divisor = frames.max(1) as f64;
let mut out = String::new();
for (name, value) in self.counters() {
if value != 0 {
let _ = writeln!(out, " {name:<27} {:>12.2}", value as f64 / divisor);
}
}
if self.distinct_widgets != 0 {
let _ = writeln!(
out,
" {:<27} {:>12}",
"distinct widgets", self.distinct_widgets
);
}
if self.distinct_text_widgets != 0 {
let _ = writeln!(
out,
" {:<27} {:>12}",
"distinct text widgets", self.distinct_text_widgets
);
}
for (name, nanos) in self.timings_ns() {
if nanos != 0 {
let ms = nanos as f64 / divisor / 1_000_000.0;
let _ = writeln!(out, " {name:<27} {ms:>12.3} ms");
}
}
if !self.hot_widgets.is_empty() {
let _ = writeln!(out, " hottest widget draws:");
for callsite in &self.hot_widgets {
let calls = callsite.calls as f64 / divisor;
let _ = writeln!(
out,
" {calls:>9.2} {:?} {}",
callsite.id, callsite.label
);
}
}
if !self.hot_text.is_empty() {
let _ = writeln!(out, " hottest text renders:");
for callsite in &self.hot_text {
let calls = callsite.calls as f64 / divisor;
let _ = writeln!(
out,
" {calls:>9.2} {:>3} widths {:?} {}",
callsite.distinct_widths, callsite.id, callsite.label
);
}
}
if !self.traces.is_empty() {
let _ = writeln!(out, " targeted layout trace:");
for event in &self.traces {
let _ = writeln!(out, " {event:?}");
}
}
out
}
}
#[derive(Clone)]
pub struct Callsite {
pub id: WidgetId,
pub label: String,
pub calls: u64,
pub distinct_widths: usize,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ReuseOutcome {
Exact,
Moved,
Dirty,
WrongParent,
WrongLayer,
Remapped,
Outside,
Undrawn,
}
/// One targeted layout event. Events are retained in execution order, making
/// repeated constraint paths visible without logging every widget globally.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TraceEvent {
DrawRequest {
id: WidgetId,
parent: Option<WidgetId>,
region: UiRegion,
pixel_size: PxVec2,
region_node: bool,
},
Reuse {
id: WidgetId,
outcome: ReuseOutcome,
},
SizeReported {
id: WidgetId,
size: Size,
},
RegionNode {
id: WidgetId,
parent: WidgetId,
region: UiRegion,
},
SizeRead {
id: WidgetId,
reader: WidgetId,
size: Size,
},
HintRead {
id: WidgetId,
reader: WidgetId,
axis: Axis,
hint: Option<LayoutLen>,
},
TextRendered {
id: WidgetId,
width: Option<f32>,
},
}
#[derive(Default)]
struct Calls {
label: String,
count: u64,
widths: HashSet<Option<u32>>,
}
#[derive(Default)]
struct Current {
report: Report,
widgets: HashMap<WidgetId, Calls>,
text_widgets: HashMap<WidgetId, Calls>,
traced: HashSet<WidgetId>,
}
thread_local! {
static CURRENT: RefCell<Current> = RefCell::new(Current::default());
}
pub(crate) fn bump(counter: Counter) {
CURRENT.with_borrow_mut(|current| current.report.counters[counter as usize] += 1);
}
pub(crate) fn draw_widget(id: WidgetId, label: &str) {
CURRENT.with_borrow_mut(|current| {
let calls = current.widgets.entry(id).or_default();
if calls.label.is_empty() {
calls.label = label.to_owned();
}
calls.count += 1;
});
}
/// Adds a widget to the targeted trace set. Selection survives [`take`]
/// until explicitly removed or cleared.
pub fn trace_widget(id: impl Into<WidgetId>) {
CURRENT.with_borrow_mut(|current| {
current.traced.insert(id.into());
});
}
pub fn untrace_widget(id: impl Into<WidgetId>) {
CURRENT.with_borrow_mut(|current| {
current.traced.remove(&id.into());
});
}
pub fn clear_traced_widgets() {
CURRENT.with_borrow_mut(|current| current.traced.clear());
}
fn trace(id: WidgetId, event: TraceEvent) {
CURRENT.with_borrow_mut(|current| {
if current.traced.contains(&id) {
current.report.traces.push(event);
}
});
}
pub(crate) fn draw_request(
id: WidgetId,
parent: Option<WidgetId>,
region: UiRegion,
pixel_size: PxVec2,
region_node: bool,
) {
trace(
id,
TraceEvent::DrawRequest {
id,
parent,
region,
pixel_size,
region_node,
},
);
}
pub(crate) fn reuse(id: WidgetId, outcome: ReuseOutcome) {
trace(id, TraceEvent::Reuse { id, outcome });
}
pub(crate) fn size_reported(id: WidgetId, size: Size) {
trace(id, TraceEvent::SizeReported { id, size });
}
pub(crate) fn region_node(id: WidgetId, parent: WidgetId, region: UiRegion) {
trace(id, TraceEvent::RegionNode { id, parent, region });
}
pub(crate) fn size_read(id: WidgetId, reader: WidgetId, size: Size) {
trace(id, TraceEvent::SizeRead { id, reader, size });
}
pub(crate) fn hint_read(id: WidgetId, reader: WidgetId, axis: Axis, hint: Option<LayoutLen>) {
trace(
id,
TraceEvent::HintRead {
id,
reader,
axis,
hint,
},
);
}
pub(crate) fn render_text(id: WidgetId, label: &str, width: Option<f32>) {
CURRENT.with_borrow_mut(|current| {
let calls = current.text_widgets.entry(id).or_default();
if calls.label.is_empty() {
calls.label = label.to_owned();
}
calls.count += 1;
calls.widths.insert(width.map(f32::to_bits));
if current.traced.contains(&id) {
current
.report
.traces
.push(TraceEvent::TextRendered { id, width });
}
});
}
pub(crate) struct Timer {
kind: TimerKind,
start: Instant,
}
pub(crate) fn timer(kind: TimerKind) -> Timer {
Timer {
kind,
start: Instant::now(),
}
}
impl Drop for Timer {
fn drop(&mut self) {
let nanos = self.start.elapsed().as_nanos().min(u64::MAX as u128) as u64;
CURRENT.with_borrow_mut(|current| current.report.nanos[self.kind as usize] += nanos);
}
}
/// Takes all diagnostics accumulated on this thread and resets them.
pub fn take() -> Report {
CURRENT.with_borrow_mut(|current| {
current.report.distinct_widgets = current.widgets.len();
current.report.distinct_text_widgets = current.text_widgets.len();
current.report.hot_widgets = hottest(&current.widgets);
current.report.hot_text = hottest(&current.text_widgets);
let report = std::mem::take(&mut current.report);
current.widgets.clear();
current.text_widgets.clear();
report
})
}
fn hottest(calls: &HashMap<WidgetId, Calls>) -> Vec<Callsite> {
let mut calls: Vec<_> = calls
.iter()
.map(|(&id, calls)| Callsite {
id,
label: calls.label.clone(),
calls: calls.count,
distinct_widths: calls.widths.len(),
})
.collect();
calls.sort_by(|a, b| b.calls.cmp(&a.calls).then_with(|| a.label.cmp(&b.label)));
calls.truncate(8);
calls
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn taking_a_report_resets_its_counters() {
let _ = take();
bump(Counter::Updates);
bump(Counter::Updates);
let report = take();
assert_eq!(report.counters().next(), Some(("updates", 2)));
assert!(take().counters().all(|(_, count)| count == 0));
}
}
-5
View File
@@ -10,12 +10,8 @@
#![feature(coerce_unsized)] #![feature(coerce_unsized)]
#![feature(option_into_flat_iter)] #![feature(option_into_flat_iter)]
#[cfg(feature = "layout-diagnostics")]
pub mod layout_diagnostics;
mod attr; mod attr;
mod event; mod event;
mod fixed;
mod num; mod num;
mod orientation; mod orientation;
mod primitive; mod primitive;
@@ -27,7 +23,6 @@ pub mod util;
pub use attr::*; pub use attr::*;
pub use event::*; pub use event::*;
pub use fixed::*;
pub use num::*; pub use num::*;
pub use orientation::*; pub use orientation::*;
pub use primitive::*; pub use primitive::*;
+43 -76
View File
@@ -1,8 +1,8 @@
use crate::{Px, Rel}; use crate::vec2;
use super::*; use super::*;
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub struct Align { pub struct Align {
pub x: Option<AxisAlign>, pub x: Option<AxisAlign>,
pub y: Option<AxisAlign>, pub y: Option<AxisAlign>,
@@ -30,32 +30,20 @@ impl Align {
} }
} }
/// Where a widget sits in a box longer than it is. The default is the middle, #[derive(Clone, Copy, PartialEq, Eq)]
/// because the two edges are the ones that assume a direction: which of them pub enum AxisAlign {
/// is the near one depends on the writing system and on which way a container Neg,
/// runs, and the middle is the same either way. Center,
#[derive(Debug, Clone, Copy, PartialEq)] Pos,
pub struct AxisAlign(Rel); }
impl AxisAlign { impl AxisAlign {
pub const NEG: Self = Self::new(0.0); pub const fn rel(&self) -> f32 {
pub const CENTER: Self = Self::new(0.5); match self {
pub const POS: Self = Self::new(1.0); Self::Neg => 0.0,
Self::Center => 0.5,
pub const fn new(rel: f32) -> Self { Self::Pos => 1.0,
Self(Rel::from_f32(rel))
} }
/// A fraction of the room left over, which is what the layout reads: the
/// three constants are the familiar places along it, not the only ones.
pub const fn rel(&self) -> Rel {
self.0
}
}
impl Default for AxisAlign {
fn default() -> Self {
Self::CENTER
} }
} }
@@ -65,60 +53,41 @@ pub struct CardinalAlign {
} }
impl CardinalAlign { impl CardinalAlign {
pub const LEFT: Self = Self::new(Axis::X, AxisAlign::NEG); pub const LEFT: Self = Self::new(Axis::X, AxisAlign::Neg);
pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::CENTER); pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::Center);
pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::POS); pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::Pos);
pub const TOP: Self = Self::new(Axis::Y, AxisAlign::NEG); pub const TOP: Self = Self::new(Axis::Y, AxisAlign::Neg);
pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::CENTER); pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::Center);
pub const BOT: Self = Self::new(Axis::Y, AxisAlign::POS); pub const BOT: Self = Self::new(Axis::Y, AxisAlign::Pos);
pub const fn new(axis: Axis, align: AxisAlign) -> Self { pub const fn new(axis: Axis, align: AxisAlign) -> Self {
Self { axis, align } Self { axis, align }
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Default)] #[derive(Clone, Copy, PartialEq, Eq)]
pub struct RegionAlign { pub struct RegionAlign {
pub x: AxisAlign, pub x: AxisAlign,
pub y: AxisAlign, pub y: AxisAlign,
} }
impl RegionAlign { impl RegionAlign {
/// Both axes at the near edge: the start of a box in its own orientation. pub const TOP_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Neg);
pub const NEAR: Self = Self { pub const TOP_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Neg);
x: AxisAlign::NEG, pub const TOP_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Neg);
y: AxisAlign::NEG, pub const CENTER_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Center);
}; pub const CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Center);
pub const CENTER_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Center);
pub fn axis(&self, axis: Axis) -> AxisAlign { pub const BOT_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Pos);
match axis { pub const BOT_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Pos);
Axis::X => self.x, pub const BOT_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Pos);
Axis::Y => self.y,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut AxisAlign {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
}
impl RegionAlign {
pub const TOP_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::NEG);
pub const TOP_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::NEG);
pub const TOP_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::NEG);
pub const CENTER_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::CENTER);
pub const CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::CENTER);
pub const CENTER_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::CENTER);
pub const BOT_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::POS);
pub const BOT_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::POS);
pub const BOT_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::POS);
pub const fn new(x: AxisAlign, y: AxisAlign) -> Self { pub const fn new(x: AxisAlign, y: AxisAlign) -> Self {
Self { x, y } Self { x, y }
} }
pub const fn rel(&self) -> Vec2 {
vec2(self.x.rel(), self.y.rel())
}
} }
impl UiVec2 { impl UiVec2 {
@@ -171,15 +140,16 @@ impl Vec2 {
} }
} }
impl Len { impl UiScalar {
pub const fn align(&self, align: AxisAlign) -> UiSpan { pub const fn align(&self, align: AxisAlign) -> UiSpan {
let rel = align.rel(); let rel = align.rel();
let rest = Rel::ONE.sub(rel); let mut start = UiScalar::rel(rel);
let at = Len::from_parts(rel, Px::ZERO); start.abs -= self.abs * rel;
UiSpan { start.rel -= self.rel * rel;
start: Len::from_parts(at.rel.sub(self.rel.mul(rel)), at.px.sub(self.px.mul(rel))), let mut end = UiScalar::rel(rel);
end: Len::from_parts(at.rel.add(self.rel.mul(rest)), at.px.add(self.px.mul(rest))), end.abs += self.abs * (1.0 - rel);
} end.rel += self.rel * (1.0 - rel);
UiSpan { start, end }
} }
} }
@@ -195,8 +165,8 @@ impl From<RegionAlign> for Align {
impl From<Align> for RegionAlign { impl From<Align> for RegionAlign {
fn from(align: Align) -> Self { fn from(align: Align) -> Self {
Self { Self {
x: align.x.unwrap_or(AxisAlign::CENTER), x: align.x.unwrap_or(AxisAlign::Center),
y: align.y.unwrap_or(AxisAlign::CENTER), y: align.y.unwrap_or(AxisAlign::Center),
} }
} }
} }
@@ -219,10 +189,7 @@ impl From<CardinalAlign> for Align {
const impl From<RegionAlign> for UiVec2 { const impl From<RegionAlign> for UiVec2 {
fn from(align: RegionAlign) -> Self { fn from(align: RegionAlign) -> Self {
Self::new( Self::rel(align.rel())
Len::from_parts(align.x.rel(), Px::ZERO),
Len::from_parts(align.y.rel(), Px::ZERO),
)
} }
} }
+1 -36
View File
@@ -1,23 +1,11 @@
use super::*; use super::*;
use crate::{Fixed, FixedVec2};
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Eq, PartialEq)]
pub enum Axis { pub enum Axis {
X, X,
Y, Y,
} }
impl Axis {
/// A per-axis pair with `aligned` on this axis and `ortho` on the other,
/// which is what `from_axis` does for a vector.
pub fn pair<T>(self, aligned: T, ortho: T) -> [T; 2] {
match self {
Self::X => [aligned, ortho],
Self::Y => [ortho, aligned],
}
}
}
impl std::ops::Not for Axis { impl std::ops::Not for Axis {
type Output = Self; type Output = Self;
@@ -52,29 +40,6 @@ pub enum Sign {
Pos, Pos,
} }
impl<const SHIFT: u32> FixedVec2<SHIFT> {
pub const fn axis(&self, axis: Axis) -> Fixed<SHIFT> {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub const fn axis_mut(&mut self, axis: Axis) -> &mut Fixed<SHIFT> {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub const fn from_axis(axis: Axis, aligned: Fixed<SHIFT>, ortho: Fixed<SHIFT>) -> Self {
match axis {
Axis::X => Self::new(aligned, ortho),
Axis::Y => Self::new(ortho, aligned),
}
}
}
impl Vec2 { impl Vec2 {
pub fn axis(&self, axis: Axis) -> f32 { pub fn axis(&self, axis: Axis) -> f32 {
match axis { match axis {
+83 -123
View File
@@ -1,30 +1,22 @@
use super::*; use super::*;
use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op}; use crate::{UiNum, util::impl_op};
#[derive(Debug, Default, Clone, Copy, PartialEq)] #[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Size { pub struct Size {
pub x: LayoutLen, pub x: Len,
pub y: LayoutLen, pub y: Len,
} }
/// What a widget asks for along one axis: a [`Len`] -- pixels and a fraction #[derive(Debug, Clone, Copy, PartialEq)]
/// of the box it is given -- plus a share of whatever is left over once pub struct Len {
/// everything fixed has been taken. The parts add up rather than choosing pub abs: f32,
/// between one another. pub rel: f32,
/// pub rest: f32,
/// Only a container dividing its room can answer a share, so a length nobody
/// divides is a `Len`: a position, a padding, a cap, anything already
/// resolved.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LayoutLen {
pub px: Px,
pub rel: Rel,
pub leftover: Weight,
} }
impl<N: UiNum> From<N> for LayoutLen { impl<N: UiNum> From<N> for Len {
fn from(value: N) -> Self { fn from(value: N) -> Self {
LayoutLen::px(value.to_f32()) Len::abs(value.to_f32())
} }
} }
@@ -37,76 +29,52 @@ impl<Nx: UiNum, Ny: UiNum> From<(Nx, Ny)> for Size {
} }
} }
/// A length with no share in it is a length a container does not have to impl From<Len> for Size {
/// divide, which is one it can always give. fn from(value: Len) -> Self {
impl From<Len> for LayoutLen {
fn from(len: Len) -> Self {
Self {
px: len.px,
rel: len.rel,
leftover: Weight::ZERO,
}
}
}
impl From<LayoutLen> for Size {
fn from(value: LayoutLen) -> Self {
Self { x: value, y: value } Self { x: value, y: value }
} }
} }
impl Size { impl Size {
pub const ZERO: Self = Self { pub const ZERO: Self = Self {
x: LayoutLen::ZERO, x: Len::ZERO,
y: LayoutLen::ZERO, y: Len::ZERO,
}; };
pub const LEFTOVER: Self = Self { pub const REST: Self = Self {
x: LayoutLen::LEFTOVER, x: Len::REST,
y: LayoutLen::LEFTOVER, y: Len::REST,
}; };
/// From something measured outside layout -- a texture, a shaped line -- pub fn abs(v: Vec2) -> Self {
/// which is where a size in floats comes from.
pub fn px(v: Vec2) -> Self {
Self::from_px(PxVec2::from_f32(v))
}
pub const fn from_px(v: PxVec2) -> Self {
Self { Self {
x: LayoutLen { x: Len::abs(v.x),
px: v.x, y: Len::abs(v.y),
..LayoutLen::ZERO
},
y: LayoutLen {
px: v.y,
..LayoutLen::ZERO
},
} }
} }
pub fn rel(v: Vec2) -> Self { pub fn rel(v: Vec2) -> Self {
Self { Self {
x: LayoutLen::rel(v.x), x: Len::rel(v.x),
y: LayoutLen::rel(v.y), y: Len::rel(v.y),
} }
} }
pub fn leftover(v: Vec2) -> Self { pub fn rest(v: Vec2) -> Self {
Self { Self {
x: LayoutLen::leftover(v.x), x: Len::rest(v.x),
y: LayoutLen::leftover(v.y), y: Len::rest(v.y),
} }
} }
pub fn to_uivec2(self) -> UiVec2 { pub fn to_uivec2(self) -> UiVec2 {
UiVec2 { UiVec2 {
x: self.x.apply_leftover(), x: self.x.apply_rest(),
y: self.y.apply_leftover(), y: self.y.apply_rest(),
} }
} }
pub fn from_axis(axis: Axis, aligned: LayoutLen, ortho: LayoutLen) -> Self { pub fn from_axis(axis: Axis, aligned: Len, ortho: Len) -> Self {
match axis { match axis {
Axis::X => Self { Axis::X => Self {
x: aligned, x: aligned,
@@ -119,73 +87,53 @@ impl Size {
} }
} }
pub fn axis(&self, axis: Axis) -> LayoutLen { pub fn axis(&self, axis: Axis) -> Len {
match axis { match axis {
Axis::X => self.x, Axis::X => self.x,
Axis::Y => self.y, Axis::Y => self.y,
} }
} }
pub fn axis_mut(&mut self, axis: Axis) -> &mut LayoutLen {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
} }
impl LayoutLen { impl Len {
pub const ZERO: Self = Self { pub const ZERO: Self = Self {
px: Px::ZERO, abs: 0.0,
rel: Rel::ZERO, rel: 0.0,
leftover: Weight::ZERO, rest: 0.0,
}; };
pub const LEFTOVER: Self = Self { pub const REST: Self = Self {
px: Px::ZERO, abs: 0.0,
rel: Rel::ZERO, rel: 0.0,
leftover: Weight::ONE, rest: 1.0,
}; };
/// The whole of what is left over counts as the whole box, which is what pub fn apply_rest(&self) -> UiScalar {
/// a length means to something that is not dividing a box between UiScalar {
/// siblings -- a scroll asking how long its content is. rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 },
pub fn apply_leftover(&self) -> Len { abs: self.abs,
let share = match self.leftover > Weight::ZERO { }
true => Rel::ONE,
false => Rel::ZERO,
};
Len::from_parts(self.rel.add(share), self.px)
} }
/// This length, given as a part of a box `len` long, as a part of the pub fn abs(abs: impl UiNum) -> Self {
/// box `len` is itself a part of. The share is untouched: it is a claim
/// on whoever divides the room, not a fraction of anything.
pub const fn within_len(self, len: Len) -> Self {
let part = Len::from_parts(self.rel, self.px).within_len(len);
Self { Self {
px: part.px, abs: abs.to_f32(),
rel: part.rel, rel: 0.0,
leftover: self.leftover, rest: 0.0,
}
}
pub fn px(px: impl UiNum) -> Self {
Self {
px: Px::from_num(px),
..Self::ZERO
} }
} }
pub fn rel(rel: impl UiNum) -> Self { pub fn rel(rel: impl UiNum) -> Self {
Self { Self {
rel: Rel::from_num(rel), abs: 0.0,
..Self::ZERO rel: rel.to_f32(),
rest: 0.0,
} }
} }
pub fn leftover(ratio: impl UiNum) -> Self { pub fn rest(ratio: impl UiNum) -> Self {
Self { Self {
leftover: Weight::from_num(ratio), abs: 0.0,
..Self::ZERO rel: 0.0,
rest: ratio.to_f32(),
} }
} }
} }
@@ -193,26 +141,38 @@ impl LayoutLen {
pub mod len_fns { pub mod len_fns {
use super::*; use super::*;
pub fn px(px: impl UiNum) -> LayoutLen { pub fn abs(abs: impl UiNum) -> Len {
LayoutLen::px(px) Len {
abs: abs.to_f32(),
rel: 0.0,
rest: 0.0,
} }
pub fn rel(rel: impl UiNum) -> LayoutLen {
LayoutLen::rel(rel)
} }
pub fn leftover(ratio: impl UiNum) -> LayoutLen { pub fn rel(rel: impl UiNum) -> Len {
LayoutLen::leftover(ratio) Len {
abs: 0.0,
rel: rel.to_f32(),
rest: 0.0,
}
}
pub fn rest(ratio: impl UiNum) -> Len {
Len {
abs: 0.0,
rel: 0.0,
rest: ratio.to_f32(),
}
} }
} }
impl_op!(same LayoutLen Add add; px rel leftover); impl_op!(Len Add add; abs rel rest);
impl_op!(same LayoutLen Sub sub; px rel leftover); impl_op!(Len Sub sub; abs rel rest);
impl_op!(same Size Add add; x y); impl_op!(Size Add add; x y);
impl_op!(same Size Sub sub; x y); impl_op!(Size Sub sub; x y);
impl Default for LayoutLen { impl Default for Len {
fn default() -> Self { fn default() -> Self {
Self::leftover(1.0) Self::rest(1.0)
} }
} }
@@ -222,16 +182,16 @@ impl std::fmt::Display for Size {
} }
} }
impl std::fmt::Display for LayoutLen { impl std::fmt::Display for Len {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.px != Px::ZERO { if self.abs != 0.0 {
write!(f, "{} px;", self.px)?; write!(f, "{} abs;", self.abs)?;
} }
if self.rel != Rel::ZERO { if self.rel != 0.0 {
write!(f, "{} rel;", self.rel)?; write!(f, "{} rel;", self.rel)?;
} }
if self.leftover != Weight::ZERO { if self.rest != 0.0 {
write!(f, "{} leftover;", self.leftover)?; write!(f, "{} rest;", self.rest)?;
} }
Ok(()) Ok(())
} }
+173 -141
View File
@@ -1,46 +1,41 @@
use std::{fmt::Display, marker::Destruct}; use std::{fmt::Display, hash::Hash, marker::Destruct};
use super::*; use super::*;
use crate::{Px, PxVec2, Rel, UiNum, util::impl_op}; use crate::{
UiNum,
util::{LerpUtil, impl_op},
};
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)]
pub struct UiVec2 { pub struct UiVec2 {
pub x: Len, pub x: UiScalar,
pub y: Len, pub y: UiScalar,
} }
impl UiVec2 { impl UiVec2 {
pub const ZERO: Self = Self { pub const ZERO: Self = Self {
x: Len::ZERO, x: UiScalar::ZERO,
y: Len::ZERO, y: UiScalar::ZERO,
}; };
pub const fn new(x: Len, y: Len) -> Self { pub const fn new(x: UiScalar, y: UiScalar) -> Self {
Self { x, y } Self { x, y }
} }
pub const fn px(px: impl const Into<Vec2>) -> Self { pub const fn abs(abs: impl const Into<Vec2>) -> Self {
let px = px.into(); let abs = abs.into();
Self { Self {
x: Len::px(px.x), x: UiScalar::abs(abs.x),
y: Len::px(px.y), y: UiScalar::abs(abs.y),
}
}
/// From lengths already on the grid, with no fraction of a box.
pub const fn from_px(px: PxVec2) -> Self {
Self {
x: Len::from_parts(Rel::ZERO, px.x),
y: Len::from_parts(Rel::ZERO, px.y),
} }
} }
pub const fn rel(rel: impl const Into<Vec2>) -> Self { pub const fn rel(rel: impl const Into<Vec2>) -> Self {
let rel = rel.into(); let rel = rel.into();
Self { Self {
x: Len::rel(rel.x), x: UiScalar::rel(rel.x),
y: Len::rel(rel.y), y: UiScalar::rel(rel.y),
} }
} }
@@ -61,29 +56,30 @@ impl UiVec2 {
} }
} }
pub fn axis_mut(&mut self, axis: Axis) -> &mut Len { pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar {
match axis { match axis {
Axis::X => &mut self.x, Axis::X => &mut self.x,
Axis::Y => &mut self.y, Axis::Y => &mut self.y,
} }
} }
pub fn axis(&self, axis: Axis) -> Len { pub fn axis(&self, axis: Axis) -> UiScalar {
match axis { match axis {
Axis::X => self.x, Axis::X => self.x,
Axis::Y => self.y, Axis::Y => self.y,
} }
} }
/// Resolved against a box of `size`, which is where a fraction stops pub fn to_abs(&self, rel: Vec2) -> Vec2 {
/// being one and becomes a place. Vec2 {
pub fn to_px(&self, size: PxVec2) -> PxVec2 { x: self.x.to_abs(rel.x),
PxVec2::new(self.x.to_px(size.x), self.y.to_px(size.y)) y: self.y.to_abs(rel.y),
}
} }
pub const FULL_SIZE: Self = Self::rel(Vec2::ONE); pub const FULL_SIZE: Self = Self::rel(Vec2::ONE);
pub const fn from_axis(axis: Axis, aligned: Len, ortho: Len) -> Self { pub const fn from_axis(axis: Axis, aligned: UiScalar, ortho: UiScalar) -> Self {
match axis { match axis {
Axis::X => Self { Axis::X => Self {
x: aligned, x: aligned,
@@ -96,27 +92,34 @@ impl UiVec2 {
} }
} }
pub fn get_px(&self) -> Vec2 { pub fn get_abs(&self) -> Vec2 {
(self.x.px.to_f32(), self.y.px.to_f32()).into() (self.x.abs, self.y.abs).into()
} }
pub fn get_rel(&self) -> Vec2 { pub fn get_rel(&self) -> Vec2 {
(self.x.rel.to_f32(), self.y.rel.to_f32()).into() (self.x.rel, self.y.rel).into()
}
pub fn abs_mut(&mut self) -> Vec2View<'_> {
Vec2View {
x: &mut self.x.abs,
y: &mut self.y.abs,
}
} }
} }
impl Display for UiVec2 { impl Display for UiVec2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "rel{};px{}", self.get_rel(), self.get_px()) write!(f, "rel{};abs{}", self.get_rel(), self.get_abs())
} }
} }
impl_op!(same UiVec2 Add add; x y); impl_op!(UiVec2 Add add; x y);
impl_op!(same UiVec2 Sub sub; x y); impl_op!(UiVec2 Sub sub; x y);
const impl From<Vec2> for UiVec2 { const impl From<Vec2> for UiVec2 {
fn from(px: Vec2) -> Self { fn from(abs: Vec2) -> Self {
Self::px(px) Self::abs(abs)
} }
} }
@@ -124,149 +127,135 @@ const impl<T: const UiNum, U: const UiNum> From<(T, U)> for UiVec2
where where
(T, U): const Destruct, (T, U): const Destruct,
{ {
fn from(px: (T, U)) -> Self { fn from(abs: (T, U)) -> Self {
Self::px(px) Self::abs(abs)
} }
} }
/// A length along one axis: a fraction of the box it is measured in plus an
/// offset, `rel * box + px`. A position is the same number -- the length from
/// the start of the box to the point -- which is why a [`UiSpan`] is two of
/// these. Both parts are fixed point, so composing one through a chain of
/// boxes rounds only where it multiplies, and lands on the same number as any
/// other route to the same place.
///
/// It carries no claim on what a container has left over. That is
/// [`crate::LayoutLen`], which is this plus a weight, and which means nothing
/// to anyone but whoever divides the room.
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, Default, bytemuck::Zeroable)] #[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)]
pub struct Len { pub struct UiScalar {
pub rel: Rel, pub rel: f32,
pub px: Px, pub abs: f32,
} }
impl_op!(same Len Add add; rel px); impl Eq for UiScalar {}
impl_op!(same Len Sub sub; rel px); impl Hash for UiScalar {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
impl Len { state.write_u32(self.rel.to_bits());
pub const ZERO: Self = Self { state.write_u32(self.abs.to_bits());
rel: Rel::ZERO, }
px: Px::ZERO,
};
pub const FULL: Self = Self {
rel: Rel::ONE,
px: Px::ZERO,
};
pub const fn new(rel: f32, px: f32) -> Self {
Self::from_parts(Rel::from_f32(rel), Px::from_f32(px))
} }
/// From parts already on the grid, rather than numbers to be put on it. impl_op!(UiScalar Add add; rel abs);
pub const fn from_parts(rel: Rel, px: Px) -> Self { impl_op!(UiScalar Sub sub; rel abs);
Self { rel, px }
impl UiScalar {
pub const ZERO: Self = Self { rel: 0.0, abs: 0.0 };
pub const FULL: Self = Self { rel: 1.0, abs: 0.0 };
pub const fn new(rel: f32, abs: f32) -> Self {
Self { rel, abs }
} }
pub const fn rel(rel: f32) -> Self { pub const fn rel(rel: f32) -> Self {
Self::from_parts(Rel::from_f32(rel), Px::ZERO) Self { rel, abs: 0.0 }
} }
pub const fn px(px: f32) -> Self { pub const fn abs(abs: f32) -> Self {
Self::from_parts(Rel::ZERO, Px::from_f32(px)) Self { rel: 0.0, abs }
} }
pub const fn rel_min() -> Self { pub const fn rel_min() -> Self {
Self::ZERO Self::new(0.0, 0.0)
} }
pub const fn rel_max() -> Self { pub const fn rel_max() -> Self {
Self::FULL Self::new(1.0, 0.0)
} }
pub const fn max(&self, other: Self) -> Self { pub const fn max(&self, other: Self) -> Self {
Self { Self {
rel: self.rel.max(other.rel), rel: self.rel.max(other.rel),
px: self.px.max(other.px), abs: self.abs.max(other.abs),
} }
} }
pub const fn min(&self, other: Self) -> Self { pub const fn min(&self, other: Self) -> Self {
Self { Self {
rel: self.rel.min(other.rel), rel: self.rel.min(other.rel),
px: self.px.min(other.px), abs: self.abs.min(other.abs),
} }
} }
/// Both parts by the same fraction, which is what a part of a length pub const fn offset(mut self, amt: f32) -> Self {
/// means when the length is part pixels and part a fraction of a box. self.abs += amt;
pub const fn scale(&self, by: Rel) -> Self {
Self {
rel: self.rel.mul(by),
px: self.px.mul(by),
}
}
pub const fn offset(mut self, amt: Px) -> Self {
self.px = self.px.add(amt);
self self
} }
pub const fn within(&self, span: &UiSpan) -> Self { pub const fn within(&self, span: &UiSpan) -> Self {
let anchor = self.rel.lerp(span.start.rel, span.end.rel);
let offset = self.abs + self.rel.lerp(span.start.abs, span.end.abs);
Self { Self {
rel: self.rel.lerp(span.start.rel, span.end.rel), rel: anchor,
px: self.px.add(self.rel.lerp(span.start.px, span.end.px)), abs: offset,
} }
} }
pub const fn within_len(&self, len: Len) -> Self { /// Undoes `within`, and `None` where the span has a fixed length: every
/// fraction of it lands on the same `rel`, so none can be told apart.
pub fn outside(&self, span: &UiSpan) -> Option<Self> {
let rel = self.rel.lerp_inv(span.start.rel, span.end.rel)?;
let abs = self.abs - rel.lerp(span.start.abs, span.end.abs);
Some(Self { rel, abs })
}
pub fn within_len(&self, len: UiScalar) -> Self {
self.within(&UiSpan { self.within(&UiSpan {
start: Len::ZERO, start: UiScalar::ZERO,
end: len, end: len,
}) })
} }
pub fn select_len(&self, len: Len) -> Self { pub fn select_len(&self, len: UiScalar) -> Self {
len.within_len(*self) len.within_len(*self)
} }
pub const fn flip(&mut self) { pub const fn flip(&mut self) {
self.rel = Rel::ONE.sub(self.rel); self.rel = 1.0 - self.rel;
self.px = self.px.neg(); self.abs = -self.abs;
} }
pub const fn to(&self, end: Self) -> UiSpan { pub const fn to(&self, end: Self) -> UiSpan {
UiSpan { start: *self, end } UiSpan { start: *self, end }
} }
/// Resolved against a box of `len`, which is the only place a fraction pub const fn to_abs(&self, rel: f32) -> f32 {
/// becomes a number of pixels. self.rel * rel + self.abs
pub const fn to_px(&self, len: Px) -> Px {
self.px.add(len.mul(self.rel))
} }
} }
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct UiSpan { pub struct UiSpan {
pub start: Len, pub start: UiScalar,
pub end: Len, pub end: UiScalar,
} }
impl UiSpan { impl UiSpan {
pub const FULL: Self = Self { pub const FULL: Self = Self {
start: Len::ZERO, start: UiScalar::ZERO,
end: Len::FULL, end: UiScalar::FULL,
}; };
pub const fn rel(rel: f32) -> Self { pub const fn rel(rel: f32) -> Self {
Self { Self {
start: Len::rel(rel), start: UiScalar::rel(rel),
end: Len::rel(rel), end: UiScalar::rel(rel),
} }
} }
pub const fn new(start: Len, end: Len) -> Self { pub const fn new(start: UiScalar, end: UiScalar) -> Self {
Self { start, end } Self { start, end }
} }
@@ -274,19 +263,14 @@ impl UiSpan {
self.start.flip(); self.start.flip();
self.end.flip(); self.end.flip();
std::mem::swap(&mut self.start.rel, &mut self.end.rel); std::mem::swap(&mut self.start.rel, &mut self.end.rel);
std::mem::swap(&mut self.start.px, &mut self.end.px); std::mem::swap(&mut self.start.abs, &mut self.end.abs);
} }
pub const fn shift(&mut self, offset: Len) { pub const fn shift(&mut self, offset: UiScalar) {
self.start += offset; self.start += offset;
self.end += offset; self.end += offset;
} }
/// Composing a box through the one it sits in, and the hottest line in
/// layout. It used to skip the multiplies where a span was the whole of
/// its parent or the parent the whole of its own; both come out of the
/// multiply unchanged anyway, and the body those comparisons cost was
/// what kept the inliner from taking this at all.
pub const fn within(&self, parent: &Self) -> Self { pub const fn within(&self, parent: &Self) -> Self {
Self { Self {
start: self.start.within(parent), start: self.start.within(parent),
@@ -294,17 +278,15 @@ impl UiSpan {
} }
} }
pub const fn len(&self) -> Len { pub fn outside(&self, parent: &Self) -> Option<Self> {
self.end - self.start Some(Self {
start: self.start.outside(parent)?,
end: self.end.outside(parent)?,
})
} }
/// Both ends by the same amount, which is what moving a box without pub const fn len(&self) -> UiScalar {
/// changing its length does to every part of it. self.end - self.start
pub const fn translated(self, by: Len) -> Self {
Self {
start: self.start + by,
end: self.end + by,
}
} }
} }
@@ -316,17 +298,6 @@ pub struct UiRegion {
} }
impl UiRegion { impl UiRegion {
/// Every part of the box by the same amount on each axis. Done to the
/// whole region rather than an end at a time, because that is what it is
/// -- and because four adds in a row are four adds, where four asked for
/// separately are four sequences.
pub const fn translated(self, x: Len, y: Len) -> Self {
Self {
x: self.x.translated(x),
y: self.y.translated(y),
}
}
pub const FULL: Self = Self { pub const FULL: Self = Self {
x: UiSpan::FULL, x: UiSpan::FULL,
y: UiSpan::FULL, y: UiSpan::FULL,
@@ -380,10 +351,10 @@ impl UiRegion {
self self
} }
pub fn to_px(&self, size: PxVec2) -> PixelRegion { pub fn to_px(&self, size: Vec2) -> PixelRegion {
PixelRegion { PixelRegion {
top_left: self.top_left().to_px(size), top_left: self.top_left().get_rel() * size + self.top_left().get_abs(),
bot_right: self.bot_right().to_px(size), bot_right: self.bot_right().get_rel() * size + self.bot_right().get_abs(),
} }
} }
@@ -426,6 +397,50 @@ impl UiRegion {
} }
} }
/// Taking a drawing out of one box and putting it in another, checked once
/// for a whole subtree so that applying it cannot fail.
///
/// A box of a fixed length holds each part as an offset from its start rather
/// than as a fraction of it, so those parts can be carried to a box of the
/// same length but never stretched to a different one.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Remap {
from: UiRegion,
to: UiRegion,
}
impl Remap {
pub fn new(from: UiRegion, to: UiRegion) -> Option<Self> {
[Axis::X, Axis::Y]
.into_iter()
.all(|axis| {
let (from, to) = (from.axis(axis), to.axis(axis));
from.start.rel != from.end.rel || from.len() == to.len()
})
.then_some(Self { from, to })
}
pub fn apply(&self, region: UiRegion) -> UiRegion {
UiRegion {
x: Self::span(region.x, self.from.x, self.to.x),
y: Self::span(region.y, self.from.y, self.to.y),
}
}
fn span(span: UiSpan, from: UiSpan, to: UiSpan) -> UiSpan {
match span.outside(&from) {
Some(out) => out.within(&to),
// `new` admits this only where the two are the same length, so
// the difference between their starts is the whole move.
None => {
let mut span = span;
span.shift(to.start - from.start);
span
}
}
}
}
impl Display for UiRegion { impl Display for UiRegion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
@@ -438,21 +453,21 @@ impl Display for UiRegion {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct PixelRegion { pub struct PixelRegion {
pub top_left: PxVec2, pub top_left: Vec2,
pub bot_right: PxVec2, pub bot_right: Vec2,
} }
impl PixelRegion { impl PixelRegion {
pub fn contains(&self, pos: PxVec2) -> bool { pub fn contains(&self, pos: Vec2) -> bool {
pos.x >= self.top_left.x pos.x >= self.top_left.x
&& pos.x <= self.bot_right.x && pos.x <= self.bot_right.x
&& pos.y >= self.top_left.y && pos.y >= self.top_left.y
&& pos.y <= self.bot_right.y && pos.y <= self.bot_right.y
} }
pub fn size(&self) -> PxVec2 { pub fn size(&self) -> Vec2 {
self.bot_right - self.top_left self.bot_right - self.top_left
} }
} }
@@ -462,3 +477,20 @@ impl Display for PixelRegion {
write!(f, "{} -> {}", self.top_left, self.bot_right) write!(f, "{} -> {}", self.top_left, self.bot_right)
} }
} }
pub struct Vec2View<'a> {
pub x: &'a mut f32,
pub y: &'a mut f32,
}
impl Vec2View<'_> {
pub fn set(&mut self, other: Vec2) {
*self.x = other.x;
*self.y = other.y;
}
pub fn add(&mut self, other: Vec2) {
*self.x += other.x;
*self.y += other.y;
}
}
-4
View File
@@ -120,10 +120,6 @@ impl<T: Default> Layers<T> {
} }
impl DrawLayers { impl DrawLayers {
/// Inlined on purpose: it is one call per glyph, the innermost thing a
/// frame does, and whether the inliner takes it turns out to depend on
/// unrelated code elsewhere in the crate -- 12% of a resize frame.
#[inline]
pub fn write<P: Primitive>( pub fn write<P: Primitive>(
&mut self, &mut self,
layer: LayerId, layer: LayerId,
+11 -152
View File
@@ -1,17 +1,11 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, TimerKind};
use crate::{ use crate::{
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, Px, PxVec2, RegionAlign, UiColor, Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
util::Vec2,
}; };
use parley::{ use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
}; };
use std::{ use std::hash::{DefaultHasher, Hash, Hasher};
collections::VecDeque,
hash::{DefaultHasher, Hash, Hasher},
};
use swash::{ use swash::{
FontRef, FontRef,
scale::{Render, ScaleContext, Source, StrikeWith}, scale::{Render, ScaleContext, Source, StrikeWith},
@@ -23,32 +17,8 @@ pub struct TextData {
pub layout_ctx: LayoutContext<UiColor>, pub layout_ctx: LayoutContext<UiColor>,
scale_ctx: ScaleContext, scale_ctx: ScaleContext,
pub atlas: GlyphAtlas, pub atlas: GlyphAtlas,
spare: VecDeque<Placed>,
} }
/// The glyphs of one text at one width. A buffer holds the ones it is drawn
/// as; these are the ones it had before, kept because a container measures a
/// child by drawing it in a box it may not keep, and so comes back to widths
/// it has already asked for.
struct Placed {
/// Where the glyphs land is a function of these three and nothing else,
/// so no widget or buffer identity is involved and two texts of the same
/// words share an answer.
text: String,
key: LayoutKey,
glyphs: RenderedText,
}
/// How many to keep. Bounding the whole store rather than each buffer is what
/// makes this a fixed cost instead of one a tree of ten thousand texts pays
/// ten thousand times; the re-asks come from laying out one subtree, so they
/// are close together and few are needed. Instructions over 500 resize frames
/// of `tests/revision_cost.rs`, both the repeating widths and the sweep that
/// cannot hit across frames: 13.7B at 32, 12.1B at 64, 10.4B and 12.1B at 128,
/// and nothing past that -- so 128, which is no worse in the case that never
/// repeats and better in the one that does.
const SPARE_PLACED: usize = 128;
impl Default for TextData { impl Default for TextData {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -56,7 +26,6 @@ impl Default for TextData {
layout_ctx: LayoutContext::new(), layout_ctx: LayoutContext::new(),
scale_ctx: ScaleContext::new(), scale_ctx: ScaleContext::new(),
atlas: GlyphAtlas::default(), atlas: GlyphAtlas::default(),
spare: VecDeque::new(),
} }
} }
} }
@@ -112,9 +81,6 @@ pub struct TextBuffer {
text: String, text: String,
layout: Layout<UiColor>, layout: Layout<UiColor>,
layout_key: Option<LayoutKey>, layout_key: Option<LayoutKey>,
/// The glyphs placed from `layout`, so drawing this text again at the
/// width it already has places them once.
placed: Option<RenderedText>,
} }
#[derive(PartialEq)] #[derive(PartialEq)]
@@ -129,7 +95,6 @@ impl TextBuffer {
text: text.into(), text: text.into(),
layout: Layout::new(), layout: Layout::new(),
layout_key: None, layout_key: None,
placed: None,
} }
} }
@@ -154,28 +119,15 @@ impl TextBuffer {
if text != self.text { if text != self.text {
self.text = text; self.text = text;
self.layout_key = None; self.layout_key = None;
self.placed = None;
} }
} }
/// Invalidates the layout and returns the underlying string for editing. /// Invalidates the layout and returns the underlying string for editing.
pub fn edit(&mut self) -> &mut String { pub fn edit(&mut self) -> &mut String {
self.layout_key = None; self.layout_key = None;
self.placed = None;
&mut self.text &mut self.text
} }
/// The glyphs of the shaping it is drawn as, once they are placed.
pub fn rendered(&self) -> Option<&RenderedText> {
self.placed.as_ref()
}
/// The width its shaping wraps at, and `None` where it does not wrap or
/// has not been shaped.
pub fn wrap_width(&self) -> Option<f32> {
self.layout_key.as_ref()?.max_width
}
pub fn size(&self) -> Vec2 { pub fn size(&self) -> Vec2 {
Vec2::new(self.layout.width(), self.layout.height()) Vec2::new(self.layout.width(), self.layout.height())
} }
@@ -186,62 +138,8 @@ impl TextBuffer {
max_width: width, max_width: width,
}; };
if self.layout_key.as_ref() == Some(&layout_key) { if self.layout_key.as_ref() == Some(&layout_key) {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextShapeHits);
return; return;
} }
// A greedy break at one width is the same break at every width down
// to the longest line it produced: each line still fits, and none can
// take a word that would not fit in the wider box. So the layout in
// hand already answers, and re-breaking would only be work.
//
// At the longest line exactly, with no margin below it. A narrower
// width really does break differently, so answering one from the
// break in hand is how a warm tree keeps lines a cold tree would
// never produce. The margin was here because a text reports the
// width it used and a parent hands that back; the report is the step
// at or above its longest line now, so what comes back fits.
if let Some(key) = &self.layout_key
&& key.attrs == *attrs
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
&& want <= broke_at
&& want >= self.layout.width()
{
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextShapeHits);
return;
}
let same_shaping = self
.layout_key
.as_ref()
.is_some_and(|key| key.attrs == *attrs);
let old_key = self.layout_key.replace(layout_key);
// The glyphs it holds are of the width it held, which the layout may
// well come back to.
if let Some(key) = old_key
&& let Some(glyphs) = self.placed.take()
{
data.keep_placed(Placed {
text: self.text.clone(),
key,
glyphs,
});
}
// Only the line breaking depends on the width: the shaped runs under
// it are a function of the text and the attrs, and parley re-breaks
// them in place. So a new width is a break, not a shaping.
if same_shaping {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextBreaks);
#[cfg(feature = "layout-diagnostics")]
let _break = diag::timer(TimerKind::TextBreak);
self.break_lines(width);
return;
}
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextShapes);
#[cfg(feature = "layout-diagnostics")]
let _shape = diag::timer(TimerKind::TextShape);
let mut builder = data let mut builder = data
.layout_ctx .layout_ctx
.ranged_builder(&mut data.font_ctx, &self.text, 1.0, true); .ranged_builder(&mut data.font_ctx, &self.text, 1.0, true);
@@ -252,13 +150,10 @@ impl TextBuffer {
))); )));
builder.push_default(StyleProperty::Brush(attrs.color)); builder.push_default(StyleProperty::Brush(attrs.color));
builder.build_into(&mut self.layout, &self.text); builder.build_into(&mut self.layout, &self.text);
self.break_lines(width);
}
fn break_lines(&mut self, width: Option<f32>) {
self.layout.break_all_lines(width); self.layout.break_all_lines(width);
self.layout self.layout
.align(Alignment::Start, AlignmentOptions::default()); .align(Alignment::Start, AlignmentOptions::default());
self.layout_key = Some(layout_key);
} }
} }
@@ -301,9 +196,9 @@ impl TextData {
}; };
placed.push(PlacedGlyph { placed.push(PlacedGlyph {
entry, entry,
offset: PxVec2::new( offset: Vec2::new(
Px::from_int(glyph.x.floor() as i32 + entry.left), glyph.x.floor() + entry.left as f32,
Px::from_int(glyph.y.floor() as i32 - entry.top), glyph.y.floor() - entry.top as f32,
), ),
}); });
} }
@@ -370,54 +265,18 @@ pub struct RenderedText {
} }
impl TextData { impl TextData {
/// The glyphs of this text at this width, taken out of what is kept. pub fn render(
fn take_placed(&mut self, text: &str, key: &LayoutKey) -> Option<RenderedText> {
// From the newest, since a re-ask is usually of something recent.
let at = self
.spare
.iter()
.rposition(|spare| spare.key == *key && spare.text == text)?;
self.spare.remove(at).map(|spare| spare.glyphs)
}
fn keep_placed(&mut self, placed: Placed) {
if self.spare.len() >= SPARE_PLACED {
self.spare.pop_front();
}
self.spare.push_back(placed);
}
pub fn render<'b>(
&mut self, &mut self,
buffer: &'b mut TextBuffer, buffer: &mut TextBuffer,
attrs: &TextAttrs, attrs: &TextAttrs,
width: Option<f32>, width: Option<f32>,
) -> &'b RenderedText { ) -> RenderedText {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextRenders);
#[cfg(feature = "layout-diagnostics")]
let _render = diag::timer(TimerKind::TextRender);
buffer.shape(self, attrs, width); buffer.shape(self, attrs, width);
// Only asked for when the buffer no longer holds them: taking one out let glyphs = self.place(buffer);
// of the store to then drop it would throw an answer away.
let placed = buffer.placed.take().or_else(|| {
let key = buffer.layout_key.as_ref()?;
self.take_placed(&buffer.text, key)
});
let placed = match placed {
Some(placed) => placed,
None => {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::GlyphPlacements);
#[cfg(feature = "layout-diagnostics")]
let _place = diag::timer(TimerKind::GlyphPlacement);
RenderedText { RenderedText {
glyphs: self.place(buffer), glyphs,
size: buffer.size(), size: buffer.size(),
color: attrs.color, color: attrs.color,
} }
} }
};
buffer.placed.insert(placed)
}
} }
+2 -4
View File
@@ -1,5 +1,5 @@
use crate::{ use crate::{
PatchRect, PxVec2, PatchRect,
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
use image::RgbaImage; use image::RgbaImage;
@@ -241,7 +241,5 @@ fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct PlacedGlyph { pub struct PlacedGlyph {
pub entry: GlyphEntry, pub entry: GlyphEntry,
/// Whole pixels from the origin of the text to this glyph's top-left, pub offset: Vec2,
/// on the grid once here rather than on every frame that draws it.
pub offset: PxVec2,
} }
+8 -52
View File
@@ -1,10 +1,11 @@
use crate::{UiRegion, util::Id, util::Vec2}; use crate::{UiRegion, util::Id};
use wgpu::*; use wgpu::*;
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
pub struct WindowUniform { pub struct WindowUniform {
pub dim: Vec2, pub width: f32,
pub height: f32,
} }
#[repr(C)] #[repr(C)]
@@ -12,19 +13,15 @@ pub struct WindowUniform {
pub struct PrimitiveInstance { pub struct PrimitiveInstance {
pub region: UiRegion, pub region: UiRegion,
pub mask_idx: MaskIdx, pub mask_idx: MaskIdx,
pub move_idx: MoveIdx,
} }
impl PrimitiveInstance { impl PrimitiveInstance {
// The region's four scalars, each a `Rel` beside a `Px`: whole counts const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![
// that the shader decodes, rather than the numbers themselves. 0 => Float32x2,
const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![ 1 => Float32x2,
0 => Sint32x2, 2 => Float32x2,
1 => Sint32x2, 3 => Float32x2,
2 => Sint32x2,
3 => Sint32x2,
4 => Uint32, 4 => Uint32,
5 => Uint32,
]; ];
pub fn desc() -> VertexBufferLayout<'static> { pub fn desc() -> VertexBufferLayout<'static> {
@@ -46,45 +43,4 @@ impl MaskIdx {
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Mask { pub struct Mask {
pub region: UiRegion, pub region: UiRegion,
pub move_idx: MoveIdx,
}
/// Its own type rather than another `Id<u32>`, because it sits beside
/// `MaskIdx` in an instance and the two must not be swappable.
#[repr(transparent)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MoveIdx(u32);
impl MoveIdx {
pub const NONE: Self = Self(u32::MAX);
pub(crate) fn slot(idx: usize) -> Self {
Self(idx as u32)
}
pub(crate) fn idx(self) -> usize {
self.0 as usize
}
}
/// One link of the chain a primitive's position is resolved through: the box
/// its contents are placed within, given in the coordinates of the slot it
/// names. Moving or resizing a subtree writes its own slot and nothing else.
///
/// The identity is `UiRegion::FULL`, not zero: a zeroed entry is a box of no
/// extent, which collapses everything under it to a point.
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct MoveOffset {
pub region: UiRegion,
pub parent: MoveIdx,
}
unsafe impl bytemuck::Pod for MoveOffset {}
unsafe impl bytemuck::Zeroable for MoveOffset {}
impl MoveOffset {
pub fn new(parent: MoveIdx, region: UiRegion) -> Self {
Self { region, parent }
}
} }
+13 -84
View File
@@ -17,22 +17,11 @@ mod texture;
mod util; mod util;
pub use atlas::*; pub use atlas::*;
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset}; pub use data::{Mask, MaskIdx};
pub use primitive::*; pub use primitive::*;
const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
fn module_source(wgsl: &str) -> String {
// The steps come from the same constants the CPU counts in, rather than
// a second copy of them written into the shader: a grid the two disagree
// about puts every coordinate somewhere else.
format!(
"const PX_STEP: f32 = 1.0 / {}.0;\nconst REL_STEP: f32 = 1.0 / {}.0;\n{PRELUDE}\n{wgsl}",
1u32 << crate::PX_SHIFT,
1u32 << crate::REL_SHIFT,
)
}
pub struct UiRenderNode { pub struct UiRenderNode {
shared_layout: BindGroupLayout, shared_layout: BindGroupLayout,
shared_group: BindGroup, shared_group: BindGroup,
@@ -45,7 +34,6 @@ pub struct UiRenderNode {
active: Vec<usize>, active: Vec<usize>,
window_buffer: Buffer, window_buffer: Buffer,
masks: ArrBuf<Mask>, masks: ArrBuf<Mask>,
moves: ArrBuf<MoveOffset>,
} }
struct RenderLayer { struct RenderLayer {
@@ -106,8 +94,7 @@ impl UiRenderNode {
self.active.push(i); self.active.push(i);
for change in draws.apply_free() { for change in draws.apply_free() {
if let Some(inst) = ui_render.active.get_mut(&change.id) { if let Some(inst) = ui_render.active.get_mut(&change.id) {
for primitive in &mut inst.primitives { for h in &mut inst.primitives {
let h = &mut primitive.handle;
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old { if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
h.inst_idx = change.new; h.inst_idx = change.new;
break; break;
@@ -140,35 +127,32 @@ impl UiRenderNode {
for primitive in &mut self.primitives { for primitive in &mut self.primitives {
primitive.render.update(ui); primitive.render.update(ui);
} }
let mut regroup = false;
if ui.masks.changed { if ui.masks.changed {
ui.masks.changed = false; ui.masks.changed = false;
regroup |= self.masks.update(device, queue, &ui.masks[..]); if self.masks.update(device, queue, &ui.masks[..]) {
}
if ui_render.moves.changed {
ui_render.moves.changed = false;
regroup |= self.moves.update(device, queue, ui_render.moves.entries());
}
if regroup {
self.shared_group = Self::shared_group( self.shared_group = Self::shared_group(
device, device,
&self.shared_layout, &self.shared_layout,
&self.window_buffer, &self.window_buffer,
&self.masks, &self.masks,
&self.moves,
); );
} }
} }
}
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) { pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
let size = size.into(); let size = size.into();
let slice = &[WindowUniform { dim: size }]; let slice = &[WindowUniform {
width: size.x,
height: size.y,
}];
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice)); queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
} }
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self { pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
let window_uniform = WindowUniform { let window_uniform = WindowUniform {
dim: Vec2::new(config.width as f32, config.height as f32), width: config.width as f32,
height: config.height as f32,
}; };
let window_buffer = device.create_buffer_init(&BufferInitDescriptor { let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("window"), label: Some("window"),
@@ -182,13 +166,7 @@ impl UiRenderNode {
BufferUsages::STORAGE | BufferUsages::COPY_DST, BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui masks", "ui masks",
); );
let moves = ArrBuf::new( let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui move offsets",
);
let shared_group =
Self::shared_group(device, &shared_layout, &window_buffer, &masks, &moves);
Self { Self {
shared_layout, shared_layout,
@@ -199,7 +177,6 @@ impl UiRenderNode {
layers: HashMap::default(), layers: HashMap::default(),
active: Vec::new(), active: Vec::new(),
masks, masks,
moves,
} }
} }
@@ -234,7 +211,7 @@ impl UiRenderNode {
) -> RenderPipeline { ) -> RenderPipeline {
let module = device.create_shader_module(ShaderModuleDescriptor { let module = device.create_shader_module(ShaderModuleDescriptor {
label: Some(label), label: Some(label),
source: ShaderSource::Wgsl(module_source(wgsl).into()), source: ShaderSource::Wgsl(format!("{PRELUDE}\n{wgsl}").into()),
}); });
device.create_render_pipeline(&RenderPipelineDescriptor { device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some(label), label: Some(label),
@@ -275,8 +252,7 @@ impl UiRenderNode {
}) })
} }
/// What every draw in the ui is given: the window, the masks and the /// What every draw in the ui is given: the window and the masks.
/// move chain every position is resolved through.
fn shared_layout(device: &Device) -> BindGroupLayout { fn shared_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor { device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[ entries: &[
@@ -300,16 +276,6 @@ impl UiRenderNode {
}, },
count: None, count: None,
}, },
BindGroupLayoutEntry {
binding: 2,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: BufferSize::new(size_of::<MoveOffset>() as u64),
},
count: None,
},
], ],
label: Some("ui shared"), label: Some("ui shared"),
}) })
@@ -320,7 +286,6 @@ impl UiRenderNode {
layout: &BindGroupLayout, layout: &BindGroupLayout,
window: &Buffer, window: &Buffer,
masks: &ArrBuf<Mask>, masks: &ArrBuf<Mask>,
moves: &ArrBuf<MoveOffset>,
) -> BindGroup { ) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor { device.create_bind_group(&BindGroupDescriptor {
layout, layout,
@@ -333,10 +298,6 @@ impl UiRenderNode {
binding: 1, binding: 1,
resource: masks.buffer.as_entire_binding(), resource: masks.buffer.as_entire_binding(),
}, },
BindGroupEntry {
binding: 2,
resource: moves.buffer.as_entire_binding(),
},
], ],
label: Some("ui shared"), label: Some("ui shared"),
}) })
@@ -413,35 +374,3 @@ impl ListBuffers {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::module_source;
use wgpu::naga::{
front::wgsl,
valid::{Capabilities, ValidationFlags, Validator},
};
/// Every shader file, composed as the renderer composes it, parses and
/// validates with no device -- so an edit that breaks one fails here and
/// not in the first window opened.
#[test]
fn every_shader_validates() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src/render/shader");
let mut checked = 0;
for entry in std::fs::read_dir(dir).unwrap() {
let path = entry.unwrap().path();
if path.extension().is_none_or(|e| e != "wgsl") || path.ends_with("prelude.wgsl") {
continue;
}
let source = module_source(&std::fs::read_to_string(&path).unwrap());
let module = wgsl::parse_str(&source)
.unwrap_or_else(|e| panic!("{}: {}", path.display(), e.emit_to_string(&source)));
Validator::new(ValidationFlags::all(), Capabilities::all())
.validate(&module)
.unwrap_or_else(|e| panic!("{}: {e:?}", path.display()));
checked += 1;
}
assert!(checked > 0, "no shaders found in {dir}");
}
}
+5 -11
View File
@@ -3,7 +3,7 @@ use std::{any::TypeId, marker::PhantomData};
use crate::{ use crate::{
Color, TextureHandle, UiData, UiRegion, WidgetId, Color, TextureHandle, UiData, UiRegion, WidgetId,
render::{ render::{
data::{MaskIdx, MoveIdx, PrimitiveInstance}, data::{MaskIdx, PrimitiveInstance},
page::GlyphRender, page::GlyphRender,
texture::ImageRender, texture::ImageRender,
}, },
@@ -246,7 +246,6 @@ impl LayerDraws {
primitive, primitive,
region, region,
mask_idx, mask_idx,
move_idx,
}: PrimitiveInst<P>, }: PrimitiveInst<P>,
) -> PrimitiveHandle { ) -> PrimitiveHandle {
self.updated = true; self.updated = true;
@@ -259,11 +258,7 @@ impl LayerDraws {
.get_or_insert_with(InstanceList::new::<P>) .get_or_insert_with(InstanceList::new::<P>)
.push( .push(
id, id,
PrimitiveInstance { PrimitiveInstance { region, mask_idx },
region,
mask_idx,
move_idx,
},
bytemuck::bytes_of(&primitive), bytemuck::bytes_of(&primitive),
); );
PrimitiveHandle { PrimitiveHandle {
@@ -309,7 +304,6 @@ pub struct PrimitiveInst<P> {
pub primitive: P, pub primitive: P,
pub region: UiRegion, pub region: UiRegion,
pub mask_idx: MaskIdx, pub mask_idx: MaskIdx,
pub move_idx: MoveIdx,
} }
pub struct PrimitiveChange { pub struct PrimitiveChange {
@@ -353,7 +347,7 @@ impl RectPrimitive {
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph /// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects. /// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
#[repr(C)] #[repr(C, align(8))]
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct GlyphPrimitive { pub struct GlyphPrimitive {
pub uv_min: Vec2, pub uv_min: Vec2,
@@ -364,8 +358,8 @@ pub struct GlyphPrimitive {
pub flags: u32, pub flags: u32,
} }
// Manual rather than derived: `Vec2`'s alignment leaves four bytes of padding // Manual rather than derived: the align(8) leaves four bytes of padding, which
// here, which is how WGSL lays the struct out. // is how WGSL lays the struct out.
unsafe impl bytemuck::Pod for GlyphPrimitive {} unsafe impl bytemuck::Pod for GlyphPrimitive {}
unsafe impl bytemuck::Zeroable for GlyphPrimitive {} unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
impl Primitive for GlyphPrimitive { impl Primitive for GlyphPrimitive {
+20 -117
View File
@@ -7,121 +7,32 @@
var<uniform> window: WindowUniform; var<uniform> window: WindowUniform;
@group(0) @binding(1) @group(0) @binding(1)
var<storage> masks: array<Mask>; var<storage> masks: array<Mask>;
@group(0) @binding(2)
var<storage> move_offsets: array<MoveOffset>;
struct WindowUniform { struct WindowUniform {
dim: vec2<f32>, dim: vec2<f32>,
}; };
struct Mask { struct Mask {
x: RawSpan,
y: RawSpan,
move_idx: u32,
}
struct MoveOffset {
x: RawSpan,
y: RawSpan,
parent: u32,
}
// `PX_STEP` and `REL_STEP` are prepended from `iris_core`'s own constants:
// what it stores is a whole count of each, both powers of two, so decoding
// is exact and the number here is the number the CPU decided.
// Every coordinate the CPU decided is a whole count of `PX_STEP`, so one that
// composes to within half a step of a pixel boundary is on that boundary and
// belongs to the pixel above it. Flooring the product instead drops a pixel
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of
// `REL_STEP` as 383.99998, and five tabs each lose their last column.
//
// Taken over the whole coordinate, fraction and pixels summed, since a floor
// does not distribute over a sum: floored apart, a half of one and a half of
// the other lose the pixel the two together make.
fn snap_floor(v: vec2<f32>) -> vec2<f32> {
return floor(v + PX_STEP * 0.5);
}
struct RawScalar {
rel: i32,
px: i32,
}
struct RawSpan {
start: RawScalar,
end: RawScalar,
}
fn scalar_of(raw: RawScalar) -> Len {
return Len(f32(raw.rel) * REL_STEP, f32(raw.px) * PX_STEP);
}
fn span_of(raw: RawSpan) -> UiSpan {
return UiSpan(scalar_of(raw.start), scalar_of(raw.end));
}
fn scalar_of_pair(raw: vec2<i32>) -> Len {
return Len(f32(raw.x) * REL_STEP, f32(raw.y) * PX_STEP);
}
struct Region {
x: UiSpan, x: UiSpan,
y: UiSpan, y: UiSpan,
} }
const MOVE_NONE: u32 = 4294967295u;
// Keep in step with `iris_core::CHAIN_LIMIT`. It bounds a malformed cycle
// rather than any real tree, and the CPU walk uses the same number so both
// resolve a deep one the same way.
const CHAIN_LIMIT: u32 = 64u;
// The same expression `Len::within` uses, in floats rather than on the
// CPU's grid: a move is resolved here so that scrolling a subtree writes one
// entry instead of walking it. What has to hold is that this agrees with
// itself frame to frame, not that it matches the CPU to the last bit.
fn scalar_within(s: Len, p: UiSpan) -> Len {
return Len(
p.start.rel + (p.end.rel - p.start.rel) * s.rel,
s.px + (p.start.px + (p.end.px - p.start.px) * s.rel),
);
}
fn span_within(s: UiSpan, p: UiSpan) -> UiSpan {
return UiSpan(scalar_within(s.start, p), scalar_within(s.end, p));
}
fn resolve_move(idx: u32, local: Region) -> Region {
var r = local;
var at = idx;
for (var step = 0u; step < CHAIN_LIMIT; step++) {
if at == MOVE_NONE {
break;
}
let entry = move_offsets[at];
r = Region(span_within(r.x, span_of(entry.x)), span_within(r.y, span_of(entry.y)));
at = entry.parent;
}
return r;
}
struct UiSpan { struct UiSpan {
start: Len, start: UiScalar,
end: Len, end: UiScalar,
} }
struct Len { struct UiScalar {
rel: f32, rel: f32,
px: f32, abs: f32,
} }
struct InstanceInput { struct InstanceInput {
@location(0) x_start: vec2<i32>, @location(0) x_start: vec2<f32>,
@location(1) x_end: vec2<i32>, @location(1) x_end: vec2<f32>,
@location(2) y_start: vec2<i32>, @location(2) y_start: vec2<f32>,
@location(3) y_end: vec2<i32>, @location(3) y_end: vec2<f32>,
@location(4) mask_idx: u32, @location(4) mask_idx: u32,
@location(5) move_idx: u32,
} }
struct VertexOutput { struct VertexOutput {
@@ -141,18 +52,13 @@ fn vs_main(
) -> VertexOutput { ) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
let local = Region( let top_left_rel = vec2(in.x_start.x, in.y_start.x);
UiSpan(scalar_of_pair(in.x_start), scalar_of_pair(in.x_end)), let top_left_abs = vec2(in.x_start.y, in.y_start.y);
UiSpan(scalar_of_pair(in.y_start), scalar_of_pair(in.y_end)), let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
); let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
let r = resolve_move(in.move_idx, local);
let top_left_rel = vec2(r.x.start.rel, r.y.start.rel);
let top_left_px = vec2(r.x.start.px, r.y.start.px);
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
let bot_right_px = vec2(r.x.end.px, r.y.end.px);
let top_left = snap_floor(top_left_rel * window.dim + top_left_px); let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
let bot_right = snap_floor(bot_right_rel * window.dim + bot_right_px); let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
let size = bot_right - top_left; let size = bot_right - top_left;
let uv = vec2<f32>( let uv = vec2<f32>(
@@ -175,16 +81,13 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
return color; return color;
} }
let mask = masks[in.mask_idx]; let mask = masks[in.mask_idx];
// Its own chain, not the drawn primitive's, so a stationary viewport let tl = vec2(mask.x.start.rel, mask.y.start.rel);
// clips content that moves inside it. let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs);
let m = resolve_move(mask.move_idx, Region(span_of(mask.x), span_of(mask.y))); let br = vec2(mask.x.end.rel, mask.y.end.rel);
let tl = vec2(m.x.start.rel, m.y.start.rel); let br_abs = vec2(mask.x.end.abs, mask.y.end.abs);
let tl_px = vec2(m.x.start.px, m.y.start.px);
let br = vec2(m.x.end.rel, m.y.end.rel);
let br_px = vec2(m.x.end.px, m.y.end.px);
let top_left = snap_floor(tl * window.dim + tl_px); let top_left = floor(tl * window.dim) + floor(tl_abs);
let bot_right = snap_floor(br * window.dim + br_px); let bot_right = floor(br * window.dim) + floor(br_abs);
let pos = in.clip_position.xy; let pos = in.clip_position.xy;
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y { if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
return color * 0.0; return color * 0.0;
+6 -76
View File
@@ -1,90 +1,20 @@
use crate::{ use crate::{LayerId, MaskIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId};
DrawRegion, LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, RegionAlign, RetainedPrimitive,
Size, TextureHandle, UiRegion, UiVec2, WidgetId,
};
/// What is kept of a widget its parent has asked about. `drawn` says whether /// important non rendering data for retained drawing
/// it currently draws; one that does not is kept so that a change to it, or
/// under it, still reaches whoever asked.
#[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
/// was asked about, and what a fraction under it is a fraction of. A
/// local redraw asks here.
pub region: UiRegion, pub region: UiRegion,
/// Where its drawing sits inside that box, in the box's own coordinates. /// What the widget said it used of `region`, the last time it drew.
pub placement: UiRegion,
/// The same box as lengths of its parent's box, which is the one route
/// to a box in pixels: a draw threads these down a level at a time, and
/// [`crate::UiRenderState::redraw`] takes the same steps back up.
pub given_len: UiVec2,
/// The lengths of the box its parent first asked about it in, as
/// lengths of the box the parent was itself offered. Any later box it
/// was given was decided knowing its answer, so this is the question
/// asked again -- and a chain of fractions has no frame in it, which is
/// why a region node between two widgets cannot break it.
pub offer_len: UiVec2,
pub offer_placement: [Option<crate::UiSpan>; 2],
/// The measured answer and its dependencies. A hint-only dependency or
/// a widget first encountered during placement has no measurement yet.
pub answer: Option<(Size, LayoutHolds)>,
/// What the widget said it used of its box, the last time it drew.
pub size: Size, pub size: Size,
/// The frame, extent and explicit placement reads that this drawing holds for.
pub holds: LayoutHolds,
pub drawn: bool,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
/// How far down the tree it was drawn, the root being 1. Carried down a
/// draw rather than worked out by walking up, so it is right for every
/// widget a frame visits and cannot drift while one is being drawn.
pub depth: usize,
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
pub primitives: Vec<RetainedPrimitive>, pub primitives: Vec<PrimitiveHandle>,
pub mask_region: Option<DrawRegion>,
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 /// Whether it read the output's size, and so is wrong when that changes.
/// opted in, otherwise the nearest ancestor's. pub reads_output: bool,
pub move_idx: MoveIdx,
/// The declared lengths whoever drew this widget resolved into its box.
/// A change to one moves a box this widget cannot fix by drawing again,
/// and comparing them is what says so.
pub declared: [Option<LayoutLen>; 2],
/// The axes along which its parent chose its box from its own answer,
/// so a local redraw asks the question its parent asked.
pub decided: [bool; 2],
/// Its alignment when it was last drawn, which a change to the property
/// is found against.
pub own_align: RegionAlign,
/// The movable region whose coordinates `region` uses.
pub parent_move: MoveIdx,
/// The mask its drawing is clipped to: one it set itself, or the one it
/// inherited from whoever drew it.
pub mask: MaskIdx, pub mask: MaskIdx,
/// That inherited one. The two differ exactly where the widget set a
/// mask of its own, which is the one it owns and the one a move rewrites
/// -- and the one a redraw of it must not be handed back, since setting
/// a mask asserts there is none.
pub parent_mask: MaskIdx,
pub layer: LayerId, pub layer: LayerId,
} }
impl ActiveData {
/// Whether what it answered still stands for a box of these pixel
/// lengths -- the box it was asked in, where `holds` is about the box its
/// answer then chose.
pub fn answers_at(&self, px: crate::PxVec2) -> bool {
self.answer.is_some_and(|(_, holds)| {
holds.contains(
px,
UiRegion {
x: self.offer_placement[0].unwrap_or(crate::UiSpan::FULL),
y: self.offer_placement[1].unwrap_or(crate::UiSpan::FULL),
},
)
})
}
}
-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,
}
-165
View File
@@ -1,165 +0,0 @@
use crate::{Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow};
use std::ops::RangeInclusive;
/// The lengths of a box, in pixels, that one drawing of a widget holds for:
/// give the widget any box in this range and it draws the same thing and
/// reports the same size. A widget that never reads its box in pixels holds
/// for every length; one that does holds for the one it read unless it says
/// otherwise, and a parent holds for whatever keeps every child it asked
/// about or drew inside its own range.
///
/// The ends are lengths on the grid rather than floats with a tolerance
/// around them: a box offered back at the length a widget reported comes back
/// as the same number, so a range means what it says. The one place a range
/// is wider than the length it came from is [`Self::through`], and what it is
/// wider by is the floor that inverting a fraction undoes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Holds {
pub lo: Px,
pub hi: Px,
}
impl Holds {
pub const ANY: Self = Self {
lo: Px::MIN,
hi: Px::MAX,
};
pub const fn at(len: Px) -> Self {
Self { lo: len, hi: len }
}
pub const fn contains(&self, len: Px) -> bool {
len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
}
pub const fn and(self, other: Self) -> Self {
Self {
lo: self.lo.max(other.lo),
hi: self.hi.min(other.hi),
}
}
/// What a box has to be for a part of it, `len` of the box long, to stay
/// in this range: the exact preimage of `px + floor(rel * box)`, which is
/// the one way a box in pixels is reached. A part with no relative extent
/// is a fixed length -- it was drawn at that length and any box keeps it
/// there.
///
/// The answer is an interval even where this range is a single length,
/// because the multiply on the way in drops to the step below and many
/// boxes therefore give one length. That is a floor rather than an
/// allowance: inverting it is two divisions and nothing else, and the
/// whole of a box maps back to itself.
pub const fn through(self, len: Len) -> Self {
let rel = len.rel.raw() as i64;
if rel == 0 {
return Self::ANY;
}
let px = len.px.raw() as i64;
// `floor(rel * box) >= lo - px` is `rel * box >= (lo - px) << REL`, and
// `floor(rel * box) <= hi - px` is `rel * box < (hi - px + 1) << REL`.
let lo = (self.lo.raw() as i64 - px) << REL_SHIFT;
let hi = (((self.hi.raw() as i64 - px) + 1) << REL_SHIFT) - 1;
// Dividing by a negative fraction turns the ends around, so which
// bound each comes from is decided before dividing rather than by
// taking the min and max of four divisions.
match rel > 0 {
true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)),
false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)),
}
}
const fn raws(lo: i64, hi: i64) -> Self {
Self {
lo: Px::from_raw(narrow(lo)),
hi: Px::from_raw(narrow(hi)),
}
}
}
impl From<RangeInclusive<Px>> for Holds {
fn from(range: RangeInclusive<Px>) -> Self {
Self {
lo: *range.start(),
hi: *range.end(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Rel;
#[test]
fn through_reverses_a_range_for_a_negative_fraction() {
// `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
let part = Len::from_parts(Rel::from_f32(-0.5), Px::from_int(10));
let holds = Holds::from(Px::from_int(20)..=Px::from_int(40)).through(part);
assert!(holds.contains(Px::from_int(-60)) && holds.contains(Px::from_int(-20)));
assert!(!holds.contains(Px::from_int(-61)) && !holds.contains(Px::from_int(-19)));
}
/// The case the widening is for: a part that holds only for the length it
/// was drawn at has to hold for the box it was drawn in, and a third of a
/// box is not a whole number of steps.
#[test]
fn a_part_maps_back_onto_the_box_it_was_measured_in() {
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
for box_len in (440..460).map(Px::from_int) {
let holds = Holds::at(part.to_px(box_len)).through(part);
assert!(holds.contains(box_len), "{box_len:?} left out by {holds:?}");
}
}
/// A widget handed the whole of its parent's box, with or without pixels
/// taken off it, has no fraction to invert: multiplying by one is exact
/// and taking the pixels off again is too, so the box maps back to
/// itself. Allowing for anything here compounded a step a level down a
/// chain of widgets each taking the whole of its parent.
#[test]
fn the_whole_of_a_box_maps_back_to_itself() {
let at = Px::from_int(956);
assert_eq!(Holds::at(at).through(Len::FULL), Holds::at(at));
let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8));
assert_eq!(
Holds::at(at).through(less_eight),
Holds::at(at + Px::from_int(8))
);
}
/// The range is the exact preimage at both ends, so a box one step
/// outside it really does give a length outside this range. What a wider
/// range costs is a drawing reused where it does not hold.
#[test]
fn a_box_one_step_outside_the_range_is_outside_it() {
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
let at = Px::from_int(300);
let holds = Holds::at(at).through(part);
for inside in [holds.lo, holds.hi] {
assert_eq!(part.to_px(inside), at, "{inside:?} left out of {holds:?}");
}
for outside in [holds.lo.next_down(), holds.hi.next_up()] {
assert_ne!(part.to_px(outside), at, "{outside:?} admitted by {holds:?}");
}
}
/// A truncating multiply only ever drops, so the step it needs allowing
/// for on the way in belongs at the top of the range and not the bottom.
#[test]
fn a_fraction_widens_further_up_than_down() {
let half = Len::from_parts(Rel::from_f32(0.5), Px::ZERO);
let holds = Holds::at(Px::from_int(100)).through(half);
let box_len = Px::from_int(200);
assert!(holds.hi - box_len > box_len - holds.lo, "{holds:?}");
}
#[test]
fn a_boundary_the_next_step_along_does_not_admit_it() {
let boundary = Px::from_int(10);
let above = Holds::from(boundary.next_up()..=Px::MAX);
assert!(!above.contains(boundary));
assert!(above.contains(boundary.next_up()));
}
}
-33
View File
@@ -1,33 +0,0 @@
use crate::{Axis, Holds, PxVec2, UiRegion};
/// Dependencies of one evaluation, before the frame and extent are composed.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHolds {
pub frame: [Holds; 2],
pub extent: [Holds; 2],
pub placement: Option<UiRegion>,
}
impl LayoutHolds {
pub const ANY: Self = Self {
frame: [Holds::ANY; 2],
extent: [Holds::ANY; 2],
placement: None,
};
pub fn contains(self, px: PxVec2, placement: UiRegion) -> bool {
self.placement.is_none_or(|old| old == placement)
&& [Axis::X, Axis::Y].into_iter().all(|axis| {
self.frame[axis as usize].contains(px.axis(axis))
&& self.extent[axis as usize]
.contains(placement.axis(axis).len().to_px(px.axis(axis)))
})
}
pub fn in_frame(self, placement: UiRegion) -> [Holds; 2] {
[Axis::X, Axis::Y].map(|axis| {
self.frame[axis as usize]
.and(self.extent[axis as usize].through(placement.axis(axis).len()))
})
}
}
+1 -102
View File
@@ -1,25 +1,12 @@
use crate::{ use crate::{
Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, UiRegion, WeakWidget, Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
WidgetId, Widgets,
util::{Arena, Id, TrackedArena},
}; };
/// How far the shader will walk a move chain. It bounds a malformed cycle
/// rather than any real tree; `Moves::resolve` uses the same number so the
/// two agree on what a deep tree resolves to.
pub const CHAIN_LIMIT: u32 = 64;
mod active; mod active;
mod draw_region;
mod holds;
mod layout_holds;
mod painter; mod painter;
mod render_state; mod render_state;
pub use active::*; pub use active::*;
pub use draw_region::*;
pub use holds::*;
pub use layout_holds::*;
pub use painter::{Painter, PrimitiveLike}; pub use painter::{Painter, PrimitiveLike};
pub use render_state::*; pub use render_state::*;
@@ -33,94 +20,6 @@ pub struct UiData {
pub masks: TrackedArena<Mask, u32>, pub masks: TrackedArena<Mask, u32>,
} }
/// Where each widget's drawing sits relative to its parent's slot, so moving
/// a subtree writes one entry rather than every descendant's primitives.
#[derive(Default)]
pub struct Moves {
arena: Arena<MoveOffset, u32>,
pub changed: bool,
}
impl Moves {
pub fn push(&mut self, parent: MoveIdx, region: UiRegion) -> MoveIdx {
self.changed = true;
MoveIdx::slot(self.arena.push(MoveOffset::new(parent, region)).idx())
}
/// Re-points a slot at a different parent, for a widget drawn somewhere
/// else in the tree than it was.
pub fn set_parent(&mut self, idx: MoveIdx, parent: MoveIdx) {
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
if entry.parent != parent {
entry.parent = parent;
self.changed = true;
}
}
pub fn remove(&mut self, idx: MoveIdx) {
self.changed = true;
self.arena.remove(Id::preset(idx.idx() as u32));
}
/// Sets the box a slot's contents are placed within, itself given in the
/// coordinates of its parent slot.
pub fn set(&mut self, idx: MoveIdx, region: UiRegion) {
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
if entry.region != region {
entry.region = region;
self.changed = true;
}
}
/// The same walk the vertex shader does, in the same `Len` the shader is
/// handed, for asking where a drawing will actually land -- hit testing,
/// and nothing layout decides on. Layout threads its lengths down the
/// draw instead, so no box it compares is composed back up this chain.
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
let mut region = local;
self.walk(idx, |entry| region = region.within(entry));
region
}
fn walk(&self, idx: MoveIdx, mut step: impl FnMut(&UiRegion)) {
let mut at = idx;
for _ in 0..CHAIN_LIMIT {
if at == MoveIdx::NONE {
return;
}
let entry = &self.arena[at.idx()];
step(&entry.region);
at = entry.parent;
}
debug_assert!(
at == MoveIdx::NONE,
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
and the shader stops at the same depth"
);
}
/// How many slots a region in `idx` is composed through, which is what
/// the shader's walk costs per primitive.
pub fn depth(&self, idx: MoveIdx) -> usize {
let mut depth = 0;
let mut at = idx;
while at != MoveIdx::NONE && depth < CHAIN_LIMIT as usize {
at = self.arena[at.idx()].parent;
depth += 1;
}
depth
}
pub fn entries(&self) -> &[MoveOffset] {
&self.arena
}
pub fn clear(&mut self) {
self.changed = true;
self.arena = Arena::default();
}
}
pub trait UiRsc { pub trait UiRsc {
fn ui(&self) -> &UiData; fn ui(&self) -> &UiData;
fn ui_mut(&mut self) -> &mut UiData; fn ui_mut(&mut self) -> &mut UiData;
+54 -556
View File
@@ -1,102 +1,52 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter};
use crate::{ use crate::{
Axis, DrawRegion, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind, GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
TexturePrimitive, TexturePrimitive,
}, },
ui::render_state::DrawInfo, util::Vec2,
}; };
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// makes your surfaces look pretty /// makes your surfaces look pretty
pub struct Painter<'a> { 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
/// fraction of this widget's area is a fraction of, and what every region
/// it writes composes within. The same box on the ask that measures and
/// the ask that places, which is what keeps a fraction under it from
/// being resolved twice.
pub(super) region: UiRegion, pub(super) region: UiRegion,
/// Where this widget's drawing sits inside that box, in the box's own
/// coordinates: `FULL` while its answer is not yet known, and the box
/// its answer or its parent chose once one of them has.
pub(super) placement: UiRegion,
/// Whether this draw read its placement, which makes the drawing one
/// that holds for that placement alone -- the way reading a length in
/// pixels makes it hold for that length.
pub(super) reads_placement: bool,
/// That box in pixels, which its children's are a length of: threaded
/// down from the box this widget was given rather than composed back up
/// the chain, so every length in layout is one multiply from its
/// parent's and [`Holds::through`] inverts exactly that.
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<PrimitiveHandle>,
pub(super) mask_region: Option<DrawRegion>,
pub(super) inherited_children: Vec<WidgetId>,
pub(super) extent_own: [Holds; 2],
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 pub(super) reads_output: bool,
/// length until it reads one, then that one, unless it says otherwise.
pub(super) own: [Holds; 2],
/// What the children it asked about and drew keep it to.
pub(super) under: [Holds; 2],
/// The movable region this widget's primitives are positioned through:
/// its own when opted in, otherwise the nearest ancestor's.
pub(super) move_idx: MoveIdx,
pub layer: usize, pub layer: usize,
/// The layer this widget was entered on, which its children's layers are
/// counted from however far `layer` has walked.
pub(super) own_layer: usize,
pub(super) depth: usize,
pub(super) id: WidgetId, pub(super) id: WidgetId,
} }
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) {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PrimitiveWrites);
let h = self.state.layers.write( let h = self.state.layers.write(
self.layer, self.layer,
PrimitiveInst { PrimitiveInst {
kind, kind,
id: self.id, id: self.id,
primitive, primitive,
region: region.resolve(self.region, self.placement), region,
mask_idx: self.mask, mask_idx: self.mask,
move_idx: self.move_idx,
}, },
); );
self.push_primitive(RetainedPrimitive { handle: h, region }); self.push_primitive(h);
} }
fn push_primitive(&mut self, h: RetainedPrimitive) { fn push_primitive(&mut self, h: PrimitiveHandle) {
if self.mask != MaskIdx::NONE { if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy: // TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask); self.rsc.ui_mut().masks.push_ref(self.mask);
@@ -104,316 +54,97 @@ impl<'a> Painter<'a> {
self.primitives.push(h); self.primitives.push(h);
} }
/// Writes a primitive over the whole of this widget's own box. /// Writes a primitive to be rendered
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, self.region)
} }
/// Writes in the frame by default. `DrawRegion::Extent` keeps the local pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
/// geometry attached to this widget's box without reading its placement.
pub fn primitive_within(
&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.within(&self.region));
} }
/// Sets a mask in the selected frame or extent coordinates. pub fn set_mask(&mut self, region: UiRegion) {
pub fn set_mask(&mut self, region: impl Into<DrawRegion>) {
let region = region.into();
self.mask_region = Some(region);
assert!(self.mask == MaskIdx::NONE); assert!(self.mask == MaskIdx::NONE);
self.mask = self.rsc.ui_mut().masks.push(Mask { self.mask = self.rsc.ui_mut().masks.push(Mask { region });
region: region.resolve(self.region, self.placement),
move_idx: self.move_idx,
});
} }
/// Draws a widget in the whole of this widget's own box: it gets the /// Draws a widget within this widget's region.
/// same region -- the same area for its fractions to be of -- and is put
/// where this widget was put. What a container that is only a wrapper
/// around one child wants, since its box is the child's.
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, self.region)
self.widget_at_inner(id, UiRegion::FULL, [Some(own.x), Some(own.y)], true)
} }
/// What a widget's rules declare its lengths to be, which whoever draws /// Draws a widget somewhere within this one. Drawing one a second time
/// it resolves into its box. Reading them depends on nothing -- the box /// gives it a new box, keeping the drawing it already has where it can.
/// 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>( pub fn widget_within<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, [None; 2]) let region = region.within(&self.region);
self.widget_at(id, region)
} }
/// Draws a widget in `region`, saying where in it the drawing goes. fn widget_at<'s, W: ?Sized>(
///
/// `region` is the child's own area: what a fraction it declares or
/// reports is a fraction of, and the coordinates the regions it writes
/// compose within. It is the same box on the ask that measures and the
/// ask that places, which is what stops a fraction under it being
/// resolved twice.
///
/// `placement` is what of that region the child's drawing takes, per
/// axis, wherever this widget is choosing. `None` leaves the axis to the
/// child's own answer and alignment, which is what
/// [`Self::widget_within`] passes. A span passes the whole row as the
/// region, so `rel(0.5)` is half the row wherever the child sits in it,
/// and places the child by passing the slot along its axis.
pub fn widget_at<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
placement: [Option<UiSpan>; 2],
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
self.widget_at_inner(id, region, placement, false)
}
fn widget_at_inner<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
placement: [Option<UiSpan>; 2],
inherited: 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 declared = self.declared_lens(id);
let align = self.rsc.widgets().alignment(id.id());
let (local, placement) = ask_box(region, declared, align, placement);
let within = match local == UiRegion::FULL {
true => self.region,
false => local.within(&self.region),
};
#[cfg(feature = "layout-diagnostics")]
if region_node {
diag::bump(Counter::RegionNodeDraws);
diag::region_node(id.id(), self.id, within);
}
// A child listed twice would be moved twice. // A child listed twice would be moved twice.
if !self.children.contains(&id.id()) { if !self.children.contains(&id.id()) {
self.children.push(id.id()); self.children.push(id.id());
} }
let first_ask = self.offer(id.id()); let size = self.state.draw_inner(
let given_len = local.size(); self.layer,
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, region,
DrawInfo { Some(self.id),
layer: self.layer, self.mask,
parent: Some(self.id),
depth: self.depth + 1,
parent_move: self.move_idx,
region_node,
mask: self.mask,
given_len,
offer_len,
offer_placement,
px,
offered_px,
placement,
},
None, None,
self.rsc, self.rsc,
); );
// Whatever the child's answer holds for keeps this one to the boxes
// that give the child a length inside it.
for axis in AXES {
let n = axis as usize;
let frame = holds.frame[n].through(local.axis(axis).len());
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,
} }
} }
/// What a child says its length is without being drawn, if it can say. /// What a child says its length is without being drawn, if it can say.
/// Asking counts as reading its size. /// 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<Len> {
let widgets = self.rsc.widgets(); let hint = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis)?;
// A rule is the answer where there is one: it wins over whatever the self.depend_on_size(id);
// 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(|| {
widgets
.get_dyn(id.id())
.and_then(|widget| widget.size_hint(axis))
});
#[cfg(feature = "layout-diagnostics")]
diag::hint_read(id.id(), self.id, axis, hint);
match hint {
Some(hint) => {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::HintHits);
self.depend_on(id);
Some(hint) Some(hint)
} }
None => {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::HintMisses);
None
}
}
}
/// A child's length in the region it is about to be offered, if it can fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
/// be had without drawing it: from its hint, or from a drawing it already
/// has that holds for that box.
pub fn known_len<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
region: UiRegion,
placement: [Option<UiSpan>; 2],
) -> Option<LayoutLen> {
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 Some(hint);
}
let px = local.size().to_px(self.px);
let (size, holds) = self.state.retained_size(
child.id(),
px,
placement,
self.move_idx,
self.rsc.widgets(),
)?;
#[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()));
}
Some(in_parent_frame(size, local.size(), declared).axis(axis))
}
/// Whether this is the first box a child is asked about in during a draw
/// that is itself in the box it was asked in -- the question a cold
/// layout asks, whose answer is the one to keep.
fn offer(&mut self, child: WidgetId) -> bool {
if !self.at_offer || self.offered.contains(&child) {
return false;
}
self.offered.push(child);
true
}
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
if !self.size_deps.contains(&child.id()) { if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id()); self.size_deps.push(child.id());
} }
} }
pub fn render_text<'b>( pub fn render_text(
&mut self, &mut self,
buffer: &'b mut TextBuffer, buffer: &mut TextBuffer,
attrs: &TextAttrs, attrs: &TextAttrs,
width: Option<f32>, width: Option<f32>,
) -> &'b RenderedText { ) -> RenderedText {
#[cfg(feature = "layout-diagnostics")]
diag::render_text(self.id, self.rsc.widgets().label(self.id), width);
let ui = self.rsc.ui_mut(); let ui = self.rsc.ui_mut();
ui.text.render(buffer, attrs, width) ui.text.render(buffer, attrs, width)
} }
/// 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();
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 mut region = origin;
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::abs(glyph.offset));
let size = PxVec2::new( region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
Px::from_int(glyph.entry.width as i32), region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
Px::from_int(glyph.entry.height as i32),
);
region.x.end = region.x.start.offset(size.x);
region.y.end = region.y.start.offset(size.y);
region
});
self.write( self.write(
kind, kind,
GlyphPrimitive { GlyphPrimitive {
@@ -428,117 +159,22 @@ impl<'a> Painter<'a> {
} }
} }
/// The box this widget's parent gave it, in the coordinates its own
/// primitives are written in -- so a region composed `within` it may be
/// drawn directly. Its own box is [`Self::placement`] of this one.
pub fn region(&self) -> UiRegion { pub fn region(&self) -> UiRegion {
self.region self.region
} }
/// Where this widget's drawing goes inside the box it was given, in that /// The output's size in pixels. A widget that reads it draws again when
/// box's coordinates: what its own answer took of it, or what its parent /// the output changes, since nothing else can put that right.
/// chose for it. `FULL` on the ask that measures, since nothing has been pub fn output_size(&mut self) -> Vec2 {
/// placed yet. self.reads_output = true;
/// self.state.output_size
/// Reading it is what says the drawing depends on it, so a widget that
/// positions its own content reads it and is drawn again once its box is
/// known, and one that fills whatever it is given never is.
pub fn placement(&mut self) -> UiRegion {
self.reads_placement = true;
self.placement
} }
/// Where this widget sits in a box longer than the length it takes. A /// This widget's box in pixels. Resolved against the output's size, so a
/// widget that positions its own content reads it to place that content /// widget that reads it draws again when the output changes.
/// the way the box around it would have placed the widget. pub fn px_size(&mut self) -> Vec2 {
pub fn alignment(&self) -> RegionAlign { self.reads_output = true;
self.rsc.widgets().alignment(self.id) self.region.size().to_abs(self.state.output_size)
}
/// Whether a rule beside this widget gives its length on `axis` outright,
/// which makes whatever it reports for that axis moot. A rule that only
/// bounds the length is not one of these: the answer is still the
/// widget's to give, and something still has to work it out.
///
/// The widget under a rule does not otherwise learn of it -- this is for
/// a container deciding whether reading its children across an axis is
/// worth anything, since reading one is also what makes its own size
/// depend on it.
pub fn has_exact_size(&self, axis: Axis) -> bool {
self.rsc
.widgets()
.size_rules(self.id)
.axis(axis)
.exact()
.is_some()
}
/// The part of this widget's box that something of `size` takes, at the
/// near edge. A container that reports one child's size gives every child
/// this, so what it draws is inside what it says it occupies.
pub fn box_of(&self, size: Size) -> UiRegion {
let lens = placed_lens(size, [None; 2], [false; 2]);
placed_box(UiRegion::FULL, lens, RegionAlign::NEAR)
}
/// This widget's own box in pixels. Reading it makes the drawing one
/// that holds for this box only, until `holds` says how far it goes.
pub fn px_size(&mut self) -> PxVec2 {
PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y))
}
/// One axis of this widget's own box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px {
let part = self.placement.axis(axis).len();
let len = part.to_px(self.px.axis(axis));
let own = &mut self.extent_own[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(len);
}
len
}
/// The lengths of this widget's own box on `axis` that what it is drawing
/// holds for -- the same primitives, in the same fractions and offsets
/// of the box, and the same reported size. A widget that read its length
/// in pixels holds for that one alone until it says otherwise.
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let part = self.placement.axis(axis).len();
let holds = holds.into();
debug_assert!(
holds.contains(part.to_px(self.px.axis(axis))),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
self.label(),
self.id
);
self.extent_own[axis as usize] = holds;
}
/// One axis of the box this widget's parent gave it, in pixels -- what a
/// fraction of its area resolves against, and so what a container divides
/// among its children. Its own box is a part of this one.
pub fn region_px_len(&mut self, axis: Axis) -> Px {
let len = self.px.axis(axis);
let own = &mut self.own[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(len);
}
len
}
/// [`Self::holds`] stated about the region rather than about this
/// widget's own box, for a container whose drawing turns on the box it
/// was given rather than on the part of it it took.
pub fn region_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into();
debug_assert!(
holds.contains(self.px.axis(axis)),
"'{}' ({:?}) says its drawing holds for lengths that leave out its region",
self.label(),
self.id
);
self.own[axis as usize] = holds;
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
@@ -549,18 +185,6 @@ impl<'a> Painter<'a> {
self.layer = self.state.layers.child(self.layer); self.layer = self.state.layers.child(self.layer);
} }
/// The layer this widget's `n`th child draws on, addressed rather than
/// walked to. A container that measures one child by drawing it can ask
/// on the layer that child will end up on, and then the second ask is a
/// reuse rather than a second drawing on another layer.
pub fn child_layer_at(&mut self, n: usize) {
let mut at = self.state.layers.child(self.own_layer);
for _ in 0..n {
at = self.state.layers.next(at);
}
self.layer = at;
}
pub fn next_layer(&mut self) { pub fn next_layer(&mut self) {
self.layer = self.state.layers.next(self.layer); self.layer = self.state.layers.next(self.layer);
} }
@@ -585,16 +209,11 @@ pub struct DrawResult<'p, 'a, W: ?Sized> {
impl<W: ?Sized> DrawResult<'_, '_, W> { impl<W: ?Sized> DrawResult<'_, '_, W> {
pub fn size(self) -> Size { pub fn size(self) -> Size {
#[cfg(feature = "layout-diagnostics")] self.painter.depend_on_size(self.child);
{
diag::bump(Counter::SizeReads);
diag::size_read(self.child.id(), self.painter.id, self.size);
}
self.painter.depend_on(self.child);
self.size self.size
} }
pub fn len(self, axis: Axis) -> LayoutLen { pub fn len(self, axis: Axis) -> Len {
self.size().axis(axis) self.size().axis(axis)
} }
} }
@@ -623,124 +242,3 @@ impl PrimitiveLike for &TextureHandle {
self.into() self.into()
} }
} }
/// A child's answer as lengths of the parent's own region. A widget reports
/// a fraction of its own region, and `of` is that region as a length of this
/// one. Pixels come through untouched, being that many pixels wherever they
/// end up. A declared axis is already the parent's: it resolved the rule in
/// its own region, and the rule is what the report says.
fn in_parent_frame(size: Size, of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size {
let mut size = size;
for (axis, declared) in AXES.into_iter().zip(declared) {
if declared.is_none() {
*size.axis_mut(axis) = size.axis(axis).within_len(of.axis(axis));
}
}
size
}
/// What a widget declares a length of its box to be. `leftover` is not one: a
/// share of what is left over is only a length to the widget dividing one,
/// so it passes up in the size instead.
pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLen>; 2] {
let rules = widgets.size_rules(id);
let widget = widgets.get_dyn(id);
AXES.map(|axis| {
rules.axis(axis).declared().or_else(|| {
// A hint still narrows the box where no rule does, which is how a
// widget with a natural pixel size -- an image, a gap -- gets that
// size rather than the whole offer. That is the offer's business
// rather than a declaration's, and this falls away once a widget
// occupies its reported size inside the box it was offered.
widget
.and_then(|widget| widget.size_hint(axis))
.filter(|len| len.leftover == Weight::ZERO)
})
})
}
/// Whether what a widget reported along an axis is the whole of the box it
/// is in rather than a part to be placed inside it. A share fills, because a
/// share is a length only to whoever divides one, and whoever did is the one
/// that handed down this box. A declared axis does too: the rule already gave
/// the region its length, and the rule's length is what the widget reports
/// there. And an axis the parent decided from the answer is
/// the answer already.
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
reported.leftover != Weight::ZERO || declared.is_some() || decided
}
/// What of the box it was given a widget's drawing occupies, as lengths of
/// that box: the size it reported wherever that is a part to be placed, and
/// the whole of the box wherever the answer fills it.
///
/// A reported fraction is a fraction of the box the widget drew in, where a
/// declared one is a fraction of the box its parent handed down -- a span
/// reporting `rel(1.0)` means all of what it was given, whatever that was a
/// fraction of. So this is a length of the box rather than a length composed
/// into it, and a box in pixels is this step from the given box's pixels.
pub(crate) fn placed_lens(
size: Size,
declared: [Option<LayoutLen>; 2],
decided: [bool; 2],
) -> UiVec2 {
let mut lens = UiVec2::FULL_SIZE;
for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) {
let reported = size.axis(axis);
if !fills(reported, declared, decided) {
*lens.axis_mut(axis) = Len::from_parts(reported.rel, reported.px);
}
}
lens
}
/// Where that drawing sits: those lengths taken of the box the widget was
/// asked in, on the side of it that the widget's alignment says.
pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) -> UiRegion {
let mut placed = region;
for axis in AXES {
// The whole of the box is already where it sits, and the arithmetic
// below is the identity for it.
if lens.axis(axis) == Len::FULL {
continue;
}
let span = placed.axis_mut(axis);
let len = lens.axis(axis).within_len(span.len());
span.start += (span.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len;
}
placed
}
/// A child's own region and where in it its drawing goes, from the box it is
/// offered, the lengths its rules declare, and what its parent chose.
///
/// A rule gives the region its length outright -- that is what makes a rule
/// win, and it is why a widget under one never learns of it -- and the region
/// then sits where its parent placed it, or where its alignment says if its
/// parent left the axis open. With no rule the region is the whole of what
/// was offered, since that is the area a fraction under it is a fraction of,
/// and what the parent chose is where in it the drawing goes. So the two
/// coordinate spaces are the same one wherever a placement survives.
pub(crate) fn ask_box(
mut region: UiRegion,
declared: [Option<LayoutLen>; 2],
align: RegionAlign,
placement: [Option<UiSpan>; 2],
) -> (UiRegion, [Option<UiSpan>; 2]) {
let mut placed = [None; 2];
for (axis, (len, chosen)) in AXES.into_iter().zip(declared.into_iter().zip(placement)) {
let Some(len) = len else {
placed[axis as usize] = chosen;
continue;
};
let span = region.axis_mut(axis);
let len = Len::from_parts(len.rel, len.px);
span.start = match chosen {
Some(chosen) => chosen.start,
None => span.start + (span.len() - len).scale(align.axis(axis).rel()),
};
span.end = span.start + len;
}
(region, placed)
}
+169 -1118
View File
File diff suppressed because it is too large. Load diff
-9
View File
@@ -34,10 +34,6 @@ impl<T, I: IdNum> Arena<T, I> {
self.tracker.free(id); self.tracker.free(id);
self.data[i] self.data[i]
} }
pub(crate) fn get_mut(&mut self, id: Id<I>) -> &mut T {
&mut self.data[id.idx()]
}
} }
impl<T, I: IdNum> Default for Arena<T, I> { impl<T, I: IdNum> Default for Arena<T, I> {
@@ -75,11 +71,6 @@ impl<T, I: IdNum> TrackedArena<T, I> {
self.refs[i.idx()] += 1; self.refs[i.idx()] += 1;
} }
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
self.changed = true;
self.inner.get_mut(id)
}
pub fn remove(&mut self, id: Id<I>) -> T pub fn remove(&mut self, id: Id<I>) -> T
where where
T: Copy, T: Copy,
+10 -29
View File
@@ -1,5 +1,6 @@
pub const trait LerpUtil { pub const trait LerpUtil: Sized {
fn lerp(self, from: Self, to: Self) -> Self; fn lerp(self, from: Self, to: Self) -> Self;
fn lerp_inv(self, from: Self, to: Self) -> Option<Self>;
} }
const impl LerpUtil for f32 { const impl LerpUtil for f32 {
@@ -8,6 +9,14 @@ const impl LerpUtil for f32 {
fn lerp(self, from: Self, to: Self) -> Self { fn lerp(self, from: Self, to: Self) -> Self {
from + (to - from) * self from + (to - from) * self
} }
/// inverse of lerp, and `None` where `from` and `to` are the same point:
/// every input lerps to it, so there is no one answer to come back to.
fn lerp_inv(self, from: Self, to: Self) -> Option<Self> {
match to == from {
true => None,
false => Some((self - from) / (to - from)),
}
}
} }
macro_rules! impl_op { macro_rules! impl_op {
@@ -56,34 +65,6 @@ macro_rules! impl_op {
} }
} }
}; };
// Without the `f32` operations, for a type whose fields are not all the
// same kind of number: there is nothing a bare float means to a fraction
// and an offset at once.
(same $T:ident $op:ident $fn:ident $opa:ident $fna:ident; $($field:ident)*) => {
#[allow(non_snake_case)]
mod ${concat($T, _op_, $fn, _same_impl)} {
use super::*;
#[allow(unused_imports)]
use std::ops::*;
const impl $op for $T {
type Output = Self;
fn $fn(self, rhs: Self) -> Self::Output {
Self {
$($field: self.$field.$fn(rhs.$field),)*
}
}
}
const impl $opa for $T {
fn $fna(&mut self, rhs: Self) {
*self = self.$fn(rhs);
}
}
}
};
(same $T:ident $op:ident $fn:ident; $($field:ident)*) => {
impl_op!(same $T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
};
($T:ident $op:ident $fn:ident; $($field:ident)*) => { ($T:ident $op:ident $fn:ident; $($field:ident)*) => {
impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*); impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
}; };
+5
View File
@@ -1,3 +1,8 @@
#[allow(clippy::missing_safety_doc)]
pub(crate) unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
unsafe { std::mem::transmute::<&T, &T>(x) }
}
#[allow(clippy::missing_safety_doc)] #[allow(clippy::missing_safety_doc)]
pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T { pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
unsafe { std::mem::transmute::<&mut T, &mut T>(x) } unsafe { std::mem::transmute::<&mut T, &mut T>(x) }
+1 -5
View File
@@ -1,11 +1,7 @@
use crate::util::impl_op; use crate::util::impl_op;
use std::{hash::Hash, ops::*}; use std::{hash::Hash, ops::*};
/// `align(8)` because that is WGSL's alignment for a `vec2<f32>`, so any GPU #[repr(C)]
/// struct holding one is laid out the way its shader reads it without having
/// to say so itself. Those structs still need a manual `unsafe impl Pod`,
/// since the trailing padding this introduces is what `derive(Pod)` refuses.
#[repr(C, align(8))]
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vec2 { pub struct Vec2 {
pub x: f32, pub x: f32,
+1 -7
View File
@@ -1,11 +1,8 @@
use crate::{RegionAlign, SizeRules, Widget}; use crate::Widget;
pub struct WidgetData { pub struct WidgetData {
pub widget: Box<dyn Widget>, pub widget: Box<dyn Widget>,
pub label: String, pub label: String,
pub(super) region_node: bool,
pub(super) size: SizeRules,
pub(super) align: RegionAlign,
/// dynamic borrow checking /// dynamic borrow checking
pub borrowed: bool, pub borrowed: bool,
} }
@@ -19,9 +16,6 @@ impl WidgetData {
Self { Self {
widget: Box::new(widget), widget: Box::new(widget),
label, label,
region_node: false,
size: SizeRules::default(),
align: RegionAlign::default(),
borrowed: false, borrowed: false,
} }
} }
+23 -6
View File
@@ -1,10 +1,9 @@
use crate::{Axis, LayoutLen, Painter, Size}; use crate::{Axis, Len, Painter, Size};
use std::any::Any; use std::any::Any;
mod data; mod data;
mod handle; mod handle;
mod like; mod like;
mod size_rule;
mod tag; mod tag;
mod view; mod view;
mod widgets; mod widgets;
@@ -12,11 +11,21 @@ mod widgets;
pub use data::*; pub use data::*;
pub use handle::*; pub use handle::*;
pub use like::*; pub use like::*;
pub use size_rule::*;
pub use tag::*; pub use tag::*;
pub use view::*; pub use view::*;
pub use widgets::*; pub use widgets::*;
/// What may be done to a widget's drawing when the box it was given changes
/// on this axis, instead of drawing it again. Asked per axis, because wrapped
/// text reads the width it is offered and not the height.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OnResize {
Scale,
Translate,
#[default]
Redraw,
}
pub trait Widget: Any { pub trait Widget: Any {
/// Draws the widget, and returns what it used of the box it was given. /// Draws the widget, and returns what it used of the box it was given.
fn draw(&mut self, painter: &mut Painter) -> Size; fn draw(&mut self, painter: &mut Painter) -> Size;
@@ -24,9 +33,13 @@ pub trait Widget: Any {
/// An exact length the widget can give without a painter or its children. /// An exact length the widget can give without a painter or its children.
/// Optional, and saves a draw rather than changing one: a hint that /// Optional, and saves a draw rather than changing one: a hint that
/// disagrees with the eventual draw fails a debug assertion. /// disagrees with the eventual draw fails a debug assertion.
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> { fn size_hint(&self, _axis: Axis) -> Option<Len> {
None None
} }
fn on_resize(&self, _axis: Axis) -> OnResize {
OnResize::default()
}
} }
impl Widget for () { impl Widget for () {
@@ -35,8 +48,12 @@ impl Widget for () {
Size::default() Size::default()
} }
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> { fn size_hint(&self, _axis: Axis) -> Option<Len> {
Some(LayoutLen::default()) Some(Len::default())
}
fn on_resize(&self, _axis: Axis) -> OnResize {
OnResize::Scale
} }
} }
-87
View File
@@ -1,87 +0,0 @@
use crate::{Axis, LayoutLen, Weight};
/// What a widget's length on one axis is, as a rule its parent applies where
/// it draws it rather than an answer the widget gives about itself.
///
/// A rule and a drawn size are not two opinions to reconcile: a rule wins on
/// the axis it names, and the `Size` returned by `draw` answers only the axes
/// with no rule. That is what lets a span divide its space around a length
/// nobody has drawn yet, and it is why a rule lives beside the widget rather
/// than inside it -- the widget under the rule never has to know about it.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum SizeRule {
/// Whatever the widget reports from drawing.
#[default]
Free,
/// This length, whatever the widget reports.
Exact(LayoutLen),
}
impl SizeRule {
/// The length this rule gives without the widget being drawn, if it can
/// give one. `leftover` is never among them: a share is a length only to
/// whoever divides one, so it passes up in the reported size instead and
/// is resolved there.
pub fn declared(&self) -> Option<LayoutLen> {
match self {
Self::Exact(len) if len.leftover == Weight::ZERO => Some(*len),
_ => None,
}
}
/// The length this rule gives outright, whatever the widget reports --
/// which makes the widget's answer on that axis moot. A share counts: it
/// is a length the widget's parent still has to divide, so it is exact
/// here and resolved there, unlike `declared`, which is only the ones
/// that give a box directly.
pub fn exact(&self) -> Option<LayoutLen> {
match self {
Self::Free => None,
Self::Exact(len) => Some(*len),
}
}
/// The length a widget reporting `reported` ends up with.
pub fn apply(&self, reported: LayoutLen) -> LayoutLen {
match self {
Self::Free => reported,
Self::Exact(len) => *len,
}
}
}
impl From<LayoutLen> for SizeRule {
fn from(len: LayoutLen) -> Self {
Self::Exact(len)
}
}
impl From<Option<LayoutLen>> for SizeRule {
fn from(len: Option<LayoutLen>) -> Self {
len.map_or(Self::Free, Self::Exact)
}
}
/// One rule per axis, which is how a widget carries a length on one axis and
/// leaves the other to whatever it draws.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct SizeRules {
pub x: SizeRule,
pub y: SizeRule,
}
impl SizeRules {
pub fn axis(&self, axis: Axis) -> SizeRule {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut SizeRule {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
}
+1 -67
View File
@@ -1,8 +1,7 @@
use std::sync::mpsc::{Receiver, Sender, channel}; use std::sync::mpsc::{Receiver, Sender, channel};
use crate::{ use crate::{
Axis, AxisAlign, IdLike, RegionAlign, SizeRule, SizeRules, StrongWidget, WeakWidget, Widget, IdLike, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
WidgetData, WidgetId,
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut}, util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
}; };
@@ -101,71 +100,6 @@ impl Widgets {
self.data_mut(id.id()).unwrap().label = label; self.data_mut(id.id()).unwrap().label = label;
} }
/// Whether this widget owns a movable retained region.
pub fn is_region_node(&self, id: impl IdLike) -> bool {
self.data(id).unwrap().region_node
}
/// Chooses whether this widget's retained drawing has one movable region
/// of its own. Changing the boundary redraws the subtree once so every
/// primitive names the right coordinate space.
pub fn set_region_node(&mut self, id: impl IdLike, region_node: bool) {
let id = id.id();
let data = self.data_mut(id).unwrap();
if data.region_node == region_node {
return;
}
data.region_node = region_node;
self.needs_redraw.insert(id);
}
/// The length rules whoever draws this widget applies to its box.
pub fn size_rules(&self, id: impl IdLike) -> SizeRules {
self.data(id).unwrap().size
}
/// Sets one axis's rule. The widget is marked rather than its parent
/// because the parent is not known here; `redraw` escalates a changed
/// declared length to whoever resolves it.
pub fn set_size_rule(&mut self, id: impl IdLike, axis: Axis, rule: SizeRule) {
let id = id.id();
let data = self.data_mut(id).unwrap();
if *data.size.axis_mut(axis) == rule {
return;
}
*data.size.axis_mut(axis) = rule;
self.needs_redraw.insert(id);
}
/// Where this widget sits in a box longer than the length it takes.
pub fn alignment(&self, id: impl IdLike) -> RegionAlign {
self.data(id).unwrap().align
}
/// Sets one axis's alignment. Which box a widget ends up in is its
/// parent's to decide, so this is escalated the way a length rule is.
pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) {
let id = id.id();
let data = self.data_mut(id).unwrap();
if *data.align.axis_mut(axis) == align {
return;
}
*data.align.axis_mut(axis) = align;
self.needs_redraw.insert(id);
}
/// Both axes at once, for a caller holding a pair.
pub fn set_size_rules(
&mut self,
id: impl IdLike,
x: impl Into<SizeRule>,
y: impl Into<SizeRule>,
) {
let id = id.id();
self.set_size_rule(id, Axis::X, x.into());
self.set_size_rule(id, Axis::Y, y.into());
}
pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> { pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> {
self.vec.get_mut(id.id()) self.vec.get_mut(id.id())
} }
-31
View File
@@ -1,31 +0,0 @@
//! The seeded random tree `tests/generated.rs` checks, drawn so it can be
//! looked at. `IRIS_SEED` and `IRIS_DEPTH` choose which one.
use iris::prelude::*;
use iris::random::Edits;
fn env(name: &str, fallback: u64) -> u64 {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(fallback)
}
fn main() {
DefaultApp::<State>::run();
}
#[derive(DefaultUiState)]
struct State {
ui_state: DefaultUiState,
}
impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
let seed = env("IRIS_SEED", 1);
let depth = env("IRIS_DEPTH", 4) as usize;
let (root, _) = iris::random::grow(rsc, seed, depth, &Edits::default());
ui_state.set_root(root);
Self { ui_state }
}
}
+6 -10
View File
@@ -20,26 +20,22 @@ impl DefaultAppState for Client {
let pad_test = ( let pad_test = (
rrect.color(Color::BLUE), rrect.color(Color::BLUE),
( (
// The square is one widget and the two shares of the row it
// sits centred in are another: a length is a property of a
// widget, so `.width` here would overwrite the `.sized`.
rrect rrect
.color(Color::RED) .color(Color::RED)
.sized((100, 100)) .sized((100, 100))
.center() .center()
.wrapper() .width(rest(2)),
.width(leftover(2)),
( (
rrect.color(Color::ORANGE), rrect.color(Color::ORANGE),
rrect.color(Color::LIME).pad(10.0), rrect.color(Color::LIME).pad(10.0),
) )
.span(Dir::RIGHT) .span(Dir::RIGHT)
.width(leftover(2)), .width(rest(2)),
rrect.color(Color::YELLOW), rrect.color(Color::YELLOW),
) )
.span(Dir::RIGHT) .span(Dir::RIGHT)
.pad(10) .pad(10)
.width(leftover(3)), .width(rest(3)),
) )
.span(Dir::RIGHT) .span(Dir::RIGHT)
.add(rsc); .add(rsc);
@@ -125,11 +121,11 @@ impl DefaultAppState for Client {
.add(rsc); .add(rsc);
let text_edit_scroll = ( let text_edit_scroll = (
msg_area.height(leftover(1)), msg_area.height(rest(1)),
( (
Rect::new(Color::WHITE.darker(0.9)), Rect::new(Color::WHITE.darker(0.9)),
( (
add_text.width(leftover(1)), add_text.width(rest(1)),
Rect::new(Color::GREEN) Rect::new(Color::GREEN)
.on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| { .on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| {
rsc.run_event::<Submit>(add_text, (), ctx.state); rsc.run_event::<Submit>(add_text, (), ctx.state);
@@ -147,7 +143,7 @@ impl DefaultAppState for Client {
.span(Dir::DOWN) .span(Dir::DOWN)
.add(rsc); .add(rsc);
let main = Wrapper::new().add(rsc); let main = WidgetPtr::new().add(rsc);
let vals = Rc::new(RefCell::new((0, Vec::new()))); let vals = Rc::new(RefCell::new((0, Vec::new())));
let mut switch_button = |color, to: WeakWidget, label| { let mut switch_button = |color, to: WeakWidget, label| {
+3 -7
View File
@@ -28,14 +28,10 @@ impl DefaultAppState for State {
.pad(16) .pad(16)
.background(panel()); .background(panel());
// Each one takes the whole width, because `text_align` puts the
// glyphs somewhere in the box the text is given and a text that
// reports the width of its own glyphs is given exactly that.
let label = |text: &str, align| wtext(text).size(24).text_align(align).width(rel(1.0));
let aligned = ( let aligned = (
label("left", Align::LEFT), wtext("left").size(24).text_align(Align::LEFT),
label("centred", Align::H_CENTER), wtext("centred").size(24).text_align(Align::CENTER),
label("right", Align::RIGHT), wtext("right").size(24).text_align(Align::RIGHT),
) )
.span(Dir::DOWN) .span(Dir::DOWN)
.gap(8) .gap(8)
-16
View File
@@ -1,16 +0,0 @@
[package]
name = "rig-input"
version.workspace = true
edition.workspace = true
# Replays `.touch` recordings through Wayland's virtual-pointer protocol;
# headless sway has no input devices for coordinate-driving tools to move.
[[bin]]
name = "replay-touch"
path = "src/main.rs"
[dependencies]
# Share the harness parser so both ways of replaying read a file the same.
iris = { path = ".." }
wayland-client = { workspace = true }
wayland-protocols-wlr = { workspace = true }
-141
View File
@@ -1,141 +0,0 @@
use iris::harness::{TouchAction, TouchScript};
use std::time::Duration;
use wayland_client::protocol::wl_pointer::ButtonState;
use wayland_client::protocol::{wl_registry, wl_seat};
use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1,
zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1,
};
const BTN_LEFT: u32 = 0x110;
const SETTLE: Duration = Duration::from_millis(200);
#[derive(Default)]
struct Globals {
seat: Option<wl_seat::WlSeat>,
manager: Option<ZwlrVirtualPointerManagerV1>,
}
impl Dispatch<wl_registry::WlRegistry, ()> for Globals {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
let wl_registry::Event::Global {
name,
interface,
version,
} = event
else {
return;
};
match interface.as_str() {
"wl_seat" => {
state.seat = Some(registry.bind(name, version.min(7), qh, ()));
}
"zwlr_virtual_pointer_manager_v1" => {
state.manager = Some(registry.bind(name, version.min(2), qh, ()));
}
_ => {}
}
}
}
delegate_noop!(Globals: ignore wl_seat::WlSeat);
delegate_noop!(Globals: ZwlrVirtualPointerManagerV1);
delegate_noop!(Globals: ZwlrVirtualPointerV1);
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let [width, height, path] = args.as_slice() else {
eprintln!("usage: replay-touch WIDTH HEIGHT FILE");
std::process::exit(2);
};
let (width, height) = (parse(width, "WIDTH"), parse(height, "HEIGHT"));
let text = std::fs::read_to_string(path)
.unwrap_or_else(|e| fail(&format!("could not read {path}: {e}")));
let script = TouchScript::parse(&text).unwrap_or_else(|e| fail(&e));
let conn = Connection::connect_to_env().unwrap_or_else(|e| {
fail(&format!(
"no wayland display ({e}); is WAYLAND_DISPLAY set?"
))
});
let mut queue = conn.new_event_queue();
let qh = queue.handle();
let display = conn.display();
display.get_registry(&qh, ());
let mut globals = Globals::default();
queue
.roundtrip(&mut globals)
.unwrap_or_else(|e| fail(&format!("wayland roundtrip failed: {e}")));
let manager = globals.manager.as_ref().unwrap_or_else(|| {
fail(
"this compositor does not offer zwlr_virtual_pointer_manager_v1, so a pointer cannot \
be synthesised; sway and every wlroots compositor do",
)
});
let pointer = manager.create_virtual_pointer(globals.seat.as_ref(), &qh, ());
// Put the pointer where the gesture starts and let the compositor
// settle before anything is pressed. Without this the press is
// dropped: sway has just learned about this pointer, and a button
// sent in the same breath as the motion that first puts it over a
// window arrives before there is a focused surface to send it to --
// winit sees `CursorEntered`, the moves and the *release*, never the
// press, so the gesture reads as a hover and nothing scrolls. Found
// by printing winit's own events; the settle is what fixed it.
if let Some(first) = script.samples.first() {
pointer.motion_absolute(0, first.pos.x as u32, first.pos.y as u32, width, height);
pointer.frame();
conn.flush()
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
std::thread::sleep(SETTLE);
}
let mut previous = 0;
for sample in &script.samples {
std::thread::sleep(Duration::from_millis(sample.t_ms - previous));
previous = sample.t_ms;
let t = sample.t_ms as u32;
pointer.motion_absolute(t, sample.pos.x as u32, sample.pos.y as u32, width, height);
pointer.frame();
// The button goes in a frame of its own, *after* the motion has
// been committed. Sent in the same frame as the motion that
// first puts the pointer over the window, sway drops it: the
// client sees `CursorEntered` and the moves but never a
// `MouseInput { state: Pressed }`, so the whole gesture reads as
// a hover and nothing scrolls. Found exactly that way, by
// printing winit's events.
let state = match sample.action {
TouchAction::Down => Some(ButtonState::Pressed),
TouchAction::Up => Some(ButtonState::Released),
TouchAction::Move => None,
};
if let Some(state) = state {
pointer.button(t, BTN_LEFT, state);
pointer.frame();
}
conn.flush()
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
}
pointer.destroy();
conn.flush().ok();
}
fn parse(text: &str, what: &str) -> u32 {
text.parse()
.unwrap_or_else(|_| fail(&format!("{what} is not a whole number: {text:?}")))
}
fn fail(message: &str) -> ! {
eprintln!("replay-touch: {message}");
std::process::exit(1);
}
-14
View File
@@ -1,14 +0,0 @@
# The compositor `scripts/run-headless.sh` starts, so that an example has a
# surface where there is no display. Nothing here is meant to be looked at
# directly; `grim` is.
#
# No Xwayland: winit talks Wayland natively, so an X server is a second thing
# to go wrong for no gain.
xwayland disable
# The default output, overridden per run by `--mode`. Larger than the window
# an example opens, so nothing is scaled or clipped.
output HEADLESS-1 mode 1920x1200@60Hz
default_border none
focus_follows_mouse no
-182
View File
@@ -1,182 +0,0 @@
#!/bin/sh
# Run an iris example on a machine with no display.
#
# ./scripts/run-headless.sh tabs
# ./scripts/run-headless.sh tabs --shot /tmp/tabs.png --seconds 4
# ./scripts/run-headless.sh tabs --replay taps.touch --shot /tmp/tabs.png
# ./scripts/run-headless.sh app --dir ../elsewhere --mode 1080x2424@120Hz
#
# `--dir DIR` names the workspace to build in, defaulting to this one, so a
# project that depends on iris can be run through the same rig. `--bin` runs a
# crate binary rather than an example, and takes its own argv from
# `$RUN_HEADLESS_ARGS`, word-split on purpose.
#
# `--mode` sets the output, for running something at a size other than a
# desktop's -- a phone's, say. Set every run rather than only when it changes:
# the compositor is reused between runs, so a default-shaped run after a
# custom one would otherwise inherit the other's output and quietly screenshot
# the wrong size.
#
# `--resize WxH@Hz` changes the output under the app once it is up, then
# screenshots. A resize is its own case: what it has to match is a cold start
# at that size, byte for byte, and nothing in `cargo test` can see it.
#
# `--replay FILE` drives a `.touch` recording into the window through
# `replay-touch`, which reads it with the same parser `iris::harness` uses. A
# recording is `<ms> down|move|up <x> <y>` in the output's own pixels. With
# `--shot` it also writes `<shot>-before.png` from just before the gesture,
# since "it moved" is a claim about two pictures.
#
# What it supplies is a compositor for winit to open a surface on: a headless
# sway, and `grim` to screenshot it. Sway gets its own socket and runtime
# directory rather than joining whatever else is running, because it tiles --
# adding a window to someone else's compositor resizes theirs.
set -eu
need() {
command -v "$1" >/dev/null 2>&1 || {
echo "run-headless: $1 is not installed ($2)" >&2
exit 127
}
}
need sway "the compositor an example opens its window on"
need swaymsg "sway's control socket"
scripts=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$scripts/.." && pwd)
workdir="$root"
cd "$root"
run="${XDG_RUNTIME_DIR:-/tmp}/iris-headless"
seconds=3
shot=""
replay=""
resize=""
example=""
kind=example
mode=1920x1200@60Hz
while [ $# -gt 0 ]; do
case "$1" in
--shot) shot=$2; shift 2 ;;
--seconds) seconds=$2; shift 2 ;;
--bin) kind=bin; shift ;;
--mode) mode=$2; shift 2 ;;
--resize) resize=$2; shift 2 ;;
--replay) replay=$2; shift 2 ;;
--dir) workdir=$(cd "$2" && pwd); shift 2 ;;
--) shift; break ;;
*) example=$1; shift ;;
esac
done
[ -n "$example" ] || { echo "usage: $0 NAME [--bin] [--dir DIR] [--mode WxH@Hz] [--resize WxH@Hz] [--replay TOUCH] [--shot PNG] [--seconds N] [-- cargo args]" >&2; exit 2; }
[ -z "$replay" ] || [ -f "$replay" ] || { echo "run-headless: no touch script at $replay" >&2; exit 2; }
[ -z "$shot" ] || need grim "the screenshot --shot writes"
mkdir -p "$run"
export SWAYSOCK="$run/sway.sock"
# Named rather than left to sway's pid-based default, so a second run reuses
# this compositor instead of starting another beside it.
if ! swaymsg -t get_version >/dev/null 2>&1; then
rm -f "$SWAYSOCK"
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 LIBSEAT_BACKEND=noop \
setsid sway -c "$scripts/headless.conf" >"$run/sway.log" 2>&1 &
i=0
while [ $i -lt 20 ]; do
swaymsg -t get_version >/dev/null 2>&1 && break
i=$((i + 1)); sleep 0.5
done
swaymsg -t get_version >/dev/null 2>&1 || {
echo "run-headless: compositor did not start; see $run/sway.log" >&2
exit 1
}
fi
rm -f "$run/display"
swaymsg exec -- "sh -c 'printf %s \"\$WAYLAND_DISPLAY\" > $run/display'" >/dev/null
i=0
while [ $i -lt 20 ]; do
[ -s "$run/display" ] && break
i=$((i + 1)); sleep 0.5
done
[ -s "$run/display" ] || { echo "run-headless: could not read WAYLAND_DISPLAY" >&2; exit 1; }
WAYLAND_DISPLAY=$(cat "$run/display")
export WAYLAND_DISPLAY
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
swaymsg output HEADLESS-1 mode "$mode" >/dev/null
# The extent `replay-touch` positions against, so a script's coordinates
# are the output's own pixels.
out_w=${mode%x*}
out_h=${mode#*x}; out_h=${out_h%@*}
# Built before the app starts, so a compile error is not reported as a
# window that failed to move.
[ -z "$replay" ] || (cd "$root" && cargo build --bin replay-touch -p rig-input) >&2
cd "$workdir"
if [ "$kind" = bin ]; then
cargo build --bin "$example" "$@" >&2
bin="$workdir/target/debug/$example"
else
cargo build --example "$example" "$@" >&2
bin="$workdir/target/debug/examples/$example"
fi
# Deliberately word-split: this is the binary's own argv, not a single path.
# shellcheck disable=SC2086
"$bin" ${RUN_HEADLESS_ARGS:-} >"$run/$example.log" 2>&1 &
pid=$!
trap 'kill "$pid" 2>/dev/null || true' EXIT INT TERM
# Wait for the window to be mapped rather than for a number of seconds. A
# fixed sleep took an all-black screenshot the first time this ran, when sway
# had started in the same invocation and had not composited its output yet --
# which is indistinguishable from an app that draws nothing.
i=0
while [ $i -lt 40 ]; do
kill -0 "$pid" 2>/dev/null || break
swaymsg -t get_tree --raw 2>/dev/null | grep -q "\"pid\":$pid," && break
i=$((i + 1)); sleep 0.25
done
i=0
while [ $i -lt "$((seconds * 2))" ]; do
kill -0 "$pid" 2>/dev/null || break
i=$((i + 1)); sleep 0.5
done
if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then
swaymsg output HEADLESS-1 mode "$resize" >/dev/null
echo "run-headless: resized to $resize" >&2
sleep 2
fi
if [ -n "$replay" ] && kill -0 "$pid" 2>/dev/null; then
if [ -n "$shot" ]; then
grim "${shot%.png}-before.png"
echo "run-headless: wrote ${shot%.png}-before.png (before the gesture)" >&2
fi
"$root/target/debug/replay-touch" "$out_w" "$out_h" "$replay"
# A fling outlives the finger: the gesture's own last sample is not
# when the list stops. Long enough for Android's spline to settle
# (`FlingCalculator::duration` tops out around a second and a half).
sleep 2
fi
if kill -0 "$pid" 2>/dev/null; then
[ -n "$shot" ] && grim "$shot" && echo "run-headless: wrote $shot" >&2
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
status=0
else
wait "$pid" 2>/dev/null || status=$?
echo "run-headless: $example exited early (status ${status:-0})" >&2
status=${status:-1}
fi
echo "--- $example output ---" >&2
cat "$run/$example.log" >&2
exit "$status"
+4 -6
View File
@@ -15,10 +15,8 @@ where
let region = ctx.data.render.window_region(&id).unwrap(); let region = ctx.data.render.window_region(&id).unwrap();
let id_pos = region.top_left; let id_pos = region.top_left;
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left; let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
// The pointer arrives from the platform in floats; everything let pos = ctx.data.pos + container_pos - id_pos;
// it is compared against is on the grid. let size = region.size();
let pos = (PxVec2::from_f32(ctx.data.pos) + container_pos - id_pos).to_f32();
let size = region.size().to_f32();
select( select(
rsc, rsc,
ctx.data.render, ctx.data.render,
@@ -72,8 +70,8 @@ fn select(
if let Some(region) = render.window_region(&id) { if let Some(region) = render.window_region(&id) {
state.window.set_ime_allowed(true); state.window.set_ime_allowed(true);
state.window.set_ime_cursor_area( state.window.set_ime_cursor_area(
LogicalPosition::<f32>::from(region.top_left.to_f32().tuple()), LogicalPosition::<f32>::from(region.top_left.tuple()),
LogicalSize::<f32>::from(region.size().to_f32().tuple()), LogicalSize::<f32>::from(region.size().tuple()),
); );
} }
state.focus = Some(id); state.focus = Some(id);
+1 -1
View File
@@ -251,7 +251,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.renderer.draw(); ui_state.renderer.draw();
} }
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => {
render.resize((size.width, size.height), rsc.widgets_mut()); render.resize((size.width, size.height));
ui_state.renderer.resize(size) ui_state.renderer.resize(size)
} }
WindowEvent::KeyboardInput { event, .. } => { WindowEvent::KeyboardInput { event, .. } => {
+3 -3
View File
@@ -198,7 +198,7 @@ impl SensorUi for UiRenderState {
let Some(region) = region_of(id) else { let Some(region) = region_of(id) else {
continue; continue;
}; };
if !cursor.exists || !region.contains(PxVec2::from_f32(cursor.pos)) { if !cursor.exists || !region.contains(cursor.pos) {
continue; continue;
} }
hovered.now.push(id); hovered.now.push(id);
@@ -249,8 +249,8 @@ fn deliver<Rsc: HasEvents>(
region: PixelRegion, region: PixelRegion,
) -> bool { ) -> bool {
let data = CursorData { let data = CursorData {
pos: cursor.pos - region.top_left.to_f32(), pos: cursor.pos - region.top_left,
size: region.size().to_f32(), size: region.bot_right - region.top_left,
scroll_delta: cursor.scroll_delta, scroll_delta: cursor.scroll_delta,
hover, hover,
cursor: cursor.clone(), cursor: cursor.clone(),
+6 -159
View File
@@ -29,95 +29,14 @@ macro_rules! assert_corners {
assert_eq!( assert_eq!(
$harness.region(&$id).expect("widget drew nothing"), $harness.region(&$id).expect("widget drew nothing"),
$crate::core::PixelRegion { $crate::core::PixelRegion {
top_left: $crate::core::PxVec2::new( top_left: $crate::core::util::Vec2::new($x0 as f32, $y0 as f32),
$crate::core::Px::from_f32($x0 as f32), bot_right: $crate::core::util::Vec2::new($x1 as f32, $y1 as f32),
$crate::core::Px::from_f32($y0 as f32),
),
bot_right: $crate::core::PxVec2::new(
$crate::core::Px::from_f32($x1 as f32),
$crate::core::Px::from_f32($y1 as f32),
),
} }
); );
}; };
} }
pub use crate::assert_corners; pub use crate::assert_corners;
/// One replayed pointer sample, cut down to what a window delivers: where the
/// pointer is, and whether the button changed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TouchAction {
Down,
Move,
Up,
}
impl TouchAction {
fn parse(word: &str) -> Option<Self> {
match word {
"down" => Some(Self::Down),
"move" => Some(Self::Move),
"up" => Some(Self::Up),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct TouchSample {
pub t_ms: u64,
pub action: TouchAction,
pub pos: Vec2,
}
/// A recorded gesture, in the output's own pixels: `<ms> down|move|up <x> <y>`
/// a line, `#` to end of line ignored.
///
/// One parser for both ways of replaying a recording -- into a harness, and
/// into a real window -- so the two cannot read the same file differently.
pub struct TouchScript {
pub samples: Vec<TouchSample>,
}
impl TouchScript {
pub fn parse(text: &str) -> Result<Self, String> {
let mut samples: Vec<TouchSample> = Vec::new();
for (i, line) in text.lines().enumerate() {
let line = line.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let at = |what: &str| format!("touch script line {}: {what}: {line:?}", i + 1);
let mut words = line.split_whitespace();
let (Some(t), Some(action), Some(x), Some(y), None) = (
words.next(),
words.next(),
words.next(),
words.next(),
words.next(),
) else {
return Err(at("expected `t_ms action x y`"));
};
let t_ms: u64 = t.parse().map_err(|_| at("t_ms is not a whole number"))?;
let action =
TouchAction::parse(action).ok_or_else(|| at("action is not down/move/up"))?;
let x: f32 = x.parse().map_err(|_| at("x is not a number"))?;
let y: f32 = y.parse().map_err(|_| at("y is not a number"))?;
if let Some(last) = samples.last()
&& t_ms < last.t_ms
{
return Err(at("samples must be in time order"));
}
samples.push(TouchSample {
t_ms,
action,
pos: Vec2::new(x, y),
});
}
Ok(Self { samples })
}
}
#[derive(Default)] #[derive(Default)]
pub struct HarnessState { pub struct HarnessState {
pub root: Option<StrongWidget>, pub root: Option<StrongWidget>,
@@ -144,9 +63,9 @@ impl Harness {
// bound that comes with `SyncSender` is far past anything a test // bound that comes with `SyncSender` is far past anything a test
// leaves unread. // leaves unread.
let (send, updates) = sync_channel(1024); let (send, updates) = sync_channel(1024);
let mut rsc = DefaultRsc::init(Arc::new(Queue(send))); let rsc = DefaultRsc::init(Arc::new(Queue(send)));
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize(size, rsc.widgets_mut()); render.resize(size);
Self { Self {
rsc, rsc,
render, render,
@@ -157,18 +76,11 @@ impl Harness {
} }
pub fn size(&self) -> Vec2 { pub fn size(&self) -> Vec2 {
self.render.output_size().to_f32() self.render.output_size()
} }
pub fn resize(&mut self, size: impl Into<Vec2>) { pub fn resize(&mut self, size: impl Into<Vec2>) {
self.render.resize(size, self.rsc.widgets_mut()); self.render.resize(size);
}
/// Changes a length rule after the fact, the way `.width()` sets one.
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<LayoutLen>) {
self.rsc
.widgets_mut()
.set_size_rule(id, axis, SizeRule::Exact(len.into()));
} }
/// Sets the root and lays it out, so a pointer event has something to hit. /// Sets the root and lays it out, so a pointer event has something to hit.
@@ -248,23 +160,6 @@ impl Harness {
self.release(CursorButton::Left); self.release(CursorButton::Left);
} }
/// Drives a recorded gesture through the harness.
pub fn replay(&mut self, script: &TouchScript) {
for sample in &script.samples {
match sample.action {
TouchAction::Down => {
self.move_to(sample.pos);
self.press(CursorButton::Left);
}
TouchAction::Move => self.move_to(sample.pos),
TouchAction::Up => {
self.move_to(sample.pos);
self.release(CursorButton::Left);
}
}
}
}
fn button(&mut self, button: CursorButton) -> &mut ActivationState { fn button(&mut self, button: CursorButton) -> &mut ActivationState {
let buttons = &mut self.cursor.buttons; let buttons = &mut self.cursor.buttons;
match button { match button {
@@ -283,51 +178,3 @@ impl Harness {
self.cursor.end_frame(); self.cursor.end_frame();
} }
} }
#[cfg(test)]
mod tests {
use super::*;
fn parse(text: &str) -> Result<Vec<(u64, TouchAction, f32, f32)>, String> {
Ok(TouchScript::parse(text)?
.samples
.iter()
.map(|s| (s.t_ms, s.action, s.pos.x, s.pos.y))
.collect())
}
#[test]
fn a_recording_is_time_action_and_a_point() {
assert_eq!(
parse("0 down 10 20\n16 move 10.5 24\n32 up 10.5 24").unwrap(),
[
(0, TouchAction::Down, 10.0, 20.0),
(16, TouchAction::Move, 10.5, 24.0),
(32, TouchAction::Up, 10.5, 24.0),
]
);
}
#[test]
fn blank_lines_and_comments_are_not_samples() {
assert_eq!(
parse("# a flick\n\n 0 down 1 2 # the finger lands\n\n").unwrap(),
[(0, TouchAction::Down, 1.0, 2.0)]
);
}
#[test]
fn a_recording_that_goes_backwards_is_rejected() {
// Replay waits out the gap between samples, so time running backwards
// would silently become no wait at all.
let err = parse("16 down 1 2\n0 up 1 2").unwrap_err();
assert!(err.contains("time order"), "{err}");
}
#[test]
fn a_line_that_is_not_a_sample_says_which_line() {
let err = parse("0 down 1 2\n16 wiggle 1 2").unwrap_err();
assert!(err.contains("line 2"), "{err}");
assert!(err.contains("down/move/up"), "{err}");
}
}
-1
View File
@@ -8,7 +8,6 @@
pub mod default; pub mod default;
pub mod event; pub mod event;
pub mod harness; pub mod harness;
pub mod random;
pub mod widget; pub mod widget;
pub use iris_core as core; pub use iris_core as core;
-915
View File
@@ -1,915 +0,0 @@
//! A seeded random widget tree, for tests and for looking at.
//!
//! One seed is one tree, on any machine and after any upgrade, so a test can
//! grow the same tree twice and a failing seed is reproduced by its number.
//! `examples/random.rs` draws one; `tests/generated.rs` checks that laying one
//! out again lands where growing it from scratch would.
use crate::prelude::*;
use std::collections::HashMap;
/// The declared lengths of one widget carrying a size rule, by axis.
pub type Lens = [Option<LayoutLen>; 2];
/// Where one widget carrying an alignment sits, by axis. `None` uses the
/// centered default.
pub type Aligns = [Option<AxisAlign>; 2];
/// What a test changes between two trees grown from the same seed, so the
/// warm one can be mutated and the cold one grown that way to begin with.
#[derive(Default)]
pub struct Edits {
/// Declared sizes, by the order the rules were put on.
pub sizes: HashMap<usize, Lens>,
/// Which children a span has, by the order the spans were made.
pub spans: HashMap<usize, SpanEdit>,
/// Alignments, by the order they were put on.
pub aligns: HashMap<usize, Aligns>,
/// Which widgets own a movable region, by the order they were offered
/// one. Region nodes change what a move writes and how deep a primitive's
/// chain is, so a tree that never grows one leaves both untested.
pub nodes: HashMap<usize, bool>,
/// Whether a [`Branch`] takes the side it would take at any measurement,
/// rather than the side the one it made says. The oracle wants the
/// measured side -- that is the whole point of a branch, and how a widget
/// believing a measurement a cold start would not have given it becomes a
/// different tree. A rig measuring cost wants this instead: a fixture
/// whose shape moves with the thing being measured cannot be compared
/// with itself across a change to it, and seed 1 at depth 8 went from 88
/// drawn widgets and 2,298 primitive writes a frame to 115 and 8,209
/// across fixed point, which is three and a half times the work behind a
/// number read as three and a half times the cost.
pub fixed_branches: bool,
}
#[derive(Default, Clone)]
pub struct SpanEdit {
/// Children to leave out, by index among the ones grown.
pub detach: Vec<usize>,
/// How many of the span's spares are in it, appended in order.
pub attach: usize,
}
/// xorshift64, written out rather than taken from a crate so that a seed
/// keeps meaning the same tree.
pub struct Rng(u64);
impl Rng {
pub fn new(seed: u64) -> Self {
Self(seed | 1)
}
pub fn bits(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
pub fn below(&mut self, n: usize) -> usize {
(self.bits() % n as u64) as usize
}
pub fn chance(&mut self) -> bool {
self.bits() & 1 == 0
}
}
const COLORS: [UiColor; 6] = [
UiColor::RED,
UiColor::GREEN,
UiColor::BLUE,
UiColor::YELLOW,
UiColor::CYAN,
UiColor::MAGENTA,
];
/// Leaves grown beside every span, for a test to put into it.
const SPARES: usize = 3;
const WORDS: &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.";
/// What growing a tree gives back: every widget in creation order, so two
/// trees from one seed line up index for index, and the declared sizes, which
/// are what a test changes to watch the change propagate.
#[derive(Default)]
pub struct Tree {
pub ids: Vec<WidgetId>,
pub sized: Vec<WidgetId>,
pub aligned: Vec<WidgetId>,
pub nodes: Vec<WidgetId>,
pub spans: Vec<Spanned>,
pub scrolls: Vec<WeakWidget<Scroll>>,
}
/// Branches on a child's measured length. Comparing boxes catches a widget
/// that moved; this catches one that believed a measurement a cold start
/// would not have given it, by turning that into a different tree. Its own
/// configuration never changes, so which side draws is a property of the
/// layout alone.
pub struct Branch {
pub probe: StrongWidget,
pub wide: StrongWidget,
pub narrow: StrongWidget,
pub threshold: f32,
}
impl Widget for Branch {
fn draw(&mut self, painter: &mut Painter) -> Size {
let mut top = UiRegion::FULL;
top.y.end = top.y.start.offset(Px::from_int(40));
let measured = painter.widget_within(&self.probe, top).len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
let mut below = UiRegion::FULL;
below.y.start = below.y.start.offset(Px::from_int(40));
match px > Px::from_f32(self.threshold) {
true => painter.widget_within(&self.wide, below),
false => painter.widget_within(&self.narrow, below),
};
Size::LEFTOVER
}
}
pub struct Spanned {
pub id: WeakWidget<Span>,
/// Everything made for this span that it does not hold -- spares never
/// attached and children detached alike. A widget belongs to one parent,
/// and one that belongs to nobody still has to be held here: dropping
/// the last share of it frees its id for the next widget to be given,
/// which puts two trees out of step.
pub spares: Vec<StrongWidget>,
/// How many children it was grown with, before any edit.
pub grown: usize,
}
/// A tree described rather than built: [`plan`] turns a seed into one of
/// these and [`build`] turns it into widgets, where growing did both at once.
///
/// The split is what makes a counterexample readable. A failing seed used to
/// be the entire record of one, because a grower that makes widgets as it
/// draws leaves nothing to take apart -- a shrinker could only grow its own
/// trees and hope to meet the same shape, which in practice it does not. A
/// plan is reduced by [`Plan::smaller`] and built again, so any seed that
/// fails can be cut down until what is left is small enough to read.
#[derive(Clone, Debug, PartialEq)]
pub struct Plan {
pub kind: Kind,
/// The declared size this widget carries. Whoever grows a widget offers
/// it one and the offer is taken or declined; a second offer to the same
/// widget is dropped, because two rules on one widget would settle in the
/// order they were applied rather than in grow order.
pub size: Option<Lens>,
/// The alignment it carries, under the same one-offer rule.
pub align: Option<Aligns>,
/// Whether it was offered a movable region of its own and what it
/// answered. `Some(false)` is an offer declined, which still uses up the
/// one offer, where `None` is an offer never made.
pub region_node: Option<bool>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Kind {
/// Wrapped and unwrapped text, because only one of them reads the width
/// it is given and so only one has to be drawn again for a new one.
Wrapped,
OneLine,
Rect {
color: usize,
alpha: u8,
},
/// Scrolling reads the pixel length of its box, which nothing else here
/// does, and gives its child a box longer than its own.
Scroll {
axis: Axis,
inner: Box<Plan>,
},
/// All three sides are grown either way, so a tree that draws one has the
/// same ids as a tree that draws another.
Branch {
probe: Box<Plan>,
wide: Box<Plan>,
narrow: Box<Plan>,
threshold: f32,
},
/// Each side its own, since a padding that is the same all round hides
/// anything that treats one edge differently from another.
Pad {
padding: [i32; 4],
inner: Box<Plan>,
},
Stack {
children: Vec<Plan>,
},
Span {
dir: usize,
gap: i32,
/// Grown for this span, in the order they are made.
children: Vec<Plan>,
/// Grown beside it whether or not they end up in it, so the widget
/// after them has the same id in a tree that leaves them out as in
/// one that puts them in.
spares: Vec<Plan>,
/// Which of `children` then `spares` are actually in the span, and
/// in what order -- kept apart from the two lists above so that a
/// tree which detaches, attaches or reorders its children still
/// makes the same widgets in the same order, and two builds line up
/// index for index. Anything not named here is built and held
/// rather than dropped, since freeing an id hands it to the next
/// widget and puts two trees out of step.
order: Vec<usize>,
},
}
impl Plan {
/// A widget carrying nothing anybody has offered it yet.
fn bare(kind: Kind) -> Self {
Self {
kind,
size: None,
align: None,
region_node: None,
}
}
/// How many widgets building it makes, spares and detached children
/// included, since those are made either way.
pub fn size(&self) -> usize {
1 + match &self.kind {
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => inner.size(),
Kind::Branch {
probe,
wide,
narrow,
..
} => probe.size() + wide.size() + narrow.size(),
Kind::Stack { children } => children.iter().map(Plan::size).sum(),
Kind::Span {
children, spares, ..
} => children.iter().chain(spares).map(Plan::size).sum(),
_ => 0,
}
}
/// The trees to try instead of this one when reducing a counterexample,
/// biggest cut first: a shrinker takes the first that still fails, so
/// offering "this subtree alone" before "this subtree with one child
/// fewer" is what gets from six hundred widgets to six rather than to
/// five hundred and ninety.
///
/// Every one of these is a tree the generator could have grown, so a
/// reduced plan is a counterexample in its own right rather than a
/// special case only the shrinker can make.
pub fn smaller(&self) -> Vec<Plan> {
let mut out = Vec::new();
// Standing in for the whole of it, which is the largest cut there is.
for kid in self.kids() {
out.push(kid.clone());
}
// Then what it carries, which costs nothing to put back if it was
// not the thing that mattered.
for dropped in [
self.region_node.map(|_| Plan {
region_node: None,
..self.clone()
}),
self.align.map(|_| Plan {
align: None,
..self.clone()
}),
self.size.map(|_| Plan {
size: None,
..self.clone()
}),
]
.into_iter()
.flatten()
{
out.push(dropped);
}
out.extend(self.kind.smaller().into_iter().map(|kind| Plan {
kind,
..self.clone()
}));
out
}
/// Visits every widget in the order [`build`] makes them, so a count
/// kept by the visitor indexes the same widget as the matching [`Tree`]
/// vector does.
pub fn walk_mut(&mut self, at: &mut impl FnMut(&mut Plan)) {
match &mut self.kind {
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => inner.walk_mut(at),
Kind::Branch {
probe,
wide,
narrow,
..
} => {
probe.walk_mut(at);
wide.walk_mut(at);
narrow.walk_mut(at);
}
Kind::Stack { children } => {
for child in children {
child.walk_mut(at);
}
}
Kind::Span {
children, spares, ..
} => {
for child in children.iter_mut().chain(spares) {
child.walk_mut(at);
}
}
_ => {}
}
at(self);
}
/// The same tree with `edits` applied, by the indices the generator would
/// have used for them.
///
/// [`plan`] resolves edits while drawing, which needs a seed. A scenario
/// needs them applied to a tree that already exists -- one it has built,
/// and one a shrinker may already have cut down, where no seed grows it
/// any more. Both routes take the same [`Edits`], so a case written
/// against one reads the same against the other.
pub fn edited(&self, edits: &Edits) -> Plan {
let mut out = self.clone();
let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0);
out.walk_mut(&mut |plan| {
if let Kind::Span {
children,
spares,
order,
..
} = &mut plan.kind
{
if let Some(edit) = edits.spans.get(&spans) {
*order = span_edited(order, children.len(), spares.len(), edit);
}
spans += 1;
}
if let Kind::Branch { threshold, .. } = &mut plan.kind
&& edits.fixed_branches
{
*threshold = f32::MIN;
}
if plan.size.is_some() {
if let Some(lens) = edits.sizes.get(&sized) {
plan.size = Some(*lens);
}
sized += 1;
}
if plan.align.is_some() {
if let Some(align) = edits.aligns.get(&aligned) {
plan.align = Some(*align);
}
aligned += 1;
}
if plan.region_node.is_some() {
if let Some(take) = edits.nodes.get(&nodes) {
plan.region_node = Some(*take);
}
nodes += 1;
}
});
out
}
fn kids(&self) -> Vec<&Plan> {
match &self.kind {
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => vec![inner],
Kind::Branch {
probe,
wide,
narrow,
..
} => vec![probe, wide, narrow],
Kind::Stack { children } => children.iter().collect(),
Kind::Span { children, .. } => children.iter().collect(),
_ => Vec::new(),
}
}
}
impl Kind {
/// Simplifications of the shape alone, leaving what the widget carries to
/// [`Plan::smaller`]. Replacing a node with one of its children is there
/// rather than here, since it answers with a whole `Plan`.
fn smaller(&self) -> Vec<Kind> {
let mut out = Vec::new();
/// One child reduced at a time, rebuilt into the same shape. Every
/// answer has the same number of children as it was given, so it is
/// for the shapes whose child count is part of what they are.
fn reduced(kids: &[Plan], rebuild: &dyn Fn(Vec<Plan>) -> Kind) -> Vec<Kind> {
let mut out = Vec::new();
for (i, kid) in kids.iter().enumerate() {
for small in kid.smaller() {
let mut next = kids.to_vec();
next[i] = small;
out.push(rebuild(next));
}
}
out
}
/// One child dropped, then [`reduced`]. For the shapes that hold any
/// number of children, where dropping one is the cut that matters.
fn each(kids: &[Plan], rebuild: &dyn Fn(Vec<Plan>) -> Kind) -> Vec<Kind> {
let mut out = Vec::new();
for i in 0..kids.len() {
if kids.len() > 1 {
let mut less = kids.to_vec();
less.remove(i);
out.push(rebuild(less));
}
}
out.extend(reduced(kids, rebuild));
out
}
match self {
// The one leaf that reads the width it is given, then the one
// that does not, then the one that measures nothing at all.
Kind::Wrapped => out.push(Kind::OneLine),
Kind::OneLine => out.push(Kind::Rect {
color: 0,
alpha: 255,
}),
Kind::Rect { .. } => {}
Kind::Scroll { axis, inner } => {
let axis = *axis;
out.extend(each(std::slice::from_ref(inner), &|mut k| Kind::Scroll {
axis,
inner: Box::new(k.remove(0)),
}));
}
Kind::Branch {
probe,
wide,
narrow,
threshold,
} => {
let threshold = *threshold;
// All three sides stay: a branch is the widget that draws
// one of two on a measurement, and one with a side missing
// is a different widget rather than a smaller one. Dropping
// the branch for a side is offered by `Plan::smaller`.
let sides = [(**probe).clone(), (**wide).clone(), (**narrow).clone()];
out.extend(reduced(&sides, &|k| Kind::Branch {
probe: Box::new(k[0].clone()),
wide: Box::new(k[1].clone()),
narrow: Box::new(k[2].clone()),
threshold,
}));
}
Kind::Pad { padding, inner } => {
let padding = *padding;
if padding != [0; 4] {
out.push(Kind::Pad {
padding: [0; 4],
inner: inner.clone(),
});
}
out.extend(each(std::slice::from_ref(inner), &|mut k| Kind::Pad {
padding,
inner: Box::new(k.remove(0)),
}));
}
Kind::Stack { children } => {
out.extend(each(children, &|children| Kind::Stack { children }))
}
Kind::Span {
dir,
gap,
children,
spares,
order,
} => {
let (dir, gap, n) = (*dir, *gap, children.len());
let span = |children: Vec<Plan>, spares: Vec<Plan>, order: Vec<usize>| Kind::Span {
dir,
gap,
children,
spares,
order,
};
let identity: Vec<usize> = (0..n).collect();
// An order the generator did not choose is part of the tree,
// so take that off before taking the tree apart.
if *order != identity {
out.push(span(children.clone(), spares.clone(), identity));
}
// Spares exist to be attached; with none attached they are
// widgets the span never holds.
if !spares.is_empty() && order.iter().all(|&i| i < n) {
out.push(span(children.clone(), Vec::new(), order.clone()));
}
if gap != 0 {
out.push(Kind::Span {
dir,
gap: 0,
children: children.clone(),
spares: spares.clone(),
order: order.clone(),
});
}
for k in 0..n {
if n > 1 {
let mut less = children.clone();
less.remove(k);
// Everything after it shifts down, spares included,
// since they are indexed past the children.
let order = order
.iter()
.filter(|&&i| i != k)
.map(|&i| if i > k { i - 1 } else { i })
.collect();
out.push(span(less, spares.clone(), order));
}
}
for (i, kid) in children.iter().enumerate() {
for small in kid.smaller() {
let mut next = children.clone();
next[i] = small;
out.push(span(next, spares.clone(), order.clone()));
}
}
}
}
out
}
}
/// A [`SpanEdit`] applied to the order a span already holds its children in.
///
/// `detach` names positions in that order and `attach` takes from the front
/// of what the span is not holding, both of which is what a test changing a
/// live span does -- so an edit means the same thing said to a tree and said
/// to the plan it was built from. On a span nobody has edited the order is
/// the children in the order they were grown, and this is then "leave these
/// out and put that many spares on the end".
fn span_edited(order: &[usize], children: usize, spares: usize, edit: &SpanEdit) -> Vec<usize> {
let mut detach = edit.detach.clone();
detach.sort_unstable();
detach.dedup();
let mut next: Vec<usize> = order
.iter()
.enumerate()
.filter(|(at, _)| !detach.contains(at))
.map(|(_, &which)| which)
.collect();
// What the span is not holding, in the order it hands them back: what it
// was already not holding first, in the order the widgets were made, and
// what this edit takes out after that, highest position first. A child
// just detached goes to the back rather than straight back in, which is
// what makes detaching one and attaching one a trade.
let mut free: Vec<usize> = (0..children + spares)
.filter(|i| !order.contains(i))
.collect();
free.extend(detach.iter().rev().filter_map(|&at| order.get(at).copied()));
next.extend(free.into_iter().take(edit.attach));
next
}
/// Plans the tree `seed` describes, `edits` replacing what it would otherwise
/// have given the widgets that carry them.
///
/// The edits are resolved here rather than at build time, so that a plan is
/// the whole of what a tree is and building one has nothing left to decide.
pub fn plan(seed: u64, depth: usize, edits: &Edits) -> Plan {
let mut sow = Sow {
rng: Rng::new(seed),
edits,
sized: 0,
aligned: 0,
nodes: 0,
spans: 0,
};
sow.node(depth)
}
/// Grows the tree `seed` describes, `edits` replacing the declared sizes it
/// would otherwise have given those wrappers.
pub fn grow<Rsc: UiRsc + 'static>(
rsc: &mut Rsc,
seed: u64,
depth: usize,
edits: &Edits,
) -> (StrongWidget, Tree) {
build(rsc, &plan(seed, depth, edits))
}
/// Draws a plan out of the random stream. Every draw happens in the order it
/// always has and before the decision it feeds, including the decisions that
/// are then dropped, because a seed has to keep meaning the same tree.
struct Sow<'a> {
rng: Rng,
edits: &'a Edits,
sized: usize,
aligned: usize,
nodes: usize,
spans: usize,
}
impl Sow<'_> {
fn leaf(&mut self) -> Plan {
Plan::bare(match self.rng.below(4) {
0 => Kind::Wrapped,
1 => Kind::OneLine,
_ => {
let color = self.rng.below(COLORS.len());
let alpha = (self.rng.below(5) * 63) as u8;
Kind::Rect { color, alpha }
}
})
}
fn len(&mut self) -> Option<LayoutLen> {
match self.rng.below(4) {
0 => Some(LayoutLen::px(20.0 + self.rng.below(180) as f32)),
1 => Some(LayoutLen::LEFTOVER),
_ => None,
}
}
fn align(&mut self) -> Aligns {
let axis = |s: &mut Self| match s.rng.below(4) {
0 => None,
1 => Some(AxisAlign::NEG),
2 => Some(AxisAlign::CENTER),
_ => Some(AxisAlign::POS),
};
let (x, y) = (axis(self), axis(self));
// Aligning on neither axis leaves the branch unexercised.
match x.is_none() && y.is_none() {
true => [Some(AxisAlign::CENTER), y],
false => [x, y],
}
}
/// A declared size over half the tree, kept where a test can change it.
fn sized(&mut self, inner: &mut Plan) {
let take = self.rng.chance();
let lens = [self.len(), self.len()];
if !take || inner.size.is_some() {
return;
}
let idx = self.sized;
self.sized += 1;
inner.size = Some(self.edits.sizes.get(&idx).copied().unwrap_or(lens));
}
/// An alignment over some of the tree, kept where a test can change it.
fn aligned(&mut self, inner: &mut Plan) {
let align = self.align();
if inner.align.is_some() {
return;
}
let idx = self.aligned;
self.aligned += 1;
inner.align = Some(self.edits.aligns.get(&idx).copied().unwrap_or(align));
}
/// A movable region of its own over some of the tree. What it changes is
/// how a move is written and how long a primitive's chain is, neither of
/// which any other branch here varies.
fn noded(&mut self, inner: &mut Plan) {
let take = self.rng.below(4) == 0;
if inner.region_node.is_some() {
return;
}
let idx = self.nodes;
self.nodes += 1;
inner.region_node = Some(self.edits.nodes.get(&idx).copied().unwrap_or(take));
}
fn offered(&mut self, inner: &mut Plan) {
self.sized(inner);
self.noded(inner);
}
fn node(&mut self, depth: usize) -> Plan {
if depth == 0 {
return self.leaf();
}
let positioned = self.rng.below(6);
if positioned == 0 {
let mut inner = self.node(depth - 1);
self.offered(&mut inner);
let axis = if self.rng.chance() { Axis::X } else { Axis::Y };
return Plan::bare(Kind::Scroll {
axis,
inner: Box::new(inner),
});
}
if positioned == 2 {
let probe = self.node(depth - 1);
let wide = self.node(depth - 1);
let narrow = self.node(depth - 1);
// Drawn either way, so the side a fixed branch takes is still a
// side the generator chose -- and it consumes the same randomness
// as a measured one, so the two grow the same ids.
let measured = self.rng.below(500) as f32;
let threshold = match self.edits.fixed_branches {
true => f32::MIN,
false => measured,
};
return Plan::bare(Kind::Branch {
probe: Box::new(probe),
wide: Box::new(wide),
narrow: Box::new(narrow),
threshold,
});
}
if positioned == 1 {
// Carries an alignment and makes no widget of its own, so the
// plan for it is the child it aligned.
let mut inner = self.node(depth - 1);
self.offered(&mut inner);
self.aligned(&mut inner);
return inner;
}
if self.rng.below(4) == 0 {
let mut inner = self.node(depth - 1);
self.offered(&mut inner);
let side = |s: &mut Self| s.rng.below(24) as i32;
let padding = [side(self), side(self), side(self), side(self)];
return Plan::bare(Kind::Pad {
padding,
inner: Box::new(inner),
});
}
let grown = 2 + self.rng.below(3);
let mut children = Vec::with_capacity(grown);
for _ in 0..grown {
let mut child = self.node(depth - 1);
self.offered(&mut child);
children.push(child);
}
if self.rng.chance() {
return Plan::bare(Kind::Stack { children });
}
let spares: Vec<Plan> = (0..SPARES).map(|_| self.leaf()).collect();
let idx = self.spans;
self.spans += 1;
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
let dir = self.rng.below(4);
// A row takes the height it is given rather than its tallest child,
// which is a rule beside it. Derived from an existing choice and
// consuming no randomness: a seed must keep growing the same tree
// when the generator gains another configuration.
let gap = self.rng.below(3) as i32 * 4;
let grown: Vec<usize> = (0..children.len()).collect();
let order = span_edited(&grown, children.len(), spares.len(), &edit);
Plan::bare(Kind::Span {
dir,
gap,
children,
spares,
order,
})
}
}
/// Builds a plan's widgets in the order it describes them, so two builds of
/// one plan line up index for index and their boxes can be compared.
pub fn build<Rsc: UiRsc + 'static>(rsc: &mut Rsc, plan: &Plan) -> (StrongWidget, Tree) {
let mut build = Build {
rsc,
tree: Tree::default(),
};
let root = build.node(plan);
(root, build.tree)
}
struct Build<'a, Rsc> {
rsc: &'a mut Rsc,
tree: Tree,
}
impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
fn node(&mut self, plan: &Plan) -> StrongWidget {
let built = self.kind(&plan.kind);
let id = built.id();
if let Some(lens) = plan.size {
self.rsc
.ui_mut()
.widgets
.set_size_rules(id, lens[0], lens[1]);
self.tree.sized.push(id);
}
if let Some(align) = plan.align {
let widgets = &mut self.rsc.ui_mut().widgets;
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
widgets.set_alignment(id, axis, align.unwrap_or_default());
}
self.tree.aligned.push(id);
}
if let Some(take) = plan.region_node {
self.rsc.ui_mut().widgets.set_region_node(id, take);
self.tree.nodes.push(id);
}
built
}
fn kind(&mut self, kind: &Kind) -> StrongWidget {
let id: StrongWidget = match kind {
Kind::Wrapped => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
Kind::OneLine => wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add_strong(self.rsc),
Kind::Rect { color, alpha } => rect(COLORS[*color].alpha(*alpha)).add_strong(self.rsc),
Kind::Scroll { axis, inner } => {
let inner = self.node(inner);
let id = Scroll::new(inner, *axis).add(self.rsc);
self.tree.scrolls.push(id);
self.tree.ids.push(id.id());
return id.add_strong(self.rsc);
}
Kind::Branch {
probe,
wide,
narrow,
threshold,
} => {
let probe = self.node(probe);
let wide = self.node(wide);
let narrow = self.node(narrow);
let id = Branch {
probe,
wide,
narrow,
threshold: *threshold,
}
.add(self.rsc);
self.tree.ids.push(id.id());
return id.add_strong(self.rsc);
}
Kind::Pad { padding, inner } => {
let inner = self.node(inner);
let [left, right, top, bottom] = padding.map(Px::from_int);
let padding = Padding {
left,
right,
top,
bottom,
};
Pad { padding, inner }.add_strong(self.rsc)
}
Kind::Stack { children } => {
let children = children.iter().map(|c| self.node(c)).collect();
Stack {
children,
size: StackSize::Child(0),
}
.add_strong(self.rsc)
}
Kind::Span {
dir,
gap,
children,
spares,
order,
} => {
let grown = children.len();
// Every one of them is made, in this order, whether or not
// the span ends up holding it.
let made: Vec<StrongWidget> = children
.iter()
.chain(spares)
.map(|c| self.node(c))
.collect();
let mut left: Vec<Option<StrongWidget>> = made.into_iter().map(Some).collect();
let children: Vec<StrongWidget> = order
.iter()
.filter_map(|&i| left.get_mut(i).and_then(Option::take))
.collect();
// What the span does not hold is still held here: dropping
// the last share of a widget frees its id for the next one
// to be given, which puts two trees out of step.
let spares: Vec<StrongWidget> = left.into_iter().flatten().collect();
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][*dir % 4];
let id = Span {
children,
dir,
gap: Px::from_int(*gap),
}
.add(self.rsc);
if dir.axis == Axis::X {
self.rsc
.widgets_mut()
.set_size_rules(id, None, Some(LayoutLen::rel(1.0)));
}
self.tree.ids.push(id.id());
self.tree.spans.push(Spanned { id, spares, grown });
return id.add_strong(self.rsc);
}
};
self.tree.ids.push(id.id());
id
}
}
+7 -3
View File
@@ -8,11 +8,15 @@ pub struct Image {
impl Widget for Image { impl Widget for Image {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.primitive(&self.handle); painter.primitive(&self.handle);
Size::px(self.handle.size()) Size::abs(self.handle.size())
} }
fn size_hint(&self, axis: Axis) -> Option<LayoutLen> { fn size_hint(&self, axis: Axis) -> Option<Len> {
Some(LayoutLen::px(self.handle.size().axis(axis))) Some(Len::abs(self.handle.size().axis(axis)))
}
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
} }
} }
+7 -8
View File
@@ -6,13 +6,12 @@ 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(painter.region());
painter.widget(&self.inner); painter.widget(&self.inner).size()
// 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
// neither take less of one nor honestly ask for more. Passing the /// It clips to the box it was given, not to the part its child used.
// inner size up instead asks to be placed at a length it does not fn on_resize(&self, _: Axis) -> OnResize {
// draw, and the framework would place the drawing it clipped away. OnResize::Redraw
Size::LEFTOVER
} }
} }
+2 -2
View File
@@ -1,15 +1,15 @@
mod image; mod image;
mod mask; mod mask;
mod position; mod position;
mod ptr;
mod rect; mod rect;
mod text; mod text;
mod trait_fns; mod trait_fns;
mod wrapper;
pub use image::*; pub use image::*;
pub use mask::*; pub use mask::*;
pub use position::*; pub use position::*;
pub use ptr::*;
pub use rect::*; pub use rect::*;
pub use text::*; pub use text::*;
pub use trait_fns::*; pub use trait_fns::*;
pub use wrapper::*;
+22
View File
@@ -0,0 +1,22 @@
use crate::prelude::*;
pub struct Aligned {
pub inner: StrongWidget,
pub align: Align,
}
impl Widget for Aligned {
fn draw(&mut self, painter: &mut Painter) -> Size {
// Drawn where it may be too big, then given its aligned box once its
// size is known.
let size = painter.widget(&self.inner).size();
let region = match self.align.tuple() {
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL),
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
(None, None) => UiRegion::FULL,
};
painter.widget_within(&self.inner, region);
size
}
}
+25
View File
@@ -0,0 +1,25 @@
use crate::prelude::*;
pub struct MaxSize {
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
}
impl Widget for MaxSize {
fn draw(&mut self, painter: &mut Painter) -> Size {
let child = painter.widget(&self.inner).size();
let output = painter.output_size();
Size {
x: capped(child.x, self.x, output.x),
y: capped(child.y, self.y, output.y),
}
}
}
fn capped(len: Len, max: Option<Len>, output: f32) -> Len {
match max {
Some(max) if len.apply_rest().to_abs(output) > max.apply_rest().to_abs(output) => max,
_ => len,
}
}
+6
View File
@@ -1,13 +1,19 @@
mod align;
mod layer; mod layer;
mod max_size;
mod offset; mod offset;
mod pad; mod pad;
mod scroll; mod scroll;
mod set_size;
mod span; mod span;
mod stack; mod stack;
pub use align::*;
pub use layer::*; pub use layer::*;
pub use max_size::*;
pub use offset::*; pub use offset::*;
pub use pad::*; pub use pad::*;
pub use scroll::*; pub use scroll::*;
pub use set_size::*;
pub use span::*; pub use span::*;
pub use stack::*; pub use stack::*;
+37 -44
View File
@@ -7,21 +7,16 @@ pub struct Pad {
impl Widget for Pad { impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
// The inner's own alignment, not the near edge. This reports the let inner = painter
// inner's size plus the padding, so where the box is that answer the .widget_within(&self.inner, self.padding.region())
// inset box is exactly the inner and alignment has no room to move .size();
// it; where the box is bigger -- a share of a row, a rule over this
// widget -- the slack is the inner's to sit in, and forcing the near
// edge pinned it to a corner it had not asked for.
let inside = self.padding.region_of(painter.placement());
let inner = painter.widget_within(&self.inner, inside).size();
Size { Size {
x: LayoutLen { x: Len {
px: inner.x.px + self.padding.left + self.padding.right, abs: inner.x.abs + self.padding.left + self.padding.right,
..inner.x ..inner.x
}, },
y: LayoutLen { y: Len {
px: inner.y.px + self.padding.top + self.padding.bottom, abs: inner.y.abs + self.padding.top + self.padding.bottom,
..inner.y ..inner.y
}, },
} }
@@ -29,22 +24,22 @@ impl Widget for Pad {
} }
pub struct Padding { pub struct Padding {
pub left: Px, pub left: f32,
pub right: Px, pub right: f32,
pub top: Px, pub top: f32,
pub bottom: Px, pub bottom: f32,
} }
impl Padding { impl Padding {
pub const ZERO: Self = Self { pub const ZERO: Self = Self {
left: Px::ZERO, left: 0.0,
right: Px::ZERO, right: 0.0,
top: Px::ZERO, top: 0.0,
bottom: Px::ZERO, bottom: 0.0,
}; };
pub fn uniform(amt: impl UiNum) -> Self { pub fn uniform(amt: impl UiNum) -> Self {
let amt = Px::from_num(amt); let amt = amt.to_f32();
Self { Self {
left: amt, left: amt,
right: amt, right: amt,
@@ -52,82 +47,80 @@ impl Padding {
bottom: amt, bottom: amt,
} }
} }
/// `region` less this padding on each side. pub fn region(&self) -> UiRegion {
pub fn region_of(&self, mut region: UiRegion) -> UiRegion { let mut region = UiRegion::FULL;
region.x.start.px += self.left; region.x.start.abs += self.left;
region.y.start.px += self.top; region.y.start.abs += self.top;
region.x.end.px -= self.right; region.x.end.abs -= self.right;
region.y.end.px -= self.bottom; region.y.end.abs -= self.bottom;
region region
} }
pub fn region(&self) -> UiRegion {
self.region_of(UiRegion::FULL)
}
pub fn x(amt: impl UiNum) -> Self { pub fn x(amt: impl UiNum) -> Self {
let amt = Px::from_num(amt); let amt = amt.to_f32();
Self { Self {
left: amt, left: amt,
right: amt, right: amt,
..Self::ZERO top: 0.0,
bottom: 0.0,
} }
} }
pub fn y(amt: impl UiNum) -> Self { pub fn y(amt: impl UiNum) -> Self {
let amt = Px::from_num(amt); let amt = amt.to_f32();
Self { Self {
left: 0.0,
right: 0.0,
top: amt, top: amt,
bottom: amt, bottom: amt,
..Self::ZERO
} }
} }
pub fn top(amt: impl UiNum) -> Self { pub fn top(amt: impl UiNum) -> Self {
let mut s = Self::ZERO; let mut s = Self::ZERO;
s.top = Px::from_num(amt); s.top = amt.to_f32();
s s
} }
pub fn bottom(amt: impl UiNum) -> Self { pub fn bottom(amt: impl UiNum) -> Self {
let mut s = Self::ZERO; let mut s = Self::ZERO;
s.bottom = Px::from_num(amt); s.bottom = amt.to_f32();
s s
} }
pub fn left(amt: impl UiNum) -> Self { pub fn left(amt: impl UiNum) -> Self {
let mut s = Self::ZERO; let mut s = Self::ZERO;
s.left = Px::from_num(amt); s.left = amt.to_f32();
s s
} }
pub fn right(amt: impl UiNum) -> Self { pub fn right(amt: impl UiNum) -> Self {
let mut s = Self::ZERO; let mut s = Self::ZERO;
s.right = Px::from_num(amt); s.right = amt.to_f32();
s s
} }
pub fn with_top(mut self, amt: impl UiNum) -> Self { pub fn with_top(mut self, amt: impl UiNum) -> Self {
self.top = Px::from_num(amt); self.top = amt.to_f32();
self self
} }
pub fn with_bottom(mut self, amt: impl UiNum) -> Self { pub fn with_bottom(mut self, amt: impl UiNum) -> Self {
self.bottom = Px::from_num(amt); self.bottom = amt.to_f32();
self self
} }
pub fn with_left(mut self, amt: impl UiNum) -> Self { pub fn with_left(mut self, amt: impl UiNum) -> Self {
self.left = Px::from_num(amt); self.left = amt.to_f32();
self self
} }
pub fn with_right(mut self, amt: impl UiNum) -> Self { pub fn with_right(mut self, amt: impl UiNum) -> Self {
self.right = Px::from_num(amt); self.right = amt.to_f32();
self self
} }
} }
impl<T: UiNum> From<T> for Padding { impl<T: UiNum> From<T> for Padding {
fn from(amt: T) -> Self { fn from(amt: T) -> Self {
Self::uniform(amt) Self::uniform(amt.to_f32())
} }
} }
+24 -69
View File
@@ -3,79 +3,36 @@ use crate::prelude::*;
pub struct Scroll { pub struct Scroll {
inner: StrongWidget, inner: StrongWidget,
axis: Axis, axis: Axis,
amt: Px, amt: f32,
snap_end: bool, snap_end: bool,
container_len: Px, container_len: f32,
content_len: Px, content_len: f32,
} }
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 output_len = painter.output_size().axis(self.axis);
// Draw in the whole container only when its scrolling-axis length is let container_len = painter.region().axis(self.axis).len();
// not already known, then draw it at the scrolled offset. // Drawn in the whole container to learn its length, then placed at
let whole = UiRegion::FULL; // the scrolled offset.
let own = painter.placement(); let child = painter.widget(&self.inner).size();
let answer_len = let content_len = child
match painter.known_len(&self.inner, self.axis, whole, [Some(own.x), Some(own.y)]) { .axis(self.axis)
Some(len) => len, .apply_rest()
None => painter.widget(&self.inner).size().axis(self.axis), .within_len(container_len)
}; .to_abs(output_len);
let content = answer_len.apply_leftover(); self.container_len = container_len.to_abs(output_len);
self.container_len = container_len; self.content_len = content_len;
self.content_len = content.to_px(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;
} }
self.update_amt(); self.update_amt();
let align = painter.alignment().axis(self.axis);
// Content of a fixed length that fits sits at the start of any box it
// fits in -- but only anchored there. Anywhere else it is a part of
// the room left over, so it moves with every length the box takes and
// the drawing holds for that length alone. One scrolled part way sits
// where it is until the box shrinks past what is left of it. Kept to
// the end, it moves with every length.
let fixed_len = content.rel == Rel::ZERO;
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG {
painter.holds(self.axis, self.content_len..=Px::MAX);
} else if fixed_len && !self.snap_end {
let left = self.content_len - self.amt;
painter.holds(self.axis, Px::MIN..=left);
}
// Content shorter than the viewport has room to sit in, and where it let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
// sits is this widget's own alignment -- the same property that would
// have placed the whole scroll in a box longer than it.
let slack = (self.container_len - self.content_len).max(Px::ZERO);
let anchor = slack.mul(align.rel());
let mut region = UiRegion::FULL;
// Content that fills the viewport and has not been scrolled is the
// viewport, and is handed back as it came. Writing the same box as
// its own length in pixels is the same box in another form, and the
// two do not round alike: a part centred in `rel 1` lands a step from
// one centred in `px 900`, since halving a difference is not halving
// each part of it.
let moved = anchor != Px::ZERO || self.amt != Px::ZERO;
if moved || self.content_len != self.container_len {
let offset = UiVec2::from_axis(
self.axis,
Len::from_parts(Rel::ZERO, anchor - self.amt),
Len::ZERO,
);
region = region.offset(offset);
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
} painter.widget_within(&self.inner, region);
// The viewport is the inner's region, so a fraction it declares or child
// reports is a fraction of what is on screen rather than of the
// content box its own answer decided. Where it is put is the content
// box, scrolled.
painter.widget_at(&self.inner, whole, [Some(region.x), Some(region.y)]);
// What it occupies is its box, on both axes: it clips its content to
// that box, so it can neither take less of one nor honestly ask for
// more. The content's length is what it scrolls through, not what it
// is.
Size::LEFTOVER
} }
} }
@@ -84,24 +41,22 @@ impl Scroll {
Self { Self {
inner, inner,
axis, axis,
amt: Px::ZERO, amt: 0.0,
snap_end: true, snap_end: true,
container_len: Px::ZERO, container_len: 0.0,
content_len: Px::ZERO, content_len: 0.0,
} }
} }
pub fn update_amt(&mut self) { pub fn update_amt(&mut self) {
self.amt = self.amt.max(Px::ZERO); self.amt = self.amt.max(0.0);
let len = (self.content_len - self.container_len).max(Px::ZERO); let len = (self.content_len - self.container_len).max(0.0);
self.amt = self.amt.min(len); self.amt = self.amt.min(len);
self.snap_end = self.amt == len; self.snap_end = self.amt == len;
} }
/// Scrolled by a distance the platform measures, which is the last place
/// a wheel notch or a finger is a float.
pub fn scroll(&mut self, amt: f32) { pub fn scroll(&mut self, amt: f32) {
self.amt -= Px::from_f32(amt); self.amt -= amt;
self.update_amt(); self.update_amt();
} }
} }
+26
View File
@@ -0,0 +1,26 @@
use crate::prelude::*;
pub struct SetSize {
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
}
impl Widget for SetSize {
fn draw(&mut self, painter: &mut Painter) -> Size {
let child = painter.widget(&self.inner).size();
Size {
x: self.x.unwrap_or(child.x),
y: self.y.unwrap_or(child.y),
}
}
/// A declared axis is known without looking at the child, which is what
/// lets a span lay out around `.height(rest(1))` without drawing it.
fn size_hint(&self, axis: Axis) -> Option<Len> {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
}
+42 -142
View File
@@ -4,173 +4,73 @@ use std::marker::PhantomData;
pub struct Span { pub struct Span {
pub children: Vec<StrongWidget>, pub children: Vec<StrongWidget>,
pub dir: Dir, pub dir: Dir,
pub gap: Px, pub gap: f32,
} }
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. // A length for every child before any is placed: from its own hint
// Its children are laid out along it, and what they declare or report // where it has one, and from drawing it where it does not.
// is a fraction of the region -- the area this span was told it has, let lens: Vec<Len> = self
// which it passes on unchanged. .children
let own = painter.placement(); .iter()
let row = *own.axis(axis); .map(|child| match painter.size_hint(child, axis) {
// Across itself the span's own box is the child's region: a span is
// what contains its children there, and nothing divides that axis.
// Along it the whole region is, so a fraction means the same thing
// for every child however much of the row is left when it is asked.
let region = UiRegion::from_axis(axis, UiSpan::FULL, *own.axis(!axis));
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();
// A length for every child before their final boxes are chosen: from
// a hint where one exists, and from drawing otherwise.
let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children {
// The whole region is the child's, so `rel(0.5)` is half the area
// this span was given whatever else is in it and wherever this
// child sits among them. What it is placed in is the room left
// from the cursor, because a text has to wrap at the width
// actually there.
let room = axis.pair(Some(along(cursor, far)), None);
let len = match painter.known_len(child, axis, region, room) {
Some(len) => len, Some(len) => len,
None => painter.widget_at(child, region, room).len(axis), None => painter.widget(child).len(axis),
}; })
cursor.px += len.px + self.gap; .collect();
cursor.rel += len.rel;
lens.push(len);
}
let gaps = self let gap = self.gap * self.children.len().saturating_sub(1) as f32;
.gap let total = lens.iter().fold(Len::abs(gap), |sum, len| sum + *len);
.mul_int(self.children.len().saturating_sub(1) as i32);
let total = lens.iter().fold(
LayoutLen {
px: gaps,
..LayoutLen::ZERO
},
|sum, len| sum + *len,
);
// What is left for the shares to divide: the row less everything let mut start = UiScalar::rel_min();
// fixed, as a length of the region rather than a number of pixels. let mut ortho = Len::ZERO;
let room = far - Len::from_parts(total.rel, total.px);
// Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. Asked of `room`
// itself, and answered back through the same expression, so the
// boundary is the drawing's own and not a second way of finding it:
// the three cases a rounded division needed -- the fixed parts
// growing slower than the box, faster, or exactly with it -- are the
// sign of `room.rel`, which `through` already reads. What the
// generated oracle checks is the consequence, since which children
// exist at all turns on this.
let mut shares = false;
if total.leftover > Weight::ZERO {
shares = room.to_px(painter.region_px_len(axis)) > Px::ZERO;
let holds = match shares {
true => Holds::from(Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=Px::ZERO),
};
painter.region_holds(axis, holds.through(room));
}
// Across itself a span is as long as its longest child -- unless a
// rule beside it gives that length outright, and then reading them
// answers nothing and makes its size depend on theirs for it. A rule
// that only bounds the length does not count: the answer is still
// this span's to give.
let shrinks = !painter.has_exact_size(!axis);
// What the fixed parts and the gaps before here take, which is a sum
// of lengths and exact, and how much of the leftover weight is
// spoken for. A position is one from the other rather than a step
// from the last child: the share of the room is rounded, and taking
// each from the one before it would carry every rounding along the
// row.
let mut fixed = Len::rel_min();
let mut taken = Weight::ZERO;
let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO;
for (child, len) in self.children.iter().zip(&lens) { for (child, len) in self.children.iter().zip(&lens) {
// A child asking for nothing but a part of what is left over, let mut span = UiSpan::FULL;
// when nothing is, is not drawn at all. One that also asked for span.start = start;
// pixels or a fraction keeps those and overflows. if len.rest > 0.0 {
if len.leftover > Weight::ZERO && len.px == Px::ZERO && len.rel == Rel::ZERO && !shares let offset = UiScalar::new(total.rel, total.abs);
{ let rel_end = UiScalar::rel(len.rest / total.rest);
painter.undraw(child); let end = (UiScalar::rel_max() + start) - offset;
fixed.px += self.gap; start = rel_end.within(&start.to(end));
continue;
} }
let from = start; start.abs += len.abs;
if len.leftover > Weight::ZERO && shares { start.rel += len.rel;
taken += len.leftover; span.end = start;
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
if self.dir.sign == Sign::Neg {
region.flip(axis);
} }
fixed.px += len.px; let used = painter.widget_within(child, region).size().axis(!axis);
fixed.rel += len.rel; // TODO: rel shouldn't do this, but no easy way before actually calculating pixels
start = shared(fixed, taken, total.leftover, room); if used.rel > 0.0 || used.rest > 0.0 {
// Along the row the span says where the child goes; across it the ortho = Len::REST;
// child sits where its own alignment says. Its region is the } else if ortho.rest == 0.0 {
// whole of what this span was given either way, which is what its ortho.abs = ortho.abs.max(used.abs);
// fractions are of.
let placed =
painter.widget_at(child, region, axis.pair(Some(along(from, start)), None));
if shrinks {
let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the
// span's own eventual width admits multiple fixed points.
// A scalable child therefore makes Children scalable too;
// only fixed children are compared with one another.
if used.rel != Rel::ZERO || used.leftover != Weight::ZERO {
ortho = LayoutLen::LEFTOVER;
} else if ortho.leftover == Weight::ZERO {
ortho.px = ortho.px.max(used.px);
} }
} start.abs += self.gap;
fixed.px += self.gap;
start = shared(fixed, taken, total.leftover, room);
} }
// Carried whole rather than collapsed to one share: a span that sizes let along = match total.rest == 0.0 && total.rel == 0.0 {
// from its children does not resolve `leftover`, it passes the weight up, true => total,
// so nesting spans divides the same space rather than re-dividing a false => Len::default(),
// share of it. Four `leftover(1)` children under two spans under one span
// get a quarter each, which collapsing to `leftover(1)` per level does
// not give. Resolution happens at the nearest ancestor with a length,
// and the root always has one.
let along = total;
let ortho = match shrinks {
true => ortho,
false => LayoutLen::rel(1.0),
}; };
Size::from_axis(axis, along, ortho) Size::from_axis(axis, along, ortho)
} }
} }
/// Where a row has reached: everything fixed before this point, which is a
/// sum and exact, plus the share of the room the weights so far are worth,
/// which is one rounding wherever it is asked for.
fn shared(fixed: Len, taken: Weight, weight: Weight, room: Len) -> Len {
if taken == Weight::ZERO {
return fixed;
}
fixed + room.scale(Rel::ratio(taken, weight))
}
impl Span { impl Span {
pub fn empty(dir: Dir) -> Self { pub fn empty(dir: Dir) -> Self {
Self { Self {
children: Vec::new(), children: Vec::new(),
dir, dir,
gap: Px::ZERO, gap: 0.0,
} }
} }
pub fn gap(mut self, gap: impl UiNum) -> Self { pub fn gap(mut self, gap: impl UiNum) -> Self {
self.gap = Px::from_num(gap); self.gap = gap.to_f32();
self self
} }
@@ -186,7 +86,7 @@ impl Span {
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> { pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
pub children: Wa, pub children: Wa,
pub dir: Dir, pub dir: Dir,
pub gap: Px, pub gap: f32,
_pd: PhantomData<(State, Tag)>, _pd: PhantomData<(State, Tag)>,
} }
@@ -212,13 +112,13 @@ impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
Self { Self {
children, children,
dir, dir,
gap: Px::ZERO, gap: 0.0,
_pd: PhantomData, _pd: PhantomData,
} }
} }
pub fn gap(mut self, gap: impl UiNum) -> Self { pub fn gap(mut self, gap: impl UiNum) -> Self {
self.gap = Px::from_num(gap); self.gap = gap.to_f32();
self self
} }
} }
+9 -28
View File
@@ -13,37 +13,18 @@ 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. let mut size = Size::default();
let placement = painter.placement();
// Whichever child sizes the stack keeps the stack's whole region as
// its own -- the stack is the length that child asked for, so taking
// the fraction of the stack's box again would take it twice -- and is
// put where the stack itself is put.
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) {
// On the layer that child ends up on, so the ask below is a reuse
// rather than a second drawing of it somewhere else: a retained
// drawing belongs to the layer it was made on.
Some((i, child)) => {
painter.child_layer_at(i);
painter
.widget_at(
child,
UiRegion::FULL,
[Some(placement.x), Some(placement.y)],
)
.size()
}
None => Size::LEFTOVER,
};
for (i, child) in self.children.iter().enumerate() { for (i, child) in self.children.iter().enumerate() {
match i {
0 => painter.child_layer(),
_ => painter.next_layer(),
}
let drawn = painter.widget(child);
// Only the child that sizes the stack is read, so the others
// changing size does not redraw it.
if sizing == Some(i) { if sizing == Some(i) {
continue; size = drawn.size();
} }
painter.child_layer_at(i);
// Every other child has the stack's own box for its region, since
// the stack is what contains it, and where it sits in one bigger
// than itself is its own business.
painter.widget_within(child, placement);
} }
size size
} }
+4 -12
View File
@@ -1,19 +1,11 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::Unsize; use std::marker::Unsize;
/// One widget in a box of its own, doing as little as possible on the way: pub struct WidgetPtr {
/// it draws its child in the whole of its box and reports back what the child
/// said. It exists because a length and an alignment are properties of one
/// widget, so a widget cannot both be 100 wide and take two shares of a row
/// -- the two lengths need two widgets, and this is the smaller one.
///
/// Its child is optional so it can also be the swappable slot a tab bar
/// needs, which is what it was written for.
pub struct Wrapper {
pub inner: Option<StrongWidget>, pub inner: Option<StrongWidget>,
} }
impl Widget for Wrapper { impl Widget for WidgetPtr {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
match &self.inner { match &self.inner {
Some(id) => painter.widget(id).size(), Some(id) => painter.widget(id).size(),
@@ -22,7 +14,7 @@ impl Widget for Wrapper {
} }
} }
impl Wrapper { impl WidgetPtr {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
@@ -43,7 +35,7 @@ impl Wrapper {
} }
} }
impl Default for Wrapper { impl Default for WidgetPtr {
fn default() -> Self { fn default() -> Self {
Self::empty() Self::empty()
} }
+8 -3
View File
@@ -35,11 +35,16 @@ impl Widget for Rect {
thickness: self.thickness, thickness: self.thickness,
inner_radius: self.inner_radius, inner_radius: self.inner_radius,
}); });
Size::LEFTOVER Size::REST
} }
fn size_hint(&self, _: Axis) -> Option<LayoutLen> { fn size_hint(&self, _: Axis) -> Option<Len> {
Some(LayoutLen::LEFTOVER) Some(Len::REST)
}
/// Its box is its primitive's own region, so a new one is written there.
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
} }
} }
+9 -7
View File
@@ -93,6 +93,10 @@ impl Widget for TextEdit {
); );
size size
} }
fn on_resize(&self, axis: Axis) -> OnResize {
self.view.on_resize(axis)
}
} }
const CARET_WIDTH: f32 = 1.0; const CARET_WIDTH: f32 = 1.0;
@@ -126,6 +130,7 @@ impl<'a> TextEditCtx<'a> {
pub fn set(&mut self, text: &str) { pub fn set(&mut self, text: &str) {
let text = self.string(text); let text = self.string(text);
self.text.view.buf.set_text(text); self.text.view.buf.set_text(text);
self.text.view.buf.changed = true;
self.text.selection = None; self.text.selection = None;
} }
@@ -172,6 +177,7 @@ impl<'a> TextEditCtx<'a> {
}; };
let at = at.min(self.text.view.buf.text().len()); let at = at.min(self.text.view.buf.text().len());
self.text.view.buf.edit().insert_str(at, text); self.text.view.buf.edit().insert_str(at, text);
self.text.view.buf.changed = true;
self.set_caret(at + text.len()); self.set_caret(at + text.len());
} }
@@ -184,6 +190,7 @@ impl<'a> TextEditCtx<'a> {
} }
let range = sel.text_range(); let range = sel.text_range();
self.text.view.buf.edit().replace_range(range.clone(), ""); self.text.view.buf.edit().replace_range(range.clone(), "");
self.text.view.buf.changed = true;
self.set_caret(range.start); self.set_caret(range.start);
true true
} }
@@ -261,6 +268,7 @@ impl<'a> TextEditCtx<'a> {
fn delete_range(&mut self, start: usize, end: usize) { fn delete_range(&mut self, start: usize, end: usize) {
self.text.view.buf.edit().replace_range(start..end, ""); self.text.view.buf.edit().replace_range(start..end, "");
self.text.view.buf.changed = true;
self.set_caret(start); self.set_caret(start);
} }
@@ -276,13 +284,7 @@ impl<'a> TextEditCtx<'a> {
} }
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) { pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
let pos = pos let pos = pos - self.text.region().top_left().to_abs(size);
- self
.text
.region()
.top_left()
.to_px(PxVec2::from_f32(size))
.to_f32();
let prev_sel = self.text.selection; let prev_sel = self.text.selection;
let prev_hit = self.text.double_hit; let prev_hit = self.text.double_hit;
+48 -32
View File
@@ -14,8 +14,11 @@ pub struct Text {
} }
pub struct TextView { pub struct TextView {
pub attrs: TextAttrs, pub attrs: MutDetect<TextAttrs>,
pub buf: TextBuffer, pub buf: MutDetect<TextBuffer>,
// cache
tex: Option<RenderedText>,
width: Option<f32>,
pub hint: Option<StrongWidget>, pub hint: Option<StrongWidget>,
} }
@@ -25,13 +28,19 @@ impl TextView {
} }
pub fn wrap_width(&self) -> Option<f32> { pub fn wrap_width(&self) -> Option<f32> {
self.buf.wrap_width() self.width
} }
} }
impl TextView { impl TextView {
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self { pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
Self { attrs, buf, hint } Self {
attrs: attrs.into(),
buf: buf.into(),
tex: None,
width: None,
hint,
}
} }
/// region where the text should be draw /// region where the text should be draw
@@ -43,31 +52,22 @@ impl TextView {
.align(self.align) .align(self.align)
} }
/// The text shaped for the width it is drawn in. The buffer keeps its
/// answers under the attrs too, so changing those asks a new question
/// rather than invalidating anything.
fn render(&mut self, painter: &mut Painter) -> &RenderedText { fn render(&mut self, painter: &mut Painter) -> &RenderedText {
let width = self.attrs.wrap.then(|| painter.px_len(Axis::X)); let width = if self.attrs.wrap {
// The shaper measures in floats, which is where a glyph advance comes Some(painter.px_size().x)
// from; what it answers goes back on the grid. } else {
let text = painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32)); None
// A greedy break is the same break at every width from its longest };
// line up to the one it was made at: each line still fits, and none if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed {
// could take a word that did not fit in the wider box. A line too self.width = width;
// long to fit at all says nothing about narrower boxes. self.tex = Some(painter.render_text(&mut self.buf, &self.attrs, width));
// self.attrs.changed = false;
// The step at or above that longest line rather than the nearest self.buf.changed = false;
// one, since the shaper measures in floats: the nearest step is
// under the line half the time, and a range starting there admits a
// box the line does not fit in, where the break is not this one.
if let Some(width) = width {
painter.holds(Axis::X, Px::ceil_from_f32(text.size.x).min(width)..=width);
} }
text self.tex.as_ref().unwrap()
} }
pub fn tex(&self) -> Option<&RenderedText> { pub fn tex(&self) -> Option<&RenderedText> {
self.buf.rendered() self.tex.as_ref()
} }
/// Draws the text, and says where the glyphs went and what they use. /// Draws the text, and says where the glyphs went and what they use.
pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) { pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) {
@@ -83,16 +83,28 @@ impl TextView {
let tex = self.render(painter); let tex = self.render(painter);
let region = tex.size.align(align); let region = tex.size.align(align);
// The step at or above what the shaper measured, so a parent that let size = Size::abs(tex.size);
// hands back the length this reports hands back a box the longest let within = region.within(&painter.region());
// line fits in. Rounded to the nearest step it is half the time a painter.glyphs(tex, within);
// hair under that line, and the break made in it is not the break a
// cold layout makes there.
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
painter.glyphs(tex, DrawRegion::Extent(region));
(region, size) (region, size)
} }
/// Wrapping reads the width it is offered, so a wider box reshapes it and
/// a taller one does not. Alignment matters too, and separately: glyphs
/// anchored to the start of an axis stay put when that extent changes,
/// but centred or end-aligned ones move even though the shaping stands.
pub fn on_resize(&self, axis: Axis) -> OnResize {
let reshapes = axis == Axis::X && self.attrs.wrap;
let anchored = match axis {
Axis::X => self.align.x,
Axis::Y => self.align.y,
} == AxisAlign::Neg;
match reshapes || !anchored {
true => OnResize::Redraw,
false => OnResize::Translate,
}
}
pub fn content(&self) -> String { pub fn content(&self) -> String {
self.buf.text().to_string() self.buf.text().to_string()
} }
@@ -119,6 +131,10 @@ impl Widget for Text {
self.update_buf(); self.update_buf();
self.view.draw(painter).1 self.view.draw(painter).1
} }
fn on_resize(&self, axis: Axis) -> OnResize {
self.view.on_resize(axis)
}
} }
impl Deref for Text { impl Deref for Text {
+42 -55
View File
@@ -12,23 +12,14 @@ widget_trait! {
} }
} }
fn align(self, align: impl Into<Align>) -> impl WidgetIdFn<Rsc, WL::Widget> { fn align(self, align: impl Into<Align>) -> impl WidgetFn<Rsc, Aligned> {
// An axis left out keeps whatever it had, which is centered unless move |state| Aligned {
// something else set it. inner: self.add_strong(state),
let align = align.into(); align: align.into(),
move |state| {
let id = self.add(state);
let widgets = &mut state.ui_mut().widgets;
for (axis, align) in [(Axis::X, align.x), (Axis::Y, align.y)] {
if let Some(align) = align {
widgets.set_alignment(id, axis, align);
}
}
id
} }
} }
fn center(self) -> impl WidgetIdFn<Rsc, WL::Widget> { fn center(self) -> impl WidgetFn<Rsc, Aligned> {
self.align(Align::CENTER) self.align(Align::CENTER)
} }
@@ -40,46 +31,48 @@ widget_trait! {
} }
} }
fn region_node(self) -> impl WidgetIdFn<Rsc, WL::Widget> { fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, SetSize> {
|state| {
let id = self.add(state);
state.ui_mut().widgets.set_region_node(id, true);
id
}
}
fn sized(self, size: impl Into<Size>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let size = size.into(); let size = size.into();
move |state| { move |state| SetSize {
let id = self.add(state); inner: self.add_strong(state),
let widgets = &mut state.ui_mut().widgets; x: Some(size.x),
widgets.set_size_rule(id, Axis::X, SizeRule::Exact(size.x)); y: Some(size.y),
widgets.set_size_rule(id, Axis::Y, SizeRule::Exact(size.y));
id
} }
} }
fn width(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> { fn max_width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
let len = len.into(); let len = len.into();
move |state| { move |state| MaxSize {
let id = self.add(state); inner: self.add_strong(state),
state x: Some(len),
.ui_mut() y: None,
.widgets
.set_size_rule(id, Axis::X, SizeRule::Exact(len));
id
} }
} }
fn height(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> { fn max_height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
let len = len.into(); let len = len.into();
move |state| { move |state| MaxSize {
let id = self.add(state); inner: self.add_strong(state),
state x: None,
.ui_mut() y: Some(len),
.widgets }
.set_size_rule(id, Axis::Y, SizeRule::Exact(len)); }
id
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
let len = len.into();
move |state| SetSize {
inner: self.add_strong(state),
x: Some(len),
y: None,
}
}
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
let len = len.into();
move |state| SetSize {
inner: self.add_strong(state),
x: None,
y: Some(len),
} }
} }
@@ -92,9 +85,7 @@ widget_trait! {
fn scrollable(self) -> impl WidgetIdFn<Rsc, Scroll> where Rsc: HasEvents { fn scrollable(self) -> impl WidgetIdFn<Rsc, Scroll> where Rsc: HasEvents {
move |state| { move |state| {
let inner = self.add(state); Scroll::new(self.add_strong(state), Axis::Y)
state.ui_mut().widgets.set_region_node(inner, true);
Scroll::new(inner.upgrade(state), Axis::Y)
.on(CursorSense::Scroll, |ctx, rsc| { .on(CursorSense::Scroll, |ctx, rsc| {
let delta = ctx.data.scroll_delta.y * 50.0; let delta = ctx.data.scroll_delta.y * 50.0;
ctx.widget(rsc).scroll(delta); ctx.widget(rsc).scroll(delta);
@@ -134,13 +125,9 @@ widget_trait! {
|state| self.add(state) |state| self.add(state)
} }
// Named for the type it makes rather than as `wrapped`, which would read fn set_ptr(self, ptr: WeakWidget<WidgetPtr>, state: &mut Rsc) {
// as the text setting. `widget_trait!` takes no attributes, so what it is let id = self.add_strong(state);
// for is on `Wrapper` itself. state.ui_mut().widgets[ptr].inner = Some(id);
fn wrapper(self) -> impl WidgetFn<Rsc, Wrapper> {
|state| Wrapper {
inner: Some(self.add_strong(state)),
}
} }
} }
-101
View File
@@ -1,101 +0,0 @@
//! A measurement that decides control flow.
//!
//! Comparing boxes catches a widget that moved. It does not catch a widget
//! that measured a child, believed a different answer from the one a cold
//! start would give, and took the other branch -- which is the same defect
//! arriving somewhere it cannot be ignored. A widget here branches on what it
//! measured, so a disagreement shows up as a different tree.
use iris::harness::Harness;
use iris::prelude::*;
/// Measures `probe` across `axis` and draws one of two children on the
/// answer. Its own configuration never changes, so which child is drawn is a
/// property of the layout alone.
struct BranchesOnMeasurement {
probe: StrongWidget,
wide: StrongWidget,
narrow: StrongWidget,
threshold: f32,
}
impl Widget for BranchesOnMeasurement {
fn draw(&mut self, painter: &mut Painter) -> Size {
let mut top = UiRegion::FULL;
top.y.end = top.y.start.offset(Px::from_int(40));
let measured = painter.widget_within(&self.probe, top).len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
let mut below = UiRegion::FULL;
below.y.start = below.y.start.offset(Px::from_int(40));
match px > Px::from_f32(self.threshold) {
true => painter.widget_within(&self.wide, below),
false => painter.widget_within(&self.narrow, below),
};
Size::LEFTOVER
}
}
fn plant(h: &mut Harness, threshold: f32) -> (WidgetId, WidgetId) {
let words = "the quick brown fox jumps over the lazy dog and keeps running";
let probe = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let wide = rect(Color::RED).add(&mut h.rsc);
let narrow = rect(Color::BLUE).add(&mut h.rsc);
let branch = BranchesOnMeasurement {
probe: probe.add_strong(&mut h.rsc),
wide: wide.add_strong(&mut h.rsc),
narrow: narrow.add_strong(&mut h.rsc),
threshold,
}
.add(&mut h.rsc);
let side = rect(Color::GREEN).width(120).add(&mut h.rsc);
h.set_root((side, branch).span(Dir::RIGHT));
(wide.id(), narrow.id())
}
/// Which of the two branches drew, as a pair a test can compare.
fn taken(h: &Harness, wide: WidgetId, narrow: WidgetId) -> (bool, bool) {
(h.region(&wide).is_some(), h.region(&narrow).is_some())
}
#[test]
fn a_branch_taken_on_a_measurement_holds_across_repaints() {
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
let mut h = Harness::new((900, 600));
let (wide, narrow) = plant(&mut h, threshold);
let first = taken(&h, wide, narrow);
assert_ne!(first, (false, false), "threshold {threshold}: neither drew");
for frame in 0..4 {
h.rsc.widgets_mut().get_dyn_mut(wide);
h.rsc.widgets_mut().get_dyn_mut(narrow);
h.frame();
assert_eq!(
taken(&h, wide, narrow),
first,
"threshold {threshold}, repaint {frame}: the branch moved when nothing did"
);
}
}
}
#[test]
fn a_branch_taken_on_a_measurement_is_the_one_a_cold_start_takes() {
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
let mut warm = Harness::new((900, 600));
let (wide, narrow) = plant(&mut warm, threshold);
warm.resize((640, 480));
warm.frame();
warm.rsc.widgets_mut().get_dyn_mut(wide);
warm.frame();
let mut cold = Harness::new((640, 480));
let (cwide, cnarrow) = plant(&mut cold, threshold);
assert_eq!(
taken(&warm, wide, narrow),
taken(&cold, cwide, cnarrow),
"threshold {threshold}: warm and cold took different branches"
);
}
}
-45
View File
@@ -1,45 +0,0 @@
//! What a retained drawing costs in accuracy when it is moved instead of made
//! again. A subtree's stored regions are the only record of where it is, so a
//! move that works from the last answer rather than from the box it is now in
//! integrates its own rounding, and nothing later recomputes it. Re-expressing
//! each part as the same fraction of the new box is what keeps a long-lived
//! layout on the one a cold start produces.
use iris::harness::Harness;
use iris::prelude::*;
/// A row of a fixed height under a bar, so changing the bar's height moves the
/// row without changing the box it is given: the move path, repeatedly.
fn plant(h: &mut Harness, bar_height: f32) -> (WeakWidget<Rect>, WeakWidget<Rect>) {
let bar = rect(Color::RED).height(bar_height).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = (inner, rect(Color::GREEN)).span(Dir::RIGHT).height(100);
h.set_root((bar, row).span(Dir::DOWN));
(bar, inner)
}
/// Enough moves to pass the 0.05 physical pixels layout treats as the same
/// place, for a move that adds an offset to the last answer. Measured on this
/// fixture on 2026-09-15: adding the offset to both ends of a span shortened
/// the row by 0.071 over this many moves and by 0.712 over ten times as many,
/// growing with the count rather than settling. Placing the far end from the
/// near one instead left 0.069, because the length is re-derived either way.
const MOVES: usize = 20_000;
#[test]
fn a_subtree_moved_many_times_stays_where_a_cold_layout_puts_it() {
let mut warm = Harness::new((640, 900));
let (bar, inner) = plant(&mut warm, 40.0);
let mut height = 40.0;
for step in 0..MOVES {
height = 40.0 + (step % 300) as f32 * 0.37;
warm.set_len(bar, Axis::Y, height);
warm.frame();
}
let mut cold = Harness::new((640, 900));
let (_, cold_inner) = plant(&mut cold, height);
cold.frame();
assert_eq!(warm.region(&inner), cold.region(&cold_inner));
}
-29
View File
@@ -1,29 +0,0 @@
//! Whether measuring a widget and then giving it the length it reported is a
//! fixed point, which is what a span that sizes to its children needs.
use iris::harness::Harness;
use iris::prelude::*;
#[test]
fn a_wrapping_text_in_a_span_settles_on_one_width() {
let mut h = Harness::new((900, 600));
let words = "the quick brown fox jumps over the lazy dog and keeps on running \
until it reaches the end of a rather long line of text";
let t = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let filler = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((t, filler).span(Dir::RIGHT));
let mut widths = Vec::new();
for _ in 0..6 {
let r = h.region(&t.id()).unwrap();
widths.push(r.bot_right.x - r.top_left.x);
// Redrawing it changes nothing about the state, so nothing may move.
h.rsc.widgets_mut().get_dyn_mut(t.id());
h.frame();
}
println!("widths over six frames: {widths:?}");
assert!(
widths.windows(2).all(|w| w[0] == w[1]),
"a repaint that changed nothing moved it: {widths:?}"
);
}
-718
View File
@@ -1,718 +0,0 @@
//! Where a frame puts things, with no window to put them in.
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
/// A fixed 100 wide, and the rest of the 400 to its neighbour.
fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) {
let left = rect(Color::RED).width(100).add(&mut h.rsc);
let right = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
(left.id(), right.id())
}
#[test]
fn a_span_gives_each_child_the_width_it_asked_for() {
let mut h = Harness::new((400, 200));
let (left, right) = two_rects(&mut h);
assert_corners!(h, left, (0, 0), (100, 200));
assert_corners!(h, right, (100, 0), (400, 200));
}
/// A span places each child in the room left after the one before, because a
/// text has to wrap at the width actually there, but the child's region is
/// the whole row. So two children asking for half each take the whole row
/// between them, however much of it was left when each was asked, and a third
/// overflows -- and a span passes its own region on unchanged, so a child of
/// a nested span asking for half asks for half of the same row.
#[test]
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
let mut h = Harness::new((400, 100));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let nested = (inner,).span(Dir::RIGHT).add(&mut h.rsc);
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
// The nested span is placed at the length it reported, and its own child
// asks for half of the row rather than half of that placement.
assert_corners!(h, nested, (200, 0), (400, 100));
assert_corners!(h, inner, (200, 0), (400, 100));
assert_corners!(h, tail, (400, 0), (500, 100));
}
/// The same fraction either way round: after a 100 px child in a 400 px row,
/// `rel(0.5)` is 100 to 300 whether the child's own rule says so or the child
/// drew half of what it was offered and reported that. Half the row, not half
/// of the 300 px left of it.
#[test]
fn a_reported_fraction_is_of_the_row_like_a_declared_one() {
let mut declaring = Harness::new((400, 100));
let head = rect(Color::RED).width(100).add(&mut declaring.rsc);
let declared = rect(Color::GREEN).width(rel(0.5)).add(&mut declaring.rsc);
declaring.set_root((head, declared).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(declaring, declared, (100, 0), (300, 100));
let mut reporting = Harness::new((400, 100));
let head = rect(Color::RED).width(100).add(&mut reporting.rsc);
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut reporting.rsc);
let reported = (inner,).span(Dir::RIGHT).add(&mut reporting.rsc);
reporting.set_root((head, reported).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(reporting, reported, (100, 0), (300, 100));
}
/// What the fraction a child reports is of and what box it is offered are
/// two different lengths, and only the first is the whole row: a text still
/// wraps at the room actually left after its neighbour, so the same
/// paragraph is taller where less of the row is left for it.
#[test]
fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
let paragraph = "Wrapping shapes one source into as many lines as the box \
leaves room for, so a paragraph's height is an answer.";
let height_after = |head_width: i32| {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).width(head_width).add(&mut h.rsc);
let text = wtext(paragraph).size(16).wrap(true).add(&mut h.rsc);
h.set_root((head, text).span(Dir::RIGHT).width(rel(1.0)));
let region = h.region(&text).unwrap();
(region.bot_right.y - region.top_left.y).to_f32()
};
let (crowded, whole_row) = (height_after(300), height_after(0));
assert!(crowded > whole_row, "{crowded} against {whole_row}");
}
/// The same reading through a pad: its inset is the whole box less the
/// padding, so half of the inset plus the padding is half the box plus one
/// padding, not two.
#[test]
fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
let mut h = Harness::new((400, 100));
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
// Ruled to the window: a root reporting a fraction of it is otherwise
// placed inside it by its own alignment, which is not what is under test.
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, padded, (0, 0), (210, 100));
assert_corners!(h, tail, (210, 0), (310, 100));
}
#[test]
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
let mut h = Harness::new((400, 200));
let child = rect(Color::RED).height(40).add(&mut h.rsc);
let span = (child,).span(Dir::RIGHT).height(rel(1.0)).add(&mut h.rsc);
h.set_root(span);
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0));
}
#[test]
fn a_span_reports_its_tallest_fixed_child() {
let mut h = Harness::new((400, 200));
let short = rect(Color::RED).height(40).add(&mut h.rsc);
let tall = rect(Color::BLUE).height(70).add(&mut h.rsc);
let span = (short, tall).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(span);
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::px(70.0));
}
#[test]
fn resizing_relays_out_against_the_new_output() {
let mut h = Harness::new((400, 200));
let (left, right) = two_rects(&mut h);
h.resize((800, 100));
assert!(h.needs_redraw());
h.frame();
assert_corners!(h, left, (0, 0), (100, 100));
assert_corners!(h, right, (100, 0), (800, 100));
}
#[test]
fn an_empty_widget_takes_a_share_of_a_span() {
let mut h = Harness::new((400, 200));
let gap = ().add(&mut h.rsc);
let right = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((gap, right).span(Dir::RIGHT));
assert_corners!(h, gap, (0, 0), (300, 200));
assert_corners!(h, right, (300, 0), (400, 200));
}
#[test]
fn a_child_drawn_twice_moves_once() {
let mut h = Harness::new((400, 200));
// The span measures a child and then places it; listing it twice would
// move it twice. The span's own fixed total is shorter than the window,
// so the span is centred in it and everything under it carries that.
let inner = rect(Color::BLUE).add(&mut h.rsc);
let centered = inner.center().width(200).add(&mut h.rsc);
let left = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((left, centered).span(Dir::RIGHT));
assert_corners!(h, inner, (150, 0), (350, 200));
h.set_len(left, Axis::X, 150);
h.frame();
assert_corners!(h, inner, (175, 0), (375, 200));
}
#[test]
fn alignment_accepts_an_arbitrary_fraction_and_changes_at_runtime() {
let mut h = Harness::new((400, 200));
let fixed = rect(Color::BLUE).sized((100, 100)).add(&mut h.rsc);
h.rsc
.widgets_mut()
.set_alignment(fixed, Axis::X, AxisAlign::new(0.25));
h.rsc
.widgets_mut()
.set_alignment(fixed, Axis::Y, AxisAlign::NEG);
h.set_root(fixed);
assert_corners!(h, fixed, (75, 0), (175, 100));
h.rsc
.widgets_mut()
.set_alignment(fixed, Axis::X, AxisAlign::new(0.75));
h.frame();
assert_corners!(h, fixed, (225, 0), (325, 100));
}
#[test]
fn a_resize_lands_where_a_cold_start_would() {
let build = |h: &mut Harness| {
let para = wtext(
"Wrapping shapes one source into as many lines as its container leaves room \
for, so the height of a paragraph is an answer rather than a setting.",
)
.size(20)
.wrap(true)
.pad(16)
.add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
let root = (para, below).span(Dir::DOWN).pad(12);
h.set_root(root);
(para, below)
};
let mut cold = Harness::new((900, 1200));
let (cold_para, cold_below) = build(&mut cold);
let mut resized = Harness::new((1920, 1200));
let (para, below) = build(&mut resized);
resized.resize((900, 1200));
resized.frame();
assert_eq!(resized.region(&para), cold.region(&cold_para), "paragraph");
assert_eq!(resized.region(&below), cold.region(&cold_below), "below");
}
#[test]
fn a_fixed_box_is_drawn_again_rather_than_stretched() {
let mut h = Harness::new((400, 400));
// The panel fills a stack sized by its sibling, so it is first asked in
// the whole box and then given the shorter one. Reusing it in that fixed
// box afterwards would leave it whatever height it happened to have.
let panel = rect(Color::BLUE).add(&mut h.rsc);
let leaf = rect(Color::RED).height(100).add(&mut h.rsc);
let stack = (panel, leaf)
.stack()
.size(StackSize::Child(1))
.add(&mut h.rsc);
h.set_root(stack.align(Align::TOP));
assert_corners!(h, panel, (0, 0), (400, 100));
h.set_len(leaf, Axis::Y, 250);
h.frame();
assert_corners!(h, panel, (0, 0), (400, 250));
}
#[test]
fn a_moved_subtree_takes_its_children_with_it() {
let mut h = Harness::new((400, 400));
let first = rect(Color::RED).height(40).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = inner.pad(10).height(40).region_node().add(&mut h.rsc);
// 80 of fixed rows in a 400 window, so the span takes 80 and sits in the
// middle of what it was given.
h.set_root((first, row).span(Dir::DOWN));
assert_corners!(h, inner, (10, 210), (390, 230));
h.set_len(first, Axis::Y, 80);
h.frame();
// The row opted into one movable region, so its descendants follow one
// entry rather than having their primitive regions rewritten.
assert_corners!(h, inner, (10, 230), (390, 250));
}
#[test]
fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() {
let mut h = Harness::new((400, 200));
let fixed = rect(Color::BLUE).width(50).add(&mut h.rsc);
let leftover = rect(Color::GREEN).add(&mut h.rsc);
let panel = (fixed, leftover).span(Dir::RIGHT).add(&mut h.rsc);
// Changing the bar's width is the only thing that changes the box the
// panel and everything under it was drawn for.
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, panel).span(Dir::RIGHT));
assert_corners!(h, fixed, (100, 0), (150, 200));
assert_corners!(h, leftover, (150, 0), (400, 200));
h.set_len(bar, Axis::X, 200);
h.frame();
// The panel's box is 100 shorter, so the fixed child is the same 50 wide
// against its new start and the one taking what is left absorbs the change.
assert_corners!(h, fixed, (200, 0), (250, 200));
assert_corners!(h, leftover, (250, 0), (400, 200));
}
#[test]
fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
let mut h = Harness::new((400, 200));
// The row is 40 tall whatever happens, which used to make its drawing
// impossible to take out of: recovering a fraction of a box needs a
// relative extent, and it has none on that axis.
let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = inner.pad(10).height(40).add(&mut h.rsc);
let filler = rect(Color::GREEN).add(&mut h.rsc);
// This column is an item in a row, so it takes the width left for it
// rather than asking for a full row-width in addition to the bar.
let column = (row, filler).span(Dir::DOWN).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, column).span(Dir::RIGHT));
assert_corners!(h, inner, (110, 10), (390, 30));
h.set_len(bar, Axis::X, 200);
h.frame();
assert_corners!(h, inner, (210, 10), (390, 30));
}
#[test]
fn only_a_region_node_lengthens_the_chain_and_it_can_be_removed() {
let mut h = Harness::new((400, 200));
let leaf = rect(Color::BLUE).add(&mut h.rsc);
let buried = leaf.pad(4).pad(4).pad(4).pad(4).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, buried).span(Dir::RIGHT));
let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(move_idx), 0, "the window is no entry");
h.rsc.widgets_mut().set_region_node(buried, true);
h.frame();
let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(
h.render.moves.depth(move_idx),
1,
"the opted-in widget's region alone"
);
h.rsc.widgets_mut().set_region_node(buried, false);
h.frame();
let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(move_idx), 0);
}
/// A span that sizes from its children passes their `leftover` weight up
/// than collapsing it to one share, so nesting divides the same space instead
/// of re-dividing a share of it.
#[test]
fn nested_spans_divide_the_space_once_however_deep_the_nesting_is() {
let mut h = Harness::new((400, 200));
let (a, b, c, d) = (
rect(Color::RED).add(&mut h.rsc),
rect(Color::BLUE).add(&mut h.rsc),
rect(Color::GREEN).add(&mut h.rsc),
rect(Color::WHITE).add(&mut h.rsc),
);
let left = (a, b).span(Dir::RIGHT).add(&mut h.rsc);
let right = (c, d).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
for (i, id) in [a, b, c, d].into_iter().enumerate() {
let x = i as f32 * 100.0;
assert_corners!(h, id, (x, 0), (x + 100.0, 200));
}
}
/// The same space, unevenly nested: weights carried up mean a share is a
/// share of the whole, not of whatever branch a widget happens to sit in.
///
/// Each edge lands on the even division or one step below it, since a share
/// is a fraction of the room and a truncating multiply gives up what that
/// fraction does not divide. What stays exact is that each share starts
/// where the last one ended and the row ends at its own edge.
#[test]
fn an_uneven_nesting_still_gives_every_share_the_same_length() {
let mut h = Harness::new((400, 200));
let (a, b, c, d) = (
rect(Color::RED).add(&mut h.rsc),
rect(Color::BLUE).add(&mut h.rsc),
rect(Color::GREEN).add(&mut h.rsc),
rect(Color::WHITE).add(&mut h.rsc),
);
let one = (a,).span(Dir::RIGHT).add(&mut h.rsc);
let three = (b, c, d).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((one, three).span(Dir::RIGHT));
let mut start = Px::ZERO;
for (i, id) in [a, b, c, d].into_iter().enumerate() {
let got = h.region(&id).expect("widget drew nothing");
let even = Px::from_int((i as i32 + 1) * 100);
assert_eq!(got.top_left, PxVec2::new(start, Px::ZERO), "share {i}");
assert_eq!(got.bot_right.y, Px::from_int(200), "share {i}");
assert!(
got.bot_right.x == even || got.bot_right.x == even.next_down(),
"share {i} ends at {:?}, not {even:?}",
got.bot_right.x
);
start = got.bot_right.x;
}
assert_eq!(
start,
Px::from_int(400),
"the row stopped short of its edge"
);
}
/// However many ways a row is divided, the shares add up to the row: each
/// one is the fixed parts before it plus a share of the room, rather than a
/// step from where the last one ended, so the roundings do not accumulate
/// along it. Chained, two hundred of them ended a step short of the edge.
#[test]
fn a_row_of_equal_shares_fills_it_exactly() {
for n in [2usize, 3, 7, 64, 200] {
let mut h = Harness::new((1000, 100));
let mut ids = Vec::new();
let mut kids: Vec<StrongWidget> = Vec::new();
for _ in 0..n {
let kid = rect(Color::RED).add(&mut h.rsc);
ids.push(kid.id());
kids.push(kid.add_strong(&mut h.rsc));
}
let span = Span {
children: kids,
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
h.set_root(span);
h.frame();
for (i, id) in ids.iter().enumerate() {
let at = h.region(id).expect("a share drew nothing").top_left.x;
let want = Px::from_f32(1000.0 * (i as f32) / (n as f32));
assert!(
(at - want).abs() <= Px::STEP,
"{n} shares: the {i}th starts at {at:?}, not {want:?}"
);
}
let end = h.region(ids.last().unwrap()).unwrap().bot_right.x;
assert_eq!(end, Px::from_int(1000), "{n} shares do not reach the edge");
}
}
/// Where the shader puts an edge: the fraction resolved against the window
/// plus the pixel offset, taken to the boundary it composes to within half
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
let active = &h.render.active[&id];
let region = h.render.moves.resolve(active.parent_move, active.region);
let dim = h.size().axis(axis);
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
let span = region.axis(axis);
(edge(span.start), edge(span.end))
}
fn hairline(h: &mut Harness, marks: &mut Vec<WidgetId>) -> StrongWidget {
let mark = rect(Color::RED).width(1).add_strong(&mut h.rsc);
marks.push(mark.id());
mark
}
fn share(h: &mut Harness, inner: StrongWidget, ratio: f32) -> StrongWidget {
h.set_len(&inner, Axis::X, LayoutLen::leftover(ratio));
inner
}
/// Shares in weights no binary fraction lands on, a padding on one branch
/// and not the other, so an edge falls near an integer as often as it can.
fn hairlines(h: &mut Harness, depth: usize, marks: &mut Vec<WidgetId>) -> StrongWidget {
let mut span = Span::empty(Dir::RIGHT);
if depth == 0 {
let left = rect(Color::BLUE).add_strong(&mut h.rsc);
let left = share(h, left, 3.0);
span.push(left);
let mark = hairline(h, marks);
span.push(mark);
let right = rect(Color::BLUE).add_strong(&mut h.rsc);
let right = share(h, right, 7.0);
span.push(right);
return span.add_strong(&mut h.rsc);
}
let first = hairlines(h, depth - 1, marks);
let first = share(h, first, 3.0);
span.push(first);
let second = hairlines(h, depth - 1, marks);
let second = Pad {
padding: Padding {
left: Px::from_int(3),
right: Px::from_int(7),
top: Px::ZERO,
bottom: Px::ZERO,
},
inner: second,
}
.add_strong(&mut h.rsc);
let second = share(h, second, 5.0);
span.push(second);
span.add_strong(&mut h.rsc)
}
/// A one-pixel line is a pixel wherever it is drawn. Both edges of a fixed
/// length share their box's fraction, so composing the chain moves them
/// together and the shader's `floor` cannot round the pixel between them
/// away -- only shift it. A separator that disappeared at one window size
/// would be a defect no size comparison catches.
#[test]
fn a_one_pixel_line_keeps_its_pixel_through_a_chain() {
let mut h = Harness::new((1920, 1200));
let mut marks = Vec::new();
let root = hairlines(&mut h, 4, &mut marks);
h.state.set_root(root);
h.frame();
assert_eq!(marks.len(), 16);
for size in [(1920, 1200), (1919, 1201), (997, 1003), (1367, 733)] {
h.resize(size);
h.frame();
for mark in &marks {
let (start, end) = drawn_edges(&h, *mark, Axis::X);
assert_eq!(end - start, 1.0, "at {size:?}, mark {mark:?}");
}
}
}
/// A span short of room takes it from its shares, which go to nothing and
/// then to nothing wider; the fixed lengths between them keep their pixels.
/// Collapsing those to make room would delete a separator the caller asked
/// for, which is worse than overflowing.
#[test]
fn a_span_out_of_room_shrinks_its_shares_and_not_its_fixed_lengths() {
let mut h = Harness::new((400, 20));
let mut marks = Vec::new();
let mut span = Span::empty(Dir::RIGHT);
for _ in 0..3 {
let share_of = rect(Color::BLUE).add_strong(&mut h.rsc);
let share_of = share(&mut h, share_of, 1.0);
span.push(share_of);
let mark = hairline(&mut h, &mut marks);
span.push(mark);
}
let root = span.add_strong(&mut h.rsc);
h.state.set_root(root);
h.frame();
for width in [400, 10, 3, 1] {
h.resize((width, 20));
h.frame();
for mark in &marks {
let (start, end) = drawn_edges(&h, *mark, Axis::X);
assert_eq!(end - start, 1.0, "at {width} wide, mark {mark:?}");
}
}
}
#[test]
fn only_a_pure_leftover_child_disappears_when_nothing_is_left() {
let mut h = Harness::new((100, 20));
let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
let leftover = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((fixed, leftover).span(Dir::RIGHT));
assert_corners!(h, fixed, (0, 0), (100, 20));
assert_eq!(h.region(&leftover), None);
// An undrawn child remains a dependency of the span, so making room for
// it draws it without rebuilding the tree.
h.set_len(fixed, Axis::X, 60);
h.frame();
assert_corners!(h, leftover, (60, 0), (100, 20));
let mut h = Harness::new((100, 20));
let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
let mixed = rect(Color::BLUE)
.width(LayoutLen::px(20) + LayoutLen::LEFTOVER)
.add(&mut h.rsc);
h.set_root((fixed, mixed).span(Dir::RIGHT));
// Pixels and fractions still overflow; only a child whose entire length
// is leftover is omitted.
assert_corners!(h, mixed, (100, 0), (120, 20));
}
#[test]
fn leftover_children_disappear_at_the_exact_fixed_content_boundary() {
let mut h = Harness::new((100, 100));
let first = rect(Color::RED).height(90).add(&mut h.rsc);
let a = rect(Color::GREEN).add(&mut h.rsc);
let b = rect(Color::BLUE).add(&mut h.rsc);
let inner = (a, b).span(Dir::DOWN).gap(4).add(&mut h.rsc);
h.set_root((first, inner).span(Dir::DOWN));
assert!(h.region(&a).is_some());
assert!(h.region(&b).is_some());
h.set_len(first, Axis::Y, 96.0);
h.frame();
assert!(h.region(&a).is_none());
assert!(h.region(&b).is_none());
}
/// **A stack child smaller than the stack sits where its own alignment
/// says.** `Stack` gives every child the box its sizing child defines and
/// used to force the near edge on all of them; that override is owed only to
/// the sizing child, which has already placed its own content in the box the
/// stack derived from its answer. Every other child is handed a box that owes
/// nothing to it, so where it sits in one bigger than itself is its own
/// business -- and with the override it could not be aligned at all, which is
/// what moved the `tabs` example's counters to the wrong corner.
#[test]
fn a_stack_child_smaller_than_the_stack_keeps_its_own_alignment() {
let mut h = Harness::new((400, 200));
let big = rect(Color::BLUE).add(&mut h.rsc);
let small = rect(Color::RED).sized((50, 50)).add(&mut h.rsc);
h.rsc
.widgets_mut()
.set_alignment(small.id(), Axis::X, AxisAlign::POS);
let (a, b) = (big.add_strong(&mut h.rsc), small.add_strong(&mut h.rsc));
let children: Vec<StrongWidget> = vec![a, b];
h.set_root(Stack {
children,
size: StackSize::Default,
});
assert_corners!(h, big, (0, 0), (400, 200));
// The far edge on X because it asked for it, the middle on Y because
// that is the default.
assert_corners!(h, small, (350, 75), (400, 125));
}
/// Five children of one span, buried under three containers that are each a
/// fraction of their parent so no length reaches the window without being
/// composed and rounded on the way. Returns each child's drawn width and
/// each gap between them, in pixels.
fn row_under_fractions(kid: Option<LayoutLen>, gap: f32, box_w: f32) -> (Vec<Px>, Vec<Px>) {
let mut h = Harness::new((box_w, 400.0));
let mut ids = Vec::new();
let mut kids: Vec<StrongWidget> = Vec::new();
for _ in 0..5 {
let r = rect(Color::RED).add(&mut h.rsc);
if let Some(len) = kid {
h.rsc
.widgets_mut()
.set_size_rule(r.id(), Axis::X, SizeRule::Exact(len));
}
ids.push(r.id());
kids.push(r.add_strong(&mut h.rsc));
}
let span = Span {
children: kids,
dir: Dir::RIGHT,
gap: Px::from_f32(gap),
}
.add(&mut h.rsc);
let a = (span.width(rel(0.9)),).span(Dir::RIGHT).add(&mut h.rsc);
let b = (a.width(rel(0.8)),).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((b.width(rel(0.7)),).span(Dir::RIGHT));
let boxes: Vec<_> = ids
.iter()
.map(|id| h.region(id).expect("a child drew nothing"))
.collect();
(
boxes.iter().map(|b| b.bot_right.x - b.top_left.x).collect(),
boxes
.windows(2)
.map(|p| p[1].top_left.x - p[0].bot_right.x)
.collect(),
)
}
/// **A length given in pixels is that many pixels, wherever it ends up.** A
/// gap and a declared width compose additively -- `Len::within` adds a part's
/// own pixels rather than scaling them, and both ends of a gap carry the same
/// fraction, so the multiply that rounds is the same on each -- which is why
/// nesting the row inside fractions of fractions cannot move them. Swept over
/// 2,100 box widths when this was written and exact at every one; five here,
/// including widths that divide badly by five.
#[test]
fn a_length_in_pixels_is_that_many_pixels_however_it_is_nested() {
for box_w in [300.0, 1000.0, 1001.0, 1003.0, 1920.0] {
let want = Px::from_int(7);
let (_, gaps) = row_under_fractions(None, 7.0, box_w);
assert!(
gaps.iter().all(|g| *g == want),
"box {box_w}: gaps between leftover children are {gaps:?}"
);
let (widths, gaps) = row_under_fractions(Some(LayoutLen::px(100.0)), 7.0, box_w);
assert!(
gaps.iter().all(|g| *g == want),
"box {box_w}: gaps between fixed children are {gaps:?}"
);
assert!(
widths.iter().all(|w| *w == Px::from_int(100)),
"box {box_w}: declared widths came out {widths:?}"
);
}
}
/// **Children asking for the same share of a row are not the same length**,
/// and this pins by how much rather than claiming they are equal. A position
/// is the quantity that gets rounded, so the row fills exactly and no two
/// children leave a seam; what that costs is a step or two between lengths
/// that were asked for identically. Exact composition would shrink the
/// spread, not remove it: five equal lengths cannot fill a row whose step
/// count is not a multiple of five.
#[test]
fn equal_shares_differ_by_at_most_two_steps_and_fill_the_row() {
for kid in [None, Some(LayoutLen::rel(0.2))] {
for box_w in [300.0, 1000.0, 1001.0, 1003.0, 1920.0] {
let (widths, gaps) = row_under_fractions(kid, 0.0, box_w);
let spread = *widths.iter().max().unwrap() - *widths.iter().min().unwrap();
assert!(
spread <= Px::from_raw(2),
"box {box_w}, {kid:?}: widths {widths:?} spread {spread:?}"
);
assert!(
gaps.iter().all(|g| *g == Px::ZERO),
"box {box_w}, {kid:?}: children left seams {gaps:?}"
);
}
}
}
#[test]
fn a_stack_sized_by_a_child_does_not_take_that_childs_fraction_twice() {
let mut h = Harness::new((400, 200));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let behind = rect(Color::BLUE).add(&mut h.rsc);
let stack = Stack {
children: vec![behind.add_strong(&mut h.rsc), half.add_strong(&mut h.rsc)],
size: StackSize::Child(1),
}
.add(&mut h.rsc);
h.set_root((stack,).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, stack, (0, 0), (200, 200));
assert_corners!(h, half, (0, 0), (200, 200));
assert_corners!(h, behind, (0, 0), (200, 200));
}
-121
View File
@@ -1,121 +0,0 @@
//! The tree a seed describes, as a value rather than as widgets.
//!
//! Two things have to hold for a plan to be worth having. Editing a plan has
//! to mean what growing with those edits means, or a scenario reads one thing
//! and the oracle another. And reducing a plan has to end, or a shrinker
//! searching for the smallest counterexample never returns.
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, plan};
use std::collections::HashMap;
fn some_edits(seed: u64, of: &Plan) -> Edits {
let mut rng = Rng::new(seed);
let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0);
let mut of = of.clone();
of.walk_mut(&mut |p| {
if matches!(p.kind, Kind::Span { .. }) {
spans += 1;
}
sized += p.size.is_some() as usize;
aligned += p.align.is_some() as usize;
nodes += p.region_node.is_some() as usize;
});
let pick =
|n: usize, rng: &mut Rng| -> Vec<usize> { (0..n).filter(|_| rng.chance()).collect() };
Edits {
sizes: pick(sized, &mut rng)
.into_iter()
.map(|i| (i, [Some(LayoutLen::LEFTOVER), None]))
.collect(),
aligns: pick(aligned, &mut rng)
.into_iter()
.map(|i| (i, [Some(AxisAlign::POS), None]))
.collect(),
nodes: pick(nodes, &mut rng)
.into_iter()
.map(|i| (i, true))
.collect(),
spans: pick(spans, &mut rng)
.into_iter()
.map(|i| {
(
i,
SpanEdit {
detach: vec![0],
attach: 2,
},
)
})
.collect::<HashMap<_, _>>(),
fixed_branches: false,
}
}
use iris::prelude::*;
/// The two routes to an edited tree are one tree. `plan` resolves edits out
/// of the random stream as it draws; `edited` puts them on a tree that
/// already exists, which is the only route a shrunk plan has, since no seed
/// grows one. A scenario written against either has to read the same.
#[test]
fn editing_a_plan_is_growing_one_with_those_edits() {
for seed in 1..=60 {
let bare = plan(seed, 5, &Edits::default());
let edits = some_edits(seed, &bare);
assert_eq!(
bare.edited(&edits),
plan(seed, 5, &edits),
"seed {seed}: edited and grown-with-edits disagree"
);
}
}
/// Every simplification is strictly smaller, so taking them in turn reaches a
/// fixed point instead of circling. A shrinker that can return to a tree it
/// has already tried does not stop.
#[test]
fn every_simplification_of_a_plan_is_smaller_than_it() {
for seed in 1..=60 {
let tree = plan(seed, 4, &Edits::default());
let mut queue = vec![tree];
let mut seen = 0;
while let Some(node) = queue.pop() {
seen += 1;
if seen > 400 {
break;
}
for small in node.smaller() {
assert!(
small.size() <= node.size(),
"seed {seed}: a simplification grew from {} to {}",
node.size(),
small.size()
);
if small.size() < node.size() {
queue.push(small);
}
}
}
}
}
/// Reducing until nothing reduces ends, and ends at something small enough to
/// read rather than at the tree it started from.
#[test]
fn reducing_a_plan_all_the_way_ends() {
for seed in 1..=30 {
let mut node = plan(seed, 5, &Edits::default());
let grown = node.size();
let mut steps = 0;
while let Some(next) = node.smaller().into_iter().next() {
node = next;
steps += 1;
assert!(steps < 10_000, "seed {seed}: reducing did not end");
}
assert!(
node.size() < grown.max(2),
"seed {seed}: reduced {grown} widgets to {}",
node.size()
);
}
}
-810
View File
@@ -1,810 +0,0 @@
//! What a second frame draws again, and what it keeps.
use std::{cell::Cell, rc::Rc};
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
/// A leaf that counts its draws and reports whatever size it is given, so a
/// test can see what the retained path skipped. One that reads its box in
/// pixels has a drawing that holds for that box alone.
struct Counted {
draws: Rc<Cell<usize>>,
size: Size,
reads_box: bool,
}
impl Widget for Counted {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
if self.reads_box {
painter.px_size();
}
self.size
}
}
struct Counts(Rc<Cell<usize>>);
impl Counts {
fn get(&self) -> usize {
self.0.get()
}
}
fn counted(h: &mut Harness, size: Size, reads_box: bool) -> (WeakWidget<Counted>, Counts) {
let draws = Rc::new(Cell::new(0));
let id = Counted {
draws: draws.clone(),
size,
reads_box,
}
.add(&mut h.rsc);
(id, Counts(draws))
}
struct Layered {
children: [StrongWidget<Rect>; 2],
_revision: usize,
}
impl Widget for Layered {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.child_layer();
painter.widget(&self.children[0]);
painter.next_layer();
painter.widget(&self.children[1]);
Size::default()
}
}
#[test]
fn a_redrawn_layered_widget_keeps_the_layer_it_was_entered_on() {
let mut h = Harness::new((400, 200));
let children = [
rect(Color::RED).add_strong(&mut h.rsc),
rect(Color::BLUE).add_strong(&mut h.rsc),
];
let root = Layered {
children,
_revision: 0,
}
.add(&mut h.rsc);
h.set_root(root);
h.rsc[root]._revision += 1;
h.frame();
let label = h.rsc.widgets().label(root.id());
let active = h
.render
.debug(h.rsc.widgets(), label)
.find(|active| active.id == root.id())
.unwrap();
assert_eq!(active.layer, 0);
}
/// A fixed-width leaf beside one that takes what is left over, so changing
/// the first hands the second a different box without the output changing.
fn pair(h: &mut Harness, reads_box: bool) -> (WeakWidget<Counted>, Counts, WidgetId) {
let (first, _) = counted(h, Size::from((100, 200)), false);
let (second, draws) = counted(h, Size::LEFTOVER, reads_box);
h.set_root((first, second).span(Dir::RIGHT));
(first, draws, second.id())
}
#[test]
fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
let mut h = Harness::new((400, 200));
let (first, draws, second) = pair(&mut h, false);
let settled = draws.get();
assert_corners!(h, second, (100, 0), (400, 200));
h.rsc[first].size = Size::from((150, 200));
h.frame();
assert_eq!(
draws.get(),
settled,
"its box is a field to write, not a reason to draw"
);
assert_corners!(h, second, (150, 0), (400, 200));
}
#[test]
fn moving_an_ordinary_subtree_remaps_its_mask() {
let mut h = Harness::new((400, 200));
let (first, _) = counted(&mut h, Size::from((100, 200)), false);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let masked = inner.masked().add(&mut h.rsc);
h.set_root((first, masked).span(Dir::RIGHT));
h.rsc[first].size = Size::from((150, 200));
h.frame();
let active = &h.render.active[&masked.id()];
assert_eq!(
h.rsc.ui().masks[active.mask.idx()].region,
UiRegion::new(UiSpan::new(Len::px(150.0), Len::rel_max()), UiSpan::FULL,)
);
assert_corners!(h, inner, (150, 0), (400, 200));
}
#[test]
fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
let mut h = Harness::new((400, 200));
let (first, draws, second) = pair(&mut h, true);
let settled = draws.get();
h.rsc[first].size = Size::from((150, 200));
h.frame();
// The preceding fixed child makes the remaining box this child's real
// box, so measuring it also draws it in its final box.
assert_eq!(draws.get(), settled + 1);
assert_corners!(h, second, (150, 0), (400, 200));
}
#[test]
fn a_span_child_that_declares_its_length_is_drawn_once() {
let mut h = Harness::new((400, 200));
let (told, told_draws) = counted(&mut h, Size::from((100, 200)), false);
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), true);
// The span takes one child's length from its hint and has to draw the
// other to find out, so only the second is drawn before its final box.
let hinted = told.width(100).add(&mut h.rsc);
h.set_root((hinted, asked).span(Dir::RIGHT));
assert_eq!(told_draws.get(), 1);
// Only the available length changes: positioning the final slot does
// not invalidate a numeric size read.
assert_eq!(asked_draws.get(), 2);
}
#[test]
fn a_span_relays_out_when_a_child_it_measured_changes() {
let mut h = Harness::new((400, 200));
let (first, _, second) = pair(&mut h, false);
h.rsc[first].size = Size::from((250, 200));
h.frame();
assert_corners!(h, first, (0, 0), (250, 200));
assert_corners!(h, second, (250, 0), (400, 200));
}
#[test]
fn a_repaint_that_keeps_its_size_does_not_relay_out() {
let mut h = Harness::new((400, 200));
let (first, draws) = counted(&mut h, Size::from((100, 200)), false);
let (second, _) = counted(&mut h, Size::LEFTOVER, false);
h.set_root((first, second).span(Dir::RIGHT));
let settled = draws.get();
// Taking mutable access is the ordinary content-change signal. This
// widget returns the same size, so the parent has nothing to lay out.
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id());
h.frame();
assert_eq!(draws.get(), settled + 1);
}
#[test]
fn a_span_child_survives_the_next_frame() {
let mut h = Harness::new((400, 200));
// Both children declare a length, so the span chooses their boxes from
// hints rather than drawing them to find out.
let top = rect(Color::RED).height(80).add(&mut h.rsc);
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
h.set_root((top, bottom).span(Dir::DOWN));
h.rsc.widgets_mut().get_dyn_mut(top.id());
h.frame();
assert_corners!(h, top, (0, 0), (400, 80));
assert_corners!(h, bottom, (0, 80), (400, 200));
}
/// Lays its child out from the hint alone, never reading what it drew.
struct FromHint {
inner: StrongWidget,
}
impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let mut region = UiRegion::FULL;
region.y.end = region.y.start.offset(len.px);
painter.widget_within(&self.inner, region);
Size::LEFTOVER
}
}
#[test]
fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
let mut h = Harness::new((400, 200));
let inner = rect(Color::RED).height(80).add(&mut h.rsc);
let parent = FromHint {
inner: inner.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
h.set_root(parent);
assert_corners!(h, inner, (0, 0), (400, 80));
h.set_len(inner, Axis::Y, 120);
h.frame();
assert_corners!(h, inner, (0, 0), (400, 120));
}
/// Reads its box's size, which nothing but its own draw can put right.
struct ReadsBox {
draws: Rc<Cell<usize>>,
}
impl Widget for ReadsBox {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
Size::from_px(painter.px_size().div_int(4))
}
}
/// Reads its box across one axis only, so its drawing holds for a taller
/// box on its own and only a wider one is worth a draw.
///
/// Both of these report a quarter of what they read, without saying that the
/// drawing holds there too, so each length they are asked at costs two draws:
/// one to answer, and one in the quarter-sized box that answer places them
/// in. The counts below are in those pairs.
struct ReadsWidth {
draws: Rc<Cell<usize>>,
}
impl Widget for ReadsWidth {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
Size::from_px(PxVec2::new(
painter.px_len(Axis::X).div_int(4),
Px::from_int(20),
))
}
}
#[test]
fn a_resize_does_not_redraw_what_the_shader_can_move() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
h.set_root(leaf);
let settled = draws.get();
h.resize((800, 100));
assert!(h.needs_redraw());
h.frame();
assert_eq!(
draws.get(),
settled,
"a scaling drawing follows its box, and the output is one"
);
assert_corners!(h, leaf, (0, 0), (800, 100));
}
#[test]
fn a_span_ruled_across_itself_moves_its_child_without_redrawing_it() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
let span = (leaf,).span(Dir::RIGHT).height(rel(1.0)).add(&mut h.rsc);
h.set_root(span);
let settled = draws.get();
h.resize((400, 100));
h.frame();
assert_eq!(draws.get(), settled);
assert_corners!(h, leaf, (0, 0), (400, 100));
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0));
}
/// The output is the root of the box chain, so a resize is a box that changed
/// length like any other -- there is not a second rule for the window. A
/// drawing that holds for one length is drawn again whichever box moved.
#[test]
fn a_resize_redraws_what_does_not_scale() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, true);
h.set_root(leaf);
let settled = draws.get();
h.resize((800, 100));
h.frame();
assert_eq!(draws.get(), settled + 1, "its box is a different length");
assert_corners!(h, leaf, (0, 0), (800, 100));
}
#[test]
fn a_resize_redraws_what_read_its_box() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsBox {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(leaf);
let settled = draws.get();
h.resize((800, 100));
h.frame();
assert_eq!(draws.get(), settled + 2);
}
#[test]
fn a_resize_only_redraws_read_axes() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsWidth {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(leaf);
let settled = draws.get();
h.resize((400, 300));
h.frame();
assert_eq!(draws.get(), settled, "height was never read");
h.resize((800, 300));
h.frame();
assert_eq!(draws.get(), settled + 2, "width changes its answer");
}
/// A window is measured onto the grid like everything else, so a resize too
/// small to reach the next step is not a resize at all -- and one that does
/// reach it is, however little of a pixel it is worth.
#[test]
fn a_resize_within_one_step_is_not_a_resize() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsWidth {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(leaf);
let settled = draws.get();
// All of these are 400 px to the nearest step.
let step = Px::STEP.to_f32();
for part in [0.1, 0.2, 0.3] {
h.resize((400.0 + step * part, 200.0));
h.frame();
assert_eq!(draws.get(), settled);
}
h.resize((400.0 + step, 200.0));
h.frame();
assert_eq!(draws.get(), settled + 2);
}
/// The same for a box that changes because a sibling did: what is compared
/// is the length on the grid, and three lengths that land on one step are
/// one length.
#[test]
fn a_box_change_within_one_step_is_not_a_change() {
let mut h = Harness::new((400, 200));
let (first, draws, _) = pair(&mut h, true);
let settled = draws.get();
let step = Px::STEP.to_f32();
for part in [0.1, 0.2, 0.3] {
h.rsc[first].size.x = LayoutLen::px(100.0 + step * part);
h.frame();
assert_eq!(draws.get(), settled);
}
h.rsc[first].size.x = LayoutLen::px(100.0 + step);
h.frame();
assert_eq!(draws.get(), settled + 1);
}
#[test]
fn reporting_the_same_output_size_does_not_start_a_resize() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsBox {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(leaf);
let settled = draws.get();
h.resize((400, 200));
assert!(!h.needs_redraw());
h.frame();
assert_eq!(draws.get(), settled);
}
#[test]
fn narrowing_the_output_reflows_text_and_relays_out_around_it() {
let mut h = Harness::new((600, 400));
let para = wtext(
"Wrapping shapes one source into as many lines as its container leaves \
room for, so the height of a paragraph is an answer rather than a setting.",
)
.size(20)
.wrap(true)
.add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((para, below).span(Dir::DOWN));
let top = h.region(&below).expect("drew nothing").top_left.y;
h.resize((300, 400));
h.frame();
let lower = h.region(&below).expect("drew nothing").top_left.y;
assert!(lower > top, "same words, half the width: {top} -> {lower}");
}
#[test]
fn a_change_two_levels_under_its_reader_still_reaches_it() {
let mut h = Harness::new((400, 400));
// Every wrapper up to the outer pad read the size below it, so the outer
// pad is what draws again -- and the span it hands the box to is the same
// size as before, which is what lets a draw reuse its way past the leaf.
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), true);
let padded = leaf.pad(10).add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((padded, below).span(Dir::DOWN).pad(12));
assert_corners!(h, below, (12, 132), (388, 388));
h.rsc[leaf].size = Size::px((100, 200).into());
h.frame();
assert_corners!(h, below, (12, 232), (388, 388));
}
/// Reads nothing of its box, so its drawing holds for any length, and has a
/// child so that whatever asks about the subtree has one to reach.
struct Stretchy {
inner: StrongWidget,
draws: Rc<Cell<usize>>,
}
impl Widget for Stretchy {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.widget(&self.inner).size()
}
}
#[test]
fn stretching_a_subtree_carries_the_children_in_it() {
let mut h = Harness::new((400, 400));
let first = rect(Color::RED).height(40).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
let outer = Stretchy {
inner: inner.add_strong(&mut h.rsc),
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root((first, outer).span(Dir::DOWN));
let settled = draws.get();
assert_corners!(h, inner, (0, 40), (400, 400));
h.set_len(first, Axis::Y, 80);
h.frame();
assert_eq!(
draws.get(),
settled,
"its drawing follows its box, rather than being made again"
);
assert_corners!(h, outer, (0, 80), (400, 400));
assert_corners!(h, inner, (0, 80), (400, 400));
}
#[test]
fn a_widened_row_redraws_what_reads_its_length_and_nothing_else() {
let mut h = Harness::new((400, 200));
// What a transcript row is: something whose shaping depends on the width
// it is given, beside something that only has to be the right shape.
let (wraps, wrap_draws) = counted(&mut h, Size::LEFTOVER, true);
let (backing, back_draws) = counted(&mut h, Size::LEFTOVER, false);
let row = (backing, wraps).span(Dir::RIGHT).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, row).span(Dir::RIGHT));
let (settled_wrap, settled_back) = (wrap_draws.get(), back_draws.get());
h.set_len(bar, Axis::X, 200);
h.frame();
// The span reads every child's size, so redrawing one takes the span
// with it -- and the span then measures and places the redrawn child.
assert!(wrap_draws.get() > settled_wrap, "reads the width it got");
assert_eq!(back_draws.get(), settled_back, "only has to be the shape");
assert_corners!(h, backing, (200, 0), (300, 200));
assert_corners!(h, wraps, (300, 0), (400, 200));
}
#[test]
fn a_declared_length_child_is_not_redrawn_when_the_box_around_it_grows() {
let mut h = Harness::new((400, 200));
// Its box is a fixed 80 wherever the row's edges end up, so drawing it
// again would be for a width it does not have. The declared width is what
// lets the span say that without drawing it: a width the span learnt by
// drawing the child in its own box is only an answer for that box.
let (counter, draws) = counted(&mut h, Size::from((80, 200)), true);
let fixed = counter.width(80).add(&mut h.rsc);
let (leftover, _) = counted(&mut h, Size::LEFTOVER, false);
let row = (fixed, leftover).span(Dir::RIGHT).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, row).span(Dir::RIGHT));
let settled = draws.get();
h.set_len(bar, Axis::X, 200);
h.frame();
assert_eq!(draws.get(), settled, "its own length did not change");
assert_corners!(h, fixed, (200, 0), (280, 200));
}
/// A retained drawing belongs to the layer it was made on: asked for again
/// on another one it has to be drawn there, since nothing about its geometry
/// says it is in a list that paints at a different moment.
#[test]
fn a_widget_asked_again_on_another_layer_is_drawn_there() {
/// Draws its child on its own layer, then again one layer in -- which is
/// what a container measuring a child by drawing it used to do.
struct Twice(StrongWidget);
impl Widget for Twice {
fn draw(&mut self, painter: &mut Painter) -> Size {
let size = painter.widget(&self.0).size();
painter.child_layer();
painter.widget(&self.0);
size
}
}
let mut h = Harness::new((400, 200));
let (front, draws) = counted(&mut h, Size::from((100, 50)), false);
let outer = Twice(front.add_strong(&mut h.rsc)).add(&mut h.rsc);
h.set_root(outer);
h.frame();
assert_ne!(
h.render.active[&front.id()].layer,
h.render.active[&outer.id()].layer,
"the first drawing was kept, on the layer it was measured on"
);
assert_eq!(draws.get(), 2, "the second ask could not reuse the first");
}
/// Which is why `Stack` measures the child that sizes it on the layer that
/// child draws on: one drawing, above the background it stacks over, rather
/// than one on each layer and the wrong one kept.
#[test]
fn a_stacks_sizing_child_is_drawn_once_where_it_belongs() {
let mut h = Harness::new((400, 200));
let background = rect(Color::RED).add(&mut h.rsc);
let (front, draws) = counted(&mut h, Size::from((100, 50)), false);
let stack = Stack {
children: vec![
background.add_strong(&mut h.rsc),
front.add_strong(&mut h.rsc),
],
size: StackSize::Child(1),
}
.add(&mut h.rsc);
h.set_root(stack);
h.frame();
let layer = |id| h.render.active[&id].layer;
assert_ne!(layer(front.id()), layer(stack.id()));
assert_ne!(layer(front.id()), layer(background.id()));
assert_eq!(draws.get(), 1);
}
/// A widget's own mask is not the one it inherited, and a redraw of it
/// inherits the second: handing back the first is handing it its own mask to
/// set a second time, which `set_mask` asserts against.
#[test]
fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
let mut h = Harness::new((400, 200));
let inner = rect(Color::BLUE).add(&mut h.rsc);
let masked = inner.masked().add(&mut h.rsc);
let other = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((other, masked).span(Dir::RIGHT));
h.rsc.widgets_mut().get_dyn_mut(masked.id());
h.frame();
assert_corners!(h, inner, (100, 0), (400, 200));
}
/// The two spans a subtree changes hands between, and the branch that is not
/// in the tree yet -- kept alive by the test until it is.
struct Handover {
leaf: WidgetId,
first: WeakWidget<Span>,
second: WeakWidget<Span>,
root: WeakWidget<Span>,
spare: StrongWidget,
}
/// A subtree that changes hands while its box does not move, so nothing about
/// reusing its drawing says it changed parents. `deeper` puts a span between
/// the root and `second`, so it changes depth by changing hands as well.
fn plant_handover(h: &mut Harness, moved: bool, deeper: bool, width: f32) -> Handover {
let leaf = rect(Color::RED).add(&mut h.rsc);
let sized = leaf.width(width).add(&mut h.rsc);
let holder = (sized,).span(Dir::RIGHT).add(&mut h.rsc);
let first = Span {
children: match moved {
true => Vec::new(),
false => vec![holder.add_strong(&mut h.rsc)],
},
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let second = Span {
children: match moved {
true => vec![holder.add_strong(&mut h.rsc)],
false => Vec::new(),
},
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let branch = match deeper {
true => (second,).span(Dir::RIGHT).add_strong(&mut h.rsc),
false => second.add_strong(&mut h.rsc),
};
let (in_tree, spare) = match moved {
true => (branch, first.add_strong(&mut h.rsc)),
false => (first.add_strong(&mut h.rsc), branch),
};
let root = Span {
children: vec![in_tree],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
Handover {
leaf: sized.id(),
first,
second,
root,
spare,
}
}
/// Moves the subtree and swaps the branch it sits in for the one it left.
fn hand_over(h: &mut Harness, tree: Handover) -> WidgetId {
let holder = h.rsc[tree.first].children.remove(0);
h.rsc[tree.second].children.push(holder);
h.rsc[tree.root].children.clear();
h.rsc[tree.root].children.push(tree.spare);
h.frame();
tree.leaf
}
#[test]
fn a_subtree_that_changed_parents_is_not_undrawn_by_the_one_it_left() {
let mut warm = Harness::new((400, 200));
let tree = plant_handover(&mut warm, false, false, 40.0);
warm.frame();
let leaf = hand_over(&mut warm, tree);
let mut cold = Harness::new((400, 200));
let grown = plant_handover(&mut cold, true, false, 40.0);
cold.frame();
assert_eq!(
warm.region(&leaf),
cold.region(&grown.leaf),
"the span it left still listed it and undrew it"
);
}
#[test]
fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
let mut warm = Harness::new((400, 200));
let tree = plant_handover(&mut warm, false, true, 40.0);
warm.frame();
let leaf = hand_over(&mut warm, tree);
// After it has changed hands, so what has to reach the new parent is a
// change made under the subtree it now holds.
warm.set_len(leaf, Axis::X, LayoutLen::px(90.0));
warm.frame();
let mut cold = Harness::new((400, 200));
let grown = plant_handover(&mut cold, true, true, 90.0);
cold.frame();
assert_eq!(
warm.region(&leaf),
cold.region(&grown.leaf),
"the span it moved to is the one the change has to reach"
);
}
fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
h.render.active[&id]
.primitives
.iter()
.map(|primitive| {
let handle = &primitive.handle;
let instance = &h.render.layers[handle.layer].primitives()[handle.kind as usize]
.as_ref()
.unwrap()
.instances()[handle.inst_idx];
h.render
.moves
.resolve(instance.move_idx, instance.region)
.to_px(h.render.output_size())
})
.collect()
}
#[test]
fn frame_geometry_and_extent_geometry_keep_their_references() {
struct Both(Rc<Cell<usize>>);
impl Widget for Both {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.primitive_within(RectPrimitive::color(Color::RED), UiRegion::FULL);
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::LEFTOVER
}
}
for node in [false, true] {
let mut h = Harness::new((400, 200));
let first = rect(Color::GREEN).width(100).add(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
let both = Both(draws.clone()).add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(both, node);
h.set_root((first, both).span(Dir::RIGHT));
let count = draws.get();
h.set_len(first, Axis::X, 200);
h.frame();
assert_eq!(draws.get(), count);
let bounds = primitive_bounds(&h, both.id());
assert_eq!(bounds[0].top_left.x, Px::ZERO);
assert_eq!(bounds[0].bot_right.x, Px::from_int(400));
assert_eq!(bounds[1].top_left.x, Px::from_int(200));
assert_eq!(bounds[1].bot_right.x, Px::from_int(400));
}
}
#[test]
fn changing_an_inherited_extent_keeps_the_original_measurement_offer() {
fn build(h: &mut Harness, width: i32, text: &str) -> (WeakWidget<Text>, WeakWidget<Rect>) {
let first = rect(Color::RED).width(width).add(&mut h.rsc);
let words = wtext(text).size(20).wrap(true).add(&mut h.rsc);
let through = Stretchy {
inner: words.add_strong(&mut h.rsc),
draws: Rc::new(Cell::new(0)),
}
.add(&mut h.rsc);
h.set_root((first, through).span(Dir::RIGHT));
(words, first)
}
let short = "one two";
let long = "one two three four five six seven eight nine ten eleven twelve";
let mut warm = Harness::new((400, 200));
let (words, first) = build(&mut warm, 50, short);
warm.set_len(first, Axis::X, 200);
warm.frame();
*warm.rsc[words].content = long.to_string();
warm.frame();
let mut cold = Harness::new((400, 200));
let (other, _) = build(&mut cold, 200, long);
assert_eq!(warm.region(&words), cold.region(&other));
assert_eq!(
primitive_bounds(&warm, words.id()),
primitive_bounds(&cold, other.id())
);
}
-84
View File
@@ -1,84 +0,0 @@
//! Scrolling moves content and stops at its ends.
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
#[test]
fn scrollable_enables_a_region_node_but_raw_scroll_does_not() {
let mut h = Harness::new((100, 100));
let default_child = ().add(&mut h.rsc);
let _default = default_child.scrollable().add(&mut h.rsc);
assert!(h.rsc.widgets().is_region_node(default_child));
h.rsc.widgets_mut().set_region_node(default_child, false);
assert!(!h.rsc.widgets().is_region_node(default_child));
let raw_child = ().add(&mut h.rsc);
let _raw = Scroll::new(raw_child.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
assert!(!h.rsc.widgets().is_region_node(raw_child));
let explicit = ().region_node().add(&mut h.rsc);
assert!(h.rsc.widgets().is_region_node(explicit));
}
#[test]
fn a_scrollable_child_can_drop_its_region_node() {
let mut h = Harness::new((400, 200));
let top = rect(Color::RED).height(200).add(&mut h.rsc);
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
let content = (top, bottom).span(Dir::DOWN).add(&mut h.rsc);
h.set_root(content.scrollable());
h.rsc.widgets_mut().set_region_node(content, false);
h.frame();
h.move_to((200, 100));
h.scroll((0, 1));
h.frame();
assert!(!h.rsc.widgets().is_region_node(content));
assert_corners!(h, top, (0, -150), (400, 50));
}
#[test]
fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
let mut h = Harness::new((400, 200));
// Twice the window's height, so there is 200 to scroll.
let top = rect(Color::RED).height(200).add(&mut h.rsc);
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
h.set_root((top, bottom).span(Dir::DOWN).scrollable());
h.move_to((200, 100));
// `Scroll` starts snapped to the end.
assert_corners!(h, top, (0, -200), (400, 0));
// The handler scales a wheel line by 50.
h.scroll((0, 1));
h.frame();
assert_corners!(h, top, (0, -150), (400, 50));
h.scroll((0, 10));
h.frame();
assert_corners!(h, top, (0, 0), (400, 200));
}
/// A widget that clips to its box may not report more than the box: its
/// parent would place the part it cut off, and the framework would put a
/// drawing longer than its box somewhere. `Masked` is the second of these
/// after `Scroll`, and the assertion in `draw_at` is what says so.
#[test]
#[should_panic = "clips to"]
fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
struct Clipper(StrongWidget);
impl Widget for Clipper {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region());
painter.widget(&self.0).size()
}
}
let mut h = Harness::new((100, 100));
let tall = rect(Color::RED).height(400).add_strong(&mut h.rsc);
let clipper = Clipper(tall).add(&mut h.rsc);
h.set_root(clipper);
h.frame();
}
-674
View File
@@ -1,674 +0,0 @@
//! The smallest trees that laid out differently warm than cold, each shrunk
//! by `tests/shrink.rs` from hundreds of widgets. The first two are a cold
//! frame that had not settled: a wrapping text shaped at a width it was
//! measured in rather than the one it was given. The rest are a widget
//! measured again in a box its own answer had decided, where the old answer
//! is a fixed point whatever the content now says. The last three are
//! neither: one box length, composed two ways, landing either side of the
//! boundary that decided whether a child was drawn at all, and two boxes
//! reached through a region node's own entry rather than through the offer
//! that node was given. The last is a wrapping text handed back the width
//! it measured, rounded to a step below the line it measured there.
use iris::harness::Harness;
use iris::prelude::*;
use iris::random::Branch;
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
/// the tree changes -- every widget is marked for redraw and the frame is
/// taken again -- so no box may move, and a warm frame has to land where a
/// cold one does.
fn plant(h: &mut Harness) -> Vec<WidgetId> {
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
let sized = wrapped.width(76).add(&mut h.rsc);
let aligned = sized;
h.rsc
.widgets_mut()
.set_alignment(sized, Axis::X, AxisAlign::POS);
h.rsc
.widgets_mut()
.set_alignment(sized, Axis::Y, AxisAlign::POS);
let stack = Stack {
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(root);
vec![
plain.id(),
wrapped.id(),
sized.id(),
aligned.id(),
stack.id(),
root.id(),
]
}
/// The first frame does not reach the layout a second one does, so "cold" is
/// not a fixed point and comparing against it compares against a tree that
/// has not settled.
#[test]
fn one_frame_is_enough() {
let mut h = Harness::new((640, 900));
let ids = plant(&mut h);
let first = h.region(&ids[1]).unwrap();
for _ in 0..3 {
for &id in &ids {
h.rsc.widgets_mut().get_dyn_mut(id);
}
h.frame();
}
let settled = h.region(&ids[1]).unwrap();
println!(
"first frame {} tall, settled {} tall",
first.bot_right.y - first.top_left.y,
settled.bot_right.y - settled.top_left.y
);
assert_eq!(
first.bot_right.y - first.top_left.y,
settled.bot_right.y - settled.top_left.y,
"the first frame had not finished laying out"
);
}
#[test]
fn repainting_everything_moves_nothing() {
let mut warm = Harness::new((640, 900));
let ids = plant(&mut warm);
for &id in &ids {
warm.rsc.widgets_mut().get_dyn_mut(id);
}
warm.frame();
let mut cold = Harness::new((640, 900));
let cold_ids = plant(&mut cold);
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box
/// is the same size whatever the output is, so a resize may not change any of
/// it -- but the text comes out 3.92px narrower warm than cold.
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
let words = "Wrapping shapes one source into as many lines as the box leaves";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let aligned = text;
h.rsc
.widgets_mut()
.set_alignment(text, Axis::X, AxisAlign::NEG);
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
let sized = inner.sized((189, 176)).add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc);
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
vec![
text.id(),
aligned.id(),
inner.id(),
sized.id(),
filler.id(),
root.id(),
]
}
#[test]
fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
let mut warm = Harness::new((1920, 1200));
let ids = plant_fixed(&mut warm);
warm.frame();
warm.resize((640, 900));
warm.frame();
let mut cold = Harness::new((640, 900));
let cold_ids = plant_fixed(&mut cold);
cold.frame();
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by
/// moving them, cold by growing them that way. Same widgets, same sizes, one
/// ends up 29.9px from where the other does.
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
let wrapped = wtext("Wrapping shapes one source into as many lines")
.size(16)
.wrap(true)
.add(&mut h.rsc);
let plain = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add(&mut h.rsc);
let first: StrongWidget = wrapped.add_strong(&mut h.rsc);
let second: StrongWidget = plain.add_strong(&mut h.rsc);
let children = match swapped {
true => vec![second, first],
false => vec![first, second],
};
let span = Span {
children,
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let span_handle = span;
let aligned = span;
h.rsc
.widgets_mut()
.set_alignment(span, Axis::X, AxisAlign::CENTER);
h.state.root = Some(aligned.add_strong(&mut h.rsc));
(
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
span_handle,
)
}
#[test]
fn swapping_two_children_lands_where_growing_them_that_way_does() {
let mut warm = Harness::new((640, 900));
let (ids, span) = plant_pair(&mut warm, false);
warm.frame();
warm.rsc[span].children.rotate_left(1);
warm.frame();
let mut cold = Harness::new((640, 900));
let (cold_ids, _) = plant_pair(&mut cold, true);
cold.frame();
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its
/// content from what the content says, and hands that box down through a
/// pass-through; the span under it was given that box once, so nothing at its
/// own edge says the box was its own answer.
fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
let words = "Wrapping shapes one source into as many lines as the box leaves room for,";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc);
let mut inner_children: Vec<StrongWidget> =
vec![text.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)];
if swapped {
inner_children.rotate_left(1);
}
let inner = Span {
children: inner_children,
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let block = rect(Color::RED).add(&mut h.rsc);
let fixed = block.width(87).add(&mut h.rsc);
let mut outer_children: Vec<StrongWidget> =
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
if swapped {
outer_children.rotate_left(1);
}
let outer = Span {
children: outer_children,
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
// Carried no rule even before rules were a property: it is here to be a
// widget between the span and the scroll, not to declare anything.
let through = (outer,).span(Dir::RIGHT).add(&mut h.rsc);
let scroll = Scroll::new(through.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
h.state.root = Some(scroll.add_strong(&mut h.rsc));
(
vec![
text.id(),
filler.id(),
inner.id(),
block.id(),
fixed.id(),
outer.id(),
through.id(),
scroll.id(),
],
[inner, outer],
)
}
#[test]
fn a_span_given_the_box_its_answer_decided_matches_a_cold_layout() {
let mut warm = Harness::new((640, 900));
let (ids, spans) = plant_scrolled(&mut warm, false);
warm.frame();
for span in spans {
warm.rsc[span].children.rotate_left(1);
}
warm.frame();
let mut cold = Harness::new((640, 900));
let (cold_ids, _) = plant_scrolled(&mut cold, true);
cold.frame();
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
/// Reports a width derived from the box it is asked in. Reading through the
/// painter is its declaration that the answer holds for that width only.
struct Wider {
extra: f32,
}
impl Widget for Wider {
fn draw(&mut self, painter: &mut Painter) -> Size {
Size {
x: LayoutLen {
px: painter.px_len(Axis::X) + Px::from_f32(self.extra),
..LayoutLen::ZERO
},
y: LayoutLen::LEFTOVER,
}
}
}
fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) {
let content = Wider { extra }.add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
let root = scroll;
h.rsc
.widgets_mut()
.set_alignment(scroll, Axis::X, AxisAlign::NEG);
h.set_root(root);
(content, scroll.id())
}
#[test]
fn a_scrolls_retained_answer_is_the_one_a_cold_layout_asks_for() {
let mut warm = Harness::new((100, 100));
let (content, scroll) = plant_wider(&mut warm, 50.0);
warm.rsc[content].extra = 70.0;
warm.frame();
let mut cold = Harness::new((100, 100));
let (_, cold_scroll) = plant_wider(&mut cold, 70.0);
assert_eq!(warm.region(&scroll), cold.region(&cold_scroll));
}
/// Six widgets, shrunk from 266. `measured`'s box is exactly the height of its
/// one fixed child, which is the box a parent sizing itself from that answer
/// hands back -- so whether its leftover-only child was drawn at all came down
/// to the 0.00003 px the composed length differs by, one way warm and the
/// other cold.
fn plant_boundary(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
let filler = rect(Color::RED).add(&mut h.rsc);
let plain = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add(&mut h.rsc);
let mut pair: Vec<StrongWidget> =
vec![filler.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)];
if swapped {
pair.rotate_left(1);
}
let measured = Span {
children: pair,
dir: Dir::DOWN,
gap: Px::ZERO,
}
.add(&mut h.rsc);
// Takes the whole box on its own, so the span above has nothing left to
// divide and `measured` is given exactly the text's height.
let whole = rect(Color::RED).add(&mut h.rsc);
h.rsc
.widgets_mut()
.set_size_rules(whole, None, Some(LayoutLen::rel(1.0)));
let mut inner_children: Vec<StrongWidget> = vec![
measured.add_strong(&mut h.rsc),
whole.add_strong(&mut h.rsc),
];
if swapped {
inner_children.rotate_left(1);
}
let inner = Span {
children: inner_children,
dir: Dir::DOWN,
gap: Px::ZERO,
}
.add(&mut h.rsc);
h.rsc
.widgets_mut()
.set_size_rules(inner, None, Some(LayoutLen::px(198.0)));
// One more span above it: without a box composed through it, both trees
// round the same way and the boundary is never crossed.
let outer = (inner,).span(Dir::DOWN).add(&mut h.rsc);
h.set_root(outer);
(
vec![
filler.id(),
plain.id(),
measured.id(),
whole.id(),
inner.id(),
outer.id(),
],
[measured, inner],
)
}
#[test]
fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() {
let mut warm = Harness::new((640, 900));
let (ids, spans) = plant_boundary(&mut warm, false);
warm.frame();
for span in spans {
warm.rsc[span].children.rotate_left(1);
}
warm.frame();
let mut cold = Harness::new((640, 900));
let (cold_ids, _) = plant_boundary(&mut cold, true);
cold.frame();
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
/// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed
/// 18 grows at depth 6. A scroll inside a scroll, the inner one owning a
/// movable region of its own, and only its text marked for redraw. Nothing
/// about the tree changes, so no box may.
fn plant_nested_scrolls(h: &mut Harness) -> Vec<WidgetId> {
let text = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add(&mut h.rsc);
let inner = Scroll::new(text.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(inner.id(), true);
let filler = rect(Color::RED).add(&mut h.rsc);
h.rsc.widgets_mut().set_size_rules(
filler.id(),
Some(LayoutLen::px(87.0)),
Some(LayoutLen::px(24.0)),
);
let span = Span {
children: vec![inner.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)],
dir: Dir::DOWN,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let root = Scroll::new(span.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
h.set_root(root);
vec![text.id(), inner.id(), filler.id(), span.id(), root.id()]
}
/// A local redraw asks a dirty widget in the box its parent gave it, and only
/// where that box is as long as the one it was offered; anything else is a
/// question its parent has to ask. This inner scroll's offer is the outer
/// scroll's whole viewport and the box it was given is 24px shorter -- the
/// height of the sized child the outer scroll snaps to the end of -- so what
/// it must not do is settle itself. It was drawn at its offer once, and the
/// inner scroll and its text stayed 24px too low.
#[test]
fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
let mut warm = Harness::new((900, 1200));
let ids = plant_nested_scrolls(&mut warm);
warm.rsc.widgets_mut().get_dyn_mut(ids[0]);
warm.frame();
let mut cold = Harness::new((900, 1200));
let cold_ids = plant_nested_scrolls(&mut cold);
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
/// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220
/// to. The pad owns a movable region and is the scroll's content, so the box
/// the scroll places it in is as long as that content while the box it was
/// offered is the viewport -- and with no padding to tell those two apart,
/// the span inside it looked like it was still at its offer. So everything
/// under the pad was asked again in the *placed* box, the offer resolving
/// against the node's own entry, which holds that box: the texts kept the
/// widths they had, the content stayed the length those widths make, and the
/// old answer confirmed itself. What the branch adds is a tree that differs
/// rather than a box that moved, since a probe measured at the wrong width
/// takes the other side.
fn plant_under_a_node(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
let probe = rect(Color::RED).add(&mut h.rsc);
let wide = rect(Color::GREEN).add(&mut h.rsc);
let narrow = rect(Color::BLUE).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: 213.0,
}
.add(&mut h.rsc);
let wrapped = 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)
.add(&mut h.rsc);
let plain = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add(&mut h.rsc);
let row = |h: &mut Harness, mut children: Vec<StrongWidget>| {
if swapped {
children.rotate_left(1);
}
Span {
children,
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc)
};
let texts: Vec<StrongWidget> =
vec![wrapped.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)];
let inner = row(h, texts);
let pair: Vec<StrongWidget> = vec![branch.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
let outer = row(h, pair);
let pad = Pad {
padding: Padding::ZERO,
inner: outer.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(pad.id(), true);
let root = Scroll::new(pad.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
h.set_root(root);
(
vec![
probe.id(),
wide.id(),
narrow.id(),
branch.id(),
wrapped.id(),
plain.id(),
inner.id(),
outer.id(),
pad.id(),
root.id(),
],
[outer, inner],
)
}
#[test]
fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
let mut warm = Harness::new((900, 1200));
let (ids, spans) = plant_under_a_node(&mut warm, false);
warm.frame();
for span in spans {
warm.rsc[span].children.rotate_left(1);
}
warm.frame();
let mut cold = Harness::new((900, 1200));
let (cold_ids, _) = plant_under_a_node(&mut cold, true);
cold.frame();
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
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.";
/// 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,
/// the span takes its width from the longest line of the texts in it, and
/// the text below the span is then wrapped at that width -- so a width the
/// shaper measured comes back to it as the box to break in.
fn plant_a_measured_width(h: &mut Harness, swapped: bool) -> (WeakWidget<Span>, WidgetId) {
let first: StrongWidget = rect(Color::YELLOW).add_strong(&mut h.rsc);
let mut inner = Span::empty(Dir::UP);
inner.children = match swapped {
true => swapped_in(h),
false => vec![first],
};
let inner = inner.height(142).add(&mut h.rsc);
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let stack = Stack {
children: vec![inner.add_strong(&mut h.rsc), text.add_strong(&mut h.rsc)],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
h.set_root((stack,).span(Dir::DOWN).width(195));
(inner, text.id())
}
/// What the span holds once its children have been swapped, which is what
/// the warm tree is changed to and what the cold one is grown with.
fn swapped_in(h: &mut Harness) -> Vec<StrongWidget> {
let paragraph = |h: &mut Harness| -> StrongWidget {
wtext(PARAGRAPH).size(16).wrap(true).add_strong(&mut h.rsc)
};
vec![
paragraph(h),
rect(Color::YELLOW).add_strong(&mut h.rsc),
paragraph(h),
]
}
/// A text handed back the width it measured breaks there the way it broke
/// when it measured it. The width the shaper answers is not on the grid, and
/// a report rounded to the nearest step is under the longest line half the
/// time: a warm tree then keeps a break made in a wider box while a cold one
/// makes a narrower break in the same box, and the paragraph gains a line.
#[test]
fn a_text_is_given_back_a_box_the_line_it_measured_fits_in() {
let mut warm = Harness::new((900, 1200));
let (inner, text) = plant_a_measured_width(&mut warm, false);
warm.frame();
warm.rsc[inner].children = swapped_in(&mut warm);
warm.frame();
let mut cold = Harness::new((900, 1200));
let (_, cold_text) = plant_a_measured_width(&mut cold, true);
cold.frame();
assert_eq!(warm.region(&text), cold.region(&cold_text));
}
#[test]
fn adding_text_to_a_reverse_row_keeps_its_shared_height() {
fn build(
h: &mut Harness,
changed: bool,
) -> (WeakWidget<Span>, WeakWidget<Text>, Vec<StrongWidget>) {
let wrap = 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).add_strong(&mut h.rsc);
let one = || {
wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
};
let plain = one().add_strong(&mut h.rsc);
let shared = one()
.width(LayoutLen::LEFTOVER)
.height(LayoutLen::LEFTOVER)
.add(&mut h.rsc);
let mut extra: Vec<StrongWidget> = vec![
rect(Color::RED).add_strong(&mut h.rsc),
one().add_strong(&mut h.rsc),
one().add_strong(&mut h.rsc),
];
let children: Vec<StrongWidget> = if changed {
let mut children: Vec<StrongWidget> = vec![plain, shared.add_strong(&mut h.rsc)];
children.append(&mut extra);
children
} else {
vec![wrap, plain, shared.add_strong(&mut h.rsc)]
};
let row = Span {
children,
dir: Dir::LEFT,
gap: Px::ZERO,
}
.height(LayoutLen::rel(1.0))
.add(&mut h.rsc);
let fill: StrongWidget = rect(Color::BLUE).add_strong(&mut h.rsc);
let children: Vec<StrongWidget> = vec![fill, row.add_strong(&mut h.rsc)];
let root = Span {
children,
dir: Dir::RIGHT,
gap: Px::from_int(4),
}
.height(LayoutLen::rel(1.0))
.add(&mut h.rsc);
h.set_root(root);
(row, shared, extra)
}
let mut warm = Harness::new((900, 1200));
let (row, shared, extra) = build(&mut warm, false);
warm.rsc[row].children.remove(0);
warm.rsc[row].children.extend(extra);
warm.frame();
let mut cold = Harness::new((900, 1200));
let (_, other, _) = build(&mut cold, true);
assert_eq!(warm.region(&shared), cold.region(&other));
}
-223
View File
@@ -1,223 +0,0 @@
//! What the vertex shader's move-chain walk costs, against how many nested
//! region nodes a primitive resolves through.
//!
//! cargo test --release --test chain_cost -- --ignored --nocapture
//!
//! Timed on the GPU with timestamp queries rather than by the clock: wall time
//! here varied by 2x between runs of one unchanged binary. The pass is
//! submitted and waited on, so this is the GPU's cost and not the recording
//! loop's -- which is what `draw_cost.rs` measures instead.
//!
//! The instances are two pixels wide so that vertex work dominates; a chain
//! walk that does not show up against small quads will not show up against
//! anything.
//!
//! The instance is leaked deliberately, for the reason `draw_cost.rs` gives.
use iris::prelude::*;
use iris_core::{
Len, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
UiRenderState, UiSpan,
};
use wgpu::{Color as GpuColor, *};
const SIZE: u32 = 1024;
const INSTANCES: usize = 200_000;
const FRAMES: u32 = 20;
/// Reported as the best of this many batches, since the mean moves by more
/// than the thing being measured.
const BATCHES: u32 = 8;
fn gpu() -> Option<(Device, Queue, f32)> {
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
}),
};
let instance: &'static Instance = Box::leak(Box::new(instance));
let adapter =
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
println!("no timestamp queries on {:?}", adapter.get_info().name);
return None;
}
println!("adapter: {:?}", adapter.get_info().name);
let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor {
required_features: Features::TIMESTAMP_QUERY,
..Default::default()
}))
.ok()?;
let period = queue.get_timestamp_period();
Some((device, queue, period))
}
fn config(format: TextureFormat) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: SIZE,
height: SIZE,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: vec![],
}
}
/// A chain `depth` slots long, and instances that all resolve through its end.
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
let kind = ui.primitives.kind::<RectPrimitive>();
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
let mut slot = MoveIdx::NONE;
for _ in 0..depth {
slot = render.moves.push(slot, UiRegion::FULL);
}
let px = |v: f32| Len::px(v);
for i in 0..INSTANCES {
let x = (i % (SIZE as usize / 2)) as f32 * 2.0;
let y = (i / (SIZE as usize / 2)) as f32;
render.layers.write(
0,
PrimitiveInst {
kind,
id,
primitive: RectPrimitive::color(UiColor::WHITE),
region: UiRegion::new(
UiSpan::new(px(x), px(x + 2.0)),
UiSpan::new(px(y), px(y + 1.0)),
),
mask_idx: MaskIdx::NONE,
move_idx: slot,
},
);
}
}
/// Nanoseconds the pass took on the GPU, best of `BATCHES`.
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
let format = TextureFormat::Bgra8Unorm;
let mut node = UiRenderNode::new(device, &config(format));
let mut ui = UiData::default();
let mut render = UiRenderState::new();
fill(&mut ui, &mut render, depth);
node.update(device, queue, &mut ui, &mut render);
let target = device.create_texture(&TextureDescriptor {
label: Some("chain cost"),
size: Extent3d {
width: SIZE,
height: SIZE,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format,
usage: TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = target.create_view(&TextureViewDescriptor::default());
let queries = device.create_query_set(&QuerySetDescriptor {
label: Some("chain cost"),
ty: QueryType::Timestamp,
count: 2,
});
let resolved = device.create_buffer(&BufferDescriptor {
label: Some("resolved"),
size: 16,
usage: BufferUsages::QUERY_RESOLVE | BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let readback = device.create_buffer(&BufferDescriptor {
label: Some("readback"),
size: 16,
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let frame = || {
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
{
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
label: None,
color_attachments: &[Some(RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(GpuColor::BLACK),
store: StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: Some(RenderPassTimestampWrites {
query_set: &queries,
beginning_of_pass_write_index: Some(0),
end_of_pass_write_index: Some(1),
}),
occlusion_query_set: None,
multiview_mask: None,
});
node.draw(pass);
}
encoder.resolve_query_set(&queries, 0..2, &resolved, 0);
encoder.copy_buffer_to_buffer(&resolved, 0, &readback, 0, 16);
queue.submit(Some(encoder.finish()));
let slice = readback.slice(..);
slice.map_async(MapMode::Read, |_| {});
let _ = device.poll(PollType::Wait {
submission_index: None,
timeout: None,
});
let ns = {
let view = slice.get_mapped_range().expect("timestamps did not map");
let stamps: [u64; 2] = [
u64::from_le_bytes(view[..8].try_into().unwrap()),
u64::from_le_bytes(view[8..16].try_into().unwrap()),
];
(stamps[1].saturating_sub(stamps[0])) as f64 * period as f64
};
readback.unmap();
ns
};
frame();
let mut best = f64::MAX;
for _ in 0..BATCHES {
let mut total = 0.0;
for _ in 0..FRAMES {
total += frame();
}
best = best.min(total / FRAMES as f64);
}
best
}
#[test]
#[ignore = "measurement, not a check"]
fn chain_cost_by_depth() {
let Some((device, queue, period)) = gpu() else {
println!("no gpu with timestamps; nothing measured");
return;
};
println!("{INSTANCES} instances, {SIZE}x{SIZE}, best of {BATCHES} batches");
let mut base = None;
for depth in [1, 2, 4, 8, 16, 32, 64] {
let ns = pass_cost(&device, &queue, period, depth);
let base = *base.get_or_insert(ns);
println!(
"depth {depth:>3}: {:>9.1} us {:+6.1}% against depth 1",
ns / 1000.0,
(ns - base) / base * 100.0
);
}
}
+2 -5
View File
@@ -22,8 +22,8 @@ use std::time::Instant;
use iris::prelude::*; use iris::prelude::*;
use iris_core::{ use iris_core::{
GlyphPrimitive, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, TextureHandle, GlyphPrimitive, MaskIdx, PrimitiveInst, RectPrimitive, TextureHandle, TexturePrimitive, UiData,
TexturePrimitive, UiData, UiRegion, UiRenderNode, UiRenderState, UiRegion, UiRenderNode, UiRenderState,
}; };
use wgpu::{Color as GpuColor, *}; use wgpu::{Color as GpuColor, *};
@@ -95,7 +95,6 @@ fn fill(
primitive: RectPrimitive::color(UiColor::WHITE), primitive: RectPrimitive::color(UiColor::WHITE),
region: UiRegion::FULL, region: UiRegion::FULL,
mask_idx: MaskIdx::NONE, mask_idx: MaskIdx::NONE,
move_idx: MoveIdx::NONE,
}, },
); );
render.layers.write( render.layers.write(
@@ -112,7 +111,6 @@ fn fill(
}, },
region: UiRegion::FULL, region: UiRegion::FULL,
mask_idx: MaskIdx::NONE, mask_idx: MaskIdx::NONE,
move_idx: MoveIdx::NONE,
}, },
); );
} }
@@ -125,7 +123,6 @@ fn fill(
primitive: TexturePrimitive::from(h), primitive: TexturePrimitive::from(h),
region: UiRegion::FULL, region: UiRegion::FULL,
mask_idx: MaskIdx::NONE, mask_idx: MaskIdx::NONE,
move_idx: MoveIdx::NONE,
}, },
); );
} }
-126
View File
@@ -1,126 +0,0 @@
//! Laying a tree out again has to land where growing it that way would.
//!
//! Every case is one of `scenario`'s, over the trees `iris::random` grows
//! from a seed. The fast test takes a handful of seeds and the ignored one
//! takes as many as it is asked for; both run the same cases the shrinker
//! does over the same trees, so a seed that fails here is reduced by
//!
//! SHRINK_SEED=<seed> SHRINK_DEPTH=<depth> SHRINK_CASE=<case> \
//! cargo test --release --test shrink -- --ignored --nocapture
//!
//! `IRIS_GENERATED_SEED`, `IRIS_GENERATED_SEEDS` and `IRIS_GENERATED_DEPTH`
//! select what the long run covers.
#[path = "scenario/mod.rs"]
mod scenario;
use iris::random::{Edits, plan};
use scenario::{ALL, Case, diverges, env, over_seeds};
/// How deep the generator branches. The generator widens two to four ways per
/// level, so depth is exponential in width and a deep narrow tree is not
/// reachable by raising this -- it buys more overlap between dependency
/// paths, not more ancestry.
fn depth() -> usize {
env("IRIS_GENERATED_DEPTH", 4)
}
/// The seeds the ordinary tests take. Seven that have never failed; 86,
/// which a `Scroll` fixed point once settled differently on; and 20, which
/// caught a locally redrawn widget being placed twice in the box its parent
/// had already placed it in.
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
fn check(seed: u64, depth: usize, case: Case) {
let grown = plan(seed, depth, &Edits::default());
if let Some(how) = diverges(&grown, case, seed) {
panic!(
"seed {seed} at depth {depth} differs after {}: {how}\n\
reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \
SHRINK_CASE={} cargo test --release --test shrink -- --ignored --nocapture",
case.name(),
case.name(),
);
}
}
macro_rules! case {
($name:ident, $case:expr) => {
#[test]
fn $name() {
for seed in SEEDS {
check(seed, depth(), $case);
}
}
};
}
case!(
many_widgets_redrawing_at_once_leaves_every_box_where_it_was,
Case::RepaintSome
);
case!(
everything_redrawing_at_once_leaves_every_box_where_it_was,
Case::Repaint
);
case!(
a_resize_lands_where_starting_at_that_size_would,
Case::Resize
);
case!(
a_resize_and_a_repaint_land_where_starting_that_way_would,
Case::ResizeRepaint
);
case!(
a_size_change_after_a_resize_lands_the_same_way,
Case::ResizeSize
);
case!(
a_size_change_lands_where_growing_it_that_way_would,
Case::Size
);
case!(
every_size_changing_at_once_lands_where_growing_it_that_way_would,
Case::EverySize
);
case!(
an_alignment_change_lands_where_growing_it_that_way_would,
Case::Align
);
case!(
giving_and_taking_a_movable_region_rebuilds_what_resolves_it,
Case::RegionNode
);
case!(
reordering_a_span_lands_where_growing_it_that_way_would,
Case::Reorder
);
#[test]
fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
for case in ALL {
if matches!(case, Case::Shuffle(_)) {
for seed in SEEDS {
check(seed, depth(), case);
}
}
}
}
#[test]
#[ignore = "as many seeds as it is asked for, rather than the nine the others check"]
fn a_long_run_of_seeds_agrees() {
let depth = depth();
let seeds: Vec<u64> = match std::env::var("IRIS_GENERATED_SEED")
.ok()
.and_then(|v| v.parse().ok())
{
Some(seed) => vec![seed],
None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(),
};
over_seeds(seeds, |seed| {
for case in ALL {
check(seed, depth, case);
}
});
}
+111
View File
@@ -0,0 +1,111 @@
//! Where a frame puts things, with no window to put them in.
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
/// A fixed 100 wide, and the rest of the 400 to its neighbour.
fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) {
let left = rect(Color::RED).width(100).add(&mut h.rsc);
let right = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
(left.id(), right.id())
}
#[test]
fn a_span_gives_each_child_the_width_it_asked_for() {
let mut h = Harness::new((400, 200));
let (left, right) = two_rects(&mut h);
assert_corners!(h, left, (0, 0), (100, 200));
assert_corners!(h, right, (100, 0), (400, 200));
}
#[test]
fn resizing_relays_out_against_the_new_output() {
let mut h = Harness::new((400, 200));
let (left, right) = two_rects(&mut h);
h.resize((800, 100));
assert!(h.needs_redraw());
h.frame();
assert_corners!(h, left, (0, 0), (100, 100));
assert_corners!(h, right, (100, 0), (800, 100));
}
#[test]
fn an_empty_widget_takes_a_share_of_a_span() {
let mut h = Harness::new((400, 200));
let gap = ().add(&mut h.rsc);
let right = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((gap, right).span(Dir::RIGHT));
assert_corners!(h, gap, (0, 0), (300, 200));
assert_corners!(h, right, (300, 0), (400, 200));
}
#[test]
fn a_child_drawn_twice_moves_once() {
let mut h = Harness::new((400, 200));
// `Aligned` draws its child twice; listing it twice would move it twice.
let inner = rect(Color::BLUE).add(&mut h.rsc);
let centered = inner.center().width(200).add(&mut h.rsc);
let left = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((left, centered).span(Dir::RIGHT));
assert_corners!(h, inner, (100, 0), (300, 200));
h.rsc[left].x = Some(Len::abs(150));
h.frame();
assert_corners!(h, inner, (150, 0), (350, 200));
}
#[test]
fn a_resize_lands_where_a_cold_start_would() {
let build = |h: &mut Harness| {
let para = wtext(
"Wrapping shapes one source into as many lines as its container leaves room \
for, so the height of a paragraph is an answer rather than a setting.",
)
.size(20)
.wrap(true)
.pad(16)
.add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
let root = (para, below).span(Dir::DOWN).pad(12);
h.set_root(root);
(para, below)
};
let mut cold = Harness::new((900, 1200));
let (cold_para, cold_below) = build(&mut cold);
let mut resized = Harness::new((1920, 1200));
let (para, below) = build(&mut resized);
resized.resize((900, 1200));
resized.frame();
assert_eq!(resized.region(&para), cold.region(&cold_para), "paragraph");
assert_eq!(resized.region(&below), cold.region(&cold_below), "below");
}
#[test]
fn a_fixed_box_is_drawn_again_rather_than_stretched() {
let mut h = Harness::new((400, 400));
// The panel fills a stack sized by its sibling, so it is drawn in the
// whole box and then placed in the shorter one. Reusing it in that fixed
// box afterwards would leave it whatever height it happened to have.
let panel = rect(Color::BLUE).add(&mut h.rsc);
let leaf = rect(Color::RED).height(100).add(&mut h.rsc);
let stack = (panel, leaf)
.stack()
.size(StackSize::Child(1))
.add(&mut h.rsc);
h.set_root(stack.align(Align::TOP));
assert_corners!(h, panel, (0, 0), (400, 100));
h.rsc[leaf].y = Some(Len::abs(250));
h.frame();
assert_corners!(h, panel, (0, 0), (400, 250));
}
-253
View File
@@ -1,253 +0,0 @@
//! Retained CPU-layout diagnostics on one reproducible random tree.
//!
//! Counters and phase timers:
//!
//! cargo test --release --features layout-diagnostics \
//! --test layout_diagnostics -- --ignored --nocapture
//!
//! Uninstrumented hardware totals for one phase:
//!
//! IRIS_PHASE=resize IRIS_FRAMES=1000 perf stat \
//! -e cycles:u,instructions:u cargo test --release \
//! --test layout_diagnostics -- --ignored --nocapture
//!
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
//! `IRIS_DIRTY` how many widgets `many` marks at once.
use iris::harness::Harness;
use iris::prelude::*;
use iris::random::{Edits, Tree, grow};
use std::time::Instant;
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
#[cfg(feature = "layout-diagnostics")]
#[test]
fn a_selected_widget_retains_its_layout_events() {
use iris::core::layout_diagnostics::{self as diagnostics, TraceEvent};
diagnostics::clear_traced_widgets();
let _ = diagnostics::take();
let mut harness = Harness::new((400, 200));
let leaf = rect(Color::RED).region_node().add(&mut harness.rsc);
let other = rect(Color::BLUE).add(&mut harness.rsc);
let root = (leaf, other).span(Dir::RIGHT).add(&mut harness.rsc);
harness.set_root(root);
diagnostics::trace_widget(leaf.id());
let _ = diagnostics::take();
let _ = harness.rsc.widgets_mut().get_dyn_mut(root.id());
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf.id());
harness.frame();
let report = diagnostics::take();
assert!(
report
.traces()
.iter()
.any(|event| matches!(event, TraceEvent::RegionNode { id, .. } if *id == leaf.id()))
);
assert!(
report
.traces()
.iter()
.any(|event| matches!(event, TraceEvent::DrawRequest { id, .. } if *id == leaf.id()))
);
assert!(
report
.traces()
.iter()
.any(|event| matches!(event, TraceEvent::SizeRead { id, .. } if *id == leaf.id()))
);
assert!(
report
.traces()
.iter()
.any(|event| matches!(event, TraceEvent::SizeReported { id, .. } if *id == leaf.id()))
);
diagnostics::clear_traced_widgets();
}
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)
}
#[cfg(feature = "layout-diagnostics")]
fn trace_selected(tree: &Tree) {
let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else {
return;
};
let index = value
.parse::<usize>()
.expect("IRIS_TRACE_INDEX must be a tree.ids index");
let id = tree.ids[index];
iris::core::layout_diagnostics::trace_widget(id);
println!("tracing tree.ids[{index}] = {id:?}");
}
#[cfg(not(feature = "layout-diagnostics"))]
fn trace_selected(_: &Tree) {}
/// The shape a cost is measured on must not depend on what layout measured,
/// or two commits are compared on two different trees. See `Edits`.
fn rig_edits() -> Edits {
Edits {
fixed_branches: true,
..Default::default()
}
}
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
let mut harness = Harness::new(OUTPUT);
let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits());
harness.state.root = Some(root);
harness.frame();
println!(
"fixture: seed {seed}, depth {depth}, {} widgets, {} active",
tree.ids.len(),
harness.render.active_widgets()
);
#[cfg(feature = "layout-diagnostics")]
let _ = iris::core::layout_diagnostics::take();
(harness, tree)
}
fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
let frames = elapsed.len();
// The worst frame is the stutter somebody sees, so it goes beside the
// median; p99 says whether it is the load or a single interruption.
println!(
"{label}: {frames} frame(s), min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
max {:.3} ms, total {:.1} ms",
elapsed[0],
elapsed[frames / 2],
elapsed[frames * 99 / 100],
elapsed[frames - 1],
elapsed.iter().sum::<f64>(),
);
#[cfg(feature = "layout-diagnostics")]
{
let diagnostics = iris::core::layout_diagnostics::take();
print!("{}", diagnostics.per_frame(frames));
for callsite in diagnostics.hot_text().iter().take(3) {
let mut ancestry = Vec::new();
let mut id = Some(callsite.id);
while let Some(widget) = id {
ancestry.push(_harness.rsc.widgets().label(widget).as_str());
id = _harness
.render
.active
.get(&widget)
.and_then(|active| active.parent);
}
println!(" text ancestry: {}", ancestry.join(" < "));
}
}
}
fn run(
label: &str,
frames: usize,
harness: &mut Harness,
mut change: impl FnMut(&mut Harness, usize),
) {
let mut elapsed = Vec::with_capacity(frames);
for frame in 0..frames {
change(harness, frame);
let start = Instant::now();
harness.frame();
elapsed.push(start.elapsed().as_secs_f64() * 1_000.0);
}
report(label, elapsed, harness);
}
#[test]
#[ignore = "measurement, not a check"]
fn layout_cost() {
let seed = env("IRIS_SEED", 1_u64);
let depth = env("IRIS_DEPTH", 7_usize);
let frames = env("IRIS_FRAMES", 100_usize);
assert!(frames > 0, "IRIS_FRAMES must be greater than zero");
let phase = env("IRIS_PHASE", String::from("all"));
assert!(
["all", "cold", "repaint", "many", "size", "scroll", "resize"].contains(&phase.as_str()),
"unknown IRIS_PHASE {phase:?}"
);
let selected = |name| phase == "all" || phase == name;
if selected("cold") {
let mut harness = Harness::new(OUTPUT);
let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits());
harness.state.root = Some(root);
println!(
"fixture: seed {seed}, depth {depth}, {} widgets",
tree.ids.len()
);
trace_selected(&tree);
#[cfg(feature = "layout-diagnostics")]
let _ = iris::core::layout_diagnostics::take();
run("cold", 1, &mut harness, |_, _| {});
drop(tree);
}
if selected("repaint") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
let leaf = tree.ids[0];
run("repaint", frames, &mut harness, move |harness, _| {
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf);
});
}
if selected("many") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
// Spread through the tree rather than taken from one subtree, so the
// dependency paths the frame settles overlap.
let wanted = env("IRIS_DIRTY", 32_usize).max(1);
let step = (tree.ids.len() / wanted).max(1);
let dirty: Vec<_> = tree.ids.iter().copied().step_by(step).collect();
println!("marking {} of {} widgets", dirty.len(), tree.ids.len());
run("many", frames, &mut harness, move |harness, _| {
for &id in &dirty {
harness.rsc.widgets_mut().get_dyn_mut(id);
}
});
}
if selected("size") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
let sized = tree.sized[0];
run("size", frames, &mut harness, move |harness, frame| {
let len = LayoutLen::px(100.0 + (frame % 2) as f32 * 40.0);
harness
.rsc
.widgets_mut()
.set_size_rule(sized, Axis::X, SizeRule::Exact(len));
});
}
if selected("scroll") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
let scroll = tree.scrolls[0];
run("scroll", frames, &mut harness, move |harness, frame| {
harness.rsc[scroll].scroll(if frame % 2 == 0 { 12.0 } else { -12.0 });
});
}
if selected("resize") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
run("resize", frames, &mut harness, |harness, frame| {
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
});
drop(tree);
}
}
+1 -26
View File
@@ -2,7 +2,7 @@
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use iris::harness::{Harness, TouchScript}; use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
#[test] #[test]
@@ -58,28 +58,3 @@ fn hover_ends_when_the_cursor_leaves_the_window() {
h.leave(); h.leave();
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1)); assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1));
} }
#[test]
fn a_recorded_gesture_presses_where_it_says() {
let mut h = Harness::new((400, 200));
let clicks = Rc::new(RefCell::new(Vec::new()));
let (on_left, on_right) = (clicks.clone(), clicks.clone());
let left = rect(Color::RED)
.width(100)
.on(CursorSense::click(), move |_, _| {
on_left.borrow_mut().push("left")
})
.add(&mut h.rsc);
let right = rect(Color::BLUE)
.on(CursorSense::click(), move |_, _| {
on_right.borrow_mut().push("right")
})
.add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
let script = TouchScript::parse("0 down 300 100\n80 up 300 100").unwrap();
h.replay(&script);
assert_eq!(*clicks.borrow(), ["right"]);
}
File renamed without changes.
+27
View File
@@ -0,0 +1,27 @@
//! What a drawing can be taken out of, and what it cannot.
use iris::core::{Remap, UiRegion, UiScalar, UiSpan};
/// A box `size` tall whose top is `rel` of the way down the window.
fn fixed(rel: f32, size: f32) -> UiRegion {
UiRegion::new(
UiSpan::FULL,
UiSpan::new(UiScalar { rel, abs: 0.0 }, UiScalar { rel, abs: size }),
)
}
#[test]
fn a_fixed_box_can_be_carried_but_not_stretched() {
let from = fixed(0.0, 164.0);
assert!(Remap::new(from, UiRegion::FULL).is_none());
assert!(Remap::new(from, fixed(0.5, 164.0)).is_some());
assert!(Remap::new(from, fixed(0.0, 98.0)).is_none());
}
#[test]
fn a_relative_box_can_be_stretched_to_any_other() {
let remap = Remap::new(UiRegion::FULL, fixed(0.0, 98.0)).expect("relative boxes remap");
// A part that filled the window keeps filling what replaced it, which is
// exactly what `outside` could not say for a box of a fixed length.
assert_eq!(remap.apply(UiRegion::FULL), fixed(0.0, 98.0));
}
-41
View File
@@ -1,41 +0,0 @@
//! What remapping a subtree costs per frame, as a load for a counter rather
//! than a check. A span of 200 fixed-height rows, five primitives each, with
//! the row above them changing height every frame, so every row below is
//! offered a box the same shape somewhere else.
//!
//! cargo test --release --test replace_cost -- --ignored
//! perf stat -e instructions:u target/release/.../replace_cost-* --ignored
//!
//! Wall time is the wrong number here; see `draw_cost.rs`.
use iris::harness::Harness;
use iris::prelude::*;
const ROWS: usize = 200;
const FRAMES: usize = 200;
#[test]
#[ignore = "measurement, not a check"]
fn remapping_rows_every_frame() {
let mut h = Harness::new((1920, 1200));
let first = rect(Color::RED).height(40).add(&mut h.rsc);
let mut span = Span::empty(Dir::DOWN);
span.push(first.add_strong(&mut h.rsc));
for i in 0..ROWS {
let row = (
rect(Color::BLUE.darker(i as f32 / (ROWS * 2) as f32)),
rect(Color::GREEN).pad(2),
wtext("row").size(16).pad(2),
)
.span(Dir::RIGHT)
.pad(4)
.height(40)
.add(&mut h.rsc);
span.push(row.add_strong(&mut h.rsc));
}
h.set_root(span);
for i in 0..FRAMES {
h.set_len(first, Axis::Y, 40.0 + (i % 2) as f32);
h.frame();
}
}
+252
View File
@@ -0,0 +1,252 @@
//! What a second frame draws again, and what it keeps.
use std::{cell::Cell, rc::Rc};
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
/// A leaf that counts its draws and reports whatever size it is given, so a
/// test can see what the retained path skipped.
struct Counted {
draws: Rc<Cell<usize>>,
size: Size,
dependence: OnResize,
}
impl Widget for Counted {
fn draw(&mut self, _: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
self.size
}
fn on_resize(&self, _: Axis) -> OnResize {
self.dependence
}
}
struct Counts(Rc<Cell<usize>>);
impl Counts {
fn get(&self) -> usize {
self.0.get()
}
}
fn counted(h: &mut Harness, size: Size, dependence: OnResize) -> (WeakWidget<Counted>, Counts) {
let draws = Rc::new(Cell::new(0));
let id = Counted {
draws: draws.clone(),
size,
dependence,
}
.add(&mut h.rsc);
(id, Counts(draws))
}
/// A fixed-width leaf beside one that takes the rest, so changing the first
/// hands the second a different box without the output changing.
fn pair(h: &mut Harness, rest: OnResize) -> (WeakWidget<Counted>, Counts, WidgetId) {
let (first, _) = counted(h, Size::from((100, 200)), OnResize::Translate);
let (second, draws) = counted(h, Size::REST, rest);
h.set_root((first, second).span(Dir::RIGHT));
(first, draws, second.id())
}
#[test]
fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
let mut h = Harness::new((400, 200));
let (first, draws, second) = pair(&mut h, OnResize::Scale);
let settled = draws.get();
assert_corners!(h, second, (100, 0), (400, 200));
h.rsc[first].size = Size::from((150, 200));
h.frame();
assert_eq!(
draws.get(),
settled,
"its box is a field to write, not a reason to draw"
);
assert_corners!(h, second, (150, 0), (400, 200));
}
#[test]
fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
let mut h = Harness::new((400, 200));
let (first, draws, second) = pair(&mut h, OnResize::Redraw);
let settled = draws.get();
h.rsc[first].size = Size::from((150, 200));
h.frame();
// Twice: once for the span to measure it, once for its real box. A child
// that can hint its length is spared the first, and a smaller number here
// means someone has made that cheaper rather than broken it.
assert_eq!(draws.get(), settled + 2);
assert_corners!(h, second, (150, 0), (400, 200));
}
#[test]
fn a_span_child_that_declares_its_length_is_drawn_once() {
let mut h = Harness::new((400, 200));
let (told, told_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
// The span takes one child's length from its hint and has to draw the
// other to find out, so only the second is drawn before it is placed.
let hinted = told.width(100).add(&mut h.rsc);
h.set_root((hinted, asked).span(Dir::RIGHT));
assert_eq!(told_draws.get(), 1);
assert_eq!(
asked_draws.get(),
2,
"drawn to be measured, then again to be placed"
);
}
#[test]
fn a_span_relays_out_when_a_child_it_measured_changes() {
let mut h = Harness::new((400, 200));
let (first, _, second) = pair(&mut h, OnResize::Translate);
h.rsc[first].size = Size::from((250, 200));
h.frame();
assert_corners!(h, first, (0, 0), (250, 200));
assert_corners!(h, second, (250, 0), (400, 200));
}
#[test]
fn a_placed_child_survives_the_next_frame() {
let mut h = Harness::new((400, 200));
// Both children declare a length, so the span places them from their hints
// rather than drawing them to find out.
let top = rect(Color::RED).height(80).add(&mut h.rsc);
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
h.set_root((top, bottom).span(Dir::DOWN));
h.rsc.widgets_mut().get_dyn_mut(top.id());
h.frame();
assert_corners!(h, top, (0, 0), (400, 80));
assert_corners!(h, bottom, (0, 80), (400, 200));
}
/// Lays its child out from the hint alone, never reading what it drew.
struct FromHint {
inner: StrongWidget,
}
impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let mut region = UiRegion::FULL;
region.y.end = region.y.start.offset(len.abs);
painter.widget_within(&self.inner, region);
Size::REST
}
}
#[test]
fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
let mut h = Harness::new((400, 200));
let inner = rect(Color::RED).height(80).add(&mut h.rsc);
let parent = FromHint {
inner: inner.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
h.set_root(parent);
assert_corners!(h, inner, (0, 0), (400, 80));
h.rsc[inner].y = Some(Len::abs(120));
h.frame();
assert_corners!(h, inner, (0, 0), (400, 120));
}
/// Reads the output's size, which nothing but its own draw can put right.
struct ReadsOutput {
draws: Rc<Cell<usize>>,
}
impl Widget for ReadsOutput {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
Size::abs(painter.output_size() / 4.0)
}
}
#[test]
fn a_resize_does_not_redraw_what_the_shader_can_move() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw);
h.set_root(leaf);
let settled = draws.get();
h.resize((800, 100));
assert!(h.needs_redraw());
h.frame();
assert_eq!(
draws.get(),
settled,
"its box is the same fraction of a different output"
);
assert_corners!(h, leaf, (0, 0), (800, 100));
}
#[test]
fn a_resize_redraws_what_read_the_output() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsOutput {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(leaf);
let settled = draws.get();
h.resize((800, 100));
h.frame();
assert_eq!(draws.get(), settled + 1);
}
#[test]
fn narrowing_the_output_reflows_text_and_relays_out_around_it() {
let mut h = Harness::new((600, 400));
let para = wtext(
"Wrapping shapes one source into as many lines as its container leaves \
room for, so the height of a paragraph is an answer rather than a setting.",
)
.size(20)
.wrap(true)
.add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((para, below).span(Dir::DOWN));
let top = h.region(&below).expect("drew nothing").top_left.y;
h.resize((300, 400));
h.frame();
let lower = h.region(&below).expect("drew nothing").top_left.y;
assert!(lower > top, "same words, half the width: {top} -> {lower}");
}
#[test]
fn a_change_two_levels_under_its_reader_still_reaches_it() {
let mut h = Harness::new((400, 400));
// Every wrapper up to the outer pad read the size below it, so the outer
// pad is what draws again -- and the span it hands the box to is the same
// size as before, which is what lets a draw reuse its way past the leaf.
let (leaf, _) = counted(&mut h, Size::abs((100, 100).into()), OnResize::Redraw);
let padded = leaf.pad(10).add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((padded, below).span(Dir::DOWN).pad(12));
assert_corners!(h, below, (12, 132), (388, 388));
h.rsc[leaf].size = Size::abs((100, 200).into());
h.frame();
assert_corners!(h, below, (12, 232), (388, 388));
}
-203
View File
@@ -1,203 +0,0 @@
//! What a resize frame costs and what it holds, on a tree the revision before
//! #16 also builds.
//!
//! Deliberately written in the API subset `43ce8c7` and this branch share, so
//! the same source can be dropped into an old worktree and measured there:
//! that is the only like-for-like comparison with the code the retained
//! layout replaced. The random tree cannot carry one, because the generator
//! itself changed with the work.
//!
//! ROWS=40 FRAMES=500 cargo test --release --test revision_cost \
//! -- --ignored --nocapture resize_cost
//! ROWS=2000 cargo test --release --test revision_cost \
//! -- --ignored --nocapture text_memory
//!
//! Wall time on this machine varies with CPU frequency; take the number from
//! `perf stat -e instructions:u` on the test binary directly.
use iris::harness::Harness;
use iris::prelude::*;
use std::time::Instant;
/// xorshift64, so one seed is one set of paragraphs on any machine.
struct Rng(u64);
impl Rng {
fn bits(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
(self.bits() % n as u64) as usize
}
}
const WORDS: [&str; 24] = [
"wrapping",
"shapes",
"one",
"source",
"into",
"as",
"many",
"lines",
"as",
"the",
"box",
"leaves",
"room",
"for",
"paragraph",
"height",
"answer",
"setting",
"container",
"width",
"before",
"knows",
"measured",
"again",
];
/// A run of its own words, so nothing here is fast for two texts being the
/// same string.
fn words(rng: &mut Rng, least: usize, most: usize) -> String {
let words = least + rng.below(most - least);
let mut out = String::new();
for _ in 0..words {
if !out.is_empty() {
out.push(' ');
}
out.push_str(WORDS[rng.below(WORDS.len())]);
}
out
}
const OUTPUT: (f32, f32) = (900.0, 1200.0);
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)
}
/// A row of a fixed-width rect beside a column of one wrapping and one
/// overflowing text: the shape that makes a container measure a child in a
/// box it will not keep.
fn build(h: &mut Harness, rows: usize) -> Vec<WidgetId> {
let mut rng = Rng(1);
let mut paragraphs = Vec::new();
let mut col = Span::empty(Dir::DOWN);
for _ in 0..rows {
let mut row = Span::empty(Dir::RIGHT);
row.push(
rect(Color::RED)
.width(LayoutLen::px(40.0))
.add_strong(&mut h.rsc),
);
let mut body = Span::empty(Dir::DOWN);
let para = wtext(words(&mut rng, 12, 52))
.size(16)
.wrap(true)
.add_strong(&mut h.rsc);
paragraphs.push(para.id());
body.push(para);
body.push(
// Short, or its unwrapped width decides the row and the
// paragraph beside it never wraps.
wtext(words(&mut rng, 2, 6))
.size(16)
.wrap(false)
.add_strong(&mut h.rsc),
);
row.push(body.add_strong(&mut h.rsc));
col.push(row.add_strong(&mut h.rsc));
}
let root = col.add(&mut h.rsc);
h.set_root(root);
paragraphs
}
#[test]
#[ignore = "measurement, not a check"]
fn resize_cost() {
let rows = env("ROWS", 40_usize);
let frames = env("FRAMES", 500_usize);
let mut h = Harness::new(OUTPUT);
let paragraphs = build(&mut h, rows);
// What it cost is only half the comparison: the old code is cheaper
// partly because it wraps at the container's whole width rather than the
// part left beside the rect, and draws past the edge of the output.
println!("output width {}", OUTPUT.0);
for (at, id) in paragraphs.iter().enumerate().take(3) {
println!("paragraph {at}: {:?}", h.region(id));
}
// Two widths in turn is the friendly case for anything that remembers an
// answer, so `SWEEP=1` never repeats one -- a drag rather than a toggle.
let sweep = env("SWEEP", 0_usize) != 0;
let mut elapsed = Vec::with_capacity(frames);
for frame in 0..frames {
let narrower = match sweep {
true => (frame % 256) as f32,
false => ((frame + 1) % 2) as f32 * 8.0,
};
h.resize((OUTPUT.0 - narrower, OUTPUT.1));
let start = Instant::now();
h.frame();
elapsed.push(start.elapsed().as_secs_f64() * 1000.0);
}
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
println!(
"resize: {frames} frames, min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
max {:.3} ms, total {:.1} ms",
elapsed[0],
elapsed[frames / 2],
elapsed[frames * 99 / 100],
elapsed[frames - 1],
elapsed.iter().sum::<f64>()
);
}
fn kb(field: &str) -> u64 {
std::fs::read_to_string("/proc/self/status")
.unwrap()
.lines()
.find(|line| line.starts_with(field))
.and_then(|line| line.split_whitespace().nth(1)?.parse().ok())
.unwrap()
}
fn report(label: &str) {
println!(
"{label:24} rss {:>7} kB peak {:>7} kB",
kb("VmRSS:"),
kb("VmHWM:")
);
}
/// Run this one on its own: the figures are the whole process's.
#[test]
#[ignore = "measurement, not a check"]
fn text_memory() {
let rows = env("ROWS", 2000_usize);
report("before");
let mut h = Harness::new(OUTPUT);
let paragraphs = build(&mut h, rows);
report("after cold frame");
for frame in 0..40 {
h.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
h.frame();
}
report("after 40 resizes");
// Settled: the output holds still and one leaf repaints per frame.
for _ in 0..10 {
let _ = h.rsc.widgets_mut().get_dyn_mut(paragraphs[0]);
h.frame();
}
report("after settling");
}
-467
View File
@@ -1,467 +0,0 @@
//! The scenarios both fuzzers run, over the tree a [`Plan`] describes.
//!
//! One implementation rather than two. The oracle grew its trees from a seed
//! and the shrinker grew its own, with every scenario written out on each
//! side, so a failure the oracle found could not be handed to the shrinker:
//! there was no tree to pass it, only a seed, and a seed cannot be made
//! smaller. Both take a plan now, so whatever finds a counterexample can also
//! reduce it.
//!
//! Each target compiles this for itself, so what only one of them calls is
//! dead code in the other.
#![allow(dead_code)]
use iris::harness::Harness;
use iris::prelude::*;
use iris::random::{Aligns, Edits, Kind, Lens, Plan, Rng, SpanEdit, Tree, build};
use std::collections::HashMap;
/// A seed per thread but one, since a seed grows, lays out and drops its tree
/// alone. A failing seed still shrinks and panics on its own thread.
pub fn over_seeds(seeds: Vec<u64>, run: impl Fn(u64) + Sync) {
let threads =
std::thread::available_parallelism().map_or(1, |n| n.get().saturating_sub(1).max(1));
let chunk = seeds.len().div_ceil(threads).max(1);
std::thread::scope(|scope| {
for part in seeds.chunks(chunk) {
let run = &run;
scope.spawn(move || part.iter().for_each(|&seed| run(seed)));
}
});
}
pub fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(fallback)
}
/// The window a tree is grown in, and the one a resize takes it to.
const OUTER: (f32, f32) = (1920.0, 1200.0);
const INNER: (f32, f32) = (640.0, 900.0);
const STILL: (f32, f32) = (900.0, 1200.0);
/// The same box, to two steps of the grid between the two ways of reaching
/// it. A move, a repaint, a row of shares and every length in pixels land on
/// the same number. What needs the slack is a position: a box centred in a
/// fraction of its parent against the same box centred in its own pixels,
/// and a box re-expressed as a fraction of a parent that changed length.
/// A step is a thousandth of a pixel, where this was a twentieth of one
/// before any of it was on a grid.
///
/// **One step is not enough**, tried 2026-09-17 once a length in pixels
/// stopped being composed: it passes the 100-seed oracle and fails the
/// 400-seed shrinker on `resize-size`, seeds 384 and 162, by 0.002 px. So
/// what is left here is the resize path's own rounding rather than a length
/// reached two ways.
const AGREE_STEPS: i32 = 2;
/// A way of changing what a span holds. Each is a shape worth its own case:
/// taking a child out of the middle is not the same as emptying a span, and
/// adding one is not the same as adding three.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Shuffle {
/// Every other child, so what is left is interleaved with what went.
EveryOther,
/// Everything but the first, which is the last step before empty.
AllButFirst,
/// Three more on the end at once.
AddThree,
/// The first out and three more on, so the count moves both ways.
SwapForThree,
/// One out of the middle and one on the end.
TradeOne,
}
impl Shuffle {
fn of(self, grown: usize) -> SpanEdit {
let all = |step: usize, from: usize| (from..grown).step_by(step).collect();
match self {
Self::EveryOther => SpanEdit {
detach: all(2, 0),
attach: 0,
},
Self::AllButFirst => SpanEdit {
detach: all(1, 1),
attach: 0,
},
Self::AddThree => SpanEdit {
detach: Vec::new(),
attach: 3,
},
Self::SwapForThree => SpanEdit {
detach: vec![0],
attach: 3,
},
Self::TradeOne => SpanEdit {
detach: vec![grown / 2],
attach: 1,
},
}
}
}
/// What a warm tree is put through before it is compared with a cold one
/// grown the way it was left.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Case {
/// Nothing changes, so no box may either. What this exercises is the
/// order a frame settles a dirty set in.
Repaint,
/// Every fifth widget rather than all of them: marking all of them
/// redraws the whole tree, which is a cold start reached the long way,
/// where the mixed case leaves a redrawn subtree beside a retained one.
RepaintSome,
Resize,
ResizeRepaint,
/// A resize and then a size change, so a retained answer is asked to
/// survive two different kinds of invalidation in a row.
ResizeSize,
/// A few declared sizes.
Size,
/// Every declared size at once, so every reader of a size has a changed
/// descendant in the same frame and the whole dirty set settles together.
EverySize,
Align,
/// Giving a widget a movable region of its own, or taking it away, is a
/// structural change: every primitive under it changes which chain
/// resolves it.
RegionNode,
/// The same children in a different order, which moves every one of them
/// without changing what any of them is.
Reorder,
Shuffle(Shuffle),
}
pub const ALL: [Case; 15] = [
Case::Repaint,
Case::RepaintSome,
Case::Resize,
Case::ResizeRepaint,
Case::ResizeSize,
Case::Size,
Case::EverySize,
Case::Align,
Case::RegionNode,
Case::Reorder,
Case::Shuffle(Shuffle::EveryOther),
Case::Shuffle(Shuffle::AllButFirst),
Case::Shuffle(Shuffle::AddThree),
Case::Shuffle(Shuffle::SwapForThree),
Case::Shuffle(Shuffle::TradeOne),
];
impl Case {
/// The name `CASE` selects it by, and the one a failure prints.
pub fn name(self) -> &'static str {
match self {
Self::Repaint => "repaint",
Self::RepaintSome => "repaint-some",
Self::Resize => "resize",
Self::ResizeRepaint => "resize-repaint",
Self::ResizeSize => "resize-size",
Self::Size => "size",
Self::EverySize => "every-size",
Self::Align => "align",
Self::RegionNode => "region-node",
Self::Reorder => "reorder",
Self::Shuffle(Shuffle::EveryOther) => "shuffle-every-other",
Self::Shuffle(Shuffle::AllButFirst) => "shuffle-all-but-first",
Self::Shuffle(Shuffle::AddThree) => "shuffle-add-three",
Self::Shuffle(Shuffle::SwapForThree) => "shuffle-swap-for-three",
Self::Shuffle(Shuffle::TradeOne) => "shuffle-trade-one",
}
}
pub fn named(name: &str) -> Option<Self> {
ALL.into_iter().find(|case| case.name() == name)
}
/// Grown in the first, compared in the second.
fn window(self) -> ((f32, f32), (f32, f32)) {
match self {
Self::Resize | Self::ResizeRepaint | Self::ResizeSize => (OUTER, INNER),
_ => (STILL, STILL),
}
}
}
fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
for &id in tree.ids.iter().step_by(step) {
warm.rsc.widgets_mut().get_dyn_mut(id);
}
}
fn a_len(rng: &mut Rng) -> Option<LayoutLen> {
Some(LayoutLen::px(20.0 + rng.below(180) as f32))
}
fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens {
let lens = [a_len(rng), a_len(rng)];
warm.rsc
.widgets_mut()
.set_size_rules(tree.sized[idx], lens[0], lens[1]);
lens
}
fn realign_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns {
let side = |rng: &mut Rng| match rng.below(4) {
0 => None,
1 => Some(AxisAlign::NEG),
2 => Some(AxisAlign::CENTER),
_ => Some(AxisAlign::POS),
};
let align = [side(rng), side(rng)];
let id = tree.aligned[idx];
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
warm.rsc
.widgets_mut()
.set_alignment(id, axis, align.unwrap_or_default());
}
align
}
/// Every span's children in a different order, said both to the warm tree and
/// to the plan the cold one is grown from.
fn reorder(warm: &mut Harness, tree: &Tree, plan: &Plan) -> Plan {
for span in &tree.spans {
let children = &mut warm.rsc[span.id].children;
if !children.is_empty() {
children.rotate_left(1);
}
}
let mut out = plan.clone();
out.walk_mut(&mut |node| {
if let Kind::Span { order, .. } = &mut node.kind
&& !order.is_empty()
{
order.rotate_left(1);
}
});
out
}
/// Applies `shuffle` to every third span. What it takes out is given back to
/// the span's spares: the last share of a widget must outlive the comparison,
/// or its id is handed to something else and the two trees stop lining up.
fn reshuffle(warm: &mut Harness, tree: &mut Tree, shuffle: Shuffle) -> HashMap<usize, SpanEdit> {
let mut edits = HashMap::new();
for (idx, span) in tree.spans.iter_mut().enumerate().step_by(3) {
let edit = shuffle.of(span.grown);
let mut take = edit.detach.clone();
take.sort_unstable();
let children = &mut warm.rsc[span.id].children;
// Highest first, so an index means the same child however many of its
// neighbours are going too.
for j in take.into_iter().rev() {
if j < children.len() {
span.spares.push(children.remove(j));
}
}
let attach = edit.attach.min(span.spares.len());
let moved: Vec<_> = span.spares.drain(..attach).collect();
warm.rsc[span.id].children.extend(moved);
edits.insert(idx, edit);
}
edits
}
/// Changes the warm tree and answers with the plan a cold tree grown that way
/// comes from. Each arm settles its own frame, so a case that changes nothing
/// does not get a second one that could settle what the first left.
fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mut Rng) -> Plan {
let some_sizes = |warm: &mut Harness, tree: &Tree, rng: &mut Rng| {
let mut sizes = HashMap::new();
for _ in 0..4 {
if tree.sized.is_empty() {
break;
}
let idx = rng.below(tree.sized.len());
sizes.insert(idx, resize_one(warm, tree, idx, rng));
}
sizes
};
let edits = match case {
Case::Resize => return plan.clone(),
Case::Repaint | Case::ResizeRepaint => {
mark(warm, tree, 1);
warm.frame();
return plan.clone();
}
Case::RepaintSome => {
mark(warm, tree, 5);
warm.frame();
return plan.clone();
}
Case::Reorder => {
let out = reorder(warm, tree, plan);
warm.frame();
return out;
}
Case::Size | Case::ResizeSize => Edits {
sizes: some_sizes(warm, tree, rng),
..Default::default()
},
Case::EverySize => Edits {
sizes: (0..tree.sized.len())
.map(|idx| (idx, resize_one(warm, tree, idx, rng)))
.collect(),
..Default::default()
},
Case::Align => Edits {
aligns: (0..tree.aligned.len())
.step_by(3)
.map(|idx| (idx, realign_one(warm, tree, idx, rng)))
.collect(),
..Default::default()
},
Case::RegionNode => {
let mut nodes = HashMap::new();
for idx in (0..tree.nodes.len()).step_by(2) {
let id = tree.nodes[idx];
let take = !warm.rsc.widgets().is_region_node(id);
warm.rsc.widgets_mut().set_region_node(id, take);
nodes.insert(idx, take);
}
Edits {
nodes,
..Default::default()
}
}
Case::Shuffle(shuffle) => Edits {
spans: reshuffle(warm, tree, shuffle),
..Default::default()
},
};
warm.frame();
plan.edited(&edits)
}
/// What a widget was configured with, so a tree a fuzzer found can be written
/// out by hand. A failure is a lead; the fast test that replaces it has to be
/// buildable from what the failure printed.
fn describe(id: WidgetId, h: &Harness) -> String {
let rules = h.rsc.widgets().size_rules(id);
let rule = |r: SizeRule| match r.exact() {
Some(len) => format!("{len}"),
None => "-".into(),
};
let align = h.rsc.widgets().alignment(id);
let side = |a: AxisAlign| {
if a == AxisAlign::NEG {
"neg".into()
} else if a == AxisAlign::CENTER {
"mid".into()
} else if a == AxisAlign::POS {
"pos".into()
} else {
format!("{:.2}", a.rel())
}
};
// A rule and an alignment are properties of whatever carries them, so
// they print with that widget rather than as widgets of their own.
let mut out = describe_widget(id, h);
if (rules.x, rules.y) != (SizeRule::Free, SizeRule::Free) {
out += &format!("[x:{},y:{}]", rule(rules.x), rule(rules.y));
}
if align != RegionAlign::default() {
out += &format!("@{},{}", side(align.x), side(align.y));
}
out
}
fn describe_widget(id: WidgetId, h: &Harness) -> String {
let label = h.rsc.widgets().label(id).to_string();
let Some(widget) = h.rsc.widgets().get_dyn(id) else {
return label;
};
let any: &dyn std::any::Any = widget;
if let Some(w) = any.downcast_ref::<Span>() {
let sign = if w.dir.sign == Sign::Neg { "-" } else { "+" };
return format!(
"Span{{dir:{:?}{sign},gap:{},n:{}}}",
w.dir.axis,
w.gap,
w.children.len()
);
}
if let Some(w) = any.downcast_ref::<Pad>() {
let p = &w.padding;
return format!(
"Pad{{l:{},r:{},t:{},b:{}}}",
p.left, p.right, p.top, p.bottom
);
}
if let Some(w) = any.downcast_ref::<Stack>() {
return format!("Stack{{n:{}}}", w.children.len());
}
label
}
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
match (got, want) {
(Some(got), Some(want)) => {
let same = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS);
same(got.top_left.x, want.top_left.x)
&& same(got.top_left.y, want.top_left.y)
&& same(got.bot_right.x, want.bot_right.x)
&& same(got.bot_right.y, want.bot_right.y)
}
(None, None) => true,
_ => false,
}
}
/// 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,
/// so one plan under one case is one comparison however it was reached.
pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
let (start, end) = case.window();
let mut warm = Harness::new(start);
let (root, mut tree) = build(&mut warm.rsc, plan);
warm.state.root = Some(root);
// The frame that makes it warm: without it nothing is retained and the
// comparison is two cold starts agreeing with each other.
warm.frame();
if start != end {
warm.resize(end);
warm.frame();
}
let cold_plan = change(case, &mut warm, &mut tree, plan, &mut Rng::new(seed));
let mut cold = Harness::new(end);
let (root, cold_tree) = build(&mut cold.rsc, &cold_plan);
cold.state.root = Some(root);
cold.frame();
let mut drawn = 0;
for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
drawn += got.is_some() as usize;
if same_region(got, want) {
continue;
}
// Where two trees disagree is rarely where the cause is, so the
// ancestry comes with it, marking the widgets that own a region.
let mut chain = Vec::new();
let mut at = Some(w);
while let Some(id) = at {
let active = &warm.render.active[&id];
let node = match active.move_idx == active.parent_move {
true => "",
false => "*",
};
chain.push(format!("{}{node}", describe(id, &warm)));
at = active.parent;
}
return Some(format!(
"widget {i}\n warm {got:?}\n cold {want:?}\n {}",
chain.join(" < ")
));
}
match drawn {
0 => Some("nothing was drawn".into()),
_ => None,
}
}
+26
View File
@@ -0,0 +1,26 @@
//! Scrolling moves content and stops at its ends.
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
#[test]
fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
let mut h = Harness::new((400, 200));
// Twice the window's height, so there is 200 to scroll.
let top = rect(Color::RED).height(200).add(&mut h.rsc);
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
h.set_root((top, bottom).span(Dir::DOWN).scrollable());
h.move_to((200, 100));
// `Scroll` starts snapped to the end.
assert_corners!(h, top, (0, -200), (400, 0));
// The handler scales a wheel line by 50.
h.scroll((0, 1));
h.frame();
assert_corners!(h, top, (0, -150), (400, 50));
h.scroll((0, 10));
h.frame();
assert_corners!(h, top, (0, 0), (400, 200));
}
-99
View File
@@ -1,99 +0,0 @@
//! A fuzzer that reduces its own counterexample.
//!
//! A seed is not a lead anybody can read: the tree is hundreds of widgets,
//! and reconstructing the part that matters by hand has failed every time it
//! has been tried. This grows the trees `iris::random` describes, takes them
//! apart, and prints the smallest one that still fails as something to write
//! a fast test from.
//!
//! cargo test --release --test shrink -- --ignored --nocapture
//!
//! `SHRINK_SEEDS` how many trees to try, `SHRINK_DEPTH` how deep to grow
//! them, `SHRINK_CASE` which scenario or `all` for every one. `SHRINK_SEED`
//! takes a single seed, which is how a failure `generated` printed is handed
//! straight here: the two run the same cases over the same trees, so a seed
//! that fails there fails here and is reduced.
//!
//! It is a fuzzer: run it once the ordinary tests pass, and turn what it
//! finds into a test of its own rather than leaving a seed as the record.
#[path = "scenario/mod.rs"]
mod scenario;
use iris::random::{Edits, Plan, plan};
use scenario::{ALL, Case, diverges, env, over_seeds};
/// Takes the first simplification that still fails, until none does. The
/// simplifications come biggest first, so this walks down rather than
/// nibbling: a six-hundred-widget tree reaches single figures in a few
/// hundred builds.
fn shrink(mut node: Plan, case: Case, seed: u64) -> Plan {
loop {
let Some(next) = node
.smaller()
.into_iter()
.find(|small| diverges(small, case, seed).is_some())
else {
return node;
};
node = next;
}
}
fn cases() -> Vec<Case> {
match env("SHRINK_CASE", String::from("all")).as_str() {
"all" => ALL.to_vec(),
name => match Case::named(name) {
Some(case) => vec![case],
None => panic!(
"unknown SHRINK_CASE {name:?}; one of all, {}",
ALL.map(Case::name).join(", ")
),
},
}
}
#[test]
#[ignore = "a fuzzer; run it once the ordinary tests pass"]
fn no_grown_tree_lays_out_differently_warm_than_cold() {
let depth: usize = env("SHRINK_DEPTH", 5);
let cases = cases();
let seeds: Vec<u64> = match std::env::var("SHRINK_SEED")
.ok()
.and_then(|v| v.parse().ok())
{
Some(seed) => vec![seed],
None => (1..=env("SHRINK_SEEDS", 400_u64)).collect(),
};
let count = seeds.len();
over_seeds(seeds, |seed| {
let grown = plan(seed, depth, &Edits::default());
for &case in &cases {
let Some(how) = diverges(&grown, case, seed) else {
continue;
};
let small = shrink(grown.clone(), case, seed);
println!(
"seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
case.name(),
grown.size(),
small.size()
);
panic!(
"seed {seed} lays out differently warm than cold after {}",
case.name()
);
}
});
let sizes: Vec<usize> = (1..=count as u64)
.map(|seed| plan(seed, depth, &Edits::default()).size())
.collect();
println!(
"{count} trees at depth {depth} agree over {} case(s): {} widgets total, largest {}",
cases.len(),
sizes.iter().sum::<usize>(),
sizes.iter().max().copied().unwrap_or(0)
);
}
-34
View File
@@ -1,34 +0,0 @@
//! Every ordinary correctness test, as modules of one target.
//!
//! One binary rather than a dozen: each `tests/*.rs` links the whole
//! dependency graph again, which is most of what `cargo test` spends its time
//! on here. Libtest still runs the cases in parallel, and a filter still
//! selects them -- `cargo test --test suite layout::` for one module.
//!
//! The rigs stay their own targets: `shrink` and `generated` are fuzzers run
//! on their own, and the `*_cost` and `*_diagnostics` ones are measurements.
#[path = "cases/determinism.rs"]
mod determinism;
#[path = "cases/drift.rs"]
mod drift;
#[path = "cases/idempotence.rs"]
mod idempotence;
#[path = "cases/layout.rs"]
mod layout;
#[path = "cases/plan.rs"]
mod plan;
#[path = "cases/pointer.rs"]
mod pointer;
#[path = "cases/pointer_routing.rs"]
mod pointer_routing;
#[path = "cases/retained.rs"]
mod retained;
#[path = "cases/scroll.rs"]
mod scroll;
#[path = "cases/tasks.rs"]
mod tasks;
#[path = "cases/text_edit.rs"]
mod text_edit;
#[path = "cases/unsettled.rs"]
mod unsettled;
File renamed without changes.
File renamed without changes.
-140
View File
@@ -1,140 +0,0 @@
//! Traces the six-widget tree in `unsettled.rs`, to see what box its text is
//! actually drawn in on a first frame against a settled one.
#![cfg(feature = "layout-diagnostics")]
use iris::core::layout_diagnostics::{self as diag, TraceEvent};
use iris::harness::Harness;
use iris::prelude::*;
fn plant(h: &mut Harness) -> Vec<WidgetId> {
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
let sized = wrapped.width(76).add(&mut h.rsc);
let aligned = sized;
h.rsc
.widgets_mut()
.set_alignment(sized, Axis::X, AxisAlign::POS);
h.rsc
.widgets_mut()
.set_alignment(sized, Axis::Y, AxisAlign::POS);
let stack = Stack {
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
vec![
plain.id(),
wrapped.id(),
sized.id(),
aligned.id(),
stack.id(),
root.id(),
]
}
fn dump(label: &str, report: &diag::Report, text: WidgetId) {
println!("--- {label} ---");
for event in report.traces() {
match event {
TraceEvent::DrawRequest {
id,
region,
pixel_size,
..
} if *id == text => {
println!(
" draw in {:.2}x{:.2} region {region:?}",
pixel_size.x, pixel_size.y
)
}
TraceEvent::SizeReported { id, size } if *id == text => {
println!(" reported {size}")
}
TraceEvent::SizeRead { id, reader, size } if *id == text => {
println!(" size read by {reader:?}: {size}")
}
TraceEvent::RegionNode { id, parent, region } if *id == text => {
println!(" region node under {parent:?} at {region:?}")
}
TraceEvent::Reuse { id, outcome } if *id == text => println!(" reuse: {outcome:?}"),
_ => {}
}
}
}
#[test]
#[ignore = "a diagnostic, not a check"]
fn what_box_the_text_is_drawn_in() {
diag::clear_traced_widgets();
let _ = diag::take();
let mut h = Harness::new((640, 900));
let ids = plant(&mut h);
let text = ids[1];
diag::trace_widget(text);
let _ = diag::take();
h.frame();
dump("first frame", &diag::take(), text);
for _ in 0..2 {
for &id in &ids {
h.rsc.widgets_mut().get_dyn_mut(id);
}
let _ = diag::take();
h.frame();
dump("repaint", &diag::take(), text);
}
diag::clear_traced_widgets();
}
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
let words = "Wrapping shapes one source into as many lines as the box leaves";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let aligned = text;
h.rsc
.widgets_mut()
.set_alignment(text, Axis::X, AxisAlign::NEG);
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
let sized = inner.sized((189, 176)).add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc);
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
vec![
text.id(),
aligned.id(),
inner.id(),
sized.id(),
filler.id(),
root.id(),
]
}
#[test]
#[ignore = "a diagnostic, not a check"]
fn what_box_the_fixed_text_is_drawn_in() {
diag::clear_traced_widgets();
let _ = diag::take();
let mut h = Harness::new((1920, 1200));
let ids = plant_fixed(&mut h);
let text = ids[0];
diag::trace_widget(text);
let _ = diag::take();
h.frame();
dump("first frame at 1920", &diag::take(), text);
h.resize((640, 900));
h.frame();
dump("after resize to 640", &diag::take(), text);
let mut cold = Harness::new((640, 900));
let cids = plant_fixed(&mut cold);
diag::clear_traced_widgets();
diag::trace_widget(cids[0]);
let _ = diag::take();
cold.frame();
dump("cold at 640", &diag::take(), cids[0]);
diag::clear_traced_widgets();
}