Compare commits
105
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
429a4f1330 | ||
|
|
97fca76108 | ||
|
|
cbccfb600a | ||
|
|
f48e04ed36 | ||
|
|
05e6ced31d | ||
|
|
ea1f836bf9 | ||
|
|
2ac0843cb2 | ||
|
|
4cb6f6882a | ||
|
|
0e838e9dd1 | ||
|
|
8780b40bb7 | ||
|
|
de1eb7e406 | ||
|
|
76aaf06c0b | ||
|
|
0d0326769c | ||
|
|
2dba90bd0f | ||
|
|
b295c8b97a | ||
|
|
c2b8bf83de | ||
|
|
77ed7a24c0 | ||
|
|
f8aa0c5cdf | ||
|
|
b7b8d09e40 | ||
|
|
1096c3167a | ||
|
|
445287c95c | ||
|
|
750217631d | ||
|
|
9b4cc329ce | ||
|
|
02048eab77 | ||
|
|
d8d51221ee | ||
|
|
781199a7c9 | ||
|
|
69ba91588a | ||
|
|
8088a1fa59 | ||
|
|
713e3e747b | ||
|
|
aea0387567 | ||
|
|
1ebd4d3a05 | ||
|
|
7e2b4cd9db | ||
|
|
3da1c71870 | ||
|
|
6c84b6f2cb | ||
|
|
8d2b7a512b | ||
|
|
40b89c1f79 | ||
|
|
4d42f1c8ca | ||
|
|
2807a925af | ||
|
|
55df32a33c | ||
|
|
58ce74dd7d | ||
|
|
c55be21761 | ||
|
|
beb138632a | ||
|
|
aeb60e50f5 | ||
|
|
a904cf4f36 | ||
|
|
5642f2010a | ||
|
|
84dad211f5 | ||
|
|
add6774980 | ||
|
|
cadfba05dd | ||
|
|
38b3a81053 | ||
|
|
f6242aa33c | ||
|
|
a888717ee9 | ||
|
|
e8a5792dcb | ||
|
|
a30971e4c5 | ||
|
|
adbedaf264 | ||
|
|
23523eea29 | ||
|
|
1512d8418b | ||
|
|
0ef87ebfcf | ||
|
|
3091fb86df | ||
|
|
4328eac756 | ||
|
|
b842e4f474 | ||
|
|
49cec82c1b | ||
|
|
0954770ceb | ||
|
|
1956be3f3d | ||
|
|
34cafb6edc | ||
|
|
3bf22935ce | ||
|
|
e6ba570d07 | ||
|
|
0e107f0e89 | ||
|
|
f860f716e6 | ||
|
|
c44bd198ee | ||
|
|
a7307d95fd | ||
|
|
7601aa2a5d | ||
|
|
c330ecec2b | ||
|
|
39f7b08c6c | ||
|
|
2ed5503717 | ||
|
|
efb416bbc3 | ||
|
|
5fcace1bfa | ||
|
|
e44dea34b4 | ||
|
|
a0693acc56 | ||
|
|
25e456e0b5 | ||
|
|
53b00c68e9 | ||
|
|
a92c6acdbf | ||
|
|
c8beca5753 | ||
|
|
4bd8607968 | ||
|
|
ffd79f32d3 | ||
|
|
0e0d4af326 | ||
|
|
ea6dbae0dc | ||
|
|
32542d0c0b | ||
|
|
5b7800264d | ||
|
|
5f16617511 | ||
|
|
45a717695b | ||
|
|
d21a21524f | ||
|
|
e166e005dc | ||
|
|
38eba543f6 | ||
|
|
2bc6bdfc77 | ||
|
|
d8ae9c3bdd | ||
|
|
08c9d5aa32 | ||
|
|
60367d806e | ||
|
|
aea878d141 | ||
|
|
98d4e98a29 | ||
|
|
4febabfd2e | ||
|
|
394d5149a5 | ||
|
|
4cbb242a5d | ||
|
|
d75a1e2129 | ||
|
|
1940e85c70 | ||
|
|
cb1bba4682 |
No files matched your search
@@ -25,6 +25,12 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"]
|
||||
[workspace]
|
||||
members = ["core", "macro", "rig-input"]
|
||||
|
||||
# Full debug info was the bulk of what the linker wrote here and almost none of
|
||||
# what anything read. `dev` keeps line tables and scopes, which is what stepping
|
||||
# through an example wants; the tests keep the line tables alone, which is what
|
||||
# a backtrace reads. Measured when the tests became one target: relinking them
|
||||
# went from 9.8 s to 7.7 s with these, and target/ from 45 GB to 13 GB with the
|
||||
# two changes together.
|
||||
[profile.dev]
|
||||
debug = 1
|
||||
|
||||
|
||||
@@ -14,3 +14,27 @@ WidgetRef<W> or smth instead of Id
|
||||
vecs for each widget type?
|
||||
|
||||
POTENTIAL BUG: closures that store IDs will not decrement the id!!! need to not increment id if moved into closure somehow??? wait no, need to decrement ID every time an event fn is added...... only if the id is used in it..??
|
||||
|
||||
transforms on a move entry (scale + rotation)
|
||||
an entry is a translation today; composing through one scales the rel
|
||||
part and passes px through untouched, so fixed-size content and glyphs
|
||||
do not follow a shortened entry
|
||||
want a real transform per entry, resolved in resolve_move the way the
|
||||
translation already is, so a whole subtree transforms with one buffer
|
||||
write and no redraw
|
||||
wanted for compose-style stretch at the end of a scroll area, and for
|
||||
rotation generally
|
||||
|
||||
a prepare stage on Event, so Data has no placeholder field
|
||||
run_sensors builds one CursorData per widget and has to put something in
|
||||
`sense` before anything knows which sense matched, so it writes
|
||||
CursorSense::Hovering and says in place that it means nothing;
|
||||
should_run then clones the whole thing to overwrite that one field
|
||||
the state is representable only because the type lets the caller say it:
|
||||
what the caller supplies and what matching adds are two different things
|
||||
wearing one struct
|
||||
the awkward part is doing it without the generics getting annoying --
|
||||
Data<'a> is already a GAT with a default, and splitting it in two adds
|
||||
another associated type to every Event impl for the sake of one field
|
||||
(Bryan, 2026-09-20; low priority, he wants a good answer rather than a
|
||||
quick one)
|
||||
+90
-84
@@ -1,7 +1,7 @@
|
||||
use crate::{UiNum, util::Vec2};
|
||||
use std::{
|
||||
fmt::{Debug, Display, Formatter},
|
||||
ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign},
|
||||
ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign},
|
||||
};
|
||||
|
||||
/// A number held as a whole count of `1 / 2^SHIFT`.
|
||||
@@ -10,23 +10,30 @@ use std::{
|
||||
/// 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 and only a
|
||||
/// multiply or a conversion rounds, back onto the same steps, so two routes
|
||||
/// that come within half a step land on one number and everything downstream
|
||||
/// compares for equality instead of for nearness.
|
||||
/// 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, to a sixty-fourth. Finer than anything
|
||||
/// a display can show, and exact in `f32` up to 262,144 px, which is what lets
|
||||
/// the same number reach the GPU.
|
||||
/// 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`]
|
||||
@@ -56,8 +63,8 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
/// 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, since arithmetic saturates
|
||||
/// here rather than wrapping past it.
|
||||
/// 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);
|
||||
|
||||
@@ -77,12 +84,13 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
}
|
||||
|
||||
pub const fn from_int(v: i32) -> Self {
|
||||
Self(v.saturating_mul(Self::one().0))
|
||||
Self(v.wrapping_mul(Self::one().0))
|
||||
}
|
||||
|
||||
/// Rounds to the nearest step, and saturates rather than wrapping. A NaN
|
||||
/// has no nearest step and becomes zero, which is a caller's mistake
|
||||
/// rather than a value worth carrying.
|
||||
/// 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.
|
||||
@@ -101,6 +109,21 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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);
|
||||
// The top of the grid has no step above it, and stepping past it
|
||||
// wraps to the bottom. A value out there is a caller's mistake, and
|
||||
// the clamp `from_f32` already made is the answer to it.
|
||||
match nearest.to_f32() < v && nearest.0 != Self::MAX.0 {
|
||||
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 {
|
||||
@@ -111,36 +134,36 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
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 => narrow((self.0 as i64) << (TO - SHIFT)),
|
||||
false => narrow(shift_round(self.0 as i64, SHIFT - TO)),
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn add(self, rhs: Self) -> Self {
|
||||
Self(self.0.saturating_add(rhs.0))
|
||||
Self(self.0.wrapping_add(rhs.0))
|
||||
}
|
||||
|
||||
pub const fn sub(self, rhs: Self) -> Self {
|
||||
Self(self.0.saturating_sub(rhs.0))
|
||||
Self(self.0.wrapping_sub(rhs.0))
|
||||
}
|
||||
|
||||
pub const fn neg(self) -> Self {
|
||||
Self(self.0.saturating_neg())
|
||||
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(narrow(shift_round(self.0 as i64 * by.0 as i64, BY)))
|
||||
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(narrow(self.0 as i64 * by as i64))
|
||||
Self(self.0.wrapping_mul(by))
|
||||
}
|
||||
|
||||
/// Divided into a whole number of parts, rounded to the nearest step.
|
||||
@@ -149,21 +172,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
if by == 0 {
|
||||
return Self::ZERO;
|
||||
}
|
||||
Self(narrow(div_round(self.0 as i64, by as i64)))
|
||||
}
|
||||
|
||||
/// 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 saturates 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(narrow(div_round((self.0 as i64) << BY, by.0 as i64)))
|
||||
Self(div_round(self.0 as i64, by as i64) as i32)
|
||||
}
|
||||
|
||||
/// `num / den` on *this* grid rather than on theirs, for weights coarser
|
||||
@@ -173,7 +182,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
if den.0 == 0 {
|
||||
return Self::ZERO;
|
||||
}
|
||||
Self(narrow(div_round((num.0 as i64) << SHIFT, den.0 as i64)))
|
||||
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
|
||||
@@ -197,7 +206,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
}
|
||||
|
||||
pub const fn abs(self) -> Self {
|
||||
Self(self.0.saturating_abs())
|
||||
Self(self.0.wrapping_abs())
|
||||
}
|
||||
|
||||
pub const fn clamp(self, lo: Self, hi: Self) -> Self {
|
||||
@@ -209,21 +218,11 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
/// 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.saturating_add(1))
|
||||
Self(self.0.wrapping_add(1))
|
||||
}
|
||||
|
||||
pub const fn next_down(self) -> Self {
|
||||
Self(self.0.saturating_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,
|
||||
Self(self.0.wrapping_sub(1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +249,8 @@ pub(crate) const fn div_toward(num: i64, den: i64, up: bool) -> 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;
|
||||
@@ -304,14 +305,6 @@ const impl<const SHIFT: u32, const BY: u32> Mul<Fixed<BY>> for Fixed<SHIFT> {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -353,6 +346,12 @@ impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
||||
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())
|
||||
}
|
||||
@@ -444,38 +443,45 @@ mod tests {
|
||||
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 halves_round_away_from_zero_either_side() {
|
||||
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(2));
|
||||
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));
|
||||
}
|
||||
|
||||
/// 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 dividing_by_a_fraction_undoes_multiplying_by_it() {
|
||||
let third = Rel::ONE / Rel::from_int(3);
|
||||
let len = Px::from_int(300);
|
||||
assert_eq!(len * third / third, len);
|
||||
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
|
||||
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 arithmetic_saturates_rather_than_wrapping() {
|
||||
assert_eq!(Px::MAX + Px::ONE, Px::MAX);
|
||||
assert_eq!(Px::MIN - Px::ONE, Px::MIN);
|
||||
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);
|
||||
assert_eq!(Px::from_int(i32::MAX), Px::MAX);
|
||||
}
|
||||
|
||||
#[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);
|
||||
// The ceiling is the other way in from a float, and there is no step
|
||||
// above the top of the grid for it to take.
|
||||
assert_eq!(Px::ceil_from_f32(1e12), Px::MAX);
|
||||
assert_eq!(Px::ceil_from_f32(-1e12), Px::MIN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+111
-92
@@ -15,7 +15,7 @@
|
||||
//! 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 crate::{Axis, LayoutHolds, LayoutLen, PxVec2, Size, UiRegion, UiVec2, WidgetId};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
@@ -23,98 +23,71 @@ use std::{
|
||||
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,
|
||||
/// Declares a counter or timer kind beside the name its report prints. Two
|
||||
/// lists in the same order was one list too many: a variant inserted without
|
||||
/// its label moving with it renames every total after it, and nothing says
|
||||
/// so.
|
||||
macro_rules! labelled {
|
||||
($(#[$meta:meta])* $vis:vis enum $Name:ident { $($variant:ident = $label:literal,)* }) => {
|
||||
$(#[$meta])*
|
||||
#[derive(Clone, Copy)]
|
||||
$vis enum $Name { $($variant,)* }
|
||||
|
||||
impl $Name {
|
||||
const COUNT: usize = [$($label,)*].len();
|
||||
const NAMES: [&'static str; Self::COUNT] = [$($label,)*];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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",
|
||||
];
|
||||
labelled! {
|
||||
pub(crate) enum Counter {
|
||||
Updates = "updates",
|
||||
DrawRequests = "draw requests",
|
||||
WidgetDraws = "widget draws",
|
||||
RegionNodeDraws = "region-node draws",
|
||||
SizeReads = "draw-result size reads",
|
||||
HintHits = "hint hits",
|
||||
HintMisses = "hint misses",
|
||||
ReuseAttempts = "reuse attempts",
|
||||
ReuseExact = "reuse exact",
|
||||
ReuseMoved = "reuse moved",
|
||||
ReuseDirty = "reuse: dirty",
|
||||
ReuseUndrawn = "reuse: nothing drawn to keep",
|
||||
ReuseWrongParent = "reuse: wrong parent",
|
||||
ReuseRemapped = "reuse remapped",
|
||||
ReuseOutside = "reuse: outside what it holds for",
|
||||
ReuseWrongLayer = "reuse: another layer",
|
||||
ReuseWrongNode = "reuse: region-node choice changed",
|
||||
ReuseWrongMask = "reuse: a different inherited mask",
|
||||
QueuePops = "redraw queue pops",
|
||||
DepthReads = "depth reads",
|
||||
LocalRedraws = "local redraws",
|
||||
SizeChanges = "size changes",
|
||||
ReaderEdges = "reader edges",
|
||||
PrimitiveWrites = "primitive writes",
|
||||
TextRenders = "text renders",
|
||||
TextShapeHits = "text shape hits",
|
||||
TextShapes = "text shapes",
|
||||
TextBreaks = "text line breaks",
|
||||
GlyphPlacements = "glyph placements",
|
||||
OutsidePinnedLen = "reuse outside: the length it was pinned to",
|
||||
OutsideWindow = "reuse outside: this window",
|
||||
OutsideRelBase = "reuse outside: a rel base",
|
||||
OutsideRegion = "reuse outside: a region length",
|
||||
}
|
||||
}
|
||||
|
||||
#[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",
|
||||
];
|
||||
labelled! {
|
||||
pub(crate) enum TimerKind {
|
||||
Update = "update total",
|
||||
FullLayout = "full layout",
|
||||
IncrementalLayout = "incremental layout",
|
||||
TextRender = "text render",
|
||||
TextShape = "text shape",
|
||||
TextBreak = "text line break",
|
||||
GlyphPlacement = "glyph placement",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -249,6 +222,8 @@ pub enum ReuseOutcome {
|
||||
Dirty,
|
||||
WrongParent,
|
||||
WrongLayer,
|
||||
WrongMask,
|
||||
WrongNode,
|
||||
Remapped,
|
||||
Outside,
|
||||
Undrawn,
|
||||
@@ -262,7 +237,7 @@ pub enum TraceEvent {
|
||||
id: WidgetId,
|
||||
parent: Option<WidgetId>,
|
||||
region: UiRegion,
|
||||
pixel_size: PxVec2,
|
||||
region_px: PxVec2,
|
||||
region_node: bool,
|
||||
},
|
||||
Reuse {
|
||||
@@ -358,7 +333,7 @@ pub(crate) fn draw_request(
|
||||
id: WidgetId,
|
||||
parent: Option<WidgetId>,
|
||||
region: UiRegion,
|
||||
pixel_size: PxVec2,
|
||||
region_px: PxVec2,
|
||||
region_node: bool,
|
||||
) {
|
||||
trace(
|
||||
@@ -367,7 +342,7 @@ pub(crate) fn draw_request(
|
||||
id,
|
||||
parent,
|
||||
region,
|
||||
pixel_size,
|
||||
region_px,
|
||||
region_node,
|
||||
},
|
||||
);
|
||||
@@ -377,6 +352,50 @@ pub(crate) fn reuse(id: WidgetId, outcome: ReuseOutcome) {
|
||||
trace(id, TraceEvent::Reuse { id, outcome });
|
||||
}
|
||||
|
||||
/// A drawing that cannot be reused because the box on offer is outside what
|
||||
/// it holds for, and which of the four contracts said so. They overlap: a
|
||||
/// drawing can be outside two of them at once, and counting each is what
|
||||
/// says where a rel base redrawing more than it should is coming from.
|
||||
pub(crate) fn outside(
|
||||
id: WidgetId,
|
||||
holds: LayoutHolds,
|
||||
region: UiRegion,
|
||||
rel_base: UiVec2,
|
||||
window: PxVec2,
|
||||
) {
|
||||
let mut reasons = 0;
|
||||
let mut why = |counter| {
|
||||
bump(counter);
|
||||
reasons += 1;
|
||||
};
|
||||
for axis in Axis::BOTH {
|
||||
let holds = holds[axis];
|
||||
let len = region[axis].len();
|
||||
let window = window[axis];
|
||||
if holds.region_len.is_some_and(|pinned| pinned != len) {
|
||||
why(Counter::OutsidePinnedLen);
|
||||
}
|
||||
if !holds.window.contains(window) {
|
||||
why(Counter::OutsideWindow);
|
||||
}
|
||||
if holds
|
||||
.rel_base
|
||||
.is_some_and(|pinned| pinned != rel_base[axis])
|
||||
{
|
||||
why(Counter::OutsideRelBase);
|
||||
}
|
||||
if !holds.region.contains(len.to_px(window)) {
|
||||
why(Counter::OutsideRegion);
|
||||
}
|
||||
}
|
||||
// These four are `AxisHolds::contains`'s four clauses written out again,
|
||||
// because the report wants which one refused rather than that one did. A
|
||||
// clause added there and not here would leave a refusal unexplained.
|
||||
debug_assert!(reasons > 0, "a reuse was refused for no reason counted");
|
||||
bump(Counter::ReuseOutside);
|
||||
reuse(id, ReuseOutcome::Outside);
|
||||
}
|
||||
|
||||
pub(crate) fn size_reported(id: WidgetId, size: Size) {
|
||||
trace(id, TraceEvent::SizeReported { id, size });
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#![feature(unsize)]
|
||||
#![feature(coerce_unsized)]
|
||||
#![feature(option_into_flat_iter)]
|
||||
#![feature(const_index)]
|
||||
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
pub mod layout_diagnostics;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::util::impl_axis_index;
|
||||
use crate::{Px, Rel};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Align {
|
||||
pub x: Option<AxisAlign>,
|
||||
pub y: Option<AxisAlign>,
|
||||
@@ -83,29 +84,6 @@ pub struct RegionAlign {
|
||||
pub y: AxisAlign,
|
||||
}
|
||||
|
||||
impl RegionAlign {
|
||||
/// Both axes at the near edge. What a container passes as an override for
|
||||
/// a child it is going to position itself.
|
||||
pub const NEAR: Self = Self {
|
||||
x: AxisAlign::NEG,
|
||||
y: AxisAlign::NEG,
|
||||
};
|
||||
|
||||
pub fn axis(&self, axis: Axis) -> AxisAlign {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
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);
|
||||
@@ -173,13 +151,15 @@ impl Vec2 {
|
||||
}
|
||||
|
||||
impl Len {
|
||||
/// This length placed in the box it is measured in: the alignment names a
|
||||
/// point along that box, and the two ends are that point less the part of
|
||||
/// the length falling before it and plus the part falling after.
|
||||
pub const fn align(&self, align: AxisAlign) -> UiSpan {
|
||||
let rel = align.rel();
|
||||
let rest = Rel::ONE.sub(rel);
|
||||
let at = Len::from_parts(rel, Px::ZERO);
|
||||
UiSpan {
|
||||
start: Len::from_parts(at.rel.sub(self.rel.mul(rel)), at.px.sub(self.px.mul(rel))),
|
||||
end: Len::from_parts(at.rel.add(self.rel.mul(rest)), at.px.add(self.px.mul(rest))),
|
||||
start: at - self.scale(rel),
|
||||
end: at + self.scale(Rel::ONE.sub(rel)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,3 +212,6 @@ impl RegionAlign {
|
||||
UiVec2::from(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl_axis_index!(RegionAlign => AxisAlign);
|
||||
impl_axis_index!(Align => Option<AxisAlign>);
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::util::impl_axis_index;
|
||||
use crate::{Fixed, FixedVec2};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
@@ -7,6 +8,11 @@ pub enum Axis {
|
||||
Y,
|
||||
}
|
||||
|
||||
impl Axis {
|
||||
/// Both of them, for the layout code that asks the same question of each.
|
||||
pub const BOTH: [Self; 2] = [Self::X, Self::Y];
|
||||
}
|
||||
|
||||
impl std::ops::Not for Axis {
|
||||
type Output = Self;
|
||||
|
||||
@@ -42,20 +48,6 @@ pub enum Sign {
|
||||
}
|
||||
|
||||
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),
|
||||
@@ -65,20 +57,6 @@ impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
pub fn axis(&self, axis: Axis) -> f32 {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut f32 {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_axis(axis: Axis, aligned: f32, ortho: f32) -> Self {
|
||||
Self {
|
||||
x: match axis {
|
||||
@@ -93,47 +71,5 @@ impl Vec2 {
|
||||
}
|
||||
}
|
||||
|
||||
pub const trait AxisT {
|
||||
fn get() -> Axis;
|
||||
}
|
||||
|
||||
pub struct XAxis;
|
||||
const impl AxisT for XAxis {
|
||||
fn get() -> Axis {
|
||||
Axis::X
|
||||
}
|
||||
}
|
||||
|
||||
pub struct YAxis;
|
||||
const impl AxisT for YAxis {
|
||||
fn get() -> Axis {
|
||||
Axis::Y
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct BothAxis<T> {
|
||||
pub x: T,
|
||||
pub y: T,
|
||||
}
|
||||
|
||||
impl<T> BothAxis<T> {
|
||||
pub const fn axis<A: const AxisT>(&mut self) -> &mut T {
|
||||
match A::get() {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
pub fn take_axis<A: const AxisT>(self) -> T {
|
||||
match A::get() {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
pub fn axis_dyn(&mut self, axis: Axis) -> &mut T {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl_axis_index!({const SHIFT: u32} FixedVec2<SHIFT> => Fixed<SHIFT>);
|
||||
impl_axis_index!(Vec2 => f32);
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::util::impl_axis_index;
|
||||
use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
@@ -22,6 +23,14 @@ pub struct LayoutLen {
|
||||
pub leftover: Weight,
|
||||
}
|
||||
|
||||
/// A bare number is pixels, which is the one length that needs no box to be
|
||||
/// read in.
|
||||
impl<N: UiNum> From<N> for Len {
|
||||
fn from(value: N) -> Self {
|
||||
Len::px(value.to_f32())
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: UiNum> From<N> for LayoutLen {
|
||||
fn from(value: N) -> Self {
|
||||
LayoutLen::px(value.to_f32())
|
||||
@@ -118,13 +127,6 @@ impl Size {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis(&self, axis: Axis) -> LayoutLen {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutLen {
|
||||
@@ -151,6 +153,46 @@ impl LayoutLen {
|
||||
Len::from_parts(self.rel.add(share), self.px)
|
||||
}
|
||||
|
||||
/// Only pixels: the same number of them whatever box it lands in, and
|
||||
/// whatever anyone else in the row asks for. A length that is any part
|
||||
/// of a box or of what is left over is not one.
|
||||
pub fn is_px(&self) -> bool {
|
||||
self.rel == Rel::ZERO && self.leftover == Weight::ZERO
|
||||
}
|
||||
|
||||
/// Nothing but a claim on what is left over, so there is no length here
|
||||
/// at all where nothing is.
|
||||
pub fn is_only_leftover(&self) -> bool {
|
||||
self.leftover > Weight::ZERO && self.without_leftover() == Len::ZERO
|
||||
}
|
||||
|
||||
/// This as a length of a box, where it is one. `leftover` is not: a
|
||||
/// share of what is left over 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<Len> {
|
||||
(self.leftover == Weight::ZERO).then(|| self.without_leftover())
|
||||
}
|
||||
|
||||
/// What this takes whatever is left over: the reading of a length for
|
||||
/// anyone not dividing a box between siblings, where a share is a claim
|
||||
/// on someone else's room rather than a length of its own.
|
||||
/// [`Self::apply_leftover`] is the opposite reading of the same value.
|
||||
pub const fn without_leftover(&self) -> Len {
|
||||
Len::from_parts(self.rel, self.px)
|
||||
}
|
||||
|
||||
/// This length, given as a part of a box `len` long, as a part of the
|
||||
/// 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 = self.without_leftover().within_len(len);
|
||||
Self {
|
||||
px: part.px,
|
||||
rel: part.rel,
|
||||
leftover: self.leftover,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn px(px: impl UiNum) -> Self {
|
||||
Self {
|
||||
px: Px::from_num(px),
|
||||
@@ -205,6 +247,12 @@ impl std::fmt::Display for Size {
|
||||
|
||||
impl std::fmt::Display for LayoutLen {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// A part that is zero is left out, so a length that is zero all
|
||||
// through would print as nothing -- which reads as no length at all
|
||||
// wherever one is printed beside something that has none.
|
||||
if *self == Self::ZERO {
|
||||
return write!(f, "0 px;");
|
||||
}
|
||||
if self.px != Px::ZERO {
|
||||
write!(f, "{} px;", self.px)?;
|
||||
}
|
||||
@@ -217,3 +265,26 @@ impl std::fmt::Display for LayoutLen {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl_axis_index!(Size => LayoutLen);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// What a request prints as is how a failing case is read and written out
|
||||
/// again, and a length that printed as nothing could not be told from a
|
||||
/// widget that has no rule at all.
|
||||
#[test]
|
||||
fn every_length_prints_as_something() {
|
||||
for len in [
|
||||
LayoutLen::ZERO,
|
||||
LayoutLen::px(8),
|
||||
LayoutLen::rel(0.5),
|
||||
LayoutLen::LEFTOVER,
|
||||
] {
|
||||
assert!(!len.to_string().is_empty(), "{len:?} printed as nothing");
|
||||
}
|
||||
assert_eq!(LayoutLen::ZERO.to_string(), "0 px;");
|
||||
}
|
||||
}
|
||||
+19
-41
@@ -1,3 +1,4 @@
|
||||
use crate::util::impl_axis_index;
|
||||
use std::{fmt::Display, marker::Destruct};
|
||||
|
||||
use super::*;
|
||||
@@ -61,20 +62,6 @@ impl UiVec2 {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut Len {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis(&self, axis: Axis) -> Len {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolved against a box of `size`, which is where a fraction stops
|
||||
/// being one and becomes a place.
|
||||
pub fn to_px(&self, size: PxVec2) -> PxVec2 {
|
||||
@@ -176,14 +163,6 @@ impl Len {
|
||||
Self::from_parts(Rel::ZERO, Px::from_f32(px))
|
||||
}
|
||||
|
||||
pub const fn rel_min() -> Self {
|
||||
Self::ZERO
|
||||
}
|
||||
|
||||
pub const fn rel_max() -> Self {
|
||||
Self::FULL
|
||||
}
|
||||
|
||||
pub const fn max(&self, other: Self) -> Self {
|
||||
Self {
|
||||
rel: self.rel.max(other.rel),
|
||||
@@ -219,17 +198,13 @@ impl Len {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn within_len(&self, len: Len) -> Self {
|
||||
pub const fn within_len(&self, len: Len) -> Self {
|
||||
self.within(&UiSpan {
|
||||
start: Len::ZERO,
|
||||
end: len,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_len(&self, len: Len) -> Self {
|
||||
len.within_len(*self)
|
||||
}
|
||||
|
||||
pub const fn flip(&mut self) {
|
||||
self.rel = Rel::ONE.sub(self.rel);
|
||||
self.px = self.px.neg();
|
||||
@@ -282,6 +257,11 @@ impl UiSpan {
|
||||
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 {
|
||||
Self {
|
||||
start: self.start.within(parent),
|
||||
@@ -289,6 +269,15 @@ impl UiSpan {
|
||||
}
|
||||
}
|
||||
|
||||
/// A box `len` long inside this one, on the side `align` says. Both must
|
||||
/// be lengths of the same rel base: it subtracts one from the other
|
||||
/// rather than composing it in, which is what keeps a fraction the same
|
||||
/// fraction however long this box turns out to be.
|
||||
pub const fn place(self, len: Len, align: AxisAlign) -> Self {
|
||||
let start = self.start + (self.len() - len).scale(align.rel());
|
||||
Self::new(start, start + len)
|
||||
}
|
||||
|
||||
pub const fn len(&self) -> Len {
|
||||
self.end - self.start
|
||||
}
|
||||
@@ -323,20 +312,6 @@ impl UiRegion {
|
||||
y: self.y.within(&parent.y),
|
||||
}
|
||||
}
|
||||
pub const fn axis(&self, axis: Axis) -> &UiSpan {
|
||||
match axis {
|
||||
Axis::X => &self.x,
|
||||
Axis::Y => &self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn axis_mut(&mut self, axis: Axis) -> &mut UiSpan {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn flip(&mut self, axis: Axis) {
|
||||
match axis {
|
||||
Axis::X => self.x.flip(),
|
||||
@@ -437,3 +412,6 @@ impl Display for PixelRegion {
|
||||
write!(f, "{} -> {}", self.top_left, self.bot_right)
|
||||
}
|
||||
}
|
||||
|
||||
impl_axis_index!(UiVec2 => Len);
|
||||
impl_axis_index!(UiRegion => UiSpan);
|
||||
+53
-37
@@ -107,13 +107,6 @@ impl Default for TextAttrs {
|
||||
}
|
||||
}
|
||||
|
||||
/// How far below the longest line a width may fall and still be answered by
|
||||
/// the break in hand. A parent that offers a child the length it reported
|
||||
/// composes that length back through the box chain, so the two differ in the
|
||||
/// last bits -- and at exactly the longest line, that decides whether a line
|
||||
/// fits. Sub-pixel, so no break it admits is one a reader could see.
|
||||
const BREAK_EPSILON_PX: f32 = 0.05;
|
||||
|
||||
/// Keeps text and its corresponding layout from getting out of sync.
|
||||
pub struct TextBuffer {
|
||||
text: String,
|
||||
@@ -140,10 +133,6 @@ impl TextBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self::new("")
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
@@ -183,42 +172,47 @@ impl TextBuffer {
|
||||
self.layout_key.as_ref()?.max_width
|
||||
}
|
||||
|
||||
/// Widths covered by the current line breaks, including a wider shaping
|
||||
/// retained when a later draw requested a narrower box.
|
||||
pub fn width_holds(&self) -> crate::Holds {
|
||||
let Some(width) = self.wrap_width() else {
|
||||
return crate::Holds::ANY;
|
||||
};
|
||||
let width = Px::from_f32(width);
|
||||
let soft_wrapped = self.layout.lines().any(|line| {
|
||||
matches!(
|
||||
line.break_reason(),
|
||||
parley::layout::BreakReason::Regular | parley::layout::BreakReason::Emergency
|
||||
)
|
||||
});
|
||||
let upper = if soft_wrapped { width } else { Px::MAX };
|
||||
crate::Holds::from(Px::ceil_from_f32(self.layout.width()).min(width)..=upper)
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2 {
|
||||
Vec2::new(self.layout.width(), self.layout.height())
|
||||
}
|
||||
|
||||
pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option<f32>) {
|
||||
let layout_key = LayoutKey {
|
||||
attrs: attrs.clone(),
|
||||
max_width: width,
|
||||
};
|
||||
if self.layout_key.as_ref() == Some(&layout_key) {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::TextShapeHits);
|
||||
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 a chance to
|
||||
// disagree with itself -- which is what happens when a parent offers
|
||||
// a child the length that child just reported, and the two land
|
||||
// either side of a float.
|
||||
if let Some(key) = &self.layout_key
|
||||
&& key.attrs == *attrs
|
||||
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
|
||||
&& want <= broke_at
|
||||
&& want + BREAK_EPSILON_PX >= self.layout.width()
|
||||
// Asked of the attrs it was given rather than of a copy: copying one
|
||||
// allocates wherever its family is named, and the hit below is what
|
||||
// this cache is for.
|
||||
let same_shaping = self
|
||||
.layout_key
|
||||
.as_ref()
|
||||
.is_some_and(|key| key.attrs == *attrs);
|
||||
if same_shaping
|
||||
&& let Some(key) = &self.layout_key
|
||||
&& self.breaks_the_same(key.max_width, 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);
|
||||
let old_key = self.layout_key.replace(LayoutKey {
|
||||
attrs: attrs.clone(),
|
||||
max_width: width,
|
||||
});
|
||||
// The glyphs it holds are of the width it held, which the layout may
|
||||
// well come back to.
|
||||
if let Some(key) = old_key
|
||||
@@ -258,6 +252,28 @@ impl TextBuffer {
|
||||
self.break_lines(width);
|
||||
}
|
||||
|
||||
/// Whether the break in hand is the break `want` would make. The attrs
|
||||
/// are the caller's to compare; this is about the width alone.
|
||||
///
|
||||
/// 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.
|
||||
fn breaks_the_same(&self, broke_at: Option<f32>, want: Option<f32>) -> bool {
|
||||
match (broke_at, want) {
|
||||
(broke_at, want) if broke_at == want => true,
|
||||
(Some(broke_at), Some(want)) => want <= broke_at && want >= self.layout.width(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn break_lines(&mut self, width: Option<f32>) {
|
||||
self.layout.break_all_lines(width);
|
||||
self.layout
|
||||
|
||||
@@ -66,7 +66,7 @@ impl Textures {
|
||||
TextureHandle {
|
||||
slot: self.push(image),
|
||||
size,
|
||||
counter: RefCounter::new(),
|
||||
counter: RefCounter::default(),
|
||||
send: self.send.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,10 +167,6 @@ impl GlyphAtlas {
|
||||
pub fn page_count(&self) -> u32 {
|
||||
self.pages.len() as u32
|
||||
}
|
||||
|
||||
pub fn glyph_count(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
|
||||
+20
-8
@@ -23,13 +23,22 @@ pub use primitive::*;
|
||||
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.
|
||||
// Every number both sides count in, written once here rather than a
|
||||
// second time in the shader: a grid the two disagree about puts every
|
||||
// coordinate somewhere else, and a sentinel they disagree about makes one
|
||||
// of them walk a chain from a slot the other says is not there.
|
||||
format!(
|
||||
"const PX_STEP: f32 = 1.0 / {}.0;\nconst REL_STEP: f32 = 1.0 / {}.0;\n{PRELUDE}\n{wgsl}",
|
||||
"const PX_STEP: f32 = 1.0 / {}.0;\n\
|
||||
const REL_STEP: f32 = 1.0 / {}.0;\n\
|
||||
const MASK_NONE: u32 = {}u;\n\
|
||||
const MOVE_NONE: u32 = {}u;\n\
|
||||
const CHAIN_LIMIT: u32 = {}u;\n\
|
||||
{PRELUDE}\n{wgsl}",
|
||||
1u32 << crate::PX_SHIFT,
|
||||
1u32 << crate::REL_SHIFT,
|
||||
MaskIdx::NONE.idx(),
|
||||
MoveIdx::NONE.idx(),
|
||||
crate::CHAIN_LIMIT,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -106,7 +115,8 @@ impl UiRenderNode {
|
||||
self.active.push(i);
|
||||
for change in draws.apply_free() {
|
||||
if let Some(inst) = ui_render.active.get_mut(&change.id) {
|
||||
for h in &mut inst.primitives {
|
||||
for primitive in &mut inst.primitives {
|
||||
let h = &mut primitive.handle;
|
||||
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
|
||||
h.inst_idx = change.new;
|
||||
break;
|
||||
@@ -275,7 +285,9 @@ impl UiRenderNode {
|
||||
}
|
||||
|
||||
/// What every draw in the ui is given: the window, the masks and the
|
||||
/// move chain every position is resolved through.
|
||||
/// move chain every position is resolved through. The last two are the
|
||||
/// vertex stage's alone -- a mask's rectangle is the same for every
|
||||
/// fragment of one instance, so it is resolved once and handed on.
|
||||
fn shared_layout(device: &Device) -> BindGroupLayout {
|
||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &[
|
||||
@@ -291,7 +303,7 @@ impl UiRenderNode {
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
visibility: ShaderStages::VERTEX,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
@@ -301,7 +313,7 @@ impl UiRenderNode {
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
||||
visibility: ShaderStages::VERTEX,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
|
||||
@@ -26,15 +26,21 @@ struct MoveOffset {
|
||||
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.
|
||||
// `PX_STEP`, `REL_STEP`, `MASK_NONE`, `MOVE_NONE` and `CHAIN_LIMIT` are
|
||||
// prepended from `iris_core`'s own constants, so none of them is written
|
||||
// twice. What the CPU stores is a whole count of each step, and both steps
|
||||
// are 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);
|
||||
}
|
||||
@@ -66,12 +72,6 @@ struct Region {
|
||||
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
|
||||
@@ -126,9 +126,25 @@ struct VertexOutput {
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) @interpolate(flat) mask_idx: u32,
|
||||
@location(4) @interpolate(flat) idx: u32,
|
||||
// The mask's rectangle in output pixels, resolved here because it is the
|
||||
// same rectangle for every fragment of one instance and resolving it is a
|
||||
// walk up a chain. Its own chain, not the drawn primitive's, so a
|
||||
// stationary viewport clips content that moves inside it.
|
||||
@location(5) @interpolate(flat) mask_top_left: vec2<f32>,
|
||||
@location(6) @interpolate(flat) mask_bot_right: vec2<f32>,
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
};
|
||||
|
||||
// The pixel corners of a region, which is where every coordinate the CPU
|
||||
// decided becomes one.
|
||||
fn corners(r: Region) -> mat2x2<f32> {
|
||||
let top_left = snap_floor(vec2(r.x.start.rel, r.y.start.rel) * window.dim
|
||||
+ vec2(r.x.start.px, r.y.start.px));
|
||||
let bot_right = snap_floor(vec2(r.x.end.rel, r.y.end.rel) * window.dim
|
||||
+ vec2(r.x.end.px, r.y.end.px));
|
||||
return mat2x2<f32>(top_left, bot_right);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@builtin(vertex_index) vi: u32,
|
||||
@@ -141,16 +157,17 @@ fn vs_main(
|
||||
UiSpan(scalar_of_pair(in.x_start), scalar_of_pair(in.x_end)),
|
||||
UiSpan(scalar_of_pair(in.y_start), scalar_of_pair(in.y_end)),
|
||||
);
|
||||
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) + snap_floor(top_left_px);
|
||||
let bot_right = snap_floor(bot_right_rel * window.dim) + snap_floor(bot_right_px);
|
||||
let own = corners(resolve_move(in.move_idx, local));
|
||||
let top_left = own[0];
|
||||
let bot_right = own[1];
|
||||
let size = bot_right - top_left;
|
||||
|
||||
var mask = mat2x2<f32>(vec2<f32>(0.0), vec2<f32>(0.0));
|
||||
if in.mask_idx != MASK_NONE {
|
||||
let m = masks[in.mask_idx];
|
||||
mask = corners(resolve_move(m.move_idx, Region(span_of(m.x), span_of(m.y))));
|
||||
}
|
||||
|
||||
let uv = vec2<f32>(
|
||||
f32(vi % 2u),
|
||||
f32(vi / 2u)
|
||||
@@ -161,26 +178,19 @@ fn vs_main(
|
||||
out.top_left = top_left;
|
||||
out.bot_right = bot_right;
|
||||
out.mask_idx = in.mask_idx;
|
||||
out.mask_top_left = mask[0];
|
||||
out.mask_bot_right = mask[1];
|
||||
out.idx = ii;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
||||
if in.mask_idx == 4294967295u {
|
||||
if in.mask_idx == MASK_NONE {
|
||||
return color;
|
||||
}
|
||||
let mask = masks[in.mask_idx];
|
||||
// Its own chain, not the drawn primitive's, so a stationary viewport
|
||||
// clips content that moves inside it.
|
||||
let m = resolve_move(mask.move_idx, Region(span_of(mask.x), span_of(mask.y)));
|
||||
let tl = vec2(m.x.start.rel, m.y.start.rel);
|
||||
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) + snap_floor(tl_px);
|
||||
let bot_right = snap_floor(br * window.dim) + snap_floor(br_px);
|
||||
let top_left = in.mask_top_left;
|
||||
let bot_right = in.mask_bot_right;
|
||||
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 {
|
||||
return color * 0.0;
|
||||
|
||||
+104
-31
@@ -1,28 +1,66 @@
|
||||
use crate::{
|
||||
Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle,
|
||||
UiRegion, WidgetId,
|
||||
Bounds, Declared, LayerId, LayoutHolds, MaskIdx, MoveIdx, PlaceDesc, RegionAlign,
|
||||
RetainedPrimitive, Size, TextureHandle, UiRegion, UiVec2, WidgetId,
|
||||
};
|
||||
|
||||
/// One draw of one widget, so that a widget it asked about can carry which
|
||||
/// draw that was. Its own type beside the indices here because it names an
|
||||
/// occasion rather than a slot: nothing is stored per draw.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DrawId(u64);
|
||||
|
||||
impl DrawId {
|
||||
/// No draw at all, which is what a widget nothing has asked about carries.
|
||||
pub const NONE: Self = Self(0);
|
||||
|
||||
/// The next one after this. Handed out in order and never reused, so a
|
||||
/// note left by an earlier draw can never be read as this one's.
|
||||
pub(crate) fn next(self) -> Self {
|
||||
Self(self.0 + 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// What is kept of a widget its parent has asked about. `drawn` says whether
|
||||
/// it currently draws; one that does not is kept so that a change to it, or
|
||||
/// under it, still reaches whoever asked.
|
||||
#[derive(Debug)]
|
||||
pub struct ActiveData {
|
||||
pub id: WidgetId,
|
||||
/// The box its drawing is in, in `parent_move`'s coordinates.
|
||||
/// Where its drawing goes, in its region node's coordinates.
|
||||
pub placement: UiRegion,
|
||||
/// What a fraction declared or reported under this widget is a fraction
|
||||
/// of, as a length of the window.
|
||||
pub rel_base: UiVec2,
|
||||
/// Where its drawing was put, and where it was asked. The two differ
|
||||
/// where a container asks in one place and puts the answer in another --
|
||||
/// a row measures from its cursor and puts the child in its slot. Each
|
||||
/// carries the rel base that ask stated, so asking again from either is
|
||||
/// the same question it was.
|
||||
pub placed: PlaceDesc,
|
||||
pub asked: PlaceDesc,
|
||||
/// The box it was asked in, in the parent's region-node coordinates: the
|
||||
/// box its drawing was made in and the one its contract is about. Its
|
||||
/// drawing is placed elsewhere by re-expression, never by asking again.
|
||||
pub region: UiRegion,
|
||||
/// The box its parent first asked about it in, as a part of the box the
|
||||
/// parent was itself asked in. Any later box it was given was decided
|
||||
/// knowing its answer, so this is where a question about it is asked
|
||||
/// again -- and it is kept relative so that it follows the parent's.
|
||||
pub offer: UiRegion,
|
||||
/// What it answered there: the size and what that held for.
|
||||
pub answer: (Size, [Holds; 2]),
|
||||
/// What the widget said it used of its box, the last time it drew.
|
||||
/// The measured answer and its dependencies. A hint-only dependency or
|
||||
/// a widget first encountered during placement has no measurement yet.
|
||||
pub answer: Option<Answer>,
|
||||
/// The draw that last asked about this widget, which is what says whether
|
||||
/// the draw now running has already asked -- a question the child list can
|
||||
/// only answer by a search, and so in the children a container has rather
|
||||
/// than in one read. Written by whoever asked, so a draw of this widget
|
||||
/// itself carries it across rather than setting it.
|
||||
pub(crate) asked_by: DrawId,
|
||||
/// Asked more than once in its parent's last draw -- measured in one box
|
||||
/// and then asked in the one the parent decided. The parent's layout
|
||||
/// rests on the first answer and its drawing on the last, so only the
|
||||
/// parent can ask either again.
|
||||
pub re_asked: bool,
|
||||
/// What the widget reported, in window-unit lengths.
|
||||
pub size: Size,
|
||||
/// The pixel lengths of `region`, per axis, that its drawing and `size`
|
||||
/// hold for.
|
||||
pub holds: [Holds; 2],
|
||||
/// The window and region reads that this drawing holds for, and the
|
||||
/// rel base and region it pinned.
|
||||
pub holds: LayoutHolds,
|
||||
pub drawn: bool,
|
||||
pub parent: Option<WidgetId>,
|
||||
/// How far down the tree it was drawn, the root being 1. Carried down a
|
||||
@@ -30,35 +68,70 @@ pub struct ActiveData {
|
||||
/// widget a frame visits and cannot drift while one is being drawn.
|
||||
pub depth: usize,
|
||||
pub textures: Vec<TextureHandle>,
|
||||
pub primitives: Vec<PrimitiveHandle>,
|
||||
/// Its primitives, each keeping the box it was written in -- in this
|
||||
/// widget's placement coordinates, which is what a move recomposes from.
|
||||
pub primitives: Vec<RetainedPrimitive>,
|
||||
/// An owned mask holds one reference independently of its primitives.
|
||||
pub mask_region: Option<UiRegion>,
|
||||
pub children: Vec<WidgetId>,
|
||||
/// The children whose size this widget read while drawing.
|
||||
pub size_deps: Vec<WidgetId>,
|
||||
pub request_deps: Vec<WidgetId>,
|
||||
pub(crate) scratch: DrawScratch,
|
||||
/// The movable region its primitives are positioned through: its own when
|
||||
/// opted in, otherwise the nearest ancestor's.
|
||||
pub move_idx: MoveIdx,
|
||||
/// The declared lengths whoever drew this widget resolved into its box.
|
||||
/// The declared lengths whoever drew this widget resolved into its rel base.
|
||||
/// 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 alignment its parent asked it with. A local redraw repeats that
|
||||
/// question, including an override chosen by a container.
|
||||
pub align: RegionAlign,
|
||||
/// Whether that alignment was the parent's override rather than the
|
||||
/// widget's own property.
|
||||
pub align_override: bool,
|
||||
/// Its own alignment when it was last drawn. A change to the property is
|
||||
/// found against this even when its parent overrode the alignment.
|
||||
pub declared: Declared,
|
||||
/// Its bounds, resolved the same way. The answer is held to these where
|
||||
/// the box was not, so a change to one changes what it answers even
|
||||
/// where its declared lengths stand.
|
||||
pub bounds: Bounds,
|
||||
/// Its alignment when it was last drawn, which a change to the property
|
||||
/// is found against.
|
||||
pub own_align: RegionAlign,
|
||||
/// The movable region whose coordinates `region` uses.
|
||||
/// The movable region whose coordinates its placement is in when this
|
||||
/// widget does not own a region node.
|
||||
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,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl ActiveData {
|
||||
/// Whether its drawing and size hold for a box of these pixel lengths.
|
||||
pub fn holds_at(&self, px: crate::PxVec2) -> bool {
|
||||
self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
|
||||
/// What it answered when its parent asked, where it has been asked at
|
||||
/// all. Not `size`, which is what its last drawing reported: a drawing
|
||||
/// re-expressed in the box that answer chose is not a second answer.
|
||||
pub fn measured(&self) -> Option<Size> {
|
||||
self.answer.map(|answer| answer.size)
|
||||
}
|
||||
|
||||
/// Whether it owns a region node rather than sharing the one it was drawn
|
||||
/// under, which is what its two move indices being different says.
|
||||
pub fn is_region_node(&self) -> bool {
|
||||
self.move_idx != self.parent_move
|
||||
}
|
||||
}
|
||||
|
||||
/// What a widget answered when it was asked: the size it reported, and the
|
||||
/// boxes and windows that answer holds for.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Answer {
|
||||
pub size: Size,
|
||||
pub holds: LayoutHolds,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct DrawScratch {
|
||||
pub children: Vec<WidgetId>,
|
||||
pub size_deps: Vec<WidgetId>,
|
||||
pub under: Vec<(WidgetId, LayoutHolds)>,
|
||||
pub requests: Vec<crate::RequestedLen>,
|
||||
pub lengths: Vec<crate::Px>,
|
||||
}
|
||||
+140
-25
@@ -1,4 +1,4 @@
|
||||
use crate::{Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow};
|
||||
use crate::{Bound, 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:
|
||||
@@ -10,15 +10,72 @@ use std::ops::RangeInclusive;
|
||||
///
|
||||
/// 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. What widening there is
|
||||
/// belongs to [`Self::through`], which has a rounding to undo, and is derived
|
||||
/// from that rounding rather than chosen.
|
||||
/// 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 Len {
|
||||
/// Whether this is longer than `than` in a window this wide, and the
|
||||
/// windows that answer holds for.
|
||||
///
|
||||
/// Which is longer is a question in pixels -- `rel(0.5)` is longer than
|
||||
/// 300 px at a box of 600 and shorter at 400 -- and it is asked of the
|
||||
/// difference and answered back through that same difference, so the
|
||||
/// boundary is the comparison's own rather than a second way of finding
|
||||
/// it.
|
||||
pub fn longer_than(&self, than: Len, window: Px) -> (bool, Holds) {
|
||||
let over = *self - than;
|
||||
let longer = over.to_px(window) > Px::ZERO;
|
||||
let side = match longer {
|
||||
true => Px::STEP..=Px::MAX,
|
||||
false => Px::MIN..=Px::ZERO,
|
||||
};
|
||||
(longer, Holds::from(side).through(over))
|
||||
}
|
||||
}
|
||||
|
||||
impl Bound {
|
||||
/// The end of this bound `len` falls outside, which is the length it
|
||||
/// gets instead of its own, and the windows that answer holds for.
|
||||
/// Nothing where it is inside, which is the answer wherever there is no
|
||||
/// bound at all.
|
||||
///
|
||||
/// `len` and this bound are lengths of the same thing, whichever that
|
||||
/// is: a box in window lengths wants the bound resolved, and a length a
|
||||
/// widget declares of its rel base wants it as the rule wrote it. Both
|
||||
/// comparisons are in pixels, so each is a question about this window,
|
||||
/// and the box is decided again on the other side of a crossing.
|
||||
pub fn outside(&self, len: Len, window: Px) -> (Option<Len>, Holds) {
|
||||
let mut held = None;
|
||||
let mut holds = Holds::ANY;
|
||||
if let Some(min) = self.min {
|
||||
let (shorter, kept) = min.longer_than(len, window);
|
||||
holds = holds.and(kept);
|
||||
if shorter {
|
||||
held = Some(min);
|
||||
}
|
||||
}
|
||||
if let Some(max) = self.max {
|
||||
let (longer, kept) = held.unwrap_or(len).longer_than(max, window);
|
||||
holds = holds.and(kept);
|
||||
if longer {
|
||||
debug_assert!(
|
||||
held.is_none(),
|
||||
"a floor of {:?} over a cap of {max:?} bounds nothing",
|
||||
self.min,
|
||||
);
|
||||
held = Some(max);
|
||||
}
|
||||
}
|
||||
(held, holds)
|
||||
}
|
||||
}
|
||||
|
||||
impl Holds {
|
||||
pub const ANY: Self = Self {
|
||||
lo: Px::MIN,
|
||||
@@ -33,7 +90,13 @@ impl Holds {
|
||||
len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
|
||||
}
|
||||
|
||||
pub const fn and(self, other: Self) -> Self {
|
||||
/// Every length `other` holds for is one this holds for, so a drawing
|
||||
/// made under this range is still good wherever `other` is.
|
||||
pub const fn covers(&self, other: Self) -> bool {
|
||||
self.lo.raw() <= other.lo.raw() && self.hi.raw() >= other.hi.raw()
|
||||
}
|
||||
|
||||
pub const fn and(&self, other: Self) -> Self {
|
||||
Self {
|
||||
lo: self.lo.max(other.lo),
|
||||
hi: self.hi.min(other.hi),
|
||||
@@ -41,32 +104,32 @@ impl Holds {
|
||||
}
|
||||
|
||||
/// What a box has to be for a part of it, `len` of the box long, to stay
|
||||
/// in this range. A part with no relative extent is a fixed length: it
|
||||
/// was drawn at that length and any box keeps it there.
|
||||
/// 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 way in is `px + rel * box` taken to the nearest step, so a part
|
||||
/// of exactly `lo` came from anything within half a step of it and the
|
||||
/// answer is an interval even where this range is one length. Inverting
|
||||
/// the length alone instead gives a point that need not even contain the
|
||||
/// box the part was drawn in, which is a range excluding the drawing it
|
||||
/// was made for.
|
||||
pub const fn through(self, len: Len) -> Self {
|
||||
/// 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 {
|
||||
if self.lo.raw() == Px::MIN.raw() && self.hi.raw() == Px::MAX.raw() {
|
||||
return Self::ANY;
|
||||
}
|
||||
let rel = len.rel.raw() as i64;
|
||||
if rel == 0 {
|
||||
return Self::ANY;
|
||||
}
|
||||
// Three half steps either side -- one for the rounding on the way
|
||||
// in, two for the difference between a length composed down the
|
||||
// chain and the same length measured against the window -- and half
|
||||
// of what a `Rel` counts in, to divide by the fraction. Exact until
|
||||
// the division takes it back to the grid.
|
||||
let px = len.px.raw() as i64;
|
||||
let half_rel = REL_SHIFT - 1;
|
||||
let lo = ((self.lo.raw() as i64 - px) * 2 - 3) << half_rel;
|
||||
let hi = ((self.hi.raw() as i64 - px) * 2 + 3) << half_rel;
|
||||
// Dividing by a negative turns the ends around, so which end each
|
||||
// bound comes from is decided before dividing rather than by taking
|
||||
// the min and max of four divisions.
|
||||
// `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)),
|
||||
@@ -95,6 +158,16 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::Rel;
|
||||
|
||||
#[test]
|
||||
fn an_unrestricted_range_stays_unrestricted_through_any_length() {
|
||||
for rel in [-2.0, -0.5, 0.0, 0.5, 1.0, 2.0] {
|
||||
for px in [-8, 0, 8] {
|
||||
let len = Len::from_parts(Rel::from_f32(rel), Px::from_int(px));
|
||||
assert_eq!(Holds::ANY.through(len), Holds::ANY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn through_reverses_a_range_for_a_negative_fraction() {
|
||||
// `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
|
||||
@@ -116,6 +189,48 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
use crate::util::impl_axis_index;
|
||||
use crate::{Axis, Holds, Len, Px, PxVec2, UiRegion, UiVec2};
|
||||
|
||||
/// What one evaluation of a widget depends on along one axis: the window
|
||||
/// lengths its reads hold for, the pixel lengths of its own box, and the
|
||||
/// symbolic lengths of that box and of its rel base where either one is what
|
||||
/// it was expressed in.
|
||||
///
|
||||
/// The symbolic lengths are pins rather than ranges: a container places its
|
||||
/// children as lengths of its rel base measured from where its own box starts,
|
||||
/// so what it draws turns on that box's length and on nothing about where it
|
||||
/// is. A box pin reaches the parent only where the box it pinned is the
|
||||
/// parent's own; anywhere else the parent chose that length itself, and a
|
||||
/// widget pinned this way is checked when it is re-placed.
|
||||
///
|
||||
/// A rel base pin says the answer or the drawing is a fraction of the rel base,
|
||||
/// which is a different length wherever the rel base is a different one -- at
|
||||
/// the same window size, so no range of window pixels can say it. A length
|
||||
/// of the rel base that is only pixels is not one: it is that many pixels
|
||||
/// whatever the rel base turns out to be.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct AxisHolds {
|
||||
pub window: Holds,
|
||||
pub rel_base: Option<Len>,
|
||||
pub region: Holds,
|
||||
pub region_len: Option<Len>,
|
||||
}
|
||||
|
||||
impl AxisHolds {
|
||||
pub const ANY: Self = Self {
|
||||
window: Holds::ANY,
|
||||
rel_base: None,
|
||||
region: Holds::ANY,
|
||||
region_len: None,
|
||||
};
|
||||
|
||||
pub fn and(&self, other: Self) -> Self {
|
||||
// Two pins of the same length disagreeing would mean one drawing was
|
||||
// a fraction of two different lengths at once.
|
||||
debug_assert!(
|
||||
self.region_len.is_none()
|
||||
|| other.region_len.is_none()
|
||||
|| self.region_len == other.region_len
|
||||
);
|
||||
debug_assert!(
|
||||
self.rel_base.is_none() || other.rel_base.is_none() || self.rel_base == other.rel_base
|
||||
);
|
||||
Self {
|
||||
window: self.window.and(other.window),
|
||||
rel_base: self.rel_base.or(other.rel_base),
|
||||
region: self.region.and(other.region),
|
||||
region_len: self.region_len.or(other.region_len),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn covers(&self, other: Self) -> bool {
|
||||
self.window.covers(other.window)
|
||||
&& self.region.covers(other.region)
|
||||
&& self
|
||||
.region_len
|
||||
.is_none_or(|len| other.region_len == Some(len))
|
||||
&& self.rel_base.is_none_or(|len| other.rel_base == Some(len))
|
||||
}
|
||||
|
||||
/// Whether a widget in a box `len` long, with that rel base, in that
|
||||
/// window, is one this drawing holds for.
|
||||
pub fn contains(&self, window: Px, rel_base: Len, len: Len) -> bool {
|
||||
self.window.contains(window)
|
||||
&& self.rel_base.is_none_or(|pinned| pinned == rel_base)
|
||||
&& self.region.contains(len.to_px(window))
|
||||
&& self.region_len.is_none_or(|pinned| pinned == len)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`AxisHolds`] on both axes. Every question asked of it is asked of one
|
||||
/// axis at a time, since a widget that read one length holds for any length
|
||||
/// of the other.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct LayoutHolds {
|
||||
pub x: AxisHolds,
|
||||
pub y: AxisHolds,
|
||||
}
|
||||
|
||||
impl LayoutHolds {
|
||||
pub const ANY: Self = Self {
|
||||
x: AxisHolds::ANY,
|
||||
y: AxisHolds::ANY,
|
||||
};
|
||||
|
||||
pub fn and(&self, other: Self) -> Self {
|
||||
Self {
|
||||
x: self.x.and(other.x),
|
||||
y: self.y.and(other.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn covers(&self, other: Self) -> bool {
|
||||
self.x.covers(other.x) && self.y.covers(other.y)
|
||||
}
|
||||
|
||||
pub fn contains(&self, window: PxVec2, rel_base: UiVec2, region: UiRegion) -> bool {
|
||||
Axis::BOTH
|
||||
.into_iter()
|
||||
.all(|axis| self[axis].contains(window[axis], rel_base[axis], region[axis].len()))
|
||||
}
|
||||
}
|
||||
|
||||
impl_axis_index!(LayoutHolds => AxisHolds);
|
||||
+21
-12
@@ -11,12 +11,16 @@ pub const CHAIN_LIMIT: u32 = 64;
|
||||
|
||||
mod active;
|
||||
mod holds;
|
||||
mod layout_holds;
|
||||
mod painter;
|
||||
mod place;
|
||||
mod render_state;
|
||||
|
||||
pub use active::*;
|
||||
pub use holds::*;
|
||||
pub use layout_holds::*;
|
||||
pub use painter::{Painter, PrimitiveLike};
|
||||
pub use place::{PlaceDesc, PlaceDescAxis, PlaceFit, RetainedPrimitive};
|
||||
pub use render_state::*;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -53,8 +57,11 @@ impl Moves {
|
||||
}
|
||||
}
|
||||
|
||||
/// Frees a slot. Not a change to the entries: the slot keeps the bytes it
|
||||
/// had, nothing names it until it is handed out again, and whoever is
|
||||
/// handed it writes it then -- so re-uploading the array here would send
|
||||
/// the GPU what it already has.
|
||||
pub fn remove(&mut self, idx: MoveIdx) {
|
||||
self.changed = true;
|
||||
self.arena.remove(Id::preset(idx.idx() as u32));
|
||||
}
|
||||
|
||||
@@ -68,17 +75,24 @@ impl Moves {
|
||||
}
|
||||
}
|
||||
|
||||
/// Composes a region held in `idx`'s coordinates down the chain, which is
|
||||
/// the same walk the vertex shader does.
|
||||
/// 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 region;
|
||||
return;
|
||||
}
|
||||
let entry = self.arena[at.idx()];
|
||||
region = region.within(&entry.region);
|
||||
let entry = &self.arena[at.idx()];
|
||||
step(&entry.region);
|
||||
at = entry.parent;
|
||||
}
|
||||
debug_assert!(
|
||||
@@ -86,18 +100,13 @@ impl Moves {
|
||||
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
|
||||
and the shader stops at the same depth"
|
||||
);
|
||||
region
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
self.walk(idx, |_| depth += 1);
|
||||
depth
|
||||
}
|
||||
|
||||
|
||||
+803
-288
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,234 @@
|
||||
use crate::util::impl_axis_index;
|
||||
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan, UiVec2};
|
||||
|
||||
/// How a child's region along one axis comes from the region of the widget
|
||||
/// asking, and what its fractions are of.
|
||||
///
|
||||
/// The three ways of saying a region are the three the geometry already has:
|
||||
/// a span composed into the caller's box, a span shifted to where that box
|
||||
/// starts, and a length placed in it by alignment. Which one is meant cannot
|
||||
/// be read off the numbers, since two of them take the same span and apply
|
||||
/// it differently, so it is said here.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PlaceDescAxis {
|
||||
pub span: PlaceSpan,
|
||||
pub fit: PlaceFit,
|
||||
pub rel_base: RelBase,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum PlaceFit {
|
||||
Align,
|
||||
Fill,
|
||||
/// The parent has already evaluated the child's size request.
|
||||
Allocated,
|
||||
}
|
||||
|
||||
impl PlaceFit {
|
||||
pub fn fills(&self) -> bool {
|
||||
!matches!(self, Self::Align)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum PlaceSpan {
|
||||
Within(UiSpan),
|
||||
Shifted(UiSpan),
|
||||
Sized(Len),
|
||||
}
|
||||
|
||||
/// What a child's fractions are of. [`PlaceSpan::Sized`] is a length the
|
||||
/// caller named, which is always its own base, so nothing here constructs one
|
||||
/// beside anything but [`Self::Len`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum RelBase {
|
||||
/// The caller's own, unchanged.
|
||||
Inherit,
|
||||
/// The caller's own, narrowed the way the region is.
|
||||
WithRegion,
|
||||
/// This length of the window.
|
||||
Len(Len),
|
||||
}
|
||||
|
||||
impl PlaceDescAxis {
|
||||
/// The whole of the caller's box.
|
||||
pub const WHOLE: Self = UiSpan::FULL.within_desc();
|
||||
|
||||
/// This region is the child's placement: its answer is not placed inside
|
||||
/// it again. A container uses it where it hands back exactly what the
|
||||
/// child asked for -- a row placing a child at the length it reported.
|
||||
pub const fn fills(mut self) -> Self {
|
||||
self.fit = PlaceFit::Fill;
|
||||
self
|
||||
}
|
||||
|
||||
/// A final allocation, including any comparisons in the child's request.
|
||||
pub const fn allocated(mut self) -> Self {
|
||||
self.fit = PlaceFit::Allocated;
|
||||
self
|
||||
}
|
||||
|
||||
/// This along `axis`, and the whole of the caller's box across it: what
|
||||
/// a container dividing one axis says, since nothing divides the other.
|
||||
/// [`PlaceDesc::from_axis`] says the across one where it is not the
|
||||
/// whole.
|
||||
pub const fn on_axis(self, axis: Axis) -> PlaceDesc {
|
||||
PlaceDesc::from_axis(axis, self, Self::WHOLE)
|
||||
}
|
||||
|
||||
/// What the child's fractions are of, as a length of the window: a
|
||||
/// resolved share, or a box a sibling's answer decided.
|
||||
pub const fn rel_base(mut self, len: Len) -> Self {
|
||||
self.rel_base = RelBase::Len(len);
|
||||
self
|
||||
}
|
||||
|
||||
/// Where it lands in the coordinates `own` is in.
|
||||
pub fn of(self, own: UiSpan, align: AxisAlign) -> UiSpan {
|
||||
match self.span {
|
||||
PlaceSpan::Within(span) => span.within(&own),
|
||||
PlaceSpan::Shifted(mut span) => {
|
||||
span.shift(own.start);
|
||||
span
|
||||
}
|
||||
PlaceSpan::Sized(len) => own.place(len, align),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a child is asked, on both axes. A [`UiRegion`] converts into the
|
||||
/// common case: that box of the caller's own, the answer placed inside it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PlaceDesc {
|
||||
pub x: PlaceDescAxis,
|
||||
pub y: PlaceDescAxis,
|
||||
}
|
||||
|
||||
impl PlaceDesc {
|
||||
/// The whole of the caller's box, on both axes.
|
||||
pub const WHOLE: Self = Self::splat(PlaceDescAxis::WHOLE);
|
||||
|
||||
pub const fn new(x: PlaceDescAxis, y: PlaceDescAxis) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
/// The same on both axes.
|
||||
pub const fn splat(place: PlaceDescAxis) -> Self {
|
||||
Self { x: place, y: place }
|
||||
}
|
||||
|
||||
/// A description per axis, where the two differ and neither is the
|
||||
/// axis a container divides.
|
||||
pub fn from_axes(f: impl Fn(Axis) -> PlaceDescAxis) -> Self {
|
||||
Self::new(f(Axis::X), f(Axis::Y))
|
||||
}
|
||||
|
||||
/// `aligned` on `axis` and `ortho` on the other, which is how a
|
||||
/// container that divides one axis says what it is doing.
|
||||
pub const fn from_axis(axis: Axis, aligned: PlaceDescAxis, ortho: PlaceDescAxis) -> Self {
|
||||
match axis {
|
||||
Axis::X => Self::new(aligned, ortho),
|
||||
Axis::Y => Self::new(ortho, aligned),
|
||||
}
|
||||
}
|
||||
|
||||
/// Both regions are the child's placement. See [`PlaceDescAxis::fills`].
|
||||
pub const fn fills(self) -> Self {
|
||||
Self::new(self.x.fills(), self.y.fills())
|
||||
}
|
||||
|
||||
/// The child's rel base on one axis. See [`PlaceDescAxis::rel_base`].
|
||||
pub const fn rel_base(mut self, axis: Axis, len: Len) -> Self {
|
||||
self[axis] = self[axis].rel_base(len);
|
||||
self
|
||||
}
|
||||
|
||||
/// What a child's fractions on one axis are of, as a length of the
|
||||
/// window: a length this place names, or the rel base of the widget
|
||||
/// giving it, which is `parent_rel_base`.
|
||||
pub(super) fn base(&self, axis: Axis, parent_rel_base: UiVec2) -> Len {
|
||||
match self[axis].rel_base {
|
||||
RelBase::Len(len) => len,
|
||||
RelBase::Inherit | RelBase::WithRegion => parent_rel_base[axis],
|
||||
}
|
||||
}
|
||||
|
||||
/// The box each axis names, in the coordinates `own` is in.
|
||||
pub fn of(self, own: UiRegion, align: RegionAlign) -> UiRegion {
|
||||
UiRegion::new(self.x.of(own.x, align.x), self.y.of(own.y, align.y))
|
||||
}
|
||||
}
|
||||
|
||||
impl UiSpan {
|
||||
/// This span composed into the caller's own box, so it moves and scales
|
||||
/// with it: [`UiSpan::within`], which is what a container that insets
|
||||
/// one speaks. Taking eleven pixels off the end needs no length, where
|
||||
/// saying the same thing in window lengths would make the container read
|
||||
/// its own box -- and a box chosen from its own answer then feeds back
|
||||
/// into the answer.
|
||||
///
|
||||
/// The child's rel base is narrowed the same way, so padding takes its
|
||||
/// pixels off both and `rel(1)` under it fills the caller rather than
|
||||
/// overflowing it.
|
||||
pub const fn within_desc(self) -> PlaceDescAxis {
|
||||
PlaceDescAxis {
|
||||
span: PlaceSpan::Within(self),
|
||||
fit: PlaceFit::Align,
|
||||
rel_base: RelBase::WithRegion,
|
||||
}
|
||||
}
|
||||
|
||||
/// This span shifted to where the caller's own box starts: window
|
||||
/// lengths along a cursor, which is what a container dividing room among
|
||||
/// its children speaks. A child's report is a window length, so the
|
||||
/// cursor that sums those reports is one too, and a moved box re-places
|
||||
/// every child by re-adding its start, exactly.
|
||||
///
|
||||
/// The child's rel base passes through: how far along the cursor a child
|
||||
/// sits says nothing about what a fraction under it is of. The same span
|
||||
/// says [`Self::within_desc`] as a part of that box instead, and which is
|
||||
/// meant cannot be read off the numbers.
|
||||
pub const fn shifted_desc(self) -> PlaceDescAxis {
|
||||
PlaceDescAxis {
|
||||
span: PlaceSpan::Shifted(self),
|
||||
fit: PlaceFit::Align,
|
||||
rel_base: RelBase::Inherit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Len {
|
||||
/// A box this long, placed in the caller's own by the child's alignment:
|
||||
/// the rule that places an answer, with the length given from above
|
||||
/// rather than reported. What a stack's sizing child decides for the
|
||||
/// rest. It is the child's rel base too.
|
||||
pub const fn as_desc(self) -> PlaceDescAxis {
|
||||
PlaceDescAxis {
|
||||
span: PlaceSpan::Sized(self),
|
||||
fit: PlaceFit::Align,
|
||||
rel_base: RelBase::Len(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UiRegion> for PlaceDesc {
|
||||
fn from(region: UiRegion) -> Self {
|
||||
Self::new(region.x.within_desc(), region.y.within_desc())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PlaceDescAxis> for PlaceDesc {
|
||||
fn from(place: PlaceDescAxis) -> Self {
|
||||
Self::splat(place)
|
||||
}
|
||||
}
|
||||
|
||||
/// A primitive as it was written: its box in the widget's own box's
|
||||
/// coordinates, which is what a move of that box re-composes from.
|
||||
#[derive(Debug)]
|
||||
pub struct RetainedPrimitive {
|
||||
pub handle: PrimitiveHandle,
|
||||
pub region: UiRegion,
|
||||
}
|
||||
|
||||
impl_axis_index!(PlaceDesc => PlaceDescAxis);
|
||||
+812
-541
File diff suppressed because it is too large.
Load diff
@@ -35,7 +35,7 @@ impl<T, I: IdNum> Arena<T, I> {
|
||||
self.data[i]
|
||||
}
|
||||
|
||||
pub(crate) fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
||||
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
||||
&mut self.data[id.idx()]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,3 +93,31 @@ macro_rules! impl_op {
|
||||
}
|
||||
|
||||
pub(crate) use impl_op;
|
||||
|
||||
/// `Index<Axis>` for a pair, which is how every pair here is read by axis.
|
||||
/// The generics clause is given in braces where the type has one.
|
||||
macro_rules! impl_axis_index {
|
||||
($({$($gen:tt)*})? $T:ty => $Out:ty) => {
|
||||
const impl $(<$($gen)*>)? std::ops::Index<crate::Axis> for $T {
|
||||
type Output = $Out;
|
||||
|
||||
fn index(&self, axis: crate::Axis) -> &$Out {
|
||||
match axis {
|
||||
crate::Axis::X => &self.x,
|
||||
crate::Axis::Y => &self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const impl $(<$($gen)*>)? std::ops::IndexMut<crate::Axis> for $T {
|
||||
fn index_mut(&mut self, axis: crate::Axis) -> &mut $Out {
|
||||
match axis {
|
||||
crate::Axis::X => &mut self.x,
|
||||
crate::Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use impl_axis_index;
|
||||
@@ -3,29 +3,18 @@ use std::sync::{
|
||||
atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// How many handles to one thing there are, less one: a fresh counter is a
|
||||
/// single handle, which is what the derived default gives. Only
|
||||
/// [`crate::TextureHandle`] has any, a texture slot being shared by every
|
||||
/// widget drawing that picture.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct RefCounter(Arc<AtomicU32>);
|
||||
|
||||
impl RefCounter {
|
||||
pub fn new() -> Self {
|
||||
Self(Arc::new(0.into()))
|
||||
}
|
||||
pub fn refs(&self) -> u32 {
|
||||
self.0.load(Ordering::Acquire)
|
||||
}
|
||||
pub fn drop(&mut self) -> bool {
|
||||
let refs = self.0.fetch_sub(1, Ordering::Release);
|
||||
refs == 0
|
||||
}
|
||||
pub fn quiet_clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RefCounter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RefCounter {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct SlotId {
|
||||
idx: u32,
|
||||
genr: u32,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use std::{marker::Unsize, ops::CoerceUnsized, sync::mpsc::Sender};
|
||||
|
||||
use crate::{
|
||||
UiRsc, Widget,
|
||||
util::{RefCounter, SlotId},
|
||||
};
|
||||
use crate::{UiRsc, Widget, util::SlotId};
|
||||
|
||||
pub type WidgetId = SlotId;
|
||||
|
||||
@@ -11,10 +8,12 @@ pub type WidgetId = SlotId;
|
||||
/// This is a strong handle that does not impl Clone, and when it is dropped,
|
||||
/// a signal is sent to the owning UI to clean up the resources.
|
||||
///
|
||||
/// There is nothing to count: not being `Clone` is what makes one handle the
|
||||
/// only one, so the drop is the last one and the widget is one container's.
|
||||
///
|
||||
/// TODO: ergonomic clones when they get put in rust-analyzer & don't cause ICEs?
|
||||
pub struct StrongWidget<W: ?Sized = dyn Widget> {
|
||||
pub(super) id: WidgetId,
|
||||
counter: RefCounter,
|
||||
send: Sender<WidgetId>,
|
||||
ty: *const W,
|
||||
}
|
||||
@@ -37,7 +36,6 @@ impl<W: ?Sized> StrongWidget<W> {
|
||||
pub(crate) fn new(id: WidgetId, send: Sender<WidgetId>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
counter: RefCounter::new(),
|
||||
send,
|
||||
ty: null_ptr(),
|
||||
}
|
||||
@@ -47,10 +45,6 @@ impl<W: ?Sized> StrongWidget<W> {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn refs(&self) -> u32 {
|
||||
self.counter.refs()
|
||||
}
|
||||
|
||||
pub fn weak(&self) -> WeakWidget<W> {
|
||||
let Self { ty, id, .. } = *self;
|
||||
WeakWidget { ty, id }
|
||||
@@ -74,10 +68,8 @@ impl<W: ?Sized> WeakWidget<W> {
|
||||
|
||||
impl<W: ?Sized> Drop for StrongWidget<W> {
|
||||
fn drop(&mut self) {
|
||||
if self.counter.drop() {
|
||||
let _ = self.send.send(self.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WidgetIdFn<Rsc, W: ?Sized = dyn Widget>: FnOnce(&mut Rsc) -> WeakWidget<W> {}
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::any::Any;
|
||||
mod data;
|
||||
mod handle;
|
||||
mod like;
|
||||
mod request;
|
||||
mod size_rule;
|
||||
mod tag;
|
||||
mod view;
|
||||
@@ -12,6 +13,7 @@ mod widgets;
|
||||
pub use data::*;
|
||||
pub use handle::*;
|
||||
pub use like::*;
|
||||
pub use request::*;
|
||||
pub use size_rule::*;
|
||||
pub use tag::*;
|
||||
pub use view::*;
|
||||
@@ -21,6 +23,12 @@ pub trait Widget: Any {
|
||||
/// Draws the widget, and returns what it used of the box it was given.
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size;
|
||||
|
||||
/// Describes an axis before painting. Return `None` when discovering it
|
||||
/// needs a concrete box or work performed by `draw`.
|
||||
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||
self.size_hint(axis).map(|len| requests.length(len))
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// disagrees with the eventual draw fails a debug assertion.
|
||||
|
||||
@@ -0,0 +1,609 @@
|
||||
use crate::{
|
||||
ActiveData, Axis, Bound, LayoutLen, Len, Px, Rel, StrongWidget, UiNum, Weight, WidgetId,
|
||||
Widgets, util::HashMap,
|
||||
};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
impl<N: UiNum> From<N> for SizeRequest {
|
||||
fn from(value: N) -> Self {
|
||||
LayoutLen::px(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutLen {
|
||||
pub fn min(self, other: impl Into<SizeRequest>) -> SizeRequest {
|
||||
SizeRequest::from(self).min(other)
|
||||
}
|
||||
|
||||
pub fn max(self, other: impl Into<SizeRequest>) -> SizeRequest {
|
||||
SizeRequest::from(self).max(other)
|
||||
}
|
||||
|
||||
pub fn clamp(self, min: impl Into<SizeRequest>, max: impl Into<SizeRequest>) -> SizeRequest {
|
||||
SizeRequest::from(self).clamp(min, max)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Op {
|
||||
Sum,
|
||||
Min,
|
||||
Max,
|
||||
}
|
||||
|
||||
/// One operand of a [`Node`]: a length, or another node. Node numbers are an
|
||||
/// arena's own, so an operand says nothing about which arena it came from --
|
||||
/// [`RequestedLen`] is the form that does, and the only one that leaves one.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Operand {
|
||||
Linear(LayoutLen),
|
||||
Node(u32),
|
||||
}
|
||||
|
||||
impl Operand {
|
||||
/// The length itself, where no comparison is waiting on an allocation.
|
||||
fn linear(&self) -> Option<LayoutLen> {
|
||||
match *self {
|
||||
Self::Linear(len) => Some(len),
|
||||
Self::Node(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One sum or comparison, with its two operands. Whether anything under it
|
||||
/// divides leftover space is carried on the node rather than walked for,
|
||||
/// because every caller of one asks.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct Node {
|
||||
op: Op,
|
||||
a: Operand,
|
||||
b: Operand,
|
||||
leftover: bool,
|
||||
}
|
||||
|
||||
/// The nodes of one expression, numbered from zero, and the folding that
|
||||
/// happens as each is added. There are two kinds of owner and one kind of
|
||||
/// arena: a rule's [`SizeRequest`] holds a small one for as long as the rule
|
||||
/// lasts, and [`RequestArena`] holds the layout pass's.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
struct Nodes(Vec<Node>);
|
||||
|
||||
impl Nodes {
|
||||
fn node(&self, index: u32) -> Node {
|
||||
self.0[index as usize]
|
||||
}
|
||||
|
||||
fn leftover(&self, at: Operand) -> bool {
|
||||
match at {
|
||||
Operand::Linear(len) => len.leftover > Weight::ZERO,
|
||||
Operand::Node(index) => self.node(index).leftover,
|
||||
}
|
||||
}
|
||||
|
||||
/// `a op b`, which is a node only where the answer needs one. Two
|
||||
/// lengths that keep their order whatever the room comes to are already
|
||||
/// decided, and so are two operands that are the same thing.
|
||||
fn combine(&mut self, op: Op, a: Operand, b: Operand) -> Operand {
|
||||
if let (Some(x), Some(y)) = (a.linear(), b.linear()) {
|
||||
if matches!(op, Op::Sum) {
|
||||
return Operand::Linear(x + y);
|
||||
}
|
||||
if let Some(order) = independent_order(x, y) {
|
||||
let take_a = match op {
|
||||
Op::Min => !order.is_gt(),
|
||||
_ => !order.is_lt(),
|
||||
};
|
||||
return if take_a { a } else { b };
|
||||
}
|
||||
}
|
||||
if a == b && !matches!(op, Op::Sum) {
|
||||
return a;
|
||||
}
|
||||
let index = u32::try_from(self.0.len()).expect("more nodes than one arena can number");
|
||||
let leftover = self.leftover(a) || self.leftover(b);
|
||||
self.0.push(Node { op, a, b, leftover });
|
||||
Operand::Node(index)
|
||||
}
|
||||
|
||||
/// Copies `at` and everything under it out of `from`, with every length
|
||||
/// it holds passed through `resolve`. Folded again on the way in, since
|
||||
/// resolving a fraction can settle a comparison that was open before it.
|
||||
fn graft(
|
||||
&mut self,
|
||||
from: &Self,
|
||||
at: Operand,
|
||||
resolve: impl Copy + Fn(LayoutLen) -> LayoutLen,
|
||||
) -> Operand {
|
||||
let index = match at {
|
||||
Operand::Linear(len) => return Operand::Linear(resolve(len)),
|
||||
Operand::Node(index) => index,
|
||||
};
|
||||
let Node { op, a, b, .. } = from.node(index);
|
||||
let a = self.graft(from, a, resolve);
|
||||
let b = self.graft(from, b, resolve);
|
||||
self.combine(op, a, b)
|
||||
}
|
||||
|
||||
fn write(&self, f: &mut std::fmt::Formatter<'_>, at: Operand) -> std::fmt::Result {
|
||||
let index = match at {
|
||||
Operand::Linear(len) => return write!(f, "{len}"),
|
||||
Operand::Node(index) => index,
|
||||
};
|
||||
let Node { op, a, b, .. } = self.node(index);
|
||||
write!(
|
||||
f,
|
||||
"{}(",
|
||||
match op {
|
||||
Op::Sum => "sum",
|
||||
Op::Min => "min",
|
||||
Op::Max => "max",
|
||||
}
|
||||
)?;
|
||||
self.write(f, a)?;
|
||||
write!(f, ", ")?;
|
||||
self.write(f, b)?;
|
||||
write!(f, ")")
|
||||
}
|
||||
}
|
||||
|
||||
/// A size request before a container has divided its leftover space.
|
||||
/// Comparisons keep both operands until the share is known.
|
||||
///
|
||||
/// An expression is the same nodes the layout pass allocates, in an arena of
|
||||
/// its own: importing one copies those nodes into the pass's arena, so there
|
||||
/// is no second shape to keep in step and one place where folding is decided.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum SizeRequest {
|
||||
Linear(LayoutLen),
|
||||
/// Behind a pointer, because a plain length is what nearly every rule
|
||||
/// holds and an expression should cost those rules nothing.
|
||||
Expr(Box<Expr>),
|
||||
}
|
||||
|
||||
/// An expression's own arena, and which of its nodes is the whole of it.
|
||||
/// `root` is a node number rather than an operand, so an expression that
|
||||
/// folded all the way down to a length cannot be written as one: that is a
|
||||
/// [`SizeRequest::Linear`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Expr {
|
||||
nodes: Nodes,
|
||||
root: u32,
|
||||
}
|
||||
|
||||
impl SizeRequest {
|
||||
pub fn min(self, other: impl Into<Self>) -> Self {
|
||||
self.join(Op::Min, other.into())
|
||||
}
|
||||
|
||||
pub fn max(self, other: impl Into<Self>) -> Self {
|
||||
self.join(Op::Max, other.into())
|
||||
}
|
||||
|
||||
pub fn clamp(self, min: impl Into<Self>, max: impl Into<Self>) -> Self {
|
||||
self.max(min).min(max)
|
||||
}
|
||||
|
||||
/// `self op other`, keeping `self`'s arena and copying `other`'s into
|
||||
/// it, so the two sets of node numbers become one. Two requests that are
|
||||
/// the same request compare to the same thing whatever the room is, which
|
||||
/// is a comparison worth not building -- but adding something to itself
|
||||
/// is twice it.
|
||||
fn join(self, op: Op, other: Self) -> Self {
|
||||
if self == other && !matches!(op, Op::Sum) {
|
||||
return self;
|
||||
}
|
||||
let (mut nodes, a) = match self {
|
||||
Self::Linear(len) => (Nodes::default(), Operand::Linear(len)),
|
||||
Self::Expr(expr) => {
|
||||
let Expr { nodes, root } = *expr;
|
||||
(nodes, Operand::Node(root))
|
||||
}
|
||||
};
|
||||
let b = match other {
|
||||
Self::Linear(len) => Operand::Linear(len),
|
||||
Self::Expr(expr) => nodes.graft(&expr.nodes, Operand::Node(expr.root), |len| len),
|
||||
};
|
||||
match nodes.combine(op, a, b) {
|
||||
Operand::Linear(len) => Self::Linear(len),
|
||||
Operand::Node(root) => Self::Expr(Box::new(Expr { nodes, root })),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LayoutLen> for SizeRequest {
|
||||
fn from(len: LayoutLen) -> Self {
|
||||
Self::Linear(len)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Len> for SizeRequest {
|
||||
fn from(len: Len) -> Self {
|
||||
LayoutLen::from(len).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for SizeRequest {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, other: Self) -> Self {
|
||||
self.join(Op::Sum, other)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SizeRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Linear(len) => write!(f, "{len}"),
|
||||
Self::Expr(expr) => expr.nodes.write(f, Operand::Node(expr.root)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The same, since an arena printed as a struct is not a tree anyone can
|
||||
/// write out again, which is what a request is printed for.
|
||||
impl std::fmt::Debug for SizeRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// A discovered length. Deferred values belong to the current layout pass;
|
||||
/// widgets must not retain them. Ordinary requests remain inline lengths.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct RequestedLen(RequestValue);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum RequestValue {
|
||||
Linear(LayoutLen),
|
||||
Deferred {
|
||||
index: u32,
|
||||
epoch: u64,
|
||||
leftover: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LayoutLen> for RequestedLen {
|
||||
fn from(len: LayoutLen) -> Self {
|
||||
Self(RequestValue::Linear(len))
|
||||
}
|
||||
}
|
||||
impl From<Len> for RequestedLen {
|
||||
fn from(len: Len) -> Self {
|
||||
LayoutLen::from(len).into()
|
||||
}
|
||||
}
|
||||
impl RequestedLen {
|
||||
/// The length itself, where no comparison is waiting on an allocation.
|
||||
pub fn linear(&self) -> Option<LayoutLen> {
|
||||
match self.0 {
|
||||
RequestValue::Linear(len) => Some(len),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn has_leftover(&self) -> bool {
|
||||
match self.0 {
|
||||
RequestValue::Linear(len) => len.leftover > Weight::ZERO,
|
||||
RequestValue::Deferred { leftover, .. } => leftover,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct RequestArena {
|
||||
nodes: Nodes,
|
||||
epoch: u64,
|
||||
}
|
||||
|
||||
impl RequestArena {
|
||||
pub(crate) fn reset(&mut self) {
|
||||
self.nodes.0.clear();
|
||||
self.epoch = self
|
||||
.epoch
|
||||
.checked_add(1)
|
||||
.expect("layout generation exhausted");
|
||||
}
|
||||
|
||||
/// An operand of this pass's arena as the handle that leaves it. The
|
||||
/// epoch says which pass numbered the node, so a handle a widget kept
|
||||
/// past its pass is caught rather than answering about whatever node
|
||||
/// took its place.
|
||||
fn handle(&self, at: Operand) -> RequestedLen {
|
||||
RequestedLen(match at {
|
||||
Operand::Linear(len) => RequestValue::Linear(len),
|
||||
Operand::Node(index) => RequestValue::Deferred {
|
||||
index,
|
||||
epoch: self.epoch,
|
||||
leftover: self.nodes.leftover(at),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// The other direction, checked once where a handle comes back in rather
|
||||
/// than again at every level of the walk it starts.
|
||||
fn operand(&self, request: RequestedLen) -> Operand {
|
||||
match request.0 {
|
||||
RequestValue::Linear(len) => Operand::Linear(len),
|
||||
RequestValue::Deferred { index, epoch, .. } => {
|
||||
assert_eq!(epoch, self.epoch, "request retained beyond its layout pass");
|
||||
Operand::Node(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A rule's expression in this pass's arena, with its fractions resolved
|
||||
/// against the rel base the widget is being asked with.
|
||||
pub(crate) fn import(&mut self, request: &SizeRequest, base: Len) -> RequestedLen {
|
||||
let at = match request {
|
||||
SizeRequest::Linear(len) => Operand::Linear(len.within_len(base)),
|
||||
SizeRequest::Expr(expr) => {
|
||||
self.nodes
|
||||
.graft(&expr.nodes, Operand::Node(expr.root), |len| {
|
||||
len.within_len(base)
|
||||
})
|
||||
}
|
||||
};
|
||||
self.handle(at)
|
||||
}
|
||||
|
||||
pub(crate) fn bounded(&mut self, request: RequestedLen, bound: Bound) -> RequestedLen {
|
||||
let request = match bound.min {
|
||||
Some(min) => self.combine(Op::Max, request, min.into()),
|
||||
None => request,
|
||||
};
|
||||
match bound.max {
|
||||
Some(max) => self.combine(Op::Min, request, max.into()),
|
||||
None => request,
|
||||
}
|
||||
}
|
||||
|
||||
fn combine(&mut self, op: Op, a: RequestedLen, b: RequestedLen) -> RequestedLen {
|
||||
let (a, b) = (self.operand(a), self.operand(b));
|
||||
let at = self.nodes.combine(op, a, b);
|
||||
self.handle(at)
|
||||
}
|
||||
|
||||
pub(crate) fn minimum(&self, request: RequestedLen, window: Px) -> Px {
|
||||
let at = self.operand(request);
|
||||
Px::from_raw(self.segment(at, Ratio::ZERO, window).fixed as i32)
|
||||
}
|
||||
|
||||
fn segment(&self, of: Operand, at: Ratio, window: Px) -> Segment {
|
||||
let index = match of {
|
||||
Operand::Linear(len) => {
|
||||
debug_assert!(
|
||||
len.leftover >= Weight::ZERO,
|
||||
"a leftover weight cannot be negative"
|
||||
);
|
||||
return Segment {
|
||||
fixed: i64::from(len.without_leftover().to_px(window).raw()),
|
||||
weight: i64::from(len.leftover.raw()),
|
||||
end: None,
|
||||
};
|
||||
}
|
||||
Operand::Node(index) => index,
|
||||
};
|
||||
let Node { op, a, b, .. } = self.nodes.node(index);
|
||||
let a = self.segment(a, at, window);
|
||||
let b = self.segment(b, at, window);
|
||||
if matches!(op, Op::Sum) {
|
||||
return a + b;
|
||||
}
|
||||
// At a crossing choose the branch to its right, so the next
|
||||
// iteration advances rather than selecting that crossing again.
|
||||
let order = a.value(at).cmp(&b.value(at)).then(a.weight.cmp(&b.weight));
|
||||
let take_a = match op {
|
||||
Op::Min => !order.is_gt(),
|
||||
_ => !order.is_lt(),
|
||||
};
|
||||
let mut selected = if take_a { a } else { b };
|
||||
selected.end = first(a.end, b.end);
|
||||
if a.weight != b.weight {
|
||||
let crossing = Ratio::new(b.fixed - a.fixed, a.weight - b.weight);
|
||||
if crossing > at {
|
||||
selected.end = first(selected.end, Some(crossing));
|
||||
}
|
||||
}
|
||||
selected
|
||||
}
|
||||
|
||||
/// Divides `room` between requests whose weights are nonnegative, one
|
||||
/// length per request. A floor can overflow the room and a cap can leave
|
||||
/// part of it unused, so the lengths need not come to `room`. Each edge
|
||||
/// is rounded from the running total rather than from the length before
|
||||
/// it, so two neighbouring slots meet exactly.
|
||||
pub(crate) fn allocate<'a>(
|
||||
&'a self,
|
||||
requests: &'a [RequestedLen],
|
||||
room: Px,
|
||||
window: Px,
|
||||
) -> impl Iterator<Item = Px> + 'a {
|
||||
let mut at = Ratio::ZERO;
|
||||
loop {
|
||||
let total = requests.iter().fold(Segment::ZERO, |total, request| {
|
||||
total + self.segment(self.operand(*request), at, window)
|
||||
});
|
||||
if total.value(at) >= i128::from(room.raw()) * i128::from(at.den) {
|
||||
break;
|
||||
}
|
||||
if total.weight != 0 {
|
||||
let solution = Ratio::new(i64::from(room.raw()) - total.fixed, total.weight);
|
||||
if total.end.is_none_or(|end| solution <= end) {
|
||||
at = solution;
|
||||
break;
|
||||
}
|
||||
}
|
||||
match total.end {
|
||||
Some(end) => at = end,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
let mut prefix = 0_i128;
|
||||
let mut previous = 0_i128;
|
||||
requests.iter().map(move |request| {
|
||||
prefix += self.segment(self.operand(*request), at, window).value(at);
|
||||
let den = i128::from(at.den);
|
||||
// Half away from zero, which is what `Fixed` rounds a division
|
||||
// to: the two decide the same edge, and a change to one of them
|
||||
// is a change to the other.
|
||||
let edge = prefix.signum() * ((prefix.abs() + den / 2) / den);
|
||||
let len = Px::from_raw((edge - previous) as i32);
|
||||
previous = edge;
|
||||
len
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq)]
|
||||
struct Ratio {
|
||||
num: i64,
|
||||
den: i64,
|
||||
}
|
||||
|
||||
impl PartialEq for Ratio {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other).is_eq()
|
||||
}
|
||||
}
|
||||
|
||||
impl Ratio {
|
||||
const ZERO: Self = Self { num: 0, den: 1 };
|
||||
|
||||
fn new(num: i64, den: i64) -> Self {
|
||||
debug_assert_ne!(den, 0, "a ratio of nothing");
|
||||
if den < 0 {
|
||||
Self {
|
||||
num: -num,
|
||||
den: -den,
|
||||
}
|
||||
} else {
|
||||
Self { num, den }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Ratio {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
(i128::from(self.num) * i128::from(other.den))
|
||||
.cmp(&(i128::from(other.num) * i128::from(self.den)))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Ratio {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Segment {
|
||||
fixed: i64,
|
||||
weight: i64,
|
||||
end: Option<Ratio>,
|
||||
}
|
||||
|
||||
impl Segment {
|
||||
const ZERO: Self = Self {
|
||||
fixed: 0,
|
||||
weight: 0,
|
||||
end: None,
|
||||
};
|
||||
|
||||
fn value(self, at: Ratio) -> i128 {
|
||||
i128::from(self.fixed) * i128::from(at.den) + i128::from(self.weight) * i128::from(at.num)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for Segment {
|
||||
type Output = Self;
|
||||
fn add(self, other: Self) -> Self {
|
||||
Self {
|
||||
fixed: self.fixed + other.fixed,
|
||||
weight: self.weight + other.weight,
|
||||
end: first(self.end, other.end),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn first(a: Option<Ratio>, b: Option<Ratio>) -> Option<Ratio> {
|
||||
match (a, b) {
|
||||
(Some(a), Some(b)) => Some(a.min(b)),
|
||||
(a, b) => a.or(b),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read-only discovery of requests through a widget's children. A request is
|
||||
/// expressed in window lengths; `rel_base` supplies the base for declarations.
|
||||
pub struct SizeRequests<'a> {
|
||||
pub(crate) arena: &'a mut RequestArena,
|
||||
pub(crate) measured: Option<&'a HashMap<WidgetId, ActiveData>>,
|
||||
pub(crate) widgets: &'a Widgets,
|
||||
pub(crate) dependencies: &'a mut Vec<WidgetId>,
|
||||
pub(crate) rel_base: Len,
|
||||
}
|
||||
|
||||
impl SizeRequests<'_> {
|
||||
pub fn sum(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
|
||||
self.arena.combine(Op::Sum, a, b)
|
||||
}
|
||||
pub fn min(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
|
||||
self.arena.combine(Op::Min, a, b)
|
||||
}
|
||||
pub fn max(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
|
||||
self.arena.combine(Op::Max, a, b)
|
||||
}
|
||||
|
||||
pub fn widget<W: ?Sized>(
|
||||
&mut self,
|
||||
child: &StrongWidget<W>,
|
||||
axis: Axis,
|
||||
) -> Option<RequestedLen> {
|
||||
self.dependencies.push(child.id());
|
||||
let rules = self.widgets.size_rules(child.id());
|
||||
let rule = &rules[axis];
|
||||
let request = match &rule.request {
|
||||
Some(request) => self.arena.import(request, self.rel_base),
|
||||
None => {
|
||||
let widget = self.widgets.get_dyn(child.id())?;
|
||||
widget.size_request(self, axis).or_else(|| {
|
||||
self.measured?
|
||||
.get(&child.id())?
|
||||
.measured()
|
||||
.map(|size| size[axis].into())
|
||||
})?
|
||||
}
|
||||
};
|
||||
Some(self.bounded(request, rule.bound))
|
||||
}
|
||||
|
||||
pub(crate) fn bounded(&mut self, request: RequestedLen, bound: Bound) -> RequestedLen {
|
||||
self.arena.bounded(request, bound.within_len(self.rel_base))
|
||||
}
|
||||
|
||||
pub fn length(&self, len: LayoutLen) -> RequestedLen {
|
||||
len.within_len(self.rel_base).into()
|
||||
}
|
||||
|
||||
pub fn inset<W: ?Sized>(
|
||||
&mut self,
|
||||
child: &StrongWidget<W>,
|
||||
axis: Axis,
|
||||
padding: Px,
|
||||
) -> Option<RequestedLen> {
|
||||
let base = self.rel_base;
|
||||
self.rel_base.px -= padding;
|
||||
let request = self.widget(child, axis);
|
||||
self.rel_base = base;
|
||||
request.map(|request| self.sum(request, Len::from_parts(Rel::ZERO, padding).into()))
|
||||
}
|
||||
}
|
||||
|
||||
// Equal fractions keep this valid even when padding makes a rel base negative.
|
||||
fn independent_order(a: LayoutLen, b: LayoutLen) -> Option<Ordering> {
|
||||
if a.rel == b.rel && a.leftover == b.leftover {
|
||||
Some(a.px.cmp(&b.px))
|
||||
} else if a.px == b.px && a.rel == b.rel {
|
||||
Some(a.leftover.cmp(&b.leftover))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
+141
-55
@@ -1,87 +1,173 @@
|
||||
use crate::{Axis, LayoutLen, Weight};
|
||||
use crate::util::impl_axis_index;
|
||||
use crate::{LayoutLen, Len, Rel, SizeRequest, 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),
|
||||
/// A preferred length and independent bounds on one axis. Without a
|
||||
/// request, the widget's drawing supplies the preferred length.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct SizeRule {
|
||||
pub request: Option<SizeRequest>,
|
||||
pub bound: Bound,
|
||||
}
|
||||
|
||||
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),
|
||||
pub const FREE: Self = Self {
|
||||
request: None,
|
||||
bound: Bound::ANY,
|
||||
};
|
||||
|
||||
pub fn min(min: Len) -> Self {
|
||||
Self::bounded(Bound {
|
||||
min: Some(min),
|
||||
max: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn max(max: Len) -> Self {
|
||||
Self::bounded(Bound {
|
||||
min: None,
|
||||
max: Some(max),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clamp(min: Len, max: Len) -> Self {
|
||||
Self::bounded(Bound {
|
||||
min: Some(min),
|
||||
max: Some(max),
|
||||
})
|
||||
}
|
||||
|
||||
/// A bound and no preferred length, so whatever the widget draws is held
|
||||
/// to it.
|
||||
fn bounded(bound: Bound) -> Self {
|
||||
Self {
|
||||
request: None,
|
||||
bound,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_fraction(&self) -> bool {
|
||||
self.exact().is_some_and(|len| len.rel != Rel::ZERO) || self.bound.has_fraction()
|
||||
}
|
||||
|
||||
/// A linear preferred length, before applying the independent bounds.
|
||||
pub fn exact(&self) -> Option<LayoutLen> {
|
||||
match self.request {
|
||||
Some(SizeRequest::Linear(len)) => 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),
|
||||
/// Requests whose final length needs allocation, including a linear
|
||||
/// share constrained by an independent bound.
|
||||
pub(crate) fn deferred(&self) -> Option<&SizeRequest> {
|
||||
let request = self.request.as_ref()?;
|
||||
match request {
|
||||
SizeRequest::Linear(len)
|
||||
if len.leftover == Weight::ZERO || self.bound == Bound::ANY =>
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
_ => Some(request),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A floor, a cap, or both, independent of the preferred length.
|
||||
/// Fractions use the incoming rel base. An allocated slot keeps the
|
||||
/// allocator's base for bounds, even when the slot narrows the widget's own.
|
||||
///
|
||||
/// A bound is a [`Len`]. Comparisons involving shares are [`SizeRequest`]s.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct Bound {
|
||||
pub min: Option<Len>,
|
||||
pub max: Option<Len>,
|
||||
}
|
||||
|
||||
impl Bound {
|
||||
/// Every length.
|
||||
pub const ANY: Self = Self {
|
||||
min: None,
|
||||
max: None,
|
||||
};
|
||||
|
||||
/// Whether either end is a fraction of the rel base, so that the same
|
||||
/// bound against a different one binds at a different length.
|
||||
pub fn has_fraction(&self) -> bool {
|
||||
[self.min, self.max]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.any(|len| len.rel != Rel::ZERO)
|
||||
}
|
||||
|
||||
/// This bound as lengths of the window, from lengths of a rel base that
|
||||
/// long.
|
||||
pub fn within_len(&self, len: Len) -> Self {
|
||||
Self {
|
||||
min: self.min.map(|min| min.within_len(len)),
|
||||
max: self.max.map(|max| max.within_len(len)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One bound per axis, as [`SizeRules`] is one rule per axis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct Bounds {
|
||||
pub x: Bound,
|
||||
pub y: Bound,
|
||||
}
|
||||
|
||||
impl Bounds {
|
||||
pub const ANY: Self = Self {
|
||||
x: Bound::ANY,
|
||||
y: Bound::ANY,
|
||||
};
|
||||
}
|
||||
|
||||
impl_axis_index!(Bounds => Bound);
|
||||
|
||||
impl From<LayoutLen> for SizeRule {
|
||||
fn from(len: LayoutLen) -> Self {
|
||||
Self::Exact(len)
|
||||
SizeRequest::from(len).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SizeRequest> for SizeRule {
|
||||
fn from(request: SizeRequest) -> Self {
|
||||
Self {
|
||||
request: Some(request),
|
||||
bound: Bound::ANY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<LayoutLen>> for SizeRule {
|
||||
fn from(len: Option<LayoutLen>) -> Self {
|
||||
len.map_or(Self::Free, Self::Exact)
|
||||
len.map_or(Self::FREE, Self::from)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
#[derive(Debug, Clone, 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,
|
||||
}
|
||||
}
|
||||
impl_axis_index!(SizeRules => SizeRule);
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut SizeRule {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
/// Box lengths chosen by the ask: a declaration, a natural-size hint, or
|
||||
/// a binding bound. These are window lengths, already resolved against the
|
||||
/// incoming rel base. Moving a drawing preserves them instead of resolving
|
||||
/// a fraction against its destination a second time.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Declared {
|
||||
pub x: Option<Len>,
|
||||
pub y: Option<Len>,
|
||||
}
|
||||
|
||||
impl Declared {
|
||||
pub const NONE: Self = Self { x: None, y: None };
|
||||
}
|
||||
|
||||
impl_axis_index!(Declared => Option<Len>);
|
||||
+55
-13
@@ -1,8 +1,8 @@
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
|
||||
use crate::{
|
||||
Axis, AxisAlign, IdLike, RegionAlign, SizeRule, SizeRules, StrongWidget, WeakWidget, Widget,
|
||||
WidgetData, WidgetId,
|
||||
Axis, AxisAlign, Bound, IdLike, Len, RegionAlign, SizeRequest, SizeRule, SizeRules,
|
||||
StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
|
||||
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
||||
};
|
||||
|
||||
@@ -30,6 +30,14 @@ impl Widgets {
|
||||
!self.needs_redraw.is_empty()
|
||||
}
|
||||
|
||||
/// Marks this widget for the next frame to draw again, with nothing about
|
||||
/// it changed. Taking a widget mutably marks it too, which is the ordinary
|
||||
/// content-change signal; this is for a change the borrow cannot express,
|
||||
/// and for asking for the same tree over again.
|
||||
pub fn mark_for_redraw(&mut self, id: impl IdLike) {
|
||||
self.needs_redraw.insert(id.id());
|
||||
}
|
||||
|
||||
pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> {
|
||||
Some(self.vec.get(id)?.widget.as_ref())
|
||||
}
|
||||
@@ -41,14 +49,14 @@ impl Widgets {
|
||||
|
||||
/// get_dyn but dynamic borrow checking of widgets
|
||||
/// lets you do recursive (tree) operations, like the painter does
|
||||
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> {
|
||||
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> DynBorrower<'a, dyn Widget> {
|
||||
// SAFETY: must guarantee no other mutable references to this widget exist
|
||||
// done through the borrow variable
|
||||
let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) };
|
||||
if data.borrowed {
|
||||
panic!("tried to mutably borrow the same widget twice");
|
||||
}
|
||||
WidgetWrapper::new(data.widget.as_mut(), &mut data.borrowed)
|
||||
DynBorrower::new(data.widget.as_mut(), &mut data.borrowed)
|
||||
}
|
||||
|
||||
pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget>
|
||||
@@ -120,8 +128,8 @@ impl Widgets {
|
||||
}
|
||||
|
||||
/// 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
|
||||
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
|
||||
@@ -130,13 +138,49 @@ impl Widgets {
|
||||
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 {
|
||||
if data.size[axis] == rule {
|
||||
return;
|
||||
}
|
||||
*data.size.axis_mut(axis) = rule;
|
||||
data.size[axis] = rule;
|
||||
self.needs_redraw.insert(id);
|
||||
}
|
||||
|
||||
/// Changes the preferred length, leaving the bounds beside it alone.
|
||||
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<SizeRequest>) {
|
||||
let id = id.id();
|
||||
let request = Some(len.into());
|
||||
let rule = &mut self.data_mut(id).unwrap().size[axis];
|
||||
if rule.request == request {
|
||||
return;
|
||||
}
|
||||
rule.request = request;
|
||||
self.needs_redraw.insert(id);
|
||||
}
|
||||
|
||||
/// Puts a floor under this widget's length on one axis, keeping a cap it
|
||||
/// already had and the preferred length beside it.
|
||||
pub fn set_min_len(&mut self, id: impl IdLike, axis: Axis, min: Len) {
|
||||
self.edit_bound(id.id(), axis, |bound| bound.min = Some(min));
|
||||
}
|
||||
|
||||
/// Puts a cap over it, keeping a floor it already had.
|
||||
pub fn set_max_len(&mut self, id: impl IdLike, axis: Axis, max: Len) {
|
||||
self.edit_bound(id.id(), axis, |bound| bound.max = Some(max));
|
||||
}
|
||||
|
||||
/// Edits one axis's bound where it sits, rather than reading the whole
|
||||
/// rule out and writing it back: an expression beside the bound is not
|
||||
/// this edit's business, and copying it to move one end would be the
|
||||
/// only thing here that ever copies one.
|
||||
fn edit_bound(&mut self, id: WidgetId, axis: Axis, edit: impl FnOnce(&mut Bound)) {
|
||||
let bound = &mut self.data_mut(id).unwrap().size[axis].bound;
|
||||
let before = *bound;
|
||||
edit(bound);
|
||||
if *bound != before {
|
||||
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
|
||||
@@ -147,14 +191,14 @@ impl Widgets {
|
||||
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 {
|
||||
if data.align[axis] == align {
|
||||
return;
|
||||
}
|
||||
*data.align.axis_mut(axis) = align;
|
||||
data.align[axis] = align;
|
||||
self.needs_redraw.insert(id);
|
||||
}
|
||||
|
||||
/// Both axes at once, for a caller holding a pair.
|
||||
/// Both axes at once.
|
||||
pub fn set_size_rules(
|
||||
&mut self,
|
||||
id: impl IdLike,
|
||||
@@ -188,8 +232,6 @@ impl Default for Widgets {
|
||||
}
|
||||
}
|
||||
|
||||
pub type WidgetWrapper<'a> = DynBorrower<'a, dyn Widget>;
|
||||
|
||||
impl<I: IdLike> std::ops::Index<I> for Widgets
|
||||
where
|
||||
I::Widget: Sized + Widget,
|
||||
|
||||
@@ -20,10 +20,14 @@ impl DefaultAppState for Client {
|
||||
let pad_test = (
|
||||
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
|
||||
.color(Color::RED)
|
||||
.sized((100, 100))
|
||||
.center()
|
||||
.wrapper()
|
||||
.width(leftover(2)),
|
||||
(
|
||||
rrect.color(Color::ORANGE),
|
||||
@@ -143,7 +147,7 @@ impl DefaultAppState for Client {
|
||||
.span(Dir::DOWN)
|
||||
.add(rsc);
|
||||
|
||||
let main = WidgetPtr::new().add(rsc);
|
||||
let main = Wrapper::new().add(rsc);
|
||||
|
||||
let vals = Rc::new(RefCell::new((0, Vec::new())));
|
||||
let mut switch_button = |color, to: WeakWidget, label| {
|
||||
|
||||
+7
-3
@@ -28,10 +28,14 @@ impl DefaultAppState for State {
|
||||
.pad(16)
|
||||
.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 = (
|
||||
wtext("left").size(24).text_align(Align::LEFT),
|
||||
wtext("centred").size(24).text_align(Align::CENTER),
|
||||
wtext("right").size(24).text_align(Align::RIGHT),
|
||||
label("left", Align::LEFT),
|
||||
label("centred", Align::H_CENTER),
|
||||
label("right", Align::RIGHT),
|
||||
)
|
||||
.span(Dir::DOWN)
|
||||
.gap(8)
|
||||
|
||||
+10
-3
@@ -18,6 +18,7 @@ struct Input {
|
||||
}
|
||||
|
||||
struct InputFn {
|
||||
attrs: Vec<Attribute>,
|
||||
sig: Signature,
|
||||
body: Block,
|
||||
}
|
||||
@@ -32,9 +33,10 @@ impl Parse for Input {
|
||||
input.parse::<Token![;]>()?;
|
||||
let mut fns = Vec::new();
|
||||
while !input.is_empty() {
|
||||
let attrs = input.call(Attribute::parse_outer)?;
|
||||
let sig = input.parse()?;
|
||||
let body = input.parse()?;
|
||||
fns.push(InputFn { sig, body })
|
||||
fns.push(InputFn { attrs, sig, body })
|
||||
}
|
||||
if !input.is_empty() {
|
||||
input.error("function expected");
|
||||
@@ -59,10 +61,15 @@ pub fn widget_trait(input: TokenStream) -> TokenStream {
|
||||
fns,
|
||||
} = parse_macro_input!(input as Input);
|
||||
|
||||
let sigs: Vec<_> = fns.iter().map(|f| f.sig.clone()).collect();
|
||||
// What a method says about itself belongs on the trait, where a reader
|
||||
// looks it up; the implementation is the same text and says it again.
|
||||
let sigs: Vec<_> = fns
|
||||
.iter()
|
||||
.map(|InputFn { attrs, sig, .. }| quote! { #(#attrs)* #sig })
|
||||
.collect();
|
||||
let impls: Vec<_> = fns
|
||||
.iter()
|
||||
.map(|InputFn { sig, body }| quote! { #sig #body })
|
||||
.map(|InputFn { attrs, sig, body }| quote! { #(#attrs)* #sig #body })
|
||||
.collect();
|
||||
|
||||
let Some(GenericParam::Type(state)) = generics.params.first() else {
|
||||
|
||||
+11
-6
@@ -106,11 +106,16 @@ 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%@*}
|
||||
# The extent `replay-touch` positions against, so a script's coordinates are
|
||||
# the output's own pixels. Set beside every mode change, since a gesture
|
||||
# scaled against a mode the output no longer has lands somewhere else and
|
||||
# still looks like a run that worked.
|
||||
set_mode() {
|
||||
swaymsg output HEADLESS-1 mode "$1" >/dev/null
|
||||
out_w=${1%x*}
|
||||
out_h=${1#*x}; out_h=${out_h%@*}
|
||||
}
|
||||
set_mode "$mode"
|
||||
|
||||
# Built before the app starts, so a compile error is not reported as a
|
||||
# window that failed to move.
|
||||
@@ -149,7 +154,7 @@ while [ $i -lt "$((seconds * 2))" ]; do
|
||||
done
|
||||
|
||||
if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
swaymsg output HEADLESS-1 mode "$resize" >/dev/null
|
||||
set_mode "$resize"
|
||||
echo "run-headless: resized to $resize" >&2
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 191 B |
+4
-3
@@ -15,9 +15,10 @@ where
|
||||
let region = ctx.data.render.window_region(&id).unwrap();
|
||||
let id_pos = region.top_left;
|
||||
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
|
||||
// The pointer arrives from the platform in floats; everything
|
||||
// it is compared against is on the grid.
|
||||
let pos = (PxVec2::from_f32(ctx.data.pos) + container_pos - id_pos).to_f32();
|
||||
// The two regions are on the grid and the pointer is not, so the
|
||||
// step between them is taken there and the pointer keeps the
|
||||
// precision the platform gave it.
|
||||
let pos = ctx.data.pos + (container_pos - id_pos).to_f32();
|
||||
let size = region.size().to_f32();
|
||||
select(
|
||||
rsc,
|
||||
|
||||
+1
-1
@@ -251,7 +251,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
ui_state.renderer.draw();
|
||||
}
|
||||
WindowEvent::Resized(size) => {
|
||||
render.resize((size.width, size.height));
|
||||
render.resize((size.width, size.height), rsc.widgets_mut());
|
||||
ui_state.renderer.resize(size)
|
||||
}
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
|
||||
@@ -22,12 +22,12 @@ impl UiRenderer {
|
||||
}
|
||||
|
||||
pub fn draw(&mut self) {
|
||||
let output = match self.surface.get_current_texture() {
|
||||
CurrentSurfaceTexture::Success(texture) => texture,
|
||||
CurrentSurfaceTexture::Suboptimal(texture) => {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
texture
|
||||
}
|
||||
let (output, suboptimal) = match self.surface.get_current_texture() {
|
||||
CurrentSurfaceTexture::Success(texture) => (texture, false),
|
||||
// Used for this frame, and the swapchain rebuilt after it has
|
||||
// been presented: configuring the surface while a texture it
|
||||
// handed out is still alive panics.
|
||||
CurrentSurfaceTexture::Suboptimal(texture) => (texture, true),
|
||||
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
return;
|
||||
@@ -60,6 +60,9 @@ impl UiRenderer {
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.window.pre_present_notify();
|
||||
self.queue.present(output);
|
||||
if suboptimal {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
|
||||
|
||||
+7
-7
@@ -144,9 +144,9 @@ impl Harness {
|
||||
// bound that comes with `SyncSender` is far past anything a test
|
||||
// leaves unread.
|
||||
let (send, updates) = sync_channel(1024);
|
||||
let rsc = DefaultRsc::init(Arc::new(Queue(send)));
|
||||
let mut rsc = DefaultRsc::init(Arc::new(Queue(send)));
|
||||
let mut render = UiRenderState::new();
|
||||
render.resize(size);
|
||||
render.resize(size, rsc.widgets_mut());
|
||||
Self {
|
||||
rsc,
|
||||
render,
|
||||
@@ -161,14 +161,14 @@ impl Harness {
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||
self.render.resize(size);
|
||||
self.render.resize(size, self.rsc.widgets_mut());
|
||||
}
|
||||
|
||||
/// Changes a length rule after the fact, the way `.width()` sets one.
|
||||
/// Changes a length rule after the fact, the way `.width()` sets one --
|
||||
/// which leaves a bound beside it alone, where writing the whole rule
|
||||
/// would drop it and pass the case for the wrong reason.
|
||||
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()));
|
||||
self.rsc.widgets_mut().set_len(id, axis, len.into());
|
||||
}
|
||||
|
||||
/// Sets the root and lays it out, so a pointer event has something to hit.
|
||||
|
||||
+799
-174
File diff suppressed because it is too large.
Load diff
+10
-1
@@ -12,7 +12,16 @@ impl Widget for Image {
|
||||
}
|
||||
|
||||
fn size_hint(&self, axis: Axis) -> Option<LayoutLen> {
|
||||
Some(LayoutLen::px(self.handle.size().axis(axis)))
|
||||
Some(LayoutLen::px(self.handle.size()[axis]))
|
||||
}
|
||||
}
|
||||
|
||||
impl Image {
|
||||
/// One texture already uploaded, for a caller holding its handle: [`image()`]
|
||||
/// uploads what it is given, and several widgets showing one picture want
|
||||
/// one upload and one slot between them.
|
||||
pub fn new(handle: TextureHandle) -> Self {
|
||||
Self { handle }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-6
@@ -6,13 +6,20 @@ pub struct Masked {
|
||||
|
||||
impl Widget for Masked {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
painter.set_mask(painter.region());
|
||||
painter.set_mask(UiRegion::FULL);
|
||||
painter.widget(&self.inner);
|
||||
// What it occupies is its box, on both axes, for the reason `Scroll`
|
||||
// reports the same: it clips what is inside to that box, so it can
|
||||
// neither take less of one nor honestly ask for more. Passing the
|
||||
// inner size up instead asks to be placed at a length it does not
|
||||
// draw, and the framework would place the drawing it clipped away.
|
||||
// What it occupies is its box, on both axes, because it clips what is
|
||||
// inside to that box: it can neither take less of one nor honestly ask
|
||||
// for more. Passing the inner size up instead asks to be placed at a
|
||||
// length it does not draw, and the framework would place the drawing it
|
||||
// clipped away. `Scroll` reports its box too, for a reason of its own:
|
||||
// it is a viewport whose content is positioned by a move rather than
|
||||
// clipped, since masking is a capability a caller opts into by putting
|
||||
// one of these around it.
|
||||
Size::LEFTOVER
|
||||
}
|
||||
|
||||
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||
Some(LayoutLen::LEFTOVER)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,15 +1,15 @@
|
||||
mod image;
|
||||
mod mask;
|
||||
mod position;
|
||||
mod ptr;
|
||||
mod rect;
|
||||
mod text;
|
||||
mod trait_fns;
|
||||
mod wrapper;
|
||||
|
||||
pub use image::*;
|
||||
pub use mask::*;
|
||||
pub use position::*;
|
||||
pub use ptr::*;
|
||||
pub use rect::*;
|
||||
pub use text::*;
|
||||
pub use trait_fns::*;
|
||||
pub use wrapper::*;
|
||||
@@ -6,6 +6,10 @@ pub struct LayerOffset {
|
||||
}
|
||||
|
||||
impl Widget for LayerOffset {
|
||||
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||
requests.widget(&self.inner, axis)
|
||||
}
|
||||
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
for _ in 0..self.offset {
|
||||
painter.next_layer();
|
||||
|
||||
@@ -6,8 +6,13 @@ pub struct Offset {
|
||||
}
|
||||
|
||||
impl Widget for Offset {
|
||||
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||
requests.widget(&self.inner, axis)
|
||||
}
|
||||
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let region = UiRegion::FULL.offset(self.amt);
|
||||
painter.widget_within(&self.inner, region).size()
|
||||
painter
|
||||
.widget_at(&self.inner, UiRegion::FULL.offset(self.amt))
|
||||
.size()
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,31 @@ pub struct Pad {
|
||||
}
|
||||
|
||||
impl Widget for Pad {
|
||||
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||
requests.inset(&self.inner, axis, self.padding.along(axis))
|
||||
}
|
||||
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let inner = painter
|
||||
.widget_aligned(&self.inner, self.padding.region(), RegionAlign::NEAR)
|
||||
.size();
|
||||
// The inner's own alignment, not the near edge. This reports the
|
||||
// inner's size plus the padding, so where the box is that answer the
|
||||
// inset box is exactly the inner and alignment has no room to move
|
||||
// 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.
|
||||
//
|
||||
// Padding is an inset of both: it comes off the rel base, so `rel(1)`
|
||||
// under it fills this widget rather than overflowing it by the
|
||||
// padding, and it comes off the box, so what is drawn sits inside.
|
||||
// The two stay distinct -- the box can be narrower still, where a row
|
||||
// asked this widget in the room left, and a text wraps at that.
|
||||
let inner = painter.widget_at(&self.inner, self.padding.region()).size();
|
||||
Size {
|
||||
x: LayoutLen {
|
||||
px: inner.x.px + self.padding.left + self.padding.right,
|
||||
px: inner.x.px + self.padding.along(Axis::X),
|
||||
..inner.x
|
||||
},
|
||||
y: LayoutLen {
|
||||
px: inner.y.px + self.padding.top + self.padding.bottom,
|
||||
px: inner.y.px + self.padding.along(Axis::Y),
|
||||
..inner.y
|
||||
},
|
||||
}
|
||||
@@ -47,14 +61,28 @@ impl Padding {
|
||||
bottom: amt,
|
||||
}
|
||||
}
|
||||
pub fn region(&self) -> UiRegion {
|
||||
let mut region = UiRegion::FULL;
|
||||
|
||||
/// Both sides of one axis together, which is what this padding takes
|
||||
/// of a length along it.
|
||||
pub fn along(&self, axis: Axis) -> Px {
|
||||
match axis {
|
||||
Axis::X => self.left + self.right,
|
||||
Axis::Y => self.top + self.bottom,
|
||||
}
|
||||
}
|
||||
|
||||
/// `region` less this padding on each side.
|
||||
pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
|
||||
region.x.start.px += self.left;
|
||||
region.y.start.px += self.top;
|
||||
region.x.end.px -= self.right;
|
||||
region.y.end.px -= self.bottom;
|
||||
region
|
||||
}
|
||||
|
||||
pub fn region(&self) -> UiRegion {
|
||||
self.region_of(UiRegion::FULL)
|
||||
}
|
||||
pub fn x(amt: impl UiNum) -> Self {
|
||||
let amt = Px::from_num(amt);
|
||||
Self {
|
||||
|
||||
@@ -12,64 +12,68 @@ pub struct Scroll {
|
||||
impl Widget for Scroll {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let container_len = painter.px_len(self.axis);
|
||||
// Draw in the whole container only when its scrolling-axis length is
|
||||
// not already known, then draw it at the scrolled offset.
|
||||
let answer_len = match painter.known_len(&self.inner, self.axis, UiRegion::FULL) {
|
||||
Some(len) => len,
|
||||
None => painter.widget(&self.inner).size().axis(self.axis),
|
||||
};
|
||||
let content = answer_len.apply_leftover();
|
||||
// Asked in the whole viewport, then put at the scrolled offset.
|
||||
let answer_len = painter
|
||||
.widget_at(&self.inner, PlaceDesc::WHOLE.fills())
|
||||
.len(self.axis);
|
||||
let answer_px = painter.to_px(answer_len.without_leftover(), self.axis);
|
||||
self.container_len = container_len;
|
||||
self.content_len = content.to_px(container_len);
|
||||
self.content_len = answer_px.max(container_len);
|
||||
|
||||
if self.snap_end {
|
||||
self.amt = self.content_len - self.container_len;
|
||||
}
|
||||
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 {
|
||||
// Reading the box in pixels above holds this drawing to that one
|
||||
// length, so these two say where it holds more widely.
|
||||
//
|
||||
// Content of a fixed length that fits is handed the whole box below,
|
||||
// and nothing here reads the box again, so every longer box gives the
|
||||
// same drawing: it holds from the length the content needs upwards,
|
||||
// and shrinking past that is what changes it. Where it sits in a box
|
||||
// longer than itself is not this widget's to say -- placing its
|
||||
// answer in the whole box is its own alignment, and that placement is
|
||||
// a fraction of the box, so it holds at every length too.
|
||||
//
|
||||
// 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 answer_is_px = answer_len.is_px();
|
||||
if answer_is_px && self.content_len <= self.container_len {
|
||||
painter.holds(self.axis, answer_px..=Px::MAX);
|
||||
} else if answer_is_px && !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
|
||||
// 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);
|
||||
// Content that fills the viewport is the viewport, and is handed back
|
||||
// as it came -- it has nothing to scroll through, so the clamp above
|
||||
// has already put `amt` at zero. 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 content = match self.content_len > self.container_len {
|
||||
true => {
|
||||
let start = Len::from_parts(Rel::ZERO, -self.amt);
|
||||
UiSpan::new(start, start.offset(self.content_len)).shifted_desc()
|
||||
}
|
||||
painter.widget_aligned(&self.inner, region, RegionAlign::NEAR);
|
||||
false => PlaceDescAxis::WHOLE,
|
||||
};
|
||||
// The viewport is the inner's rel base, so a fraction it declares or
|
||||
// reports is a fraction of what is on screen rather than of the
|
||||
// content box its own answer decided. Where it goes is the content
|
||||
// box, scrolled: its drawing moved there, not made again there.
|
||||
painter.place_at(&self.inner, content.on_axis(self.axis).fills());
|
||||
// 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
|
||||
}
|
||||
|
||||
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||
Some(LayoutLen::LEFTOVER)
|
||||
}
|
||||
}
|
||||
|
||||
impl Scroll {
|
||||
|
||||
+170
-109
@@ -8,76 +8,75 @@ pub struct Span {
|
||||
}
|
||||
|
||||
impl Widget for Span {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let axis = self.dir.axis;
|
||||
// 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 {
|
||||
let mut span = UiSpan::new(cursor, Len::rel_max());
|
||||
if self.dir.sign == Sign::Neg {
|
||||
span.flip();
|
||||
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||
if axis != self.dir.axis {
|
||||
// A share can be hidden when the other axis has no room. Its
|
||||
// cross-axis length then contributes nothing to the drawn answer.
|
||||
return None;
|
||||
}
|
||||
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
||||
let len = match painter.known_len(child, axis, region) {
|
||||
Some(len) => len,
|
||||
None => painter.widget_within(child, region).len(axis),
|
||||
};
|
||||
cursor.px += len.px + self.gap;
|
||||
cursor.rel += len.rel;
|
||||
lens.push(len);
|
||||
let mut total = RequestedLen::from(Len::from_parts(Rel::ZERO, self.gaps()));
|
||||
for child in &self.children {
|
||||
let child = requests.widget(child, axis)?;
|
||||
total = requests.sum(total, child);
|
||||
}
|
||||
Some(total)
|
||||
}
|
||||
|
||||
let gaps = self
|
||||
.gap
|
||||
.mul_int(self.children.len().saturating_sub(1) as i32);
|
||||
let total = lens.iter().fold(
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
painter.with_requests(|painter, lens, values| self.layout(painter, lens, values))
|
||||
}
|
||||
}
|
||||
|
||||
impl Span {
|
||||
fn layout(
|
||||
&self,
|
||||
painter: &mut Painter,
|
||||
lens: &mut Vec<RequestedLen>,
|
||||
values: &mut Vec<Px>,
|
||||
) -> Size {
|
||||
let axis = self.dir.axis;
|
||||
// The row this span lays its children out along, as a length of the
|
||||
// rel base they are laid out against. Where it starts is nothing's
|
||||
// business -- a slot is a length from there -- so what this reads is
|
||||
// the length alone.
|
||||
let row = painter.region_len(axis);
|
||||
self.collect(painter, row, lens, true);
|
||||
let gaps = self.gaps();
|
||||
let fixed = lens
|
||||
.iter()
|
||||
.try_fold(Len::from_parts(Rel::ZERO, gaps), |sum, len| {
|
||||
Some(sum + len.linear()?.without_leftover())
|
||||
});
|
||||
if let Some(fixed) = fixed
|
||||
&& lens.iter().any(|len| len.has_leftover())
|
||||
&& !painter.longer_than(row, fixed, axis)
|
||||
{
|
||||
// With no share to assign, intrinsic drawings keep the remaining
|
||||
// offer, including overflow. Their answer is only moved into a slot.
|
||||
self.collect(painter, row, lens, false);
|
||||
}
|
||||
let nonlinear = lens.iter().any(|len| len.linear().is_none());
|
||||
if nonlinear {
|
||||
painter.allocate(lens, row - Len::from_parts(Rel::ZERO, gaps), axis, values);
|
||||
}
|
||||
let allocated = nonlinear.then(|| &values[..]);
|
||||
let total = match allocated {
|
||||
Some(allocated) => LayoutLen {
|
||||
px: allocated.iter().fold(gaps, |sum, len| sum + *len),
|
||||
..LayoutLen::ZERO
|
||||
},
|
||||
None => lens.iter().fold(
|
||||
LayoutLen {
|
||||
px: gaps,
|
||||
..LayoutLen::ZERO
|
||||
},
|
||||
|sum, len| sum + *len,
|
||||
);
|
||||
|
||||
// Whether anything is left over is a question in pixels: `rel(0.5)`
|
||||
// beside 300 px is full at 600 and overfull at 400. The room to
|
||||
// divide is `len * fixed - total.px`, and the length where it runs
|
||||
// out is exactly the box a parent sizing itself from this answer
|
||||
// hands back -- which is why this used to need a margin either side
|
||||
// of the boundary, and why it does not now: that box and this sum are
|
||||
// whole counts of the same step, and both routes to it land on the
|
||||
// same count. What the generated oracle checks is the consequence,
|
||||
// since which children exist at all turns on this.
|
||||
let fixed = Rel::ONE - total.rel;
|
||||
let mut shares = false;
|
||||
if total.leftover > Weight::ZERO {
|
||||
let current = painter.px_len(axis);
|
||||
let holds = if fixed > Rel::ZERO {
|
||||
// The box length the fixed parts alone fill.
|
||||
let full = total.px.div(fixed);
|
||||
shares = current > full;
|
||||
match shares {
|
||||
true => Holds::from(full.next_up()..=Px::MAX),
|
||||
false => Holds::from(Px::MIN..=full),
|
||||
}
|
||||
} else if fixed < Rel::ZERO {
|
||||
// The relative parts grow faster than the box does, so here
|
||||
// a shorter box is the one that leaves room.
|
||||
let full = total.px.div(fixed);
|
||||
shares = current < full;
|
||||
match shares {
|
||||
true => Holds::from(Px::MIN..=full.next_down()),
|
||||
false => Holds::from(full..=Px::MAX),
|
||||
}
|
||||
} else {
|
||||
// The relative parts take exactly the box, whatever it is, so
|
||||
// the only room is what negative pixels leave.
|
||||
shares = total.px < Px::ZERO;
|
||||
Holds::ANY
|
||||
|sum, len| sum + len.linear().unwrap(),
|
||||
),
|
||||
};
|
||||
painter.holds(axis, holds);
|
||||
}
|
||||
let all_fixed = total.without_leftover();
|
||||
let room = row - all_fixed;
|
||||
let any_leftover = total.leftover > Weight::ZERO;
|
||||
let has_room = any_leftover && painter.longer_than(row, all_fixed, axis);
|
||||
|
||||
// Across itself a span is as long as its longest child -- unless a
|
||||
// rule beside it gives that length outright, and then reading them
|
||||
@@ -87,82 +86,144 @@ impl Widget for Span {
|
||||
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();
|
||||
// spoken for. Both ends of a slot are read from those two rather
|
||||
// than stepped from the last child: the share of the room is
|
||||
// rounded, and taking each end from the one before it would carry
|
||||
// every rounding along the row.
|
||||
let mut fixed = Len::ZERO;
|
||||
let mut taken = Weight::ZERO;
|
||||
let room = Len::rel_max() - Len::from_parts(total.rel, total.px);
|
||||
let mut start = Len::rel_min();
|
||||
let mut ortho = LayoutLen::ZERO;
|
||||
for (child, len) in self.children.iter().zip(&lens) {
|
||||
// A child asking for nothing but a part of what is left over,
|
||||
// when nothing is, is not drawn at all. One that also asked for
|
||||
// pixels or a fraction keeps those and overflows.
|
||||
if len.leftover > Weight::ZERO && len.px == Px::ZERO && len.rel == Rel::ZERO && !shares
|
||||
{
|
||||
// Nothing divides the room where no child asked for any of it, and a
|
||||
// ratio of a whole of nothing has no answer.
|
||||
let reached = |fixed: Len, taken: Weight| match any_leftover {
|
||||
false => fixed,
|
||||
true => fixed + room.scale(Rel::ratio(taken, total.leftover)),
|
||||
};
|
||||
for (index, (child, request)) in self.children.iter().zip(lens.iter()).enumerate() {
|
||||
// An allocated row already has a length for every child; without
|
||||
// one the request is the length and the room is divided here.
|
||||
// Either way a child asking for nothing but a part of what is
|
||||
// left over, when nothing is, is not drawn at all -- one that
|
||||
// also asked for pixels or a fraction keeps those and overflows.
|
||||
let (len, shares, nothing_left) = match allocated {
|
||||
Some(allocated) => {
|
||||
let len = LayoutLen {
|
||||
px: allocated[index],
|
||||
..LayoutLen::ZERO
|
||||
};
|
||||
let shares = request.has_leftover();
|
||||
(len, shares, shares && len.px == Px::ZERO)
|
||||
}
|
||||
None => {
|
||||
let len = request.linear().unwrap();
|
||||
let shares = len.leftover > Weight::ZERO && has_room;
|
||||
(len, shares, len.is_only_leftover() && !has_room)
|
||||
}
|
||||
};
|
||||
if nothing_left {
|
||||
painter.undraw(child);
|
||||
fixed.px += self.gap;
|
||||
continue;
|
||||
}
|
||||
let mut span = UiSpan::FULL;
|
||||
span.start = start;
|
||||
if len.leftover > Weight::ZERO && shares {
|
||||
let from = reached(fixed, taken);
|
||||
if shares {
|
||||
taken += len.leftover;
|
||||
}
|
||||
fixed.px += len.px;
|
||||
fixed.rel += len.rel;
|
||||
start = shared(fixed, taken, total.leftover, room);
|
||||
span.end = start;
|
||||
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
||||
if self.dir.sign == Sign::Neg {
|
||||
region.flip(axis);
|
||||
fixed += len.without_leftover();
|
||||
let to = reached(fixed, taken);
|
||||
// Along the row the span says where the child goes, and that slot
|
||||
// is the child's box outright rather than something to place an
|
||||
// answer inside again. A share is decided here and nowhere
|
||||
// else: its slot narrows its rel base, and the child is asked in
|
||||
// it, since a text wraps at the width it is actually given. A
|
||||
// fixed child's slot is its own answer, so a drawing made in the
|
||||
// room is put there as it is, and one not made yet is made here.
|
||||
let slot = self.slot(row, from, to);
|
||||
let mut place = slot.shifted_desc().allocated().on_axis(axis);
|
||||
if shares {
|
||||
place = place.rel_base(axis, slot.len());
|
||||
}
|
||||
let placed = painter.widget_within(child, region);
|
||||
let used = painter.place_at(child, place).len(!axis);
|
||||
if shrinks {
|
||||
let used = placed.len(!axis);
|
||||
// Choosing between a fixed and a relative length from the
|
||||
// span's own eventual width admits multiple fixed points.
|
||||
// A scalable child therefore makes Children scalable too;
|
||||
// A scalable child therefore makes the span scalable too;
|
||||
// only fixed children are compared with one another.
|
||||
if used.rel != Rel::ZERO || used.leftover != Weight::ZERO {
|
||||
if !used.is_px() {
|
||||
ortho = LayoutLen::LEFTOVER;
|
||||
} else if ortho.leftover == Weight::ZERO {
|
||||
ortho.px = ortho.px.max(used.px);
|
||||
}
|
||||
}
|
||||
fixed.px += self.gap;
|
||||
start = shared(fixed, taken, total.leftover, room);
|
||||
}
|
||||
|
||||
// Carried whole rather than collapsed to one share: a span that sizes
|
||||
// from its children does not resolve `leftover`, it passes the weight up,
|
||||
// so nesting spans divides the same space rather than re-dividing a
|
||||
// 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;
|
||||
// Where nothing was allocated the weight is carried whole rather
|
||||
// than collapsed to one share, so nesting spans divides the same
|
||||
// space rather than re-dividing a share of it: four `leftover(1)`
|
||||
// children under two spans under one span get a quarter each, which
|
||||
// one share per level does not give. Resolution happens at the
|
||||
// nearest ancestor with a length, and the root always has one --
|
||||
// or, where a comparison deferred the row, at the ancestor whose
|
||||
// allocation discovery carried these requests to, and `total` is
|
||||
// pixels by the time it gets here.
|
||||
let ortho = match shrinks {
|
||||
true => ortho,
|
||||
false => LayoutLen::rel(1.0),
|
||||
};
|
||||
Size::from_axis(axis, along, ortho)
|
||||
Size::from_axis(axis, total, 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 {
|
||||
/// What the gaps between this span's children take, which is a length of
|
||||
/// the row before anything is divided.
|
||||
fn gaps(&self) -> Px {
|
||||
self.gap
|
||||
.mul_int(self.children.len().saturating_sub(1) as i32)
|
||||
}
|
||||
|
||||
fn collect(
|
||||
&self,
|
||||
painter: &mut Painter,
|
||||
row: Len,
|
||||
lens: &mut Vec<RequestedLen>,
|
||||
discover: bool,
|
||||
) {
|
||||
let axis = self.dir.axis;
|
||||
let mut cursor = Len::ZERO;
|
||||
lens.clear();
|
||||
for child in &self.children {
|
||||
let request = if discover {
|
||||
painter.size_request(child, axis)
|
||||
} else {
|
||||
painter.size_hint(child, axis).map(Into::into)
|
||||
};
|
||||
let len = match request {
|
||||
Some(len) => len,
|
||||
None => {
|
||||
let room = self.slot(row, cursor, row).shifted_desc().on_axis(axis);
|
||||
let len = painter.widget_at(child, room).len(axis);
|
||||
painter.measured_request(child, axis, len)
|
||||
}
|
||||
};
|
||||
cursor += painter.minimum_request(&len, axis);
|
||||
cursor.px += self.gap;
|
||||
lens.push(len);
|
||||
}
|
||||
}
|
||||
|
||||
/// The stretch of the row between two distances from where this span
|
||||
/// starts laying children out, as a span of its own box. A negative
|
||||
/// direction lays out from the far end, so the same two distances mirror
|
||||
/// in a row `row` long.
|
||||
fn slot(&self, row: Len, from: Len, to: Len) -> UiSpan {
|
||||
match self.dir.sign {
|
||||
Sign::Pos => from.to(to),
|
||||
Sign::Neg => (row - to).to(row - from),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty(dir: Dir) -> Self {
|
||||
Self {
|
||||
children: Vec::new(),
|
||||
|
||||
@@ -8,31 +8,67 @@ pub struct Stack {
|
||||
}
|
||||
|
||||
impl Widget for Stack {
|
||||
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||
let sizing = match self.size {
|
||||
StackSize::Default => None,
|
||||
StackSize::Child(i) => self.children.get(i),
|
||||
};
|
||||
// With nothing sizing it a stack is a share of the box it is given,
|
||||
// which is what its draw answers too.
|
||||
match sizing {
|
||||
Some(child) => requests.widget(child, axis),
|
||||
None => Some(LayoutLen::LEFTOVER.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let sizing = match self.size {
|
||||
StackSize::Default => None,
|
||||
StackSize::Child(i) => Some(i),
|
||||
};
|
||||
// Whichever child sizes the stack decides the box every child gets.
|
||||
// The stack reports that size, so a child given a longer box would
|
||||
// draw outside what the stack says it occupies.
|
||||
// Whichever child sizes the stack is given the stack's whole box --
|
||||
// the stack is the length that child asked for, so placing that
|
||||
// answer inside the box it decided would apply it twice.
|
||||
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) {
|
||||
// On the layer that child ends up on, so the ask below is a reuse
|
||||
// rather than a second drawing of it somewhere else: a retained
|
||||
// drawing belongs to the layer it was made on.
|
||||
Some((i, child)) => {
|
||||
painter.child_layer_at(i);
|
||||
painter.widget(child).size()
|
||||
painter.widget_at(child, PlaceDesc::WHOLE.fills()).size()
|
||||
}
|
||||
None => Size::LEFTOVER,
|
||||
};
|
||||
let region = painter.box_of(size);
|
||||
// Every other child gets the box the sizing child decided: the
|
||||
// stack is that length, so that is the box they are asked in, and a
|
||||
// fraction under them is a fraction of it. A share leaves the axis
|
||||
// to whoever gave the stack its box. Where a child sits in a box
|
||||
// bigger than itself is its own business.
|
||||
let place = PlaceDesc::from_axes(|axis| {
|
||||
let len = size[axis];
|
||||
match len.leftover == Weight::ZERO {
|
||||
true => len.without_leftover().as_desc().fills(),
|
||||
false => PlaceDescAxis::WHOLE,
|
||||
}
|
||||
});
|
||||
for (i, child) in self.children.iter().enumerate() {
|
||||
if sizing == Some(i) {
|
||||
continue;
|
||||
}
|
||||
painter.child_layer_at(i);
|
||||
painter.widget_aligned(child, region, RegionAlign::NEAR);
|
||||
painter.widget_at(child, place);
|
||||
}
|
||||
size
|
||||
}
|
||||
|
||||
/// Without a sizing child a stack is whatever box it is given, which it
|
||||
/// can say without drawing anything.
|
||||
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||
match self.size {
|
||||
StackSize::Default => Some(LayoutLen::LEFTOVER),
|
||||
StackSize::Child(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
|
||||
@@ -321,12 +321,10 @@ impl<'a> TextEditCtx<'a> {
|
||||
let old = (self.text.view.buf.text().to_string(), self.text.selection);
|
||||
let mut undo = false;
|
||||
let res = self.apply_event_inner(event, modifiers, &mut undo);
|
||||
if undo {
|
||||
if let Some((old, selection)) = self.text.history.pop() {
|
||||
if undo && let Some((old, selection)) = self.text.history.pop() {
|
||||
self.set(&old);
|
||||
self.text.selection = selection;
|
||||
self.clamp_selection_to_layout();
|
||||
}
|
||||
} else if self.text.view.buf.text() != old.0 {
|
||||
self.text.history.push(old);
|
||||
}
|
||||
|
||||
+11
-11
@@ -50,15 +50,11 @@ impl TextView {
|
||||
let width = self.attrs.wrap.then(|| painter.px_len(Axis::X));
|
||||
// The shaper measures in floats, which is where a glyph advance comes
|
||||
// from; what it answers goes back on the grid.
|
||||
let text = painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32));
|
||||
// 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
|
||||
// could take a word that did not fit in the wider box. A line too
|
||||
// long to fit at all says nothing about narrower boxes.
|
||||
if let Some(width) = width {
|
||||
painter.holds(Axis::X, Px::from_f32(text.size.x).min(width)..=width);
|
||||
painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32));
|
||||
if width.is_some() {
|
||||
painter.holds(Axis::X, self.buf.width_holds());
|
||||
}
|
||||
text
|
||||
self.buf.rendered().expect("render_text placed the glyphs")
|
||||
}
|
||||
|
||||
pub fn tex(&self) -> Option<&RenderedText> {
|
||||
@@ -78,9 +74,13 @@ impl TextView {
|
||||
|
||||
let tex = self.render(painter);
|
||||
let region = tex.size.align(align);
|
||||
let size = Size::px(tex.size);
|
||||
let within = region.within(&painter.region());
|
||||
painter.glyphs(tex, within);
|
||||
// The step at or above what the shaper measured, so a parent that
|
||||
// hands back the length this reports hands back a box the longest
|
||||
// line fits in. Rounded to the nearest step it is half the time a
|
||||
// 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, region);
|
||||
(region, size)
|
||||
}
|
||||
|
||||
|
||||
+53
-17
@@ -19,8 +19,8 @@ widget_trait! {
|
||||
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 {
|
||||
for axis in Axis::BOTH {
|
||||
if let Some(align) = align[axis] {
|
||||
widgets.set_alignment(id, axis, align);
|
||||
}
|
||||
}
|
||||
@@ -53,32 +53,64 @@ widget_trait! {
|
||||
move |state| {
|
||||
let id = self.add(state);
|
||||
let widgets = &mut state.ui_mut().widgets;
|
||||
widgets.set_size_rule(id, Axis::X, SizeRule::Exact(size.x));
|
||||
widgets.set_size_rule(id, Axis::Y, SizeRule::Exact(size.y));
|
||||
widgets.set_len(id, Axis::X, size.x);
|
||||
widgets.set_len(id, Axis::Y, size.y);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
fn width(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||
fn width(self, len: impl Into<SizeRequest>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||
let len = len.into();
|
||||
move |state| {
|
||||
let id = self.add(state);
|
||||
state
|
||||
.ui_mut()
|
||||
.widgets
|
||||
.set_size_rule(id, Axis::X, SizeRule::Exact(len));
|
||||
state.ui_mut().widgets.set_len(id, Axis::X, len);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
fn height(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||
/// Sets a floor on this widget's offered width and reported width.
|
||||
fn min_width(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||
let len = len.into();
|
||||
move |state| {
|
||||
let id = self.add(state);
|
||||
state
|
||||
.ui_mut()
|
||||
.widgets
|
||||
.set_size_rule(id, Axis::Y, SizeRule::Exact(len));
|
||||
state.ui_mut().widgets.set_min_len(id, Axis::X, len);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
fn min_height(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||
let len = len.into();
|
||||
move |state| {
|
||||
let id = self.add(state);
|
||||
state.ui_mut().widgets.set_min_len(id, Axis::Y, len);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
/// Caps this widget's offered width and reported width.
|
||||
fn max_width(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||
let len = len.into();
|
||||
move |state| {
|
||||
let id = self.add(state);
|
||||
state.ui_mut().widgets.set_max_len(id, Axis::X, len);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
fn max_height(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||
let len = len.into();
|
||||
move |state| {
|
||||
let id = self.add(state);
|
||||
state.ui_mut().widgets.set_max_len(id, Axis::Y, len);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
fn height(self, len: impl Into<SizeRequest>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||
let len = len.into();
|
||||
move |state| {
|
||||
let id = self.add(state);
|
||||
state.ui_mut().widgets.set_len(id, Axis::Y, len);
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -134,9 +166,13 @@ widget_trait! {
|
||||
|state| self.add(state)
|
||||
}
|
||||
|
||||
fn set_ptr(self, ptr: WeakWidget<WidgetPtr>, state: &mut Rsc) {
|
||||
let id = self.add_strong(state);
|
||||
state.ui_mut().widgets[ptr].inner = Some(id);
|
||||
/// This widget in a [`Wrapper`], which is how it gets a second length or
|
||||
/// alignment beside the one it already carries. Named for the type it
|
||||
/// makes rather than as `wrapped`, which would read as the text setting.
|
||||
fn wrapper(self) -> impl WidgetFn<Rsc, Wrapper> {
|
||||
|state| Wrapper {
|
||||
inner: Some(self.add_strong(state)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
use crate::prelude::*;
|
||||
use std::marker::Unsize;
|
||||
|
||||
pub struct WidgetPtr {
|
||||
/// One widget in a box of its own, doing as little as possible on the way:
|
||||
/// 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.
|
||||
#[derive(Default)]
|
||||
pub struct Wrapper {
|
||||
pub inner: Option<StrongWidget>,
|
||||
}
|
||||
|
||||
impl Widget for WidgetPtr {
|
||||
impl Widget for Wrapper {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
match &self.inner {
|
||||
Some(id) => painter.widget(id).size(),
|
||||
@@ -14,15 +23,11 @@ impl Widget for WidgetPtr {
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetPtr {
|
||||
impl Wrapper {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
inner: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) {
|
||||
self.inner = Some(to)
|
||||
}
|
||||
@@ -34,9 +39,3 @@ impl WidgetPtr {
|
||||
self.inner.replace(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WidgetPtr {
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use iris::{harness::Harness, prelude::*};
|
||||
use std::{
|
||||
alloc::{GlobalAlloc, Layout, System},
|
||||
cell::Cell,
|
||||
};
|
||||
|
||||
struct Counting;
|
||||
thread_local! {
|
||||
static COUNT: Cell<Option<usize>> = const { Cell::new(None) };
|
||||
}
|
||||
fn count() {
|
||||
COUNT.with(|count| {
|
||||
if let Some(n) = count.get() {
|
||||
count.set(Some(n + 1));
|
||||
}
|
||||
});
|
||||
}
|
||||
// The wrapper preserves System's allocation and deallocation contracts;
|
||||
// observing calls here also counts allocations hidden inside layout helpers.
|
||||
unsafe impl GlobalAlloc for Counting {
|
||||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||
count();
|
||||
unsafe { System.alloc(layout) }
|
||||
}
|
||||
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||
unsafe { System.dealloc(ptr, layout) }
|
||||
}
|
||||
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 {
|
||||
count();
|
||||
unsafe { System.realloc(ptr, layout, size) }
|
||||
}
|
||||
}
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: Counting = Counting;
|
||||
|
||||
#[test]
|
||||
fn unchanged_tree_reuses_layout_storage() {
|
||||
for rule in [
|
||||
SizeRule::FREE,
|
||||
leftover(1).clamp(20, 80).into(),
|
||||
SizeRule::clamp(20.into(), 80.into()),
|
||||
] {
|
||||
let mut h = Harness::new((600, 200));
|
||||
let mut children: Vec<StrongWidget> = Vec::new();
|
||||
for _ in 0..8 {
|
||||
let a = rect(Color::RED).add(&mut h.rsc);
|
||||
h.rsc.widgets_mut().set_size_rule(a, Axis::X, rule.clone());
|
||||
let row = (a, rect(Color::BLUE))
|
||||
.span(Dir::RIGHT)
|
||||
.add_strong(&mut h.rsc);
|
||||
children.push(row);
|
||||
}
|
||||
let root = h.rsc.widgets_mut().add_strong(Span {
|
||||
children,
|
||||
dir: Dir::DOWN,
|
||||
gap: Px::ZERO,
|
||||
});
|
||||
h.state.root = Some(root);
|
||||
h.frame();
|
||||
let ids: Vec<_> = h.render.active.keys().copied().collect();
|
||||
for frame in 0..8 {
|
||||
for &id in &ids {
|
||||
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
h.resize((600 + frame % 2, 200));
|
||||
h.frame();
|
||||
}
|
||||
COUNT.set(Some(0));
|
||||
for frame in 0..100 {
|
||||
for &id in &ids {
|
||||
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
h.resize((600 + frame % 2, 200));
|
||||
h.frame();
|
||||
}
|
||||
let allocations = COUNT.replace(None).unwrap();
|
||||
println!("rule={rule:?}: {allocations} allocations over 100 resize frames");
|
||||
assert_eq!(allocations, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// A text drawn again at the width it already has places no glyphs and shapes
|
||||
/// nothing, so the frame costs nothing at all -- which is what the shaping
|
||||
/// cache is for, and a copy of the attrs made to ask it undid for any text
|
||||
/// naming its font family.
|
||||
///
|
||||
/// Only that case: a text drawn at a width it has not seen places its glyphs,
|
||||
/// and placing them allocates a list to hold them.
|
||||
#[test]
|
||||
fn redrawing_a_text_at_one_width_allocates_nothing() {
|
||||
let mut h = Harness::new((600, 200));
|
||||
let mut col = Span::empty(Dir::DOWN);
|
||||
let mut texts = Vec::new();
|
||||
for _ in 0..8 {
|
||||
let text =
|
||||
wtext("wrapping shapes one source into as many lines as the box leaves room for")
|
||||
.size(16)
|
||||
// Named rather than generic, because a named one is the family
|
||||
// that costs an allocation to copy.
|
||||
.family(Family::Named("sans-serif".into()))
|
||||
.wrap(true)
|
||||
.add_strong(&mut h.rsc);
|
||||
texts.push(text.id());
|
||||
col.push(text);
|
||||
}
|
||||
let root = col.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
let redraw = |h: &mut Harness| {
|
||||
for &id in &texts {
|
||||
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
h.frame();
|
||||
};
|
||||
for _ in 0..8 {
|
||||
redraw(&mut h);
|
||||
}
|
||||
COUNT.set(Some(0));
|
||||
for _ in 0..100 {
|
||||
redraw(&mut h);
|
||||
}
|
||||
let allocations = COUNT.replace(None).unwrap();
|
||||
println!("text: {allocations} allocations over 100 redraws of 8 texts");
|
||||
assert_eq!(allocations, 0);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//! CPU comparison of a bounds attribute against the `MaxSize` wrapper it
|
||||
//! replaced, using the same builder calls and the same geometry. The wrapper
|
||||
//! is gone from this tree, so its side of the comparison is run by checking
|
||||
//! out a commit that still has it: the fixture is written to build the same
|
||||
//! way at both.
|
||||
//!
|
||||
//! MODE=cap FRAMES=2000 cargo test --release --test bounds_cost \
|
||||
//! -- --ignored --nocapture
|
||||
//!
|
||||
//! `MODE` is `plain`, `exact` or `cap`; `REDRAW=1` marks every widget for
|
||||
//! redraw each frame; `FRAMES` is how many resize frames to measure. Use
|
||||
//! repeated `perf stat -e instructions:u` runs on the executable directly.
|
||||
//! Process totals include the cold frame, so compare identical modes and
|
||||
//! frame counts. Wall time on this machine is not a stable comparison.
|
||||
|
||||
mod rig;
|
||||
|
||||
use iris::{harness::Harness, prelude::*};
|
||||
use rig::env;
|
||||
|
||||
/// The two widths the loop alternates. The cap of 80 binds at 300 and does
|
||||
/// not at 100, so the measured frames cross it in both directions.
|
||||
const WIDTHS: [i32; 2] = [100, 300];
|
||||
|
||||
#[test]
|
||||
#[ignore = "instruction-count measurement"]
|
||||
fn bounds_cost() {
|
||||
let mode = env("MODE", "cap".to_string());
|
||||
let redraw = env("REDRAW", 0_u8) != 0;
|
||||
let frames = env("FRAMES", 2000_usize);
|
||||
let mut h = Harness::new((300, 512));
|
||||
let mut column = Span::empty(Dir::DOWN);
|
||||
let mut leaves = Vec::new();
|
||||
for _ in 0..128 {
|
||||
let first = match mode.as_str() {
|
||||
"plain" => rect(Color::RED).add_strong(&mut h.rsc).any(),
|
||||
"exact" => rect(Color::RED).width(40).add_strong(&mut h.rsc).any(),
|
||||
"cap" => rect(Color::RED).max_width(80).add_strong(&mut h.rsc).any(),
|
||||
_ => panic!("unknown MODE {mode}"),
|
||||
};
|
||||
leaves.push(first.id());
|
||||
let second = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||
let row = Span {
|
||||
children: vec![first, second],
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
};
|
||||
column.push(row.height(4).add_strong(&mut h.rsc));
|
||||
}
|
||||
h.set_root(column);
|
||||
let ids: Vec<_> = h.render.active.keys().copied().collect();
|
||||
println!(
|
||||
"mode={mode}, widgets={}, rule_bytes={}",
|
||||
ids.len(),
|
||||
std::mem::size_of::<SizeRule>()
|
||||
);
|
||||
// That the fixture measures what it says is checked on both sides of the
|
||||
// crossing here rather than inside the measured loop, which is what the
|
||||
// other rigs do. Per frame it measured only 0.65% of the total (5.780B
|
||||
// against 5.743B instructions at MODE=cap, FRAMES=2000), but none of it
|
||||
// is the layout the number is about.
|
||||
for width in WIDTHS {
|
||||
h.resize((width, 512));
|
||||
h.frame();
|
||||
let expected = match mode.as_str() {
|
||||
"plain" => width / 2,
|
||||
"exact" => 40,
|
||||
"cap" => (width / 2).min(80),
|
||||
_ => unreachable!("the mode was checked while building"),
|
||||
};
|
||||
for id in &leaves {
|
||||
assert_eq!(h.region(id).unwrap().size().x, Px::from_int(expected));
|
||||
}
|
||||
println!(
|
||||
"width {width}: first leaf {}",
|
||||
h.region(&leaves[0]).unwrap()
|
||||
);
|
||||
}
|
||||
for frame in 0..frames {
|
||||
if redraw {
|
||||
for &id in &ids {
|
||||
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
}
|
||||
h.resize((WIDTHS[frame % WIDTHS.len()], 512));
|
||||
h.frame();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use iris::harness::{Harness, assert_corners};
|
||||
use iris::prelude::*;
|
||||
|
||||
struct Counted {
|
||||
draws: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Widget for Counted {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.draws.set(self.draws.get() + 1);
|
||||
painter.px_size();
|
||||
painter.primitive(RectPrimitive::color(Color::RED));
|
||||
Size::LEFTOVER
|
||||
}
|
||||
|
||||
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||
Some(LayoutLen::LEFTOVER)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_capped_share_returns_room_to_its_sibling() {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let first = rect(Color::RED).max_width(80).add(&mut h.rsc);
|
||||
let second = rect(Color::BLUE).add(&mut h.rsc);
|
||||
h.set_root((first, second).span(Dir::RIGHT));
|
||||
assert_corners!(h, first, (0, 0), (80, 100));
|
||||
assert_corners!(h, second, (80, 0), (300, 100));
|
||||
h.resize((100, 100));
|
||||
h.frame();
|
||||
assert_corners!(h, first, (0, 0), (50, 100));
|
||||
assert_corners!(h, second, (50, 0), (100, 100));
|
||||
h.resize((300, 100));
|
||||
h.frame();
|
||||
assert_corners!(h, first, (0, 0), (80, 100));
|
||||
assert_corners!(h, second, (80, 0), (300, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_shares_are_discovered_without_provisional_paint() {
|
||||
let mut h = Harness::new((400, 100));
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let leaf = h.rsc.ui_mut().widgets.add_strong(Counted {
|
||||
draws: draws.clone(),
|
||||
});
|
||||
let leaf_id = leaf.id();
|
||||
let mut inner: StrongWidget = leaf;
|
||||
for _ in 0..8 {
|
||||
let sibling = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||
inner = h.rsc.ui_mut().widgets.add_strong(Span {
|
||||
children: vec![inner, sibling],
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
});
|
||||
}
|
||||
h.state.root = Some(inner);
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), 1);
|
||||
assert!(h.region(&leaf_id).is_some());
|
||||
h.resize((800, 100));
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_bounds_are_resolved_in_the_outer_allocation() {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let a = rect(Color::RED).max_width(40).add(&mut h.rsc);
|
||||
let b = rect(Color::GREEN).max_width(60).add(&mut h.rsc);
|
||||
let inner = (a, b).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let tail = rect(Color::BLUE).add(&mut h.rsc);
|
||||
h.set_root((inner, tail).span(Dir::RIGHT));
|
||||
assert_corners!(h, a, (0, 0), (40, 100));
|
||||
assert_corners!(h, b, (40, 0), (100, 100));
|
||||
assert_corners!(h, tail, (100, 0), (300, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_edits_in_a_nested_child_reach_the_allocator() {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let a = rect(Color::RED).max_width(80).add(&mut h.rsc);
|
||||
let inner = (a,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let tail = rect(Color::BLUE).add(&mut h.rsc);
|
||||
h.set_root((inner, tail).span(Dir::RIGHT));
|
||||
h.rsc.widgets_mut().set_max_len(a, Axis::X, Len::px(40.0));
|
||||
h.frame();
|
||||
assert_corners!(h, a, (0, 0), (40, 100));
|
||||
assert_corners!(h, tail, (40, 0), (300, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_a_bound_to_a_previously_unbounded_share_reallocates_the_row() {
|
||||
for hinted in [true, false] {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let a = if hinted {
|
||||
rect(Color::RED).add_strong(&mut h.rsc).any()
|
||||
} else {
|
||||
h.rsc.widgets_mut().add_strong(Unhinted).any()
|
||||
};
|
||||
let id = a.id();
|
||||
let b = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let mut row = Span::empty(Dir::RIGHT);
|
||||
row.push(a);
|
||||
row.push(b.add_strong(&mut h.rsc));
|
||||
h.set_root(row);
|
||||
h.resize((400, 100));
|
||||
h.frame();
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(id, Axis::X, SizeRule::max(Len::px(80.0)));
|
||||
h.frame();
|
||||
assert_corners!(h, id, (0, 0), (80, 100));
|
||||
assert_corners!(h, b, (80, 0), (400, 100));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_deferred_comparison_can_compare_two_different_weights() {
|
||||
let a = SizeRequest::from(leftover(1.0) + px(30.0)).min(leftover(2.0));
|
||||
let b = SizeRequest::from(leftover(1.0)).clamp(px(20.0), px(100.0));
|
||||
let mut h = Harness::new((60, 100));
|
||||
let a = rect(Color::RED).width(a).add(&mut h.rsc);
|
||||
let b = rect(Color::BLUE).width(b).add(&mut h.rsc);
|
||||
h.set_root((a, b).span(Dir::RIGHT));
|
||||
assert_corners!(h, a, (0, 0), (40, 100));
|
||||
assert_corners!(h, b, (40, 0), (60, 100));
|
||||
h.resize((300, 100));
|
||||
h.frame();
|
||||
assert_corners!(h, a, (0, 0), (200, 100));
|
||||
assert_corners!(h, b, (200, 0), (300, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_comparison_between_two_comparisons_keeps_both_of_them() {
|
||||
// Joining two expressions is the one path that copies a request's nodes
|
||||
// into another's arena; the floor puts a node under the copied one, so
|
||||
// its operands have to be renumbered as they land.
|
||||
let capped = leftover(1).min(px(40));
|
||||
let floored = leftover(2).min(px(70)).max(px(10));
|
||||
let mut h = Harness::new((90, 100));
|
||||
let both = rect(Color::RED).width(capped.max(floored)).add(&mut h.rsc);
|
||||
let rest = rect(Color::BLUE).add(&mut h.rsc);
|
||||
h.set_root((both, rest).span(Dir::RIGHT));
|
||||
// Under either cap, so the doubled share is the longer of the two.
|
||||
assert_corners!(h, both, (0, 0), (60, 100));
|
||||
assert_corners!(h, rest, (60, 0), (90, 100));
|
||||
h.resize((300, 100));
|
||||
h.frame();
|
||||
// Over both caps, so the comparison is between 40 and 70.
|
||||
assert_corners!(h, both, (0, 0), (70, 100));
|
||||
assert_corners!(h, rest, (70, 0), (300, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_length_expression_is_resolved_before_wrapping_text() {
|
||||
let mut h = Harness::new((300, 500));
|
||||
let text = wtext("one two three four five six seven eight nine ten")
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.width(leftover(1).clamp(40, 80))
|
||||
.add(&mut h.rsc);
|
||||
let other = rect(Color::BLUE).add(&mut h.rsc);
|
||||
h.set_root((text, other).span(Dir::RIGHT));
|
||||
let box_ = h.region(&text).unwrap();
|
||||
assert_eq!(box_.top_left.x, Px::ZERO);
|
||||
assert_eq!(box_.bot_right.x, Px::from_int(80));
|
||||
assert!(box_.bot_right.y - box_.top_left.y > Px::from_int(30));
|
||||
assert_corners!(h, other, (80, 0), (300, 500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_bounds_keep_the_allocators_base() {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let head = rect(Color::BLUE).width(30).add(&mut h.rsc);
|
||||
let bounded = rect(Color::RED)
|
||||
.width(leftover(1).min(rel(0.25)))
|
||||
.add(&mut h.rsc);
|
||||
let tail = rect(Color::GREEN).add(&mut h.rsc);
|
||||
h.set_root((head, bounded, tail).span(Dir::RIGHT));
|
||||
assert_corners!(h, bounded, (30, 0), (105, 100));
|
||||
assert_corners!(h, tail, (105, 0), (300, 100));
|
||||
h.resize((400, 100));
|
||||
h.frame();
|
||||
assert_corners!(h, bounded, (30, 0), (130, 100));
|
||||
assert_corners!(h, tail, (130, 0), (400, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_root_resolves_a_deferred_request_again_after_resize() {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let bounded = rect(Color::RED)
|
||||
.width(leftover(1).min(rel(0.25)))
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(bounded);
|
||||
assert_corners!(h, bounded, (112.5, 0), (187.5, 100));
|
||||
h.resize((400, 100));
|
||||
h.frame();
|
||||
assert_corners!(h, bounded, (150, 0), (250, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filling_a_stack_does_not_mean_its_sizing_child_was_already_allocated() {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let child = rect(Color::RED).width(leftover(1).min(80)).add(&mut h.rsc);
|
||||
let overlay = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let children: Vec<StrongWidget> =
|
||||
vec![child.add_strong(&mut h.rsc), overlay.add_strong(&mut h.rsc)];
|
||||
h.set_root(Stack {
|
||||
children,
|
||||
size: StackSize::Child(0),
|
||||
});
|
||||
assert_corners!(h, child, (110, 0), (190, 100));
|
||||
assert_corners!(h, overlay, (110, 0), (190, 100));
|
||||
}
|
||||
|
||||
struct Unhinted;
|
||||
impl Widget for Unhinted {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
painter.px_size();
|
||||
painter.primitive(RectPrimitive::color(Color::RED));
|
||||
Size::LEFTOVER
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounds_also_apply_to_shares_discovered_by_drawing() {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let a = h.rsc.widgets_mut().add_strong(Unhinted);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(a.id(), Axis::X, SizeRule::max(Len::px(80.0)));
|
||||
let id = a.id();
|
||||
let b = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||
let b_id = b.id();
|
||||
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
|
||||
children: vec![a, b],
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
}));
|
||||
h.frame();
|
||||
assert_corners!(h, id, (0, 0), (80, 100));
|
||||
assert_corners!(h, b_id, (80, 0), (300, 100));
|
||||
h.resize((100, 100));
|
||||
h.frame();
|
||||
assert_corners!(h, id, (0, 0), (50, 100));
|
||||
assert_corners!(h, b_id, (50, 0), (100, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comparisons_with_a_known_order_remain_plain_lengths() {
|
||||
assert_eq!(leftover(2).max(leftover(5)), SizeRequest::from(leftover(5)));
|
||||
assert_eq!(leftover(2).min(leftover(5)), SizeRequest::from(leftover(2)));
|
||||
assert_eq!(
|
||||
(px(10) + rel(0.5)).max(px(30) + rel(0.5)),
|
||||
SizeRequest::from(px(30) + rel(0.5))
|
||||
);
|
||||
assert_eq!(LayoutLen::px(10).clamp(20, 80), SizeRequest::from(20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_measured_nested_share_keeps_its_comparison_for_the_outer_span() {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let a = h.rsc.widgets_mut().add_strong(Unhinted);
|
||||
let a_id = a.id();
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(a_id, Axis::X, SizeRule::max(Len::px(80.0)));
|
||||
let b = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||
let b_id = b.id();
|
||||
let inner = h.rsc.widgets_mut().add_strong(Span {
|
||||
children: vec![a, b],
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
});
|
||||
let c = rect(Color::GREEN).add_strong(&mut h.rsc);
|
||||
let c_id = c.id();
|
||||
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
|
||||
children: vec![inner, c],
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
}));
|
||||
h.frame();
|
||||
assert_corners!(h, a_id, (0, 0), (80, 100));
|
||||
assert_corners!(h, b_id, (80, 0), (190, 100));
|
||||
assert_corners!(h, c_id, (190, 0), (300, 100));
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.mark_for_redraw(h.state.root.as_ref().unwrap().id());
|
||||
h.frame();
|
||||
assert_corners!(h, c_id, (190, 0), (300, 100));
|
||||
}
|
||||
|
||||
struct Natural;
|
||||
impl Widget for Natural {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
painter.primitive(RectPrimitive::color(Color::RED));
|
||||
Size::from_axis(Axis::X, LayoutLen::px(64), LayoutLen::px(64))
|
||||
}
|
||||
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||
Some(LayoutLen::px(64))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_bounds_on_a_hinted_child_track_the_offer_before_its_declared_size() {
|
||||
fn tree(h: &mut Harness) -> WidgetId {
|
||||
let natural = h.rsc.widgets_mut().add_strong(Natural);
|
||||
let id = natural.id();
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(id, Axis::X, SizeRule::max(Len::rel(0.75)));
|
||||
h.rsc.widgets_mut().set_size_rule(
|
||||
id,
|
||||
Axis::Y,
|
||||
SizeRule::clamp(Len::rel(0.25), Len::rel(0.75)),
|
||||
);
|
||||
let inner = h.rsc.widgets_mut().add_strong(Stack {
|
||||
children: vec![natural],
|
||||
size: StackSize::Child(0),
|
||||
});
|
||||
let inner_id = inner.id();
|
||||
let fill = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||
let overlay = h.rsc.widgets_mut().add_strong(Stack {
|
||||
children: vec![fill, inner],
|
||||
size: StackSize::Child(0),
|
||||
});
|
||||
let share = rect(Color::GREEN).add_strong(&mut h.rsc);
|
||||
let bounded = rect(Color::GREEN).add_strong(&mut h.rsc);
|
||||
h.rsc.widgets_mut().set_size_rule(
|
||||
bounded.id(),
|
||||
Axis::X,
|
||||
SizeRule::clamp(Len::rel(0.25), Len::rel(0.75)),
|
||||
);
|
||||
let fixed = wtext("one line, overflowing whatever it is given")
|
||||
.size(16)
|
||||
.wrap(false)
|
||||
.add_strong(&mut h.rsc);
|
||||
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
|
||||
children: vec![share, bounded, fixed, overlay],
|
||||
dir: Dir::LEFT,
|
||||
gap: Px::from_int(8),
|
||||
}));
|
||||
h.rsc.widgets_mut().set_size_rule(
|
||||
h.state.root.as_ref().unwrap().id(),
|
||||
Axis::Y,
|
||||
LayoutLen::rel(1).into(),
|
||||
);
|
||||
h.frame();
|
||||
inner_id
|
||||
}
|
||||
let mut warm = Harness::new((1920, 1200));
|
||||
let a = tree(&mut warm);
|
||||
warm.resize((640, 900));
|
||||
warm.frame();
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let b = tree(&mut cold);
|
||||
assert_eq!(warm.region(&a), cold.region(&b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bound_can_extend_an_explicit_share_request() {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let a = rect(Color::RED)
|
||||
.width(leftover(1))
|
||||
.min_width(100)
|
||||
.add(&mut h.rsc);
|
||||
let b = rect(Color::BLUE).add(&mut h.rsc);
|
||||
h.set_root((a, b).span(Dir::RIGHT));
|
||||
assert_corners!(h, a, (0, 0), (150, 100));
|
||||
h.resize((120, 100));
|
||||
h.frame();
|
||||
assert_corners!(h, a, (0, 0), (100, 100));
|
||||
assert_corners!(h, b, (100, 0), (120, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_scroll_content_preserves_its_resolved_expression_size() {
|
||||
let mut h = Harness::new((300, 300));
|
||||
let leaf = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||
let leaf_id = leaf.id();
|
||||
let content = h.rsc.widgets_mut().add_strong(Stack {
|
||||
children: vec![leaf],
|
||||
size: StackSize::Child(0),
|
||||
});
|
||||
let content_id = content.id();
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(content_id, Axis::Y, leftover(1).min(120).into());
|
||||
let scroll = h
|
||||
.rsc
|
||||
.widgets_mut()
|
||||
.add_strong(Scroll::new(content, Axis::Y));
|
||||
let scroll_id = scroll.id();
|
||||
h.state.root = Some(scroll);
|
||||
for _ in 0..3 {
|
||||
h.rsc.widgets_mut().mark_for_redraw(scroll_id);
|
||||
h.frame();
|
||||
assert_corners!(h, content_id, (0, 90), (300, 210));
|
||||
assert_corners!(h, leaf_id, (0, 90), (300, 210));
|
||||
}
|
||||
h.resize((300, 600));
|
||||
h.frame();
|
||||
assert_corners!(h, leaf_id, (0, 240), (300, 360));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_intrinsic_share_cap_uses_the_allocators_fractional_base() {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let head = rect(Color::BLUE).width(30).add(&mut h.rsc);
|
||||
let bounded = rect(Color::RED).max_width(Len::rel(0.25)).add(&mut h.rsc);
|
||||
let tail = rect(Color::GREEN).add(&mut h.rsc);
|
||||
h.set_root((head, bounded, tail).span(Dir::RIGHT));
|
||||
assert_corners!(h, bounded, (30, 0), (105, 100));
|
||||
assert_corners!(h, tail, (105, 0), (300, 100));
|
||||
h.resize((400, 100));
|
||||
h.frame();
|
||||
assert_corners!(h, bounded, (30, 0), (130, 100));
|
||||
assert_corners!(h, tail, (130, 0), (400, 100));
|
||||
}
|
||||
+13
-11
@@ -21,16 +21,18 @@ struct BranchesOnMeasurement {
|
||||
|
||||
impl Widget for BranchesOnMeasurement {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let mut top = UiRegion::FULL;
|
||||
top.y.end = top.y.start.offset(Px::from_int(40));
|
||||
let measured = painter.widget_within(&self.probe, top).len(Axis::X);
|
||||
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
|
||||
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
|
||||
let top = UiSpan::new(Len::ZERO, cut).shifted_desc();
|
||||
let measured = painter
|
||||
.widget_at(&self.probe, top.on_axis(Axis::Y))
|
||||
.len(Axis::X);
|
||||
let px = painter.to_px(measured.apply_leftover(), Axis::X);
|
||||
|
||||
let mut below = UiRegion::FULL;
|
||||
below.y.start = below.y.start.offset(Px::from_int(40));
|
||||
let below = UiSpan::new(cut, painter.region_len(Axis::Y)).shifted_desc();
|
||||
let place = below.on_axis(Axis::Y);
|
||||
match px > Px::from_f32(self.threshold) {
|
||||
true => painter.widget_within(&self.wide, below),
|
||||
false => painter.widget_within(&self.narrow, below),
|
||||
true => painter.widget_at(&self.wide, place),
|
||||
false => painter.widget_at(&self.narrow, place),
|
||||
};
|
||||
Size::LEFTOVER
|
||||
}
|
||||
@@ -67,8 +69,8 @@ fn a_branch_taken_on_a_measurement_holds_across_repaints() {
|
||||
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.rsc.widgets_mut().mark_for_redraw(wide);
|
||||
h.rsc.widgets_mut().mark_for_redraw(narrow);
|
||||
h.frame();
|
||||
assert_eq!(
|
||||
taken(&h, wide, narrow),
|
||||
@@ -86,7 +88,7 @@ fn a_branch_taken_on_a_measurement_is_the_one_a_cold_start_takes() {
|
||||
let (wide, narrow) = plant(&mut warm, threshold);
|
||||
warm.resize((640, 480));
|
||||
warm.frame();
|
||||
warm.rsc.widgets_mut().get_dyn_mut(wide);
|
||||
warm.rsc.widgets_mut().mark_for_redraw(wide);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 480));
|
||||
|
||||
@@ -18,7 +18,7 @@ fn a_wrapping_text_in_a_span_settles_on_one_width() {
|
||||
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.rsc.widgets_mut().mark_for_redraw(t.id());
|
||||
h.frame();
|
||||
}
|
||||
println!("widths over six frames: {widths:?}");
|
||||
|
||||
+861
-15
@@ -1,5 +1,7 @@
|
||||
//! Where a frame puts things, with no window to put them in.
|
||||
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use iris::harness::{Harness, assert_corners};
|
||||
use iris::prelude::*;
|
||||
|
||||
@@ -20,6 +22,160 @@ fn a_span_gives_each_child_the_width_it_asked_for() {
|
||||
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}");
|
||||
}
|
||||
|
||||
/// Padding is an inset: it narrows the frame a fraction resolves against and
|
||||
/// adds itself back to the padded widget's reported length.
|
||||
#[test]
|
||||
fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() {
|
||||
let mut h = Harness::new((400, 100));
|
||||
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
||||
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
|
||||
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, inner, (10, 10), (200, 90));
|
||||
assert_corners!(h, padded, (0, 0), (210, 100));
|
||||
assert_corners!(h, tail, (210, 0), (310, 100));
|
||||
}
|
||||
|
||||
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the box \
|
||||
leaves room for, so a paragraph's height is an answer and not a setting.";
|
||||
|
||||
/// The worked example of what padding insets: in a 900 px row after a 24 px
|
||||
/// icon, a `rel(1.0)` inside `pad(16)` is 900 - 32 and overflows the row by
|
||||
/// the icon's width, while a wrapping text beside it is asked in the room
|
||||
/// left, 900 - 24 - 32, and wraps there.
|
||||
#[test]
|
||||
fn padding_keeps_the_rel_base_distinct_from_the_room_left_in_a_row() {
|
||||
let mut h = Harness::new((900, 200));
|
||||
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
|
||||
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
|
||||
let padded = fill.pad(16).add(&mut h.rsc);
|
||||
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
|
||||
let fill_width = h.region(&fill).unwrap().size().x;
|
||||
assert_eq!(fill_width, Px::from_int(868));
|
||||
|
||||
let mut h = Harness::new((900, 200));
|
||||
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
|
||||
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||
let padded = text.pad(16).add(&mut h.rsc);
|
||||
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
|
||||
let active = &h.render.active[&text.id()];
|
||||
let window = h.render.output_size().x;
|
||||
let asked = active.region.x.len().to_px(window);
|
||||
assert_eq!(active.rel_base.x.to_px(window), Px::from_int(868));
|
||||
assert_eq!(asked, Px::from_int(844));
|
||||
}
|
||||
|
||||
/// The other way round: a share inside padding. A slot is a length of the
|
||||
/// row, which is already the padded width, so what the span decided reaches
|
||||
/// the child as it stands -- taking the padding off a second time would make
|
||||
/// `rel(1.0)` in the slot shorter than the slot.
|
||||
#[test]
|
||||
fn a_share_inside_padding_fills_the_slot_it_was_given() {
|
||||
let mut h = Harness::new((900, 200));
|
||||
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
|
||||
let first = Span {
|
||||
children: vec![fill.add_strong(&mut h.rsc)],
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
}
|
||||
.width(leftover(1))
|
||||
.add(&mut h.rsc);
|
||||
let second = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
|
||||
let row = (first, second).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.set_root(row.pad(16));
|
||||
|
||||
assert_eq!(h.region(&first).unwrap().size().x, Px::from_int(434));
|
||||
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(434));
|
||||
}
|
||||
|
||||
/// The same padding in a share instead: the slot is 450, so both the
|
||||
/// fraction and the wrap are the slot less the padding, and the two agree.
|
||||
#[test]
|
||||
fn padding_narrows_both_rel_base_and_box_inside_a_share() {
|
||||
let mut h = Harness::new((900, 200));
|
||||
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
|
||||
let padded = fill.pad(16).width(leftover(1)).add(&mut h.rsc);
|
||||
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
|
||||
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
|
||||
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(418));
|
||||
|
||||
let mut h = Harness::new((900, 200));
|
||||
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||
let padded = text.pad(16).width(leftover(1)).add(&mut h.rsc);
|
||||
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
|
||||
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
|
||||
let active = &h.render.active[&text.id()];
|
||||
let window = h.render.output_size().x;
|
||||
assert_eq!(active.rel_base.x.to_px(window), Px::from_int(418));
|
||||
assert_eq!(active.region.x.len().to_px(window), Px::from_int(418));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
@@ -65,6 +221,134 @@ fn an_empty_widget_takes_a_share_of_a_span() {
|
||||
assert_corners!(h, right, (300, 0), (400, 200));
|
||||
}
|
||||
|
||||
/// A widget with a natural pixel size, like an image, which records the box
|
||||
/// it was asked in so a test can see which length decided it.
|
||||
struct NaturalSize {
|
||||
len: f32,
|
||||
asked: Rc<Cell<f32>>,
|
||||
}
|
||||
|
||||
impl Widget for NaturalSize {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.asked.set(painter.px_len(Axis::X).to_f32());
|
||||
Size::px(Vec2::new(self.len, self.len))
|
||||
}
|
||||
|
||||
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||
Some(LayoutLen::px(self.len))
|
||||
}
|
||||
}
|
||||
|
||||
/// A rule wins over what the widget says about itself, and a share is a rule:
|
||||
/// it is a length only to whoever divides one, and nobody here does, so the
|
||||
/// widget is asked in the whole box rather than in the size it asked for.
|
||||
#[test]
|
||||
fn a_share_rule_beats_the_widgets_own_pixel_size() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let asked = Rc::new(Cell::new(0.0));
|
||||
let natural = NaturalSize {
|
||||
len: 50.0,
|
||||
asked: asked.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(natural.wrapper());
|
||||
assert_eq!(asked.get(), 50.0, "its hint gives it its own size");
|
||||
|
||||
h.set_len(natural, Axis::X, LayoutLen::LEFTOVER);
|
||||
h.frame();
|
||||
|
||||
assert_eq!(asked.get(), 400.0, "the share is all of the box");
|
||||
}
|
||||
|
||||
/// Every box a widget is given comes of one ask, and the window is one of
|
||||
/// them: the root is asked in it exactly as a child is asked in its parent's
|
||||
/// box, so a rule of its own reads the same way at either place.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Asked {
|
||||
Root,
|
||||
Wrapped,
|
||||
InASpan,
|
||||
}
|
||||
|
||||
impl Asked {
|
||||
const ALL: [Self; 3] = [Self::Root, Self::Wrapped, Self::InASpan];
|
||||
|
||||
/// The width the probe is given under this parent, in a 400 px window.
|
||||
fn width(&self, rule: LayoutLen) -> Px {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_len(probe, Axis::X, rule);
|
||||
match self {
|
||||
Self::Root => h.set_root(probe),
|
||||
Self::Wrapped => h.set_root(probe.wrapper()),
|
||||
Self::InASpan => h.set_root((probe,).span(Dir::RIGHT)),
|
||||
}
|
||||
h.region(&probe).unwrap().size().x
|
||||
}
|
||||
}
|
||||
|
||||
/// A share with pixels or a fraction beside it is the longer of the two: it
|
||||
/// fills what they leave of the box and overflows the box where they are
|
||||
/// longer than it. A parent that divides nothing gives the same length as a
|
||||
/// span with one child, because in both there is nobody else to divide with --
|
||||
/// and so does the window, which divides nothing either.
|
||||
#[test]
|
||||
fn a_share_is_a_minimum_wherever_nothing_divides_it() {
|
||||
for (rule, want) in [
|
||||
(LayoutLen::LEFTOVER, 400),
|
||||
(LayoutLen::px(50.0) + LayoutLen::LEFTOVER, 400),
|
||||
(LayoutLen::px(500.0) + LayoutLen::LEFTOVER, 500),
|
||||
(LayoutLen::rel(0.5) + LayoutLen::LEFTOVER, 400),
|
||||
(LayoutLen::rel(2.0) + LayoutLen::LEFTOVER, 800),
|
||||
(LayoutLen::px(500.0), 500),
|
||||
] {
|
||||
let want = Px::from_int(want);
|
||||
for asked in Asked::ALL {
|
||||
assert_eq!(asked.width(rule), want, "{rule:?} asked {asked:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which of the two is longer is a question in pixels, so the box is decided
|
||||
/// again wherever the answer can change: a window that crosses the length the
|
||||
/// pixels ask for, and the rule itself crossing it while the window holds
|
||||
/// still. The first is a range the drawing holds for; the second cannot be
|
||||
/// seen in what the widget declares, since a share declares nothing either
|
||||
/// way, so it reaches the parent as a length only the parent can resolve.
|
||||
#[test]
|
||||
fn a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing() {
|
||||
// At the root as well as under a parent: the comparison is the same one,
|
||||
// and nothing above the root will make it again on its behalf, so the
|
||||
// range it holds for is the root's own.
|
||||
for wrapped in [false, true] {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_len(probe, Axis::X, LayoutLen::px(500.0) + LayoutLen::LEFTOVER);
|
||||
match wrapped {
|
||||
true => h.set_root(probe.wrapper()),
|
||||
false => h.set_root(probe),
|
||||
}
|
||||
let width = |h: &Harness| h.region(&probe).unwrap().size().x;
|
||||
assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
|
||||
|
||||
h.resize((900, 200));
|
||||
h.frame();
|
||||
assert_eq!(width(&h), Px::from_int(900), "wrapped: {wrapped}");
|
||||
|
||||
h.resize((400, 200));
|
||||
h.frame();
|
||||
assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
|
||||
|
||||
h.set_len(probe, Axis::X, LayoutLen::px(50.0) + LayoutLen::LEFTOVER);
|
||||
h.frame();
|
||||
assert_eq!(width(&h), Px::from_int(400), "wrapped: {wrapped}");
|
||||
|
||||
h.set_len(probe, Axis::X, LayoutLen::px(500.0) + LayoutLen::LEFTOVER);
|
||||
h.frame();
|
||||
assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_child_drawn_twice_moves_once() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
@@ -225,21 +509,21 @@ fn only_a_region_node_lengthens_the_chain_and_it_can_be_removed() {
|
||||
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), 1, "only the root region");
|
||||
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),
|
||||
2,
|
||||
"the opted-in widget's region and the root region"
|
||||
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), 1);
|
||||
assert_eq!(h.render.moves.depth(move_idx), 0);
|
||||
}
|
||||
|
||||
/// A span that sizes from its children passes their `leftover` weight up
|
||||
@@ -266,6 +550,11 @@ fn nested_spans_divide_the_space_once_however_deep_the_nesting_is() {
|
||||
|
||||
/// 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));
|
||||
@@ -279,10 +568,24 @@ fn an_uneven_nesting_still_gives_every_share_the_same_length() {
|
||||
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 x = i as f32 * 100.0;
|
||||
assert_corners!(h, id, (x, 0), (x + 100.0, 200));
|
||||
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
|
||||
@@ -322,17 +625,16 @@ fn a_row_of_equal_shares_fills_it_exactly() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the shader puts an edge: the two parts of a scalar are floored
|
||||
/// apart, so a fraction and a pixel offset snap independently, and each is
|
||||
/// taken to the boundary it composes to within half a step of. Kept in step
|
||||
/// with `snap_floor` in `prelude.wgsl`.
|
||||
/// 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 region = h.render.moves.resolve(active.move_idx, active.placement);
|
||||
let dim = h.size()[axis];
|
||||
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
|
||||
let edge = |s: Len| snap(s.rel.to_f32() * dim) + snap(s.px.to_f32());
|
||||
let span = region.axis(axis);
|
||||
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
|
||||
let span = region[axis];
|
||||
(edge(span.start), edge(span.end))
|
||||
}
|
||||
|
||||
@@ -454,7 +756,7 @@ 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 mixed = rect(Color::BLUE)
|
||||
.width(LayoutLen::px(20) + LayoutLen::LEFTOVER)
|
||||
.width(LayoutLen::px(20.0) + LayoutLen::LEFTOVER)
|
||||
.add(&mut h.rsc);
|
||||
h.set_root((fixed, mixed).span(Dir::RIGHT));
|
||||
|
||||
@@ -480,3 +782,547 @@ fn leftover_children_disappear_at_the_exact_fixed_content_boundary() {
|
||||
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::from(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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fixed_child_is_centered_in_its_wrappers_share() {
|
||||
let mut h = Harness::new((600, 300));
|
||||
let leaf = rect(Color::RED).sized((100, 100)).center().add(&mut h.rsc);
|
||||
let wrapper = leaf
|
||||
.wrapper()
|
||||
.width(leftover(2))
|
||||
.height(rel(1.0))
|
||||
.add(&mut h.rsc);
|
||||
let other = rect(Color::BLUE).width(200).add(&mut h.rsc);
|
||||
h.set_root((other, wrapper).span(Dir::RIGHT));
|
||||
|
||||
assert_corners!(h, wrapper, (200, 0), (600, 300));
|
||||
assert_corners!(h, leaf, (350, 100), (450, 200));
|
||||
|
||||
h.resize((900, 400));
|
||||
h.frame();
|
||||
assert_corners!(h, wrapper, (200, 0), (900, 400));
|
||||
assert_corners!(h, leaf, (500, 150), (600, 250));
|
||||
}
|
||||
|
||||
/// The root's frame is the window and its rule is a fraction of that, which
|
||||
/// is one resolution and not two: nothing above it narrowed anything.
|
||||
#[test]
|
||||
fn a_root_with_a_fraction_rule_is_that_fraction_of_the_window() {
|
||||
let mut h = Harness::new((900, 200));
|
||||
let root = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_collapsed_share_keeps_the_gaps_before_the_next_slot() {
|
||||
for dir in [Dir::RIGHT, Dir::LEFT, Dir::DOWN, Dir::UP] {
|
||||
for collapsed in [1, 2] {
|
||||
let mut h = Harness::new((400, 400));
|
||||
let head = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_len(head, dir.axis, 200);
|
||||
let tail = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let tail_len = 200 - 10 * (collapsed + 1);
|
||||
h.set_len(tail, dir.axis, tail_len);
|
||||
let mut children: Vec<StrongWidget> = vec![head.add_strong(&mut h.rsc)];
|
||||
let mut shares = Vec::new();
|
||||
for _ in 0..collapsed {
|
||||
let share = rect(Color::GREEN).add(&mut h.rsc);
|
||||
shares.push(share);
|
||||
children.push(share.add_strong(&mut h.rsc));
|
||||
}
|
||||
children.push(tail.add_strong(&mut h.rsc));
|
||||
h.set_root(Span {
|
||||
children,
|
||||
dir,
|
||||
gap: Px::from_int(10),
|
||||
});
|
||||
for share in shares {
|
||||
assert!(h.region(&share).is_none());
|
||||
}
|
||||
let region = h.region(&tail).unwrap();
|
||||
let (from, to) = match dir.sign {
|
||||
Sign::Pos => (400 - tail_len, 400),
|
||||
Sign::Neg => (0, tail_len),
|
||||
};
|
||||
assert_eq!(region.top_left[dir.axis], Px::from_int(from));
|
||||
assert_eq!(region.bot_right[dir.axis], Px::from_int(to));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The root is asked the way any child is, so what it says about itself is
|
||||
/// read there too: a root that opted into a region node gets one, where the
|
||||
/// path it used to have ignored the flag.
|
||||
#[test]
|
||||
fn a_region_node_root_is_a_region_node() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||
let root = (probe,).span(Dir::RIGHT).region_node().add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(400));
|
||||
|
||||
h.resize((900, 200));
|
||||
h.frame();
|
||||
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900));
|
||||
}
|
||||
|
||||
/// A bound holds the length a widget reports as well as narrowing the box it
|
||||
/// is offered, and the two are not the same question. Here two 200-wide rects
|
||||
/// fill a row in a 250 window, so a cap of 300 leaves the box alone and only
|
||||
/// cuts what the row reports -- which the window then centres, past both its
|
||||
/// edges -- while a floor raises the report and the rects stay where the 250
|
||||
/// box put them.
|
||||
#[test]
|
||||
fn a_bound_holds_what_a_widget_answers() {
|
||||
let bounded_row = |rule: SizeRule| {
|
||||
let mut h = Harness::new((250, 200));
|
||||
let left = rect(Color::RED).width(200).add(&mut h.rsc);
|
||||
let right = rect(Color::BLUE).width(200).add(&mut h.rsc);
|
||||
let row = (left, right).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.rsc.widgets_mut().set_size_rule(row, Axis::X, rule);
|
||||
h.set_root(row);
|
||||
(h, row, left)
|
||||
};
|
||||
let (h, row, left) = bounded_row(SizeRule::max(Len::px(300.0)));
|
||||
assert_eq!(
|
||||
h.region(&row).unwrap().size().x,
|
||||
Px::from_int(300),
|
||||
"the cap, not the 400 drawn"
|
||||
);
|
||||
assert_eq!(
|
||||
h.region(&left).unwrap().size().x,
|
||||
Px::from_int(200),
|
||||
"the box the children were given"
|
||||
);
|
||||
assert_corners!(h, row, (-25, 0), (275, 200));
|
||||
|
||||
let (h, row, _) = bounded_row(SizeRule::min(Len::px(600.0)));
|
||||
assert_eq!(
|
||||
h.region(&row).unwrap().size().x,
|
||||
Px::from_int(600),
|
||||
"the floor, not the 400 drawn"
|
||||
);
|
||||
|
||||
let (h, row, _) = bounded_row(SizeRule::FREE);
|
||||
assert_eq!(
|
||||
h.region(&row).unwrap().size().x,
|
||||
Px::from_int(400),
|
||||
"what it drew"
|
||||
);
|
||||
}
|
||||
|
||||
/// A cap narrows the box the widget is asked in, whether a declaration of its
|
||||
/// own decides that box or the allocator divides a share into it. The cap is
|
||||
/// an attribute of the widget rather than something wrapped around it, which
|
||||
/// is what the id assertions say.
|
||||
#[test]
|
||||
fn a_cap_attribute_narrows_the_widgets_box() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
// A fraction of its box, so it says what box it was asked in.
|
||||
let fills = rect(Color::RED).width(rel(1.0)).add(&mut h.rsc);
|
||||
let capped = fills.max_width(300).add(&mut h.rsc);
|
||||
assert_eq!(fills.id(), capped.id());
|
||||
h.set_root(capped);
|
||||
assert_eq!(h.region(&fills).unwrap().size().x, Px::from_int(300));
|
||||
|
||||
// A share with nothing beside it: the cap is composed into the request
|
||||
// and the allocator answers with it rather than the whole 400.
|
||||
let mut h = Harness::new((400, 200));
|
||||
let share = rect(Color::RED).width(leftover(1)).add(&mut h.rsc);
|
||||
let capped = share.max_width(300).add(&mut h.rsc);
|
||||
assert_eq!(share.id(), capped.id());
|
||||
h.set_root(capped);
|
||||
assert_eq!(h.region(&share).unwrap().size().x, Px::from_int(300));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cap_attribute_is_decided_again_on_either_side_of_the_crossing() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_root(probe.max_width(300));
|
||||
let width = |h: &Harness| h.region(&probe).unwrap().size().x;
|
||||
assert_eq!(width(&h), Px::from_int(300));
|
||||
|
||||
h.resize((250, 200));
|
||||
h.frame();
|
||||
assert_eq!(
|
||||
width(&h),
|
||||
Px::from_int(250),
|
||||
"its box, which is under the cap"
|
||||
);
|
||||
|
||||
h.resize((400, 200));
|
||||
h.frame();
|
||||
assert_eq!(width(&h), Px::from_int(300));
|
||||
}
|
||||
|
||||
/// A fraction in a cap is a fraction of the box the widget capping it was
|
||||
/// given, which is the box a declared length of its own would be a fraction
|
||||
/// of -- not of the window, and not of what the cap itself decided.
|
||||
#[test]
|
||||
fn a_cap_is_a_fraction_of_the_box_it_was_given() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_root(probe.max_width(Len::rel(0.5)).pad(Padding::uniform(50)));
|
||||
|
||||
// Half of the 300 left by the padding, not half of the window.
|
||||
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(150));
|
||||
}
|
||||
|
||||
struct Offered {
|
||||
seen: Rc<Cell<PxVec2>>,
|
||||
answer: Size,
|
||||
}
|
||||
|
||||
impl Widget for Offered {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.seen.set(painter.px_size());
|
||||
painter.primitive(RectPrimitive::color(Color::RED));
|
||||
self.answer
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounds_constrain_the_offer_without_replacing_an_intrinsic_answer() {
|
||||
for axis in Axis::BOTH {
|
||||
for node in [false, true] {
|
||||
let mut h = Harness::new((400, 400));
|
||||
let seen = Rc::new(Cell::new(PxVec2::ZERO));
|
||||
let probe = h.rsc.widgets_mut().add_strong(Offered {
|
||||
seen: seen.clone(),
|
||||
answer: Size::px(Vec2::new(40.0, 40.0)),
|
||||
});
|
||||
let id = probe.id();
|
||||
h.rsc.widgets_mut().set_region_node(id, node);
|
||||
h.rsc.widgets_mut().set_max_len(id, axis, 100.into());
|
||||
h.state.root = Some(probe);
|
||||
h.frame();
|
||||
assert_eq!(seen.get()[axis], Px::from_int(100));
|
||||
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(40));
|
||||
h.rsc.widgets_mut().set_min_len(id, axis, 60.into());
|
||||
h.frame();
|
||||
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(60));
|
||||
h.resize((50, 50));
|
||||
h.frame();
|
||||
assert_eq!(seen.get()[axis], Px::from_int(60));
|
||||
h.rsc.widgets_mut().set_size_rule(id, axis, SizeRule::FREE);
|
||||
h.frame();
|
||||
assert_eq!(seen.get()[axis], Px::from_int(50));
|
||||
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(40));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cap_attribute_is_the_scroll_viewport() {
|
||||
for node in [false, true] {
|
||||
let mut h = Harness::new((400, 400));
|
||||
let content = rect(Color::RED).height(400).add(&mut h.rsc);
|
||||
let inner = content.add_strong(&mut h.rsc);
|
||||
let scroll = Scroll::new(inner, Axis::Y).max_height(100).add(&mut h.rsc);
|
||||
h.rsc.widgets_mut().set_region_node(scroll, node);
|
||||
h.set_root(scroll);
|
||||
assert_corners!(h, scroll, (0, 150), (400, 250));
|
||||
assert_corners!(h, content, (0, -150), (400, 250));
|
||||
h.rsc.widgets_mut().get_mut(&scroll).unwrap().scroll(1000.0);
|
||||
h.frame();
|
||||
assert_corners!(h, content, (0, 150), (400, 550));
|
||||
h.resize((400, 80));
|
||||
h.frame();
|
||||
assert_corners!(h, scroll, (0, 0), (400, 80));
|
||||
assert_corners!(h, content, (0, 0), (400, 400));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cap_attribute_wraps_text_before_it_answers() {
|
||||
let mut h = Harness::new((400, 500));
|
||||
let text = wtext("one two three four five six seven eight nine ten")
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.max_width(80)
|
||||
.align(Align::TOP_LEFT)
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(text);
|
||||
let capped = h.region(&text).unwrap().size();
|
||||
assert!(capped.x <= Px::from_int(80));
|
||||
assert!(capped.y > Px::from_int(30));
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(text, Axis::X, SizeRule::FREE);
|
||||
h.frame();
|
||||
let free = h.region(&text).unwrap().size();
|
||||
assert!(free.x > capped.x);
|
||||
assert!(free.y < capped.y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimensions_and_bounds_are_independent_attributes_in_either_order() {
|
||||
for bounds_first in [false, true] {
|
||||
let mut h = Harness::new((400, 400));
|
||||
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||
let bounded = if bounds_first {
|
||||
probe
|
||||
.max_width(80)
|
||||
.min_height(60)
|
||||
.width(120)
|
||||
.height(40)
|
||||
.add(&mut h.rsc)
|
||||
} else {
|
||||
probe
|
||||
.width(120)
|
||||
.height(40)
|
||||
.max_width(80)
|
||||
.min_height(60)
|
||||
.add(&mut h.rsc)
|
||||
};
|
||||
assert_eq!(probe.id(), bounded.id());
|
||||
h.set_root(bounded);
|
||||
assert_eq!(
|
||||
h.region(&probe).unwrap().size(),
|
||||
PxVec2::from_f32((80, 60).into())
|
||||
);
|
||||
h.rsc.widgets_mut().set_len(probe, Axis::X, 50);
|
||||
h.rsc.widgets_mut().set_len(probe, Axis::Y, 100);
|
||||
h.frame();
|
||||
assert_eq!(
|
||||
h.region(&probe).unwrap().size(),
|
||||
PxVec2::from_f32((50, 100).into())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_a_share_cap_replaces_it_without_losing_the_share() {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let capped = rect(Color::RED)
|
||||
.max_width(80)
|
||||
.width(leftover(1))
|
||||
.add(&mut h.rsc);
|
||||
let sibling = rect(Color::BLUE).add(&mut h.rsc);
|
||||
h.set_root((capped, sibling).span(Dir::RIGHT));
|
||||
assert_corners!(h, capped, (0, 0), (80, 100));
|
||||
assert_corners!(h, sibling, (80, 0), (300, 100));
|
||||
h.rsc.widgets_mut().set_max_len(capped, Axis::X, 160.into());
|
||||
h.frame();
|
||||
assert_corners!(h, capped, (0, 0), (150, 100));
|
||||
assert_corners!(h, sibling, (150, 0), (300, 100));
|
||||
}
|
||||
|
||||
// Reduced from seed 104 at depth 5: a widget widening its own window
|
||||
// contract must not erase the bound's crossing at a quarter-window of 173.
|
||||
#[test]
|
||||
fn a_widgets_window_contract_cannot_widen_its_bounds_contract() {
|
||||
fn tree(h: &mut Harness) -> WidgetId {
|
||||
let content = rect(Color::RED)
|
||||
.width(137)
|
||||
.min_height(194)
|
||||
.add_strong(&mut h.rsc);
|
||||
let scroll = Scroll::new(content, Axis::X).add_strong(&mut h.rsc);
|
||||
let probe = rect(Color::RED).add_strong(&mut h.rsc);
|
||||
let id = probe.id();
|
||||
let wide = rect(Color::GREEN).add_strong(&mut h.rsc);
|
||||
let narrow = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||
let branch = iris::random::Branch {
|
||||
probe,
|
||||
wide,
|
||||
narrow,
|
||||
threshold: 459.0,
|
||||
}
|
||||
.min_width(94)
|
||||
.max_width(173)
|
||||
.add_strong(&mut h.rsc);
|
||||
let stack = Stack {
|
||||
children: vec![scroll, branch],
|
||||
size: StackSize::Child(0),
|
||||
}
|
||||
.add_strong(&mut h.rsc);
|
||||
let mut children: Vec<StrongWidget> = (0..3)
|
||||
.map(|_| rect(Color::RED).add_strong(&mut h.rsc).any())
|
||||
.collect();
|
||||
children.push(stack);
|
||||
h.set_root(
|
||||
Span {
|
||||
children,
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
}
|
||||
.height(rel(1)),
|
||||
);
|
||||
id
|
||||
}
|
||||
let mut warm = Harness::new((1920, 1200));
|
||||
let probe = tree(&mut warm);
|
||||
warm.resize((640, 900));
|
||||
warm.frame();
|
||||
assert_corners!(warm, probe, (480, 0), (640, 40));
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let other = tree(&mut cold);
|
||||
assert_eq!(warm.region(&probe), cold.region(&other));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fixed_declaration_keeps_its_cap_when_the_row_has_no_leftover() {
|
||||
let mut h = Harness::new((100, 100));
|
||||
let fixed = rect(Color::RED).width(200).max_width(100).add(&mut h.rsc);
|
||||
let share = rect(Color::BLUE).add(&mut h.rsc);
|
||||
h.set_root((fixed, share).span(Dir::RIGHT));
|
||||
assert_corners!(h, fixed, (0, 0), (100, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_bound_reaches_the_parent_even_when_the_current_answer_is_unchanged() {
|
||||
let mut h = Harness::new((400, 100));
|
||||
let leaf = rect(Color::RED).add(&mut h.rsc);
|
||||
let inner = leaf.add_strong(&mut h.rsc);
|
||||
let root = Scroll::new(inner, Axis::Y)
|
||||
.pad(Padding::uniform(0))
|
||||
.width(154)
|
||||
.height(100)
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
assert_eq!(h.region(&leaf).unwrap().size().x, Px::from_int(154));
|
||||
h.rsc.widgets_mut().set_min_len(leaf, Axis::X, 140.into());
|
||||
h.rsc.widgets_mut().set_len(root, Axis::X, 78);
|
||||
h.frame();
|
||||
assert_corners!(h, leaf, (130, 0), (270, 100));
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
//! 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::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, grow, 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,
|
||||
SizeRules {
|
||||
x: SizeRule::from(LayoutLen::LEFTOVER),
|
||||
y: SizeRule::FREE,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
aligns: pick(aligned, &mut rng)
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
(
|
||||
i,
|
||||
Align {
|
||||
x: Some(AxisAlign::POS),
|
||||
y: 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// No simplification is larger, which is the half of "the shrinker stops" a
|
||||
/// widget count can see. Most are not smaller either -- a dropped alignment
|
||||
/// and a simpler leaf both keep the count -- so what rules out circling is
|
||||
/// that those are one-way too: a `Some` becomes a `None`, and a kind steps
|
||||
/// down a ladder with no way back up.
|
||||
#[test]
|
||||
fn no_simplification_of_a_plan_is_larger_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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every image in a tree is the same picture, and a handle is a reference to
|
||||
/// the texture rather than a copy of it, so one upload and one slot serve all
|
||||
/// of them however many a tree grows -- and the trees are grown in hundreds.
|
||||
#[test]
|
||||
fn a_tree_of_images_uploads_one_texture() {
|
||||
let mut images = 0;
|
||||
let mut tree = plan(1, 4, &Edits::default());
|
||||
tree.walk_mut(&mut |p| images += (p.kind == Kind::Image) as usize);
|
||||
assert!(images > 1, "a tree of {images} images tests nothing");
|
||||
|
||||
let mut h = Harness::new((900, 1200));
|
||||
let (root, _) = grow(&mut h.rsc, 1, 4, &Edits::default());
|
||||
h.state.root = Some(root);
|
||||
h.frame();
|
||||
assert_eq!(h.rsc.ui().textures.count(), 1);
|
||||
}
|
||||
+1009
-32
File diff suppressed because it is too large.
Load diff
+108
-1
@@ -60,10 +60,78 @@ fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
|
||||
assert_corners!(h, top, (0, 0), (400, 200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_content_and_a_share_fill_one_viewport() {
|
||||
let mut h = Harness::new((900, 100));
|
||||
let content = rect(Color::RED)
|
||||
.width(LayoutLen {
|
||||
px: Px::from_int(600),
|
||||
rel: Rel::ZERO,
|
||||
leftover: Weight::ONE,
|
||||
})
|
||||
.add(&mut h.rsc);
|
||||
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
||||
h.set_root(scroll);
|
||||
|
||||
assert_corners!(h, content, (0, 0), (900, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_content_wider_than_the_viewport_still_scrolls() {
|
||||
let mut h = Harness::new((900, 100));
|
||||
let content = rect(Color::RED).width(1200).add(&mut h.rsc);
|
||||
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
||||
h.set_root(scroll);
|
||||
|
||||
assert_corners!(h, content, (-300, 0), (900, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lone_share_fills_without_scrolling() {
|
||||
let mut h = Harness::new((900, 100));
|
||||
let content = rect(Color::RED).width(LayoutLen::LEFTOVER).add(&mut h.rsc);
|
||||
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
||||
h.set_root(scroll);
|
||||
|
||||
assert_corners!(h, content, (0, 0), (900, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrapping_content_beside_a_fixed_length_is_stable_warm_and_cold() {
|
||||
fn plant(h: &mut Harness) -> (WidgetId, WidgetId) {
|
||||
let fixed = rect(Color::RED).width(600).add(&mut h.rsc);
|
||||
let text = wtext("Wrapping shapes one source into as many lines as the box leaves room for, so a paragraph's height is an answer and not a setting.")
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.width(LayoutLen::LEFTOVER)
|
||||
.add(&mut h.rsc);
|
||||
let content = (fixed, text).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
||||
h.set_root(scroll);
|
||||
(text.id(), content.id())
|
||||
}
|
||||
|
||||
let mut warm = Harness::new((900, 300));
|
||||
let (text, content) = plant(&mut warm);
|
||||
warm.rsc.widgets_mut().mark_for_redraw(text);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 300));
|
||||
let (cold_text, cold_content) = plant(&mut cold);
|
||||
|
||||
assert_eq!(warm.region(&text), cold.region(&cold_text));
|
||||
assert_eq!(warm.region(&content), cold.region(&cold_content));
|
||||
}
|
||||
|
||||
/// A widget that clips to its box may not report more than the box: its
|
||||
/// parent would place the part it cut off, and the framework would put a
|
||||
/// drawing longer than its box somewhere. `Masked` is the second of these
|
||||
/// after `Scroll`, and the assertion in `draw_at` is what says so.
|
||||
// What it checks is a debug assertion, which a release build does not compile
|
||||
// -- and a `should_panic` test of one fails there rather than passing
|
||||
// vacuously, so it is not built either. Every measurement rig here is run in
|
||||
// release, so `cargo test --release` has to pass.
|
||||
#[cfg(debug_assertions)]
|
||||
#[test]
|
||||
#[should_panic = "clips to"]
|
||||
fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
|
||||
@@ -71,7 +139,7 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
|
||||
|
||||
impl Widget for Clipper {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
painter.set_mask(painter.region());
|
||||
painter.set_mask(UiRegion::FULL);
|
||||
painter.widget(&self.0).size()
|
||||
}
|
||||
}
|
||||
@@ -79,6 +147,45 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
|
||||
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);
|
||||
// `set_root` lays the tree out, so this is where it is caught.
|
||||
h.set_root(clipper);
|
||||
}
|
||||
|
||||
/// Content that fits sits in the viewport, not in a box of the window's
|
||||
/// length anchored at the viewport's start. `Part::From` takes window
|
||||
/// lengths, so a `rel(1.0)` span in one is the window, and only a scroll
|
||||
/// filling the window would land right.
|
||||
#[test]
|
||||
fn content_that_fits_is_placed_in_the_viewport_and_not_in_the_window() {
|
||||
let mut h = Harness::new((400, 400));
|
||||
let head = rect(Color::RED).height(100).add(&mut h.rsc);
|
||||
let inner = rect(Color::BLUE).height(50).add(&mut h.rsc);
|
||||
let scroll = Scroll::new(inner.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
||||
h.set_root((head, scroll).span(Dir::DOWN));
|
||||
|
||||
assert_corners!(h, scroll, (0, 100), (400, 400));
|
||||
assert_corners!(h, inner, (0, 225), (400, 275));
|
||||
}
|
||||
|
||||
/// A cap narrows the box the widget is asked in, which is what a scroll
|
||||
/// measures its viewport from: the content scrolls within the cap rather than
|
||||
/// within the room the cap was cut from.
|
||||
#[test]
|
||||
fn a_capped_scroll_takes_its_viewport_from_the_cap() {
|
||||
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 scroll = (top, bottom).span(Dir::DOWN).scrollable().add(&mut h.rsc);
|
||||
let capped = scroll.max_height(100).add(&mut h.rsc);
|
||||
h.set_root(capped);
|
||||
h.move_to((200, 50));
|
||||
|
||||
// 400 of content in a viewport of 100, so 300 to scroll and the end
|
||||
// showing: the top is 300 above the box, which the window centres.
|
||||
assert_eq!(h.region(&scroll).unwrap().size().y, Px::from_int(100));
|
||||
assert_corners!(h, top, (0, -250), (400, -50));
|
||||
|
||||
h.scroll((0, 1));
|
||||
h.frame();
|
||||
assert_corners!(h, top, (0, -200), (400, 0));
|
||||
}
|
||||
+740
-85
@@ -3,43 +3,244 @@
|
||||
//! 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 is neither: one
|
||||
//! box length, composed two ways, landing either side of the boundary that
|
||||
//! decided whether a child was drawn at all.
|
||||
//! 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.
|
||||
//!
|
||||
//! Each says which seed it was shrunk from, of the generator as it stood when
|
||||
//! it was found. Those numbers no longer grow those trees -- a seed names one
|
||||
//! only while the generator draws the same things in the same order, and the
|
||||
//! leaves have grown an image since -- so what is written out below is the
|
||||
//! record of the case, and the seed is where it came from.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use iris::random::Branch;
|
||||
|
||||
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
||||
/// Every widget in the same place warm as cold, reported all at once: which
|
||||
/// of a dozen boxes moved is the whole of what a shrunk case has to say.
|
||||
///
|
||||
/// A list that names one widget twice is an error rather than a redundant
|
||||
/// check. `width`, `sized` and `align` give back the widget they were handed,
|
||||
/// so a fixture built through them can name one text three times, and then a
|
||||
/// case comparing six boxes compares four and says nothing about it. One
|
||||
/// fixture builds both lists, so checking the warm one checks both.
|
||||
#[track_caller]
|
||||
fn assert_same_regions(
|
||||
warm: &Harness,
|
||||
warm_ids: &[WidgetId],
|
||||
cold: &Harness,
|
||||
cold_ids: &[WidgetId],
|
||||
) {
|
||||
assert_eq!(
|
||||
warm_ids.len(),
|
||||
cold_ids.len(),
|
||||
"the warm and cold fixtures list different widgets"
|
||||
);
|
||||
let named: HashSet<&WidgetId> = warm_ids.iter().collect();
|
||||
assert_eq!(
|
||||
named.len(),
|
||||
warm_ids.len(),
|
||||
"a widget is listed twice: {warm_ids:?}"
|
||||
);
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in warm_ids.iter().zip(cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
|
||||
/// Ten widgets, shrunk from seed 2 at depth 5. The stack is as tall as its
|
||||
/// first child, so its other children belong in that one-line box. A cold
|
||||
/// layout used to keep the span's answer from the larger measuring box while
|
||||
/// a repaint asked it in the stack's final box.
|
||||
fn plant_stack_in_its_sizing_childs_box(h: &mut Harness) -> Vec<WidgetId> {
|
||||
let sizing = wtext("one line, overflowing whatever it is given")
|
||||
.size(16)
|
||||
.wrap(false)
|
||||
.add(&mut h.rsc);
|
||||
let filler = rect(Color::CYAN.alpha(252)).add(&mut h.rsc);
|
||||
let plain = wtext("one line, overflowing whatever it is given")
|
||||
.size(16)
|
||||
.wrap(false)
|
||||
.add(&mut h.rsc);
|
||||
let span = (filler, plain).span(Dir::DOWN).add(&mut h.rsc);
|
||||
let pad = Pad {
|
||||
padding: Padding::ZERO,
|
||||
inner: span.add_strong(&mut h.rsc),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||
let wide = rect(Color::YELLOW.alpha(252)).add(&mut h.rsc);
|
||||
let narrow = rect(Color::RED).add(&mut h.rsc);
|
||||
let branch = Branch {
|
||||
probe: probe.add_strong(&mut h.rsc),
|
||||
wide: wide.add_strong(&mut h.rsc),
|
||||
narrow: narrow.add_strong(&mut h.rsc),
|
||||
threshold: 55.0,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let stack = Stack {
|
||||
children: vec![
|
||||
sizing.add_strong(&mut h.rsc),
|
||||
pad.add_strong(&mut h.rsc),
|
||||
branch.add_strong(&mut h.rsc),
|
||||
],
|
||||
size: StackSize::Child(0),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rules(stack.id(), Some(LayoutLen::LEFTOVER), None);
|
||||
h.set_root(stack);
|
||||
vec![
|
||||
sizing.id(),
|
||||
filler.id(),
|
||||
plain.id(),
|
||||
span.id(),
|
||||
pad.id(),
|
||||
probe.id(),
|
||||
wide.id(),
|
||||
narrow.id(),
|
||||
branch.id(),
|
||||
stack.id(),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repainting_a_stack_uses_the_box_its_sizing_child_decided() {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let ids = plant_stack_in_its_sizing_childs_box(&mut warm);
|
||||
for &id in &ids {
|
||||
warm.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let cold_ids = plant_stack_in_its_sizing_childs_box(&mut cold);
|
||||
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Ten widgets, shrunk from seed 108 at depth 5. The nested reverse spans
|
||||
/// evaluate the branch in successively narrower boxes. The answer from the
|
||||
/// final, decided box must be the one retained after every span is reordered.
|
||||
fn plant_branch_in_nested_reverse_spans(
|
||||
h: &mut Harness,
|
||||
reordered: bool,
|
||||
) -> (Vec<WidgetId>, [WeakWidget<Span>; 3]) {
|
||||
let pair = |first: StrongWidget, second: StrongWidget| match reordered {
|
||||
true => vec![second, first],
|
||||
false => vec![first, second],
|
||||
};
|
||||
let probe = rect(Color::RED.alpha(63)).add(&mut h.rsc);
|
||||
let wide = rect(Color::RED).add(&mut h.rsc);
|
||||
let narrow = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||
let branch = Branch {
|
||||
probe: probe.add_strong(&mut h.rsc),
|
||||
wide: wide.add_strong(&mut h.rsc),
|
||||
narrow: narrow.add_strong(&mut h.rsc),
|
||||
threshold: 483.0,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let wrapped = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||
let down = Span {
|
||||
children: pair(
|
||||
branch.add_strong(&mut h.rsc),
|
||||
wrapped.add_strong(&mut h.rsc),
|
||||
),
|
||||
dir: Dir::DOWN,
|
||||
gap: Px::ZERO,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let inner_filler = rect(Color::CYAN.alpha(63)).add(&mut h.rsc);
|
||||
let inner = Span {
|
||||
children: pair(
|
||||
down.add_strong(&mut h.rsc),
|
||||
inner_filler.add_strong(&mut h.rsc),
|
||||
),
|
||||
dir: Dir::LEFT,
|
||||
gap: Px::ZERO,
|
||||
}
|
||||
.height(LayoutLen::rel(1.0))
|
||||
.add(&mut h.rsc);
|
||||
let outer_filler = rect(Color::GREEN.alpha(63)).add(&mut h.rsc);
|
||||
let outer = Span {
|
||||
children: pair(
|
||||
inner.add_strong(&mut h.rsc),
|
||||
outer_filler.add_strong(&mut h.rsc),
|
||||
),
|
||||
dir: Dir::LEFT,
|
||||
gap: Px::ZERO,
|
||||
}
|
||||
.height(LayoutLen::rel(1.0))
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(outer);
|
||||
(
|
||||
vec![
|
||||
probe.id(),
|
||||
wide.id(),
|
||||
narrow.id(),
|
||||
branch.id(),
|
||||
wrapped.id(),
|
||||
down.id(),
|
||||
inner_filler.id(),
|
||||
inner.id(),
|
||||
outer_filler.id(),
|
||||
outer.id(),
|
||||
],
|
||||
[down, inner, outer],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reordering_nested_spans_keeps_the_answer_from_the_decided_box() {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let (ids, spans) = plant_branch_in_nested_reverse_spans(&mut warm, false);
|
||||
for span in spans {
|
||||
warm.rsc[span].children.rotate_left(1);
|
||||
}
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let (cold_ids, _) = plant_branch_in_nested_reverse_spans(&mut cold, true);
|
||||
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Four 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;
|
||||
let wrapped = wtext("Wrapping shapes")
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.width(76)
|
||||
.add(&mut h.rsc);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(sized, Axis::X, AxisAlign::POS);
|
||||
.set_alignment(wrapped, Axis::X, AxisAlign::POS);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(sized, Axis::Y, AxisAlign::POS);
|
||||
.set_alignment(wrapped, Axis::Y, AxisAlign::POS);
|
||||
let stack = Stack {
|
||||
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
||||
children: vec![plain.add_strong(&mut h.rsc), wrapped.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(),
|
||||
]
|
||||
vec![plain.id(), wrapped.id(), stack.id(), root.id()]
|
||||
}
|
||||
|
||||
/// The first frame does not reach the layout a second one does, so "cold" is
|
||||
@@ -52,7 +253,7 @@ fn one_frame_is_enough() {
|
||||
let first = h.region(&ids[1]).unwrap();
|
||||
for _ in 0..3 {
|
||||
for &id in &ids {
|
||||
h.rsc.widgets_mut().get_dyn_mut(id);
|
||||
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
h.frame();
|
||||
}
|
||||
@@ -74,46 +275,30 @@ 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.rsc.widgets_mut().mark_for_redraw(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"));
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box
|
||||
/// Four 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 inner = (text,).span(Dir::RIGHT).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);
|
||||
let root = (filler, inner).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(),
|
||||
]
|
||||
vec![text.id(), inner.id(), filler.id(), root.id()]
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -128,17 +313,10 @@ fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
|
||||
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"));
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by
|
||||
/// Three 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>) {
|
||||
@@ -162,16 +340,11 @@ fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span
|
||||
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,
|
||||
)
|
||||
h.state.root = Some(span.add_strong(&mut h.rsc));
|
||||
(vec![wrapped.id(), plain.id(), span.id()], span)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -186,17 +359,10 @@ fn swapping_two_children_lands_where_growing_them_that_way_does() {
|
||||
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"));
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its
|
||||
/// Seven 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.
|
||||
@@ -215,8 +381,7 @@ fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
|
||||
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 fixed = rect(Color::RED).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 {
|
||||
@@ -238,7 +403,6 @@ fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
|
||||
text.id(),
|
||||
filler.id(),
|
||||
inner.id(),
|
||||
block.id(),
|
||||
fixed.id(),
|
||||
outer.id(),
|
||||
through.id(),
|
||||
@@ -262,14 +426,7 @@ fn a_span_given_the_box_its_answer_decided_matches_a_cold_layout() {
|
||||
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"));
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Reports a width derived from the box it is asked in. Reading through the
|
||||
@@ -293,11 +450,10 @@ impl Widget for Wider {
|
||||
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);
|
||||
h.set_root(scroll);
|
||||
(content, scroll.id())
|
||||
}
|
||||
|
||||
@@ -389,12 +545,511 @@ fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() {
|
||||
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_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// 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().mark_for_redraw(ids[0]);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let cold_ids = plant_nested_scrolls(&mut cold);
|
||||
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
.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();
|
||||
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
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.";
|
||||
|
||||
fn plant_stack_resized_from_free(h: &mut Harness, fixed: bool) -> (Vec<WidgetId>, WidgetId) {
|
||||
let sizing = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
|
||||
h.rsc.widgets_mut().set_size_rules(sizing.id(), None, None);
|
||||
if fixed {
|
||||
h.rsc.widgets_mut().set_size_rules(
|
||||
sizing.id(),
|
||||
Some(LayoutLen::px(112)),
|
||||
Some(LayoutLen::px(101)),
|
||||
);
|
||||
}
|
||||
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||
let pad = Pad {
|
||||
padding: Padding::ZERO,
|
||||
inner: text.add_strong(&mut h.rsc),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let stack = Stack {
|
||||
children: vec![sizing.add_strong(&mut h.rsc), pad.add_strong(&mut h.rsc)],
|
||||
size: StackSize::Child(0),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(stack);
|
||||
(
|
||||
vec![sizing.id(), text.id(), pad.id(), stack.id()],
|
||||
sizing.id(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixing_a_stacks_sizing_child_repositions_its_overlay() {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let (ids, sizing) = plant_stack_resized_from_free(&mut warm, false);
|
||||
warm.frame();
|
||||
warm.rsc.widgets_mut().set_size_rules(
|
||||
sizing,
|
||||
Some(LayoutLen::px(112)),
|
||||
Some(LayoutLen::px(101)),
|
||||
);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let (cold_ids, _) = plant_stack_resized_from_free(&mut cold, true);
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4,
|
||||
/// `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));
|
||||
}
|
||||
|
||||
/// Nine widgets, shrunk from seed 946 at depth 6. The column is a share of
|
||||
/// the row while its rect has room to draw and a fixed width once it has
|
||||
/// not, so the row asks it twice: in the room, where it answers a share,
|
||||
/// and in its slot, where it answers its text's width. Emptying the column
|
||||
/// changes only the first answer. A local redraw that asked only the second
|
||||
/// question kept the row as it was; the column has to defer to the row.
|
||||
fn plant_column_that_is_a_share_only_while_its_rect_fits(
|
||||
h: &mut Harness,
|
||||
emptied: bool,
|
||||
) -> (Vec<WidgetId>, WeakWidget<Span>, Vec<StrongWidget>) {
|
||||
let first = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||
let filler = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
|
||||
let second = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||
let mut spare: Vec<StrongWidget> =
|
||||
vec![filler.add_strong(&mut h.rsc), second.add_strong(&mut h.rsc)];
|
||||
let mut children: Vec<StrongWidget> = vec![first.add_strong(&mut h.rsc)];
|
||||
if !emptied {
|
||||
children.append(&mut spare);
|
||||
}
|
||||
let column = Span {
|
||||
children,
|
||||
dir: Dir::DOWN,
|
||||
gap: Px::ZERO,
|
||||
}
|
||||
.height(159)
|
||||
.add(&mut h.rsc);
|
||||
let left = rect(Color::MAGENTA.alpha(189)).add(&mut h.rsc);
|
||||
let right = rect(Color::BLUE.alpha(0)).add(&mut h.rsc);
|
||||
let row = Span {
|
||||
children: vec![
|
||||
left.add_strong(&mut h.rsc),
|
||||
column.add_strong(&mut h.rsc),
|
||||
right.add_strong(&mut h.rsc),
|
||||
],
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let end = rect(Color::MAGENTA.alpha(189)).add(&mut h.rsc);
|
||||
let root = Span {
|
||||
children: vec![end.add_strong(&mut h.rsc), row.add_strong(&mut h.rsc)],
|
||||
dir: Dir::LEFT,
|
||||
gap: Px::ZERO,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
(
|
||||
vec![
|
||||
first.id(),
|
||||
filler.id(),
|
||||
second.id(),
|
||||
column.id(),
|
||||
left.id(),
|
||||
right.id(),
|
||||
row.id(),
|
||||
end.id(),
|
||||
root.id(),
|
||||
],
|
||||
column,
|
||||
spare,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emptying_a_column_the_row_asked_twice_asks_the_row_again() {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let (ids, column, _spare) =
|
||||
plant_column_that_is_a_share_only_while_its_rect_fits(&mut warm, false);
|
||||
warm.frame();
|
||||
// Kept alive: dropping the last share of a widget frees its id.
|
||||
let _removed: Vec<StrongWidget> = warm.rsc[column].children.drain(1..).collect();
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let (cold_ids, _, _spare) =
|
||||
plant_column_that_is_a_share_only_while_its_rect_fits(&mut cold, true);
|
||||
cold.frame();
|
||||
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Six widgets, shrunk from seed 59 at depth 5 (`resize-size`). The column
|
||||
/// divides the box it is given between two shares, so its drawing holds for
|
||||
/// that box's length alone, and the pads above it pass that dependency up:
|
||||
/// each one's box is a part of the box it was asked in. Padding narrowing
|
||||
/// the frame it hands down does not change that, and while it was taken to,
|
||||
/// changing the rule over the pads relocated the column's drawing into the
|
||||
/// new box instead of dividing it again.
|
||||
fn plant_two_shares_under_two_pads(h: &mut Harness, height: f32) -> Vec<WidgetId> {
|
||||
let top = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
|
||||
let bottom = rect(Color::RED).add(&mut h.rsc);
|
||||
let column = (top, bottom).span(Dir::DOWN).add(&mut h.rsc);
|
||||
let inner = Pad {
|
||||
padding: Padding::ZERO,
|
||||
inner: column.add_strong(&mut h.rsc),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let outer = Pad {
|
||||
padding: Padding::ZERO,
|
||||
inner: inner.add_strong(&mut h.rsc),
|
||||
}
|
||||
.height(height)
|
||||
.add(&mut h.rsc);
|
||||
let beside = rect(Color::BLUE).add(&mut h.rsc);
|
||||
h.set_root((outer, beside).span(Dir::RIGHT));
|
||||
vec![
|
||||
top.id(),
|
||||
bottom.id(),
|
||||
column.id(),
|
||||
inner.id(),
|
||||
outer.id(),
|
||||
beside.id(),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_a_rule_over_two_pads_divides_the_column_again() {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let ids = plant_two_shares_under_two_pads(&mut warm, 88.0);
|
||||
warm.frame();
|
||||
warm.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rules(ids[4], None, Some(LayoutLen::px(105)));
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let cold_ids = plant_two_shares_under_two_pads(&mut cold, 105.0);
|
||||
cold.frame();
|
||||
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Six widgets, shrunk from seed 942 at depth 6 (`resize`). A `Branch` asks
|
||||
/// its probe in the top 40 px of its box and forwards the frame, so the
|
||||
/// scroll's own box is 40 px tall whatever the window is -- but its content
|
||||
/// is as tall as the frame, which is the window, and a scroll kept to its
|
||||
/// end has to be told when that changes. Resolving a length against the
|
||||
/// window is what reads it, so that is where the dependency is taken.
|
||||
fn plant_a_window_tall_column_in_a_short_scroll(h: &mut Harness) -> Vec<WidgetId> {
|
||||
let leaf = rect(Color::RED).add(&mut h.rsc);
|
||||
let column = Span {
|
||||
children: vec![leaf.add_strong(&mut h.rsc)],
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
}
|
||||
.height(rel(1.0))
|
||||
.add(&mut h.rsc);
|
||||
let scroll = Scroll::new(column.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
||||
let wide = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let narrow = rect(Color::GREEN).add(&mut h.rsc);
|
||||
let root = Branch {
|
||||
probe: scroll.add_strong(&mut h.rsc),
|
||||
wide: wide.add_strong(&mut h.rsc),
|
||||
narrow: narrow.add_strong(&mut h.rsc),
|
||||
threshold: 55.0,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
vec![leaf.id(), column.id(), scroll.id(), root.id()]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resizing_under_a_short_scroll_snaps_its_window_tall_content_again() {
|
||||
let mut warm = Harness::new((1920, 1200));
|
||||
let ids = plant_a_window_tall_column_in_a_short_scroll(&mut warm);
|
||||
warm.frame();
|
||||
warm.resize((640, 900));
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let cold_ids = plant_a_window_tall_column_in_a_short_scroll(&mut cold);
|
||||
cold.frame();
|
||||
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// A scroll clamps its position against the box it is drawn in, so drawing it
|
||||
/// once at one viewport and again at another writes state the second draw then
|
||||
/// reads. That the answer is still the one a cold layout gives is a property
|
||||
/// of the clamp, not something the layout enforces.
|
||||
#[test]
|
||||
fn a_scrolled_view_resized_lands_where_a_cold_layout_puts_it() {
|
||||
for amt in [10.0, 40.0, 90.0, 140.0] {
|
||||
let mut warm = Harness::new((100, 100));
|
||||
let (_, warm_scroll) = plant_wider(&mut warm, 100.0);
|
||||
warm.move_to((50.0, 50.0));
|
||||
warm.scroll((-amt, 0.0));
|
||||
warm.frame();
|
||||
warm.resize((160, 100));
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((160, 100));
|
||||
let (_, cold_scroll) = plant_wider(&mut cold, 100.0);
|
||||
cold.move_to((50.0, 50.0));
|
||||
cold.scroll((-amt, 0.0));
|
||||
cold.frame();
|
||||
|
||||
assert_eq!(
|
||||
warm.region(&warm_scroll),
|
||||
cold.region(&cold_scroll),
|
||||
"scrolled by {amt} then widened"
|
||||
);
|
||||
}
|
||||
}
|
||||
+80
-61
@@ -8,19 +8,22 @@
|
||||
//! 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.
|
||||
//! Two fixtures, because the walk happens in both stages. `chain_cost_by_depth`
|
||||
//! draws instances two pixels wide so that vertex work dominates; a walk that
|
||||
//! does not show up against small quads will not show up against anything.
|
||||
//! `mask_cost_by_depth` draws one screenful through a mask instead, which is
|
||||
//! where a walk in the fragment stage would show and nowhere else.
|
||||
|
||||
use iris::prelude::*;
|
||||
use iris_core::{
|
||||
Len, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
|
||||
Len, Mask, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
|
||||
UiRenderState, UiSpan,
|
||||
};
|
||||
use wgpu::{Color as GpuColor, *};
|
||||
|
||||
#[path = "gpu/mod.rs"]
|
||||
mod gpu;
|
||||
|
||||
const SIZE: u32 = 1024;
|
||||
const INSTANCES: usize = 200_000;
|
||||
const FRAMES: u32 = 20;
|
||||
@@ -29,18 +32,7 @@ const FRAMES: u32 = 20;
|
||||
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()?;
|
||||
let adapter = gpu::adapter()?;
|
||||
if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
|
||||
println!("no timestamp queries on {:?}", adapter.get_info().name);
|
||||
return None;
|
||||
@@ -55,22 +47,17 @@ fn gpu() -> Option<(Device, Queue, f32)> {
|
||||
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![],
|
||||
}
|
||||
/// Which stage the fill puts the work in: many small quads, where a walk per
|
||||
/// vertex is what shows, or one screenful of masked rows, where a walk per
|
||||
/// fragment would.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Fixture {
|
||||
Quads,
|
||||
Masked,
|
||||
}
|
||||
|
||||
/// A chain `depth` slots long, and instances that all resolve through its end.
|
||||
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
||||
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize, fixture: Fixture) {
|
||||
let kind = ui.primitives.kind::<RectPrimitive>();
|
||||
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
||||
|
||||
@@ -80,20 +67,51 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
||||
}
|
||||
|
||||
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;
|
||||
let rows = SIZE as usize;
|
||||
let mask_idx = match fixture {
|
||||
Fixture::Quads => MaskIdx::NONE,
|
||||
// Its own chain as long as the instances', since a viewport sits as
|
||||
// deep in the tree as the content it clips.
|
||||
Fixture::Masked => {
|
||||
let idx = ui.masks.push(Mask {
|
||||
region: UiRegion::FULL,
|
||||
move_idx: slot,
|
||||
});
|
||||
// Nothing frees it here, but the owner's reference is what a real
|
||||
// one is kept alive by.
|
||||
ui.masks.push_ref(idx);
|
||||
idx
|
||||
}
|
||||
};
|
||||
let instances = match fixture {
|
||||
Fixture::Quads => INSTANCES,
|
||||
Fixture::Masked => rows,
|
||||
};
|
||||
for i in 0..instances {
|
||||
let region = match fixture {
|
||||
Fixture::Quads => {
|
||||
let x = (i % (rows / 2)) as f32 * 2.0;
|
||||
let y = (i / (rows / 2)) as f32;
|
||||
UiRegion::new(
|
||||
UiSpan::new(px(x), px(x + 2.0)),
|
||||
UiSpan::new(px(y), px(y + 1.0)),
|
||||
)
|
||||
}
|
||||
// A full row each, so one screenful of fragments goes through the
|
||||
// mask and the vertex stage is four corners per row.
|
||||
Fixture::Masked => UiRegion::new(
|
||||
UiSpan::new(px(0.0), px(SIZE as f32)),
|
||||
UiSpan::new(px(i as f32), px(i as f32 + 1.0)),
|
||||
),
|
||||
};
|
||||
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,
|
||||
region,
|
||||
mask_idx,
|
||||
move_idx: slot,
|
||||
},
|
||||
);
|
||||
@@ -101,28 +119,15 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
||||
}
|
||||
|
||||
/// Nanoseconds the pass took on the GPU, best of `BATCHES`.
|
||||
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
|
||||
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize, fixture: Fixture) -> f64 {
|
||||
let format = TextureFormat::Bgra8Unorm;
|
||||
let mut node = UiRenderNode::new(device, &config(format));
|
||||
let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
|
||||
let mut ui = UiData::default();
|
||||
let mut render = UiRenderState::new();
|
||||
fill(&mut ui, &mut render, depth);
|
||||
fill(&mut ui, &mut render, depth, fixture);
|
||||
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 target = gpu::target(device, format, SIZE, false);
|
||||
let view = target.create_view(&TextureViewDescriptor::default());
|
||||
|
||||
let queries = device.create_query_set(&QuerySetDescriptor {
|
||||
@@ -202,17 +207,15 @@ fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
|
||||
best
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "measurement, not a check"]
|
||||
fn chain_cost_by_depth() {
|
||||
fn by_depth(fixture: Fixture, instances: usize) {
|
||||
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");
|
||||
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 ns = pass_cost(&device, &queue, period, depth, fixture);
|
||||
let base = *base.get_or_insert(ns);
|
||||
println!(
|
||||
"depth {depth:>3}: {:>9.1} us {:+6.1}% against depth 1",
|
||||
@@ -221,3 +224,19 @@ fn chain_cost_by_depth() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "measurement, not a check"]
|
||||
fn chain_cost_by_depth() {
|
||||
by_depth(Fixture::Quads, INSTANCES);
|
||||
}
|
||||
|
||||
/// One screenful of rows, every one clipped by a mask whose own chain is that
|
||||
/// deep. What this says that the quads cannot is whether a mask costs the walk
|
||||
/// once per instance or once per fragment: at a screenful of fragments per
|
||||
/// chain, the second is the difference between these two tables.
|
||||
#[test]
|
||||
#[ignore = "measurement, not a check"]
|
||||
fn mask_cost_by_depth() {
|
||||
by_depth(Fixture::Masked, SIZE as usize);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//! What one container's draw costs against the number of children it has.
|
||||
//!
|
||||
//! cargo test --release --test children_cost -- --ignored --nocapture
|
||||
//!
|
||||
//! Every other rig here varies depth, the window, or what changed between
|
||||
//! frames; this one varies width, which is the dimension a container's own
|
||||
//! per-child bookkeeping is counted in. A list of rows is the shape that gets
|
||||
//! wide -- a transcript, a file tree -- and a cost per child that is not flat
|
||||
//! down this table is a cost paid twice for every child added.
|
||||
//!
|
||||
//! Wall time rather than instructions, because what is being told apart here
|
||||
//! is a factor rather than a few percent, and the table says which it is: a
|
||||
//! flat right-hand column is linear and a rising one is not.
|
||||
|
||||
mod rig;
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use rig::env;
|
||||
use std::time::Instant;
|
||||
|
||||
/// A column of leaves each with a length of its own, so the span asks every
|
||||
/// one of them and reads what each answered.
|
||||
fn build(h: &mut Harness, children: usize) -> WidgetId {
|
||||
let mut col = Span::empty(Dir::DOWN);
|
||||
for _ in 0..children {
|
||||
col.push(
|
||||
rect(Color::RED)
|
||||
.height(LayoutLen::px(4.0))
|
||||
.add_strong(&mut h.rsc),
|
||||
);
|
||||
}
|
||||
let root = col.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
root.id()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "measurement, not a check"]
|
||||
fn draw_cost_by_children() {
|
||||
let frames = env("FRAMES", 40_usize);
|
||||
println!("{frames} full redraws of one span, per child in the last column");
|
||||
for children in [100_usize, 200, 400, 800, 1600] {
|
||||
// Tall enough that no child is collapsed for want of room.
|
||||
let mut h = Harness::new((600.0, children as f32 * 8.0));
|
||||
let root = build(&mut h, children);
|
||||
h.frame();
|
||||
let start = Instant::now();
|
||||
for _ in 0..frames {
|
||||
h.rsc.widgets_mut().mark_for_redraw(root);
|
||||
h.frame();
|
||||
}
|
||||
let ms = start.elapsed().as_secs_f64() * 1000.0 / frames as f64;
|
||||
println!(
|
||||
"children {children:>5}: {ms:>8.3} ms per redraw, {:>7.4} ms each",
|
||||
ms / children as f64
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
mod rig;
|
||||
#[path = "scenario/mod.rs"]
|
||||
mod scenario;
|
||||
|
||||
use iris::prelude::*;
|
||||
use iris::random::{Edits, Plan, plan};
|
||||
|
||||
fn check_requests(edit: impl Fn(&mut Plan) + Sync) {
|
||||
let depth = rig::env("IRIS_DEFERRED_DEPTH", 4_usize);
|
||||
let seeds = rig::seeds("IRIS_DEFERRED_SEED", "IRIS_DEFERRED_SEEDS", 20);
|
||||
scenario::over_seeds(seeds, |seed| {
|
||||
let mut grown = plan(seed, depth, &Edits::default());
|
||||
edit(&mut grown);
|
||||
for case in scenario::ALL {
|
||||
if let Some(how) = scenario::diverges(&grown, case, seed) {
|
||||
panic!(
|
||||
"request seed {seed} depth {depth} after {}: {how}",
|
||||
case.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_requests_agree_warm_and_cold() {
|
||||
check_requests(|grown| {
|
||||
let mut index = 0;
|
||||
grown.walk_mut(&mut |node| {
|
||||
if let Some(rules) = &mut node.size {
|
||||
for axis in Axis::BOTH {
|
||||
index += 1;
|
||||
rules[axis] = match index % 7 {
|
||||
0 => leftover(1).clamp(20, 120).into(),
|
||||
1 => leftover(1).min(rel(0.5)).into(),
|
||||
2 => (leftover(1) + px(30)).min(leftover(2)).into(),
|
||||
// Both sides an expression, the one shape that
|
||||
// copies a request's nodes into another's.
|
||||
3 => leftover(1).min(px(40)).max(leftover(2).min(px(70))).into(),
|
||||
_ => rules[axis].clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_intrinsic_bounds_agree_warm_and_cold() {
|
||||
check_requests(|grown| {
|
||||
grown.walk_mut(&mut |node| {
|
||||
if let Some(rules) = &mut node.size {
|
||||
for axis in Axis::BOTH {
|
||||
let bound = &mut rules[axis].bound;
|
||||
if bound.min.is_some() {
|
||||
bound.min = Some(Len::rel(0.25));
|
||||
}
|
||||
if bound.max.is_some() {
|
||||
bound.max = Some(Len::rel(0.75));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferred_requests_with_independent_bounds_agree_warm_and_cold() {
|
||||
check_requests(|grown| {
|
||||
let mut index = 0;
|
||||
grown.walk_mut(&mut |node| {
|
||||
if let Some(rules) = &mut node.size {
|
||||
for axis in Axis::BOTH {
|
||||
index += 1;
|
||||
rules[axis].request = Some(match index % 4 {
|
||||
0 => leftover(1).into(),
|
||||
1 => rel(0.5).into(),
|
||||
2 => px(80).into(),
|
||||
_ => (leftover(1) + px(30)).min(leftover(2)),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
+5
-34
@@ -13,10 +13,6 @@
|
||||
//! That is how `PrimitiveRender` was measured against a match in the renderer:
|
||||
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording
|
||||
//! one.
|
||||
//!
|
||||
//! The instance is leaked deliberately. A Vulkan loader may unload the driver
|
||||
//! when the last one drops, which can fault as a thread that used it exits --
|
||||
//! and every test runs on a spawned thread.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -27,6 +23,9 @@ use iris_core::{
|
||||
};
|
||||
use wgpu::{Color as GpuColor, *};
|
||||
|
||||
#[path = "gpu/mod.rs"]
|
||||
mod gpu;
|
||||
|
||||
const SIZE: u32 = 1024;
|
||||
const FRAMES: u32 = 200;
|
||||
/// Reported as the best of this many batches, since the mean moves by more
|
||||
@@ -34,39 +33,11 @@ const FRAMES: u32 = 200;
|
||||
const BATCHES: u32 = 8;
|
||||
|
||||
fn gpu() -> Option<(Device, Queue)> {
|
||||
// Probed rather than assumed: there may be no Vulkan adapter, and GL is
|
||||
// what is left when there is not.
|
||||
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()
|
||||
}),
|
||||
};
|
||||
// Leaked rather than dropped: see the note at the top of the file.
|
||||
let instance: &'static Instance = Box::leak(Box::new(instance));
|
||||
let adapter =
|
||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
|
||||
let adapter = gpu::adapter()?;
|
||||
println!("adapter: {:?}", adapter.get_info());
|
||||
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
|
||||
}
|
||||
|
||||
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![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Every layer draws all three primitives, so the renderer takes a different
|
||||
/// path for each list it walks -- which is the case a single-primitive layer
|
||||
/// would never exercise. Images are bound per instance, so there are few.
|
||||
@@ -136,7 +107,7 @@ fn fill(
|
||||
|
||||
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 {
|
||||
let format = TextureFormat::Bgra8Unorm;
|
||||
let mut node = UiRenderNode::new(device, &config(format));
|
||||
let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
|
||||
let mut ui = UiData::default();
|
||||
let mut render = UiRenderState::new();
|
||||
let _handles = fill(&mut ui, &mut render, layers, per_layer);
|
||||
|
||||
+80
-541
@@ -1,19 +1,23 @@
|
||||
//! Random trees, checked against building the same tree cold.
|
||||
//! Laying a tree out again has to land where growing it that way would.
|
||||
//!
|
||||
//! A frame reaches its layout by keeping most of the last one: movable regions
|
||||
//! or primitive boxes rewritten, some widgets drawn again, the rest untouched.
|
||||
//! The result must be the tree a cold start would have produced, so anything
|
||||
//! wrongly retained shows up as a difference in somebody's box.
|
||||
//! 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
|
||||
//!
|
||||
//! `iris::random` grows the tree and `examples/random.rs` draws one. A seed is
|
||||
//! the whole reproduction; `a_long_run_of_seeds_agrees` is the ignored sweep
|
||||
//! for when it is worth spending the time.
|
||||
//! 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.
|
||||
|
||||
use std::collections::HashMap;
|
||||
mod rig;
|
||||
#[path = "scenario/mod.rs"]
|
||||
mod scenario;
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use iris::random::{Aligns, Edits, Lens, Rng, SpanEdit, Tree, grow};
|
||||
use iris::random::{Edits, Plan, plan};
|
||||
use rig::env;
|
||||
use scenario::{ALL, Case, diverges, 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
|
||||
@@ -23,562 +27,97 @@ fn depth() -> usize {
|
||||
env("IRIS_GENERATED_DEPTH", 4)
|
||||
}
|
||||
|
||||
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)
|
||||
/// The seeds the ordinary tests take: a corpus rather than a set of
|
||||
/// regression cases, since a seed names a tree only for as long as the
|
||||
/// generator draws the same things in the same order. Adding images to the
|
||||
/// leaves moved every one of them, so 20 and 86 -- which once caught a widget
|
||||
/// placed twice in a box its parent had already placed it in, and a `Scroll`
|
||||
/// fixed point settling differently -- no longer grow those trees. Both
|
||||
/// defects are pinned by the shrunk fixtures in `cases/unsettled.rs`, which
|
||||
/// are trees rather than numbers.
|
||||
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
|
||||
|
||||
fn check(seed: u64, depth: usize, case: Case) {
|
||||
check_plan(&plan(seed, depth, &Edits::default()), seed, depth, case);
|
||||
}
|
||||
const SEEDS: [u64; 9] = [1, 2, 3, 5, 8, 10, 13, 86, 98];
|
||||
|
||||
/// The same box, to a step of the grid per level of nesting between the two
|
||||
/// ways of reaching it. A move, a repaint and a row of shares land on the
|
||||
/// same number now; what is left is a box centred in a fraction of its parent
|
||||
/// against the same box centred in its own pixels. A step is a thousandth of
|
||||
/// a pixel, where this was a twentieth of one before any of it was on a grid.
|
||||
const AGREE_STEPS: i32 = 2;
|
||||
|
||||
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,
|
||||
fn check_plan(grown: &Plan, seed: u64, depth: usize, case: Case) {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree {
|
||||
let (root, tree) = grow(&mut h.rsc, seed, depth(), edits);
|
||||
h.state.root = Some(root);
|
||||
h.frame();
|
||||
tree
|
||||
}
|
||||
|
||||
fn resize_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens {
|
||||
let lens = [
|
||||
Some(LayoutLen::px(20.0 + rng.below(180) as f32)),
|
||||
Some(LayoutLen::px(20.0 + rng.below(180) as f32)),
|
||||
];
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rules(tree.sized[idx], lens[0], lens[1]);
|
||||
lens
|
||||
}
|
||||
|
||||
/// Changes a few of the declared sizes, and says which, so the cold tree can
|
||||
/// be grown with the same ones.
|
||||
fn edit(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap<usize, Lens> {
|
||||
let mut edits = HashMap::new();
|
||||
for _ in 0..4 {
|
||||
let idx = rng.below(tree.sized.len());
|
||||
edits.insert(idx, resize_one(h, tree, idx, rng));
|
||||
}
|
||||
edits
|
||||
}
|
||||
|
||||
/// Every declared size at once, so every reader of a size in the tree has a
|
||||
/// changed descendant in the same frame and the whole dirty set has to settle
|
||||
/// together.
|
||||
fn edit_every(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap<usize, Lens> {
|
||||
(0..tree.sized.len())
|
||||
.map(|idx| (idx, resize_one(h, tree, idx, rng)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
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,
|
||||
}
|
||||
|
||||
const SHUFFLES: [Shuffle; 5] = [
|
||||
Shuffle::EveryOther,
|
||||
Shuffle::AllButFirst,
|
||||
Shuffle::AddThree,
|
||||
Shuffle::SwapForThree,
|
||||
Shuffle::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,
|
||||
},
|
||||
/// A test per case, and the list of which cases have one, from the same
|
||||
/// place. A case the ordinary suite leaves out runs only in the long scan,
|
||||
/// which nobody runs by hand.
|
||||
macro_rules! cases {
|
||||
($($name:ident = $case:expr,)*) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() {
|
||||
for seed in SEEDS {
|
||||
check(seed, depth(), $case);
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
/// Applies `shuffle` to every third span, and says what it did so the cold
|
||||
/// tree can be grown that way. The widgets it takes out are given back: the
|
||||
/// last share of one must outlive the comparison, or its id is handed to
|
||||
/// something else and the two trees stop lining up.
|
||||
fn reshuffle(
|
||||
h: &mut Harness,
|
||||
tree: &mut Tree,
|
||||
shuffle: Shuffle,
|
||||
) -> (HashMap<usize, SpanEdit>, Vec<StrongWidget>) {
|
||||
let mut edits = HashMap::new();
|
||||
let mut detached = Vec::new();
|
||||
for (idx, span) in tree.spans.iter_mut().enumerate().step_by(3) {
|
||||
let span_edit = shuffle.of(span.grown);
|
||||
let mut take = span_edit.detach.clone();
|
||||
take.sort_unstable();
|
||||
let children = &mut h.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() {
|
||||
detached.push(children.remove(j));
|
||||
}
|
||||
}
|
||||
let attach = span_edit.attach.min(span.spares.len());
|
||||
children.extend(span.spares.drain(..attach));
|
||||
edits.insert(idx, span_edit);
|
||||
}
|
||||
(edits, detached)
|
||||
}
|
||||
|
||||
/// What a widget was configured with, so a tree the generator found can be
|
||||
/// written out by hand. A fuzz 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(),
|
||||
const NAMED: [Case; [$($case,)*].len()] = [$($case,)*];
|
||||
};
|
||||
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
|
||||
cases! {
|
||||
many_widgets_redrawing_at_once_leaves_every_box_where_it_was = Case::RepaintSome,
|
||||
everything_redrawing_at_once_leaves_every_box_where_it_was = Case::Repaint,
|
||||
a_resize_lands_where_starting_at_that_size_would = Case::Resize,
|
||||
a_resize_and_a_repaint_land_where_starting_that_way_would = Case::ResizeRepaint,
|
||||
a_size_change_after_a_resize_lands_the_same_way = Case::ResizeSize,
|
||||
a_resize_after_a_size_change_lands_the_same_way = Case::SizeResize,
|
||||
a_size_change_lands_where_growing_it_that_way_would = Case::Size,
|
||||
every_size_changing_at_once_lands_where_growing_it_that_way_would = Case::EverySize,
|
||||
an_alignment_change_lands_where_growing_it_that_way_would = Case::Align,
|
||||
giving_and_taking_a_movable_region_rebuilds_what_resolves_it = Case::RegionNode,
|
||||
reordering_a_span_lands_where_growing_it_that_way_would = Case::Reorder,
|
||||
}
|
||||
|
||||
/// Every widget in one tree against the matching widget in the other. A
|
||||
/// mismatch prints the widget's ancestry, marking region nodes, since where
|
||||
/// two trees disagree is rarely where the cause is.
|
||||
fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness, &Tree)) {
|
||||
let ((wh, wt), (ch, ct)) = (warm, cold);
|
||||
assert_eq!(wt.ids.len(), ct.ids.len(), "seed {seed}: different trees");
|
||||
let mut drawn = 0;
|
||||
let mut wrong = 0;
|
||||
for (i, (&w, &c)) in wt.ids.iter().zip(&ct.ids).enumerate() {
|
||||
let (got, want) = (wh.region(&w), ch.region(&c));
|
||||
drawn += usize::from(got.is_some());
|
||||
// This oracle cares where rasterization lands, not whether equivalent
|
||||
// arithmetic produced the same f32. Keep the tolerance to one
|
||||
// twentieth of a physical pixel, while whether a widget drew remains
|
||||
// exact.
|
||||
if same_region(got, want) {
|
||||
continue;
|
||||
}
|
||||
wrong += 1;
|
||||
if wrong <= 3 {
|
||||
let mut chain = Vec::new();
|
||||
let mut at = Some(w);
|
||||
while let Some(id) = at {
|
||||
let active = &wh.render.active[&id];
|
||||
let node = match active.move_idx == active.parent_move {
|
||||
true => "",
|
||||
false => "*",
|
||||
};
|
||||
chain.push(format!("{}{node}", describe(id, wh)));
|
||||
at = active.parent;
|
||||
}
|
||||
println!(
|
||||
"seed {seed} after {what}: widget {i}\n warm {got:?}\n cold {want:?}\n {}",
|
||||
chain.join(" < ")
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(drawn > 0, "seed {seed}: nothing was drawn");
|
||||
assert_eq!(wrong, 0, "seed {seed}: {wrong} widgets differ after {what}");
|
||||
}
|
||||
|
||||
fn changed_size(seed: u64) {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let grown = plant(&mut warm, seed, &Edits::default());
|
||||
// Not every tree grows a declared size to change.
|
||||
if grown.sized.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut rng = Rng::new(seed ^ 0x5eed);
|
||||
let sizes = edit(&mut warm, &grown, &mut rng);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let same = plant(
|
||||
&mut cold,
|
||||
seed,
|
||||
&Edits {
|
||||
sizes,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_same(seed, "a size change", (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
/// Moves one widget to a different corner of the box it is given.
|
||||
fn realign_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns {
|
||||
let mut side = || match rng.below(4) {
|
||||
0 => None,
|
||||
1 => Some(AxisAlign::NEG),
|
||||
2 => Some(AxisAlign::CENTER),
|
||||
_ => Some(AxisAlign::POS),
|
||||
};
|
||||
let aligns = [side(), side()];
|
||||
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(aligns) {
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(tree.aligned[idx], axis, align.unwrap_or_default());
|
||||
}
|
||||
aligns
|
||||
}
|
||||
|
||||
fn changed_alignment(seed: u64) {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let grown = plant(&mut warm, seed, &Edits::default());
|
||||
if grown.aligned.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut rng = Rng::new(seed ^ 0xa11);
|
||||
let aligns = (0..grown.aligned.len())
|
||||
.step_by(3)
|
||||
.map(|idx| (idx, realign_one(&mut warm, &grown, idx, &mut rng)))
|
||||
.collect();
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let same = plant(
|
||||
&mut cold,
|
||||
seed,
|
||||
&Edits {
|
||||
aligns,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_same(seed, "an alignment change", (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
/// 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. A cold tree built that way is what says the rebuild was complete.
|
||||
fn changed_region_node(seed: u64) {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let grown = plant(&mut warm, seed, &Edits::default());
|
||||
if grown.nodes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let nodes: HashMap<usize, bool> = (0..grown.nodes.len())
|
||||
.step_by(2)
|
||||
.map(|idx| {
|
||||
let id = grown.nodes[idx];
|
||||
let was = warm.rsc.widgets().is_region_node(id);
|
||||
warm.rsc.widgets_mut().set_region_node(id, !was);
|
||||
(idx, !was)
|
||||
})
|
||||
.collect();
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let same = plant(
|
||||
&mut cold,
|
||||
seed,
|
||||
&Edits {
|
||||
nodes,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_same(
|
||||
seed,
|
||||
"a region-node change",
|
||||
(&warm, &grown),
|
||||
(&cold, &same),
|
||||
);
|
||||
}
|
||||
|
||||
fn reshuffled(seed: u64, shuffle: Shuffle) {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let mut grown = plant(&mut warm, seed, &Edits::default());
|
||||
// Some seeds grow nothing but wrappers, and a shuffle with no span to
|
||||
// shuffle is not the same thing as one that had no effect. A span behind
|
||||
// a branch nobody took is the same kind of nothing: it is not drawn, so
|
||||
// shuffling it cannot move anything.
|
||||
let shuffles = grown
|
||||
.spans
|
||||
.iter()
|
||||
.step_by(3)
|
||||
.any(|span| warm.region(&span.id.id()).is_some());
|
||||
if !shuffles {
|
||||
return;
|
||||
}
|
||||
let (spans, _held) = reshuffle(&mut warm, &mut grown, shuffle);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let same = plant(
|
||||
&mut cold,
|
||||
seed,
|
||||
&Edits {
|
||||
spans,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let what = format!("{shuffle:?}");
|
||||
assert_same(seed, &what, (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
fn changed_every_size(seed: u64) {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let grown = plant(&mut warm, seed, &Edits::default());
|
||||
if grown.sized.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut rng = Rng::new(seed ^ 0xa11);
|
||||
let sizes = edit_every(&mut warm, &grown, &mut rng);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let same = plant(
|
||||
&mut cold,
|
||||
seed,
|
||||
&Edits {
|
||||
sizes,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_same(seed, "every size at once", (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
/// Marks a spread of widgets for redraw at once. Nothing changes, so no box
|
||||
/// may either; what this exercises is the order a frame settles a dirty set
|
||||
/// in, which the other cases reach one dependency path at a time.
|
||||
fn repainted_together(seed: u64) {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let grown = plant(&mut warm, seed, &Edits::default());
|
||||
for &id in grown.ids.iter().step_by(5) {
|
||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||
}
|
||||
/// The shuffles are one test between them, so they are the only cases `ALL`
|
||||
/// may hold without a test of their own.
|
||||
#[test]
|
||||
fn every_case_runs_without_the_long_scan() {
|
||||
for case in ALL {
|
||||
assert!(
|
||||
!warm.rsc.widgets().needs_redraw.is_empty(),
|
||||
"seed {seed}: nothing was marked"
|
||||
NAMED.contains(&case) || matches!(case, Case::Shuffle(_)),
|
||||
"{} runs only in the long seed scan; give it a case here",
|
||||
case.name()
|
||||
);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let same = plant(&mut cold, seed, &Edits::default());
|
||||
|
||||
let what = "many repaints at once";
|
||||
assert_same(seed, what, (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
fn resized(seed: u64) {
|
||||
let mut warm = Harness::new((1920, 1200));
|
||||
let grown = plant(&mut warm, seed, &Edits::default());
|
||||
warm.resize((640, 900));
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let same = plant(&mut cold, seed, &Edits::default());
|
||||
|
||||
assert_same(seed, "a resize", (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
fn resized_then_changed(seed: u64) {
|
||||
let mut warm = Harness::new((1920, 1200));
|
||||
let grown = plant(&mut warm, seed, &Edits::default());
|
||||
if grown.sized.is_empty() {
|
||||
return;
|
||||
}
|
||||
warm.resize((640, 900));
|
||||
warm.frame();
|
||||
|
||||
let mut rng = Rng::new(seed ^ 0xb0a7);
|
||||
let sizes = edit(&mut warm, &grown, &mut rng);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let same = plant(
|
||||
&mut cold,
|
||||
seed,
|
||||
&Edits {
|
||||
sizes,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let what = "a resize then a size change";
|
||||
assert_same(seed, what, (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_changed_size_lands_where_growing_it_that_way_would() {
|
||||
SEEDS.into_iter().for_each(changed_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_changed_alignment_lands_where_growing_it_that_way_would() {
|
||||
SEEDS.into_iter().for_each(changed_alignment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_toggled_region_node_lands_where_growing_it_that_way_would() {
|
||||
SEEDS.into_iter().for_each(changed_region_node);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_size_changing_at_once_lands_where_growing_it_that_way_would() {
|
||||
SEEDS.into_iter().for_each(changed_every_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_widgets_redrawing_at_once_leaves_every_box_where_it_was() {
|
||||
SEEDS.into_iter().for_each(repainted_together);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resize_lands_where_starting_at_that_size_would() {
|
||||
SEEDS.into_iter().for_each(resized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_size_change_after_a_resize_lands_the_same_way() {
|
||||
SEEDS.into_iter().for_each(resized_then_changed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
|
||||
for shuffle in SHUFFLES {
|
||||
for case in ALL {
|
||||
if matches!(case, Case::Shuffle(_)) {
|
||||
for seed in SEEDS {
|
||||
reshuffled(seed, shuffle);
|
||||
check(seed, depth(), case);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The same property over a hundred seeds and every scenario. What it has
|
||||
/// found so far was never where the trees disagreed: a text measured in a box
|
||||
/// it was not going to get, and a widget re-measured in a box its own answer
|
||||
/// had decided. `tests/shrink.rs` is how a seed from here becomes a tree
|
||||
/// small enough to read.
|
||||
#[test]
|
||||
#[ignore = "a hundred seeds, rather than the nine the others check"]
|
||||
#[ignore = "as many seeds as it is asked for, rather than the ten the others check"]
|
||||
fn a_long_run_of_seeds_agrees() {
|
||||
let seeds = std::env::var("IRIS_GENERATED_SEED")
|
||||
.ok()
|
||||
.and_then(|seed| seed.parse().ok())
|
||||
.map(|seed| seed..=seed)
|
||||
.unwrap_or_else(|| 1..=env("IRIS_GENERATED_SEEDS", 100));
|
||||
over_seeds(seeds.collect(), |seed| {
|
||||
changed_size(seed);
|
||||
changed_every_size(seed);
|
||||
repainted_together(seed);
|
||||
resized(seed);
|
||||
resized_then_changed(seed);
|
||||
for shuffle in SHUFFLES {
|
||||
reshuffled(seed, shuffle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Every seed on its own thread's share of them. A tree is grown, laid out
|
||||
/// and dropped inside one call, so seeds share nothing, and this is most of
|
||||
/// the time a run takes. A thread that fails takes the scope down with it,
|
||||
/// which is the same panic libtest would have seen.
|
||||
///
|
||||
/// One core short of all of them, so the machine this runs on stays usable.
|
||||
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)));
|
||||
let depth = depth();
|
||||
let seeds = rig::seeds("IRIS_GENERATED_SEED", "IRIS_GENERATED_SEEDS", 100);
|
||||
over_seeds(seeds, |seed| {
|
||||
let grown = plan(seed, depth, &Edits::default());
|
||||
for case in ALL {
|
||||
check_plan(&grown, seed, depth, case);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! The adapter, the surface configuration and the target the GPU rigs share,
|
||||
//! so no two of them can probe for a device or make a target in different
|
||||
//! ways.
|
||||
|
||||
use wgpu::*;
|
||||
|
||||
/// An adapter on whatever this machine has, or `None` where there is none.
|
||||
///
|
||||
/// Probed rather than assumed: there may be no Vulkan adapter, and GL is what
|
||||
/// is left when there is not.
|
||||
///
|
||||
/// The instance is leaked deliberately. A Vulkan loader may unload the driver
|
||||
/// when the last one drops, which can fault as a thread that used it exits --
|
||||
/// and every test runs on a spawned thread.
|
||||
pub fn adapter() -> Option<Adapter> {
|
||||
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));
|
||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()
|
||||
}
|
||||
|
||||
pub fn config(format: TextureFormat, size: u32) -> 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 square colour target to draw a pass into. `copy` adds the usage a rig
|
||||
/// that reads the pixels back needs; one that only times the pass does not.
|
||||
// This module is compiled into each rig target separately, so a helper the
|
||||
// ones that make no target of their own do not call is dead code there.
|
||||
#[allow(dead_code)]
|
||||
pub fn target(device: &Device, format: TextureFormat, size: u32, copy: bool) -> Texture {
|
||||
device.create_texture(&TextureDescriptor {
|
||||
label: Some("gpu rig target"),
|
||||
size: Extent3d {
|
||||
width: size,
|
||||
height: size,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format,
|
||||
usage: match copy {
|
||||
true => TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
|
||||
false => TextureUsages::RENDER_ATTACHMENT,
|
||||
},
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
+77
-22
@@ -5,23 +5,51 @@
|
||||
//! cargo test --release --features layout-diagnostics \
|
||||
//! --test layout_diagnostics -- --ignored --nocapture
|
||||
//!
|
||||
//! Uninstrumented hardware totals for one phase:
|
||||
//! Build the uninstrumented test with `cargo test --release --test
|
||||
//! layout_diagnostics --no-run`, then run the emitted executable directly:
|
||||
//!
|
||||
//! IRIS_PHASE=resize IRIS_FRAMES=1000 perf stat \
|
||||
//! -e cycles:u,instructions:u cargo test --release \
|
||||
//! --test layout_diagnostics -- --ignored --nocapture
|
||||
//! IRIS_PHASE=resize IRIS_FRAMES=10000 perf stat -r 7 \
|
||||
//! -e cycles:u,instructions:u /path/to/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.
|
||||
//! `IRIS_DIRTY` how many widgets `many` marks at once. `IRIS_UNBOUNDED=1`
|
||||
//! removes intrinsic bounds while preserving the rest of the generated tree.
|
||||
|
||||
mod rig;
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use iris::random::{Edits, Tree, grow};
|
||||
use iris::random::{Edits, Tree, build, plan};
|
||||
use rig::env;
|
||||
use std::time::Instant;
|
||||
|
||||
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
|
||||
|
||||
/// A scroll whose content fits is the same drawing in every box it still
|
||||
/// fits in, so a longer or shorter one relays out nothing. Where the content
|
||||
/// sits in that box is decided by placing its answer in the whole of it,
|
||||
/// which is a fraction of the box and holds at every length -- so the
|
||||
/// contract must not turn on the alignment. It did, and at the default
|
||||
/// alignment, which is the middle, every box change redrew the scroll.
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
#[test]
|
||||
fn a_fitting_scroll_holds_for_every_box_its_content_fits_in() {
|
||||
use iris::core::layout_diagnostics as diag;
|
||||
|
||||
for align in [Align::TOP_LEFT, Align::CENTER, Align::BOT_RIGHT] {
|
||||
let mut harness = Harness::new((400, 200));
|
||||
let inner = rect(Color::RED).height(50).add(&mut harness.rsc);
|
||||
harness.set_root(inner.scrollable().align(align));
|
||||
harness.frame();
|
||||
let _ = diag::take();
|
||||
// Still far longer than the 50 the content needs.
|
||||
harness.resize((400, 180));
|
||||
harness.frame();
|
||||
assert_eq!(diag::take().distinct_widgets(), 0, "{align:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
#[test]
|
||||
fn a_selected_widget_retains_its_layout_events() {
|
||||
@@ -37,8 +65,8 @@ fn a_selected_widget_retains_its_layout_events() {
|
||||
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.rsc.widgets_mut().mark_for_redraw(root.id());
|
||||
harness.rsc.widgets_mut().mark_for_redraw(leaf.id());
|
||||
harness.frame();
|
||||
|
||||
let report = diagnostics::take();
|
||||
@@ -69,13 +97,6 @@ fn a_selected_widget_retains_its_layout_events() {
|
||||
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 {
|
||||
@@ -92,9 +113,26 @@ fn trace_selected(tree: &Tree) {
|
||||
#[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 fixture(harness: &mut Harness, seed: u64, depth: usize) -> (StrongWidget, Tree) {
|
||||
let mut plan = plan(seed, depth, &rig_edits());
|
||||
if env("IRIS_UNBOUNDED", 0_u8) != 0 {
|
||||
plan.drop_bounds();
|
||||
}
|
||||
build(&mut harness.rsc, &plan)
|
||||
}
|
||||
|
||||
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
||||
let mut harness = Harness::new(OUTPUT);
|
||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
||||
let (root, tree) = fixture(&mut harness, seed, depth);
|
||||
harness.state.root = Some(root);
|
||||
harness.frame();
|
||||
println!(
|
||||
@@ -125,6 +163,9 @@ fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
|
||||
{
|
||||
let diagnostics = iris::core::layout_diagnostics::take();
|
||||
print!("{}", diagnostics.per_frame(frames));
|
||||
for event in diagnostics.traces() {
|
||||
println!(" {event:?}");
|
||||
}
|
||||
for callsite in diagnostics.hot_text().iter().take(3) {
|
||||
let mut ancestry = Vec::new();
|
||||
let mut id = Some(callsite.id);
|
||||
@@ -173,7 +214,7 @@ fn layout_cost() {
|
||||
|
||||
if selected("cold") {
|
||||
let mut harness = Harness::new(OUTPUT);
|
||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
||||
let (root, tree) = fixture(&mut harness, seed, depth);
|
||||
harness.state.root = Some(root);
|
||||
println!(
|
||||
"fixture: seed {seed}, depth {depth}, {} widgets",
|
||||
@@ -183,7 +224,6 @@ fn layout_cost() {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _ = iris::core::layout_diagnostics::take();
|
||||
run("cold", 1, &mut harness, |_, _| {});
|
||||
drop(tree);
|
||||
}
|
||||
|
||||
if selected("repaint") {
|
||||
@@ -191,7 +231,7 @@ fn layout_cost() {
|
||||
trace_selected(&tree);
|
||||
let leaf = tree.ids[0];
|
||||
run("repaint", frames, &mut harness, move |harness, _| {
|
||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf);
|
||||
harness.rsc.widgets_mut().mark_for_redraw(leaf);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -206,7 +246,7 @@ fn layout_cost() {
|
||||
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);
|
||||
harness.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -220,7 +260,7 @@ fn layout_cost() {
|
||||
harness
|
||||
.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(sized, Axis::X, SizeRule::Exact(len));
|
||||
.set_size_rule(sized, Axis::X, SizeRule::from(len));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -239,6 +279,21 @@ fn layout_cost() {
|
||||
run("resize", frames, &mut harness, |harness, frame| {
|
||||
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
|
||||
});
|
||||
drop(tree);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
#[test]
|
||||
fn repainting_measured_text_does_not_invalidate_its_span() {
|
||||
use iris::core::layout_diagnostics as diag;
|
||||
|
||||
let mut h = Harness::new((400, 200));
|
||||
let text = wtext("a paragraph that fits").wrap(true).add(&mut h.rsc);
|
||||
h.set_root((text, wtext("another paragraph")).span(Dir::DOWN));
|
||||
let _ = diag::take();
|
||||
h.rsc.widgets_mut().mark_for_redraw(text);
|
||||
h.frame();
|
||||
let report = diag::take();
|
||||
assert_eq!(report.distinct_widgets(), 1);
|
||||
assert_eq!(report.hot_widgets()[0].id, text.id());
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//! Prints where a cold layout puts every widget of many grown trees, so two
|
||||
//! commits can be compared on cold layout alone. The warm/cold oracle cannot
|
||||
//! see a change that moves cold layout, since both of its sides move; this
|
||||
//! can, by diffing its output across the change:
|
||||
//!
|
||||
//! IRIS_DUMP_SEEDS=400 IRIS_DUMP_DEPTH=5 cargo test --release \
|
||||
//! --test layout_dump -- --ignored --nocapture > /tmp/before.txt
|
||||
//!
|
||||
//! then the same after, and `diff` the two. A line is one widget: the seed,
|
||||
//! its index in creation order, and its box in window pixels, or `-` where
|
||||
//! it is not drawn. `IRIS_UNBOUNDED=1` drops the trees' intrinsic bounds, as
|
||||
//! in the diagnostics rig, which compares the two paths over the same shapes.
|
||||
|
||||
mod rig;
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::random::{Edits, build, plan};
|
||||
use rig::env;
|
||||
|
||||
#[test]
|
||||
#[ignore = "a dump to diff across commits, not a check"]
|
||||
fn every_cold_layout_is_printed() {
|
||||
let seeds = env("IRIS_DUMP_SEEDS", 400_u64);
|
||||
let depth = env("IRIS_DUMP_DEPTH", 5_usize);
|
||||
let mut out = String::new();
|
||||
for seed in 1..=seeds {
|
||||
let mut harness = Harness::new((1920.0, 1200.0));
|
||||
let mut plan = plan(seed, depth, &Edits::default());
|
||||
if env("IRIS_UNBOUNDED", 0_u8) != 0 {
|
||||
plan.drop_bounds();
|
||||
}
|
||||
let (root, tree) = build(&mut harness.rsc, &plan);
|
||||
harness.state.root = Some(root);
|
||||
harness.frame();
|
||||
for (index, id) in tree.ids.iter().enumerate() {
|
||||
match harness.region(id) {
|
||||
Some(region) => out.push_str(&format!("{seed} {index} {region:?}\n")),
|
||||
None => out.push_str(&format!("{seed} {index} -\n")),
|
||||
}
|
||||
}
|
||||
}
|
||||
print!("{out}");
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
//! Which pixels a mask lets through, read back off the GPU.
|
||||
//!
|
||||
//! cargo test --release --test mask_clip -- --ignored --nocapture
|
||||
//!
|
||||
//! Ignored because it needs a device, which not every machine running the
|
||||
//! suite has -- and a deliberate run on one without fails rather than passing
|
||||
//! with nothing checked. Nothing else here sees a mask at all: `iris::harness`
|
||||
//! draws no pixels, and a mask's rectangle is resolved through its own move
|
||||
//! chain in the shader, so the CPU's idea of it is not what clips anything.
|
||||
|
||||
use iris::prelude::*;
|
||||
use iris_core::{
|
||||
Len, Mask, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
|
||||
UiRenderState, UiSpan,
|
||||
};
|
||||
use wgpu::{Color as GpuColor, *};
|
||||
|
||||
#[path = "gpu/mod.rs"]
|
||||
mod gpu;
|
||||
|
||||
const SIZE: u32 = 256;
|
||||
|
||||
/// The mask is a box inside a move chain two links long and the drawing
|
||||
/// overflows it on both axes, so what comes back is the mask's own rectangle
|
||||
/// composed through that chain -- and a clip resolved through the wrong one,
|
||||
/// or not composed at all, lands somewhere else.
|
||||
#[test]
|
||||
#[ignore = "needs a gpu"]
|
||||
fn a_mask_clips_its_own_box_composed_through_its_chain() {
|
||||
let adapter = gpu::adapter().expect("no adapter to draw with");
|
||||
println!("adapter: {:?}", adapter.get_info().name);
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor::default()))
|
||||
.expect("no device on that adapter");
|
||||
let format = TextureFormat::Bgra8Unorm;
|
||||
let mut node = UiRenderNode::new(&device, &gpu::config(format, SIZE));
|
||||
let mut ui = UiData::default();
|
||||
let mut render = UiRenderState::new();
|
||||
let kind = ui.primitives.kind::<RectPrimitive>();
|
||||
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
||||
let px = Len::px;
|
||||
|
||||
let outer = render.moves.push(MoveIdx::NONE, UiRegion::FULL);
|
||||
let shift = (16.0, 24.0);
|
||||
let inner = render.moves.push(
|
||||
outer,
|
||||
UiRegion::new(
|
||||
UiSpan::new(px(shift.0), px(shift.0) + Len::FULL),
|
||||
UiSpan::new(px(shift.1), px(shift.1) + Len::FULL),
|
||||
),
|
||||
);
|
||||
let clip = (20.0, 30.0, 120.0, 90.0);
|
||||
let mask = ui.masks.push(Mask {
|
||||
region: UiRegion::new(
|
||||
UiSpan::new(px(clip.0), px(clip.2)),
|
||||
UiSpan::new(px(clip.1), px(clip.3)),
|
||||
),
|
||||
move_idx: inner,
|
||||
});
|
||||
// The owner's reference, which is what keeps a real one alive.
|
||||
ui.masks.push_ref(mask);
|
||||
render.layers.write(
|
||||
0,
|
||||
PrimitiveInst {
|
||||
kind,
|
||||
id,
|
||||
primitive: RectPrimitive::color(UiColor::WHITE),
|
||||
region: UiRegion::new(
|
||||
UiSpan::new(px(0.0), px(200.0)),
|
||||
UiSpan::new(px(0.0), px(200.0)),
|
||||
),
|
||||
mask_idx: mask,
|
||||
move_idx: inner,
|
||||
},
|
||||
);
|
||||
node.update(&device, &queue, &mut ui, &mut render);
|
||||
|
||||
let target = gpu::target(&device, format, SIZE, true);
|
||||
let view = target.create_view(&TextureViewDescriptor::default());
|
||||
let row = SIZE * 4;
|
||||
let readback = device.create_buffer(&BufferDescriptor {
|
||||
label: Some("mask clip"),
|
||||
size: (row * SIZE) as u64,
|
||||
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
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: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
node.draw(pass);
|
||||
}
|
||||
let whole = Extent3d {
|
||||
width: SIZE,
|
||||
height: SIZE,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
encoder.copy_texture_to_buffer(
|
||||
TexelCopyTextureInfo {
|
||||
texture: &target,
|
||||
mip_level: 0,
|
||||
origin: Origin3d::ZERO,
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
TexelCopyBufferInfo {
|
||||
buffer: &readback,
|
||||
layout: TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(row),
|
||||
rows_per_image: Some(SIZE),
|
||||
},
|
||||
},
|
||||
whole,
|
||||
);
|
||||
queue.submit(Some(encoder.finish()));
|
||||
let slice = readback.slice(..);
|
||||
slice.map_async(MapMode::Read, |_| {});
|
||||
device
|
||||
.poll(PollType::Wait {
|
||||
submission_index: None,
|
||||
timeout: None,
|
||||
})
|
||||
.expect("the pass did not finish");
|
||||
let pixels = slice.get_mapped_range().expect("the target did not map");
|
||||
|
||||
let mut lit = 0;
|
||||
let mut bounds: Option<(u32, u32, u32, u32)> = None;
|
||||
for y in 0..SIZE {
|
||||
for x in 0..SIZE {
|
||||
if pixels[(y * row + x * 4) as usize] == 0 {
|
||||
continue;
|
||||
}
|
||||
lit += 1;
|
||||
let (x0, y0, x1, y1) = bounds.unwrap_or((x, y, x, y));
|
||||
bounds = Some((x0.min(x), y0.min(y), x1.max(x), y1.max(y)));
|
||||
}
|
||||
}
|
||||
// The clip shifted by the chain. Its far edge is exclusive: a fragment
|
||||
// exactly on it is the first one outside.
|
||||
let want = (
|
||||
(clip.0 + shift.0) as u32,
|
||||
(clip.1 + shift.1) as u32,
|
||||
(clip.2 + shift.0) as u32 - 1,
|
||||
(clip.3 + shift.1) as u32 - 1,
|
||||
);
|
||||
assert_eq!(bounds, Some(want), "{lit} pixels through the mask");
|
||||
let (x0, y0, x1, y1) = want;
|
||||
assert_eq!(lit, (x1 - x0 + 1) * (y1 - y0 + 1), "the clip has a hole");
|
||||
}
|
||||
+88
-20
@@ -1,22 +1,28 @@
|
||||
//! 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.
|
||||
//! Text-layout workloads with stable paragraphs for comparisons across revisions.
|
||||
//! PR #19's base uses the older spelling of the fixed 40-pixel width and has
|
||||
//! no diagnostics. The random generator changed with layout, so it cannot
|
||||
//! provide the same workload across the full PR.
|
||||
//!
|
||||
//! ROWS=40 FRAMES=500 cargo test --release --test revision_cost \
|
||||
//! -- --ignored --nocapture resize_cost
|
||||
//! PHASE=edit ROWS=40 FRAMES=2000 cargo test --release --test revision_cost \
|
||||
//! -- --ignored --nocapture text_updates_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.
|
||||
//! `text_updates_cost` selects idle, repaint, edit, or scroll with `PHASE`.
|
||||
//! It alternates a short suffix for edits so later frames do not get a longer
|
||||
//! paragraph than earlier ones. These are CPU fixtures, with no GPU submission.
|
||||
//!
|
||||
//! Use repeated `perf stat -e instructions:u` runs on the executable directly;
|
||||
//! process totals include font loading and the cold frame, so compare identical
|
||||
//! row and frame counts. Wall time on this machine is not a stable comparison.
|
||||
|
||||
mod rig;
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use rig::env;
|
||||
use std::time::Instant;
|
||||
|
||||
/// xorshift64, so one seed is one set of paragraphs on any machine.
|
||||
@@ -78,13 +84,6 @@ fn words(rng: &mut Rng, least: usize, most: usize) -> String {
|
||||
|
||||
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.
|
||||
@@ -137,9 +136,10 @@ fn resize_cost() {
|
||||
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.
|
||||
// The sweep cycles 256 widths, avoiding the two-width cache-friendly case.
|
||||
let sweep = env("SWEEP", 0_usize) != 0;
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _ = iris::core::layout_diagnostics::take();
|
||||
let mut elapsed = Vec::with_capacity(frames);
|
||||
for frame in 0..frames {
|
||||
let narrower = match sweep {
|
||||
@@ -151,6 +151,11 @@ fn resize_cost() {
|
||||
h.frame();
|
||||
elapsed.push(start.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
print!(
|
||||
"{}",
|
||||
iris::core::layout_diagnostics::take().per_frame(frames)
|
||||
);
|
||||
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
println!(
|
||||
"resize: {frames} frames, min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
|
||||
@@ -194,10 +199,73 @@ fn text_memory() {
|
||||
h.frame();
|
||||
}
|
||||
report("after 40 resizes");
|
||||
// Settled: the output holds still and one leaf repaints per frame.
|
||||
// Settled: the output holds still and one leaf repaints per frame. Marked
|
||||
// by taking it mutably because the revision at the top of this file has no
|
||||
// `mark_for_redraw`, and the same source has to build against both.
|
||||
for _ in 0..10 {
|
||||
let _ = h.rsc.widgets_mut().get_dyn_mut(paragraphs[0]);
|
||||
h.frame();
|
||||
}
|
||||
report("after settling");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "measurement, not a check"]
|
||||
fn text_updates_cost() {
|
||||
let rows = env("ROWS", 40_usize);
|
||||
let frames = env("FRAMES", 1000_usize);
|
||||
let phase = env("PHASE", String::from("edit"));
|
||||
assert!(rows > 0 && frames > 0);
|
||||
assert!(["idle", "repaint", "edit", "scroll"].contains(&phase.as_str()));
|
||||
let mut h = Harness::new(OUTPUT);
|
||||
let mut rng = Rng(1);
|
||||
let mut col = Span::empty(Dir::DOWN);
|
||||
let first = wtext(words(&mut rng, 12, 52))
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.add(&mut h.rsc);
|
||||
col.push(first.add_strong(&mut h.rsc));
|
||||
for _ in 1..rows {
|
||||
col.push(
|
||||
wtext(words(&mut rng, 12, 52))
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.add_strong(&mut h.rsc),
|
||||
);
|
||||
}
|
||||
let root = col.scrollable().add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _ = iris::core::layout_diagnostics::take();
|
||||
let original = h.rsc[first].content.to_string();
|
||||
let alternate = format!("{original} another word");
|
||||
let start = Instant::now();
|
||||
for frame in 0..frames {
|
||||
match phase.as_str() {
|
||||
"idle" => {}
|
||||
"repaint" => {
|
||||
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id());
|
||||
}
|
||||
"edit" => {
|
||||
h.rsc[first].content.clear();
|
||||
h.rsc[first].content.push_str(if frame % 2 == 0 {
|
||||
&alternate
|
||||
} else {
|
||||
&original
|
||||
});
|
||||
}
|
||||
"scroll" => h.rsc[root].scroll(if frame % 2 == 0 { -12.0 } else { 12.0 }),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
h.frame();
|
||||
}
|
||||
println!(
|
||||
"{phase}: {rows} rows, {frames} frames, {:.1} ms",
|
||||
start.elapsed().as_secs_f64() * 1000.0
|
||||
);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
print!(
|
||||
"{}",
|
||||
iris::core::layout_diagnostics::take().per_frame(frames)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//! What the rigs need and none of them should spell its own way. Every
|
||||
//! fuzzer and measurement here is run by hand with its parameters in the
|
||||
//! environment, so one reader is shared rather than copied into each target.
|
||||
|
||||
/// A rig's parameter from the environment, or its default. A switch is
|
||||
/// `env("NAME", 0_u8) != 0`, so `NAME=1` turns it on.
|
||||
pub 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)
|
||||
}
|
||||
|
||||
/// The seeds a scan runs: the one `one` names on its own, or `1..=` the
|
||||
/// count `many` gives. One seed replaces the range rather than narrowing
|
||||
/// it, which is how a tree a scan failed on is run again by itself.
|
||||
// This module is compiled into each rig target separately, so a helper the
|
||||
// measurement rigs have no seeds to choose is dead code in those builds.
|
||||
#[allow(dead_code)]
|
||||
pub fn seeds(one: &str, many: &str, count: u64) -> Vec<u64> {
|
||||
match std::env::var(one).ok().and_then(|seed| seed.parse().ok()) {
|
||||
Some(seed) => vec![seed],
|
||||
None => (1..=env(many, count)).collect(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
//! 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::{Edits, Kind, 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)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
/// 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 size change and then a resize, which is the other order and not the
|
||||
/// same test: a length answered as a fraction of one box and kept as a
|
||||
/// fraction of another agrees at the size it was changed at and parts
|
||||
/// from it at every other one.
|
||||
SizeResize,
|
||||
/// A few declared sizes.
|
||||
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; 16] = [
|
||||
Case::Repaint,
|
||||
Case::RepaintSome,
|
||||
Case::Resize,
|
||||
Case::ResizeRepaint,
|
||||
Case::ResizeSize,
|
||||
Case::SizeResize,
|
||||
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::SizeResize => "size-resize",
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
/// The window the warm tree is taken to after the change, where the case
|
||||
/// is about what the change left behind rather than about the change.
|
||||
fn then_resize(self) -> Option<(f32, f32)> {
|
||||
match self {
|
||||
Self::SizeResize => Some(INNER),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
|
||||
for &id in tree.ids.iter().step_by(step) {
|
||||
warm.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// A length in pixels, or a cap over one: a rule that reads the box it is
|
||||
/// given is the one a resize can change the effect of without changing the
|
||||
/// rule, so a tree that never grows one leaves that unexercised.
|
||||
fn a_rule(rng: &mut Rng) -> SizeRule {
|
||||
let len = Len::px(20.0 + rng.below(180) as f32);
|
||||
match rng.below(4) {
|
||||
0 => SizeRule::max(len),
|
||||
1 => SizeRule::min(len),
|
||||
_ => LayoutLen::from(len).into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> SizeRules {
|
||||
let lens = SizeRules {
|
||||
x: a_rule(rng),
|
||||
y: a_rule(rng),
|
||||
};
|
||||
warm.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rules(tree.sized[idx], lens.x.clone(), lens.y.clone());
|
||||
lens
|
||||
}
|
||||
|
||||
fn realign_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Align {
|
||||
let side = |rng: &mut Rng| match rng.below(4) {
|
||||
0 => None,
|
||||
1 => Some(AxisAlign::NEG),
|
||||
2 => Some(AxisAlign::CENTER),
|
||||
_ => Some(AxisAlign::POS),
|
||||
};
|
||||
let align = Align {
|
||||
x: side(rng),
|
||||
y: side(rng),
|
||||
};
|
||||
let id = tree.aligned[idx];
|
||||
let taken = RegionAlign::from(align);
|
||||
for axis in Axis::BOTH {
|
||||
warm.rsc.widgets_mut().set_alignment(id, axis, taken[axis]);
|
||||
}
|
||||
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 | Case::SizeResize => 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);
|
||||
// The tree a failure names is written out again from what it printed, so
|
||||
// every part of a rule prints: the preferred length and the bounds are
|
||||
// independent, and a bound lumped into "no rule" could not be rebuilt.
|
||||
let rule = |r: &SizeRule| {
|
||||
let mut out = r
|
||||
.request
|
||||
.as_ref()
|
||||
.map_or_else(String::new, ToString::to_string);
|
||||
if let Some(min) = r.bound.min {
|
||||
out += &format!(">{}", LayoutLen::from(min));
|
||||
}
|
||||
if let Some(max) = r.bound.max {
|
||||
out += &format!("<{}", LayoutLen::from(max));
|
||||
}
|
||||
if out.is_empty() { "-".into() } else { out }
|
||||
};
|
||||
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 != SizeRules::default() {
|
||||
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
|
||||
}
|
||||
|
||||
/// One widget's layout as it stands: the frame its fractions resolved
|
||||
/// against, the box it was asked in, the box its drawing went in, and what
|
||||
/// it reported. In window units, which is what both trees are in.
|
||||
fn record(id: WidgetId, h: &Harness) -> String {
|
||||
let active = &h.render.active[&id];
|
||||
format!(
|
||||
"rel_base {} region {} placement {} size {}",
|
||||
active.rel_base, active.region, active.placement, active.size,
|
||||
)
|
||||
}
|
||||
|
||||
/// 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));
|
||||
// Whatever the change left, seen at another window: an answer kept as a
|
||||
// fraction of the wrong length is the same number of pixels where it was
|
||||
// made and a different one everywhere else.
|
||||
let end = match case.then_resize() {
|
||||
Some(after) => {
|
||||
warm.resize(after);
|
||||
warm.frame();
|
||||
after
|
||||
}
|
||||
None => end,
|
||||
};
|
||||
|
||||
let mut cold = Harness::new(end);
|
||||
let (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 got == want {
|
||||
continue;
|
||||
}
|
||||
let places: HashMap<WidgetId, usize> = tree
|
||||
.ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &id)| (id, i))
|
||||
.collect();
|
||||
// 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 records = 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)));
|
||||
// What each level was asked in on both sides, since the level
|
||||
// where the two stop agreeing is the one to look at rather than
|
||||
// the leaf that reported the difference.
|
||||
let cold_id = places.get(&id).and_then(|&i| cold_tree.ids.get(i));
|
||||
records.push(format!(
|
||||
" {}\n warm {}\n cold {}",
|
||||
describe(id, &warm),
|
||||
record(id, &warm),
|
||||
cold_id.map_or("-".into(), |&id| record(id, &cold)),
|
||||
));
|
||||
at = active.parent;
|
||||
}
|
||||
return Some(format!(
|
||||
"widget {i}\n warm {got:?}\n cold {want:?}\n {}\n{}",
|
||||
chain.join(" < "),
|
||||
records.join("\n"),
|
||||
));
|
||||
}
|
||||
match drawn {
|
||||
0 => Some("nothing was drawn".into()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
+63
-583
@@ -1,556 +1,40 @@
|
||||
//! A property test that shrinks its own counterexample.
|
||||
//! A fuzzer that reduces its own counterexample.
|
||||
//!
|
||||
//! `generated.rs` reproduces a failure from a seed, but 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 trees it can take apart, so a failure is reduced to the smallest
|
||||
//! tree that still shows it and printed as something to write a fast test
|
||||
//! from.
|
||||
//! 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. 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.
|
||||
//! 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.
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use iris::random::{Branch, Rng};
|
||||
mod rig;
|
||||
#[path = "scenario/mod.rs"]
|
||||
mod scenario;
|
||||
|
||||
/// The same two leaves `iris::random` grows, since only one of them reads the
|
||||
/// width it is given and that is the difference that matters.
|
||||
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.",
|
||||
];
|
||||
use iris::random::{Edits, Plan, plan};
|
||||
use rig::env;
|
||||
use scenario::{ALL, Case, diverges, over_seeds};
|
||||
|
||||
const ONE_LINE: &str = "one line, overflowing whatever it is given";
|
||||
|
||||
const OUTER: (f32, f32) = (1920.0, 1200.0);
|
||||
/// Steps of the grid two ways of reaching a box may differ by: one per level
|
||||
/// of nesting between them, and these trees are five deep. See
|
||||
/// `docs/HANDOFF.md`'s "Fixed point" in `ai-app-2` for what is left.
|
||||
const AGREE_STEPS: i32 = 2;
|
||||
const INNER: (f32, f32) = (640.0, 900.0);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Node {
|
||||
/// Words taken from [`WORDS`], and whether it wraps.
|
||||
Text(usize, bool),
|
||||
/// The leaf that overflows whatever box it is given rather than wrapping.
|
||||
OneLine,
|
||||
Rect,
|
||||
/// Direction, gap, children in creation order, and the order they are
|
||||
/// attached in -- separate so a tree that reorders its children
|
||||
/// still makes the same widgets in the same order, and two
|
||||
/// builds line up index for index.
|
||||
Span(bool, f32, Vec<Node>, Vec<usize>),
|
||||
Stack(Vec<Node>),
|
||||
Pad(f32, Box<Node>),
|
||||
Aligned(u8, u8, Box<Node>),
|
||||
Sized(Option<LayoutLen>, Option<LayoutLen>, Box<Node>),
|
||||
Scroll(bool, Box<Node>),
|
||||
Branch(Box<Node>, Box<Node>, Box<Node>, f32),
|
||||
}
|
||||
|
||||
fn axis_align(v: u8) -> Option<AxisAlign> {
|
||||
match v % 4 {
|
||||
0 => None,
|
||||
1 => Some(AxisAlign::NEG),
|
||||
2 => Some(AxisAlign::CENTER),
|
||||
_ => Some(AxisAlign::POS),
|
||||
}
|
||||
}
|
||||
|
||||
fn dir(down: bool) -> Dir {
|
||||
if down { Dir::DOWN } else { Dir::RIGHT }
|
||||
}
|
||||
|
||||
impl Node {
|
||||
/// Builds into `h`, pushing every id in tree order, so two builds of one
|
||||
/// node line up index for index and their boxes can be compared.
|
||||
fn build(
|
||||
&self,
|
||||
h: &mut Harness,
|
||||
out: &mut Vec<WidgetId>,
|
||||
spans: &mut Vec<WeakWidget<Span>>,
|
||||
sized: &mut Vec<WidgetId>,
|
||||
) -> StrongWidget {
|
||||
let id: StrongWidget = match self {
|
||||
Node::Text(words, wrap) => {
|
||||
let n = (*words).clamp(1, WORDS.len());
|
||||
wtext(WORDS[..n].join(" "))
|
||||
.size(16)
|
||||
.wrap(*wrap)
|
||||
.add_strong(&mut h.rsc)
|
||||
}
|
||||
Node::OneLine => wtext(ONE_LINE).size(16).wrap(false).add_strong(&mut h.rsc),
|
||||
Node::Rect => rect(Color::RED).add_strong(&mut h.rsc),
|
||||
Node::Span(down, gap, kids, order) => {
|
||||
let mut built: Vec<_> = kids
|
||||
.iter()
|
||||
.map(|k| Some(k.build(h, out, spans, sized)))
|
||||
.collect();
|
||||
// `order` is a permutation, so each is taken exactly once.
|
||||
let children = order
|
||||
.iter()
|
||||
.map(|&i| built[i].take().expect("order repeats an index"))
|
||||
.collect();
|
||||
let handle = Span {
|
||||
children,
|
||||
dir: dir(*down),
|
||||
gap: Px::from_f32(*gap),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
// A row takes the height it is given; a column is as wide
|
||||
// as its widest child, which needs no rule.
|
||||
if !*down {
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rules(handle, None, Some(LayoutLen::rel(1.0)));
|
||||
}
|
||||
spans.push(handle);
|
||||
handle.add_strong(&mut h.rsc)
|
||||
}
|
||||
Node::Stack(kids) => {
|
||||
let children = kids.iter().map(|k| k.build(h, out, spans, sized)).collect();
|
||||
Stack {
|
||||
children,
|
||||
size: StackSize::Child(0),
|
||||
}
|
||||
.add_strong(&mut h.rsc)
|
||||
}
|
||||
Node::Pad(p, kid) => {
|
||||
let inner = kid.build(h, out, spans, sized);
|
||||
Pad {
|
||||
padding: Padding::uniform(*p),
|
||||
inner,
|
||||
}
|
||||
.add_strong(&mut h.rsc)
|
||||
}
|
||||
Node::Aligned(x, y, kid) => {
|
||||
let inner = kid.build(h, out, spans, sized);
|
||||
for (axis, align) in [(Axis::X, axis_align(*x)), (Axis::Y, axis_align(*y))] {
|
||||
if let Some(align) = align {
|
||||
h.rsc.widgets_mut().set_alignment(&inner, axis, align);
|
||||
}
|
||||
}
|
||||
inner
|
||||
}
|
||||
Node::Sized(x, y, kid) => {
|
||||
let inner = kid.build(h, out, spans, sized);
|
||||
h.rsc.widgets_mut().set_size_rules(&inner, *x, *y);
|
||||
sized.push(inner.id());
|
||||
inner
|
||||
}
|
||||
Node::Scroll(down, kid) => {
|
||||
let inner = kid.build(h, out, spans, sized);
|
||||
let axis = if *down { Axis::Y } else { Axis::X };
|
||||
Scroll::new(inner, axis).add_strong(&mut h.rsc)
|
||||
}
|
||||
Node::Branch(probe, a, b, at) => {
|
||||
let probe = probe.build(h, out, spans, sized);
|
||||
let wide = a.build(h, out, spans, sized);
|
||||
let narrow = b.build(h, out, spans, sized);
|
||||
Branch {
|
||||
probe,
|
||||
wide,
|
||||
narrow,
|
||||
threshold: *at,
|
||||
}
|
||||
.add_strong(&mut h.rsc)
|
||||
}
|
||||
};
|
||||
out.push(id.id());
|
||||
id
|
||||
}
|
||||
|
||||
/// The lengths every `Sized` node would carry after `resized`, in the
|
||||
/// order `build` pushes them.
|
||||
fn sized_lens(&self, out: &mut Vec<(Option<LayoutLen>, Option<LayoutLen>)>) {
|
||||
match self {
|
||||
Node::Text(..) | Node::OneLine | Node::Rect => {}
|
||||
Node::Span(_, _, kids, _) | Node::Stack(kids) => {
|
||||
kids.iter().for_each(|k| k.sized_lens(out));
|
||||
}
|
||||
Node::Pad(_, k) | Node::Aligned(_, _, k) | Node::Scroll(_, k) => k.sized_lens(out),
|
||||
Node::Sized(x, y, k) => {
|
||||
k.sized_lens(out);
|
||||
out.push((resized_len(*x), resized_len(*y)));
|
||||
}
|
||||
Node::Branch(p, a, b, _) => {
|
||||
p.sized_lens(out);
|
||||
a.sized_lens(out);
|
||||
b.sized_lens(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
1 + match self {
|
||||
Node::Text(..) | Node::OneLine | Node::Rect => 0,
|
||||
Node::Span(_, _, kids, _) | Node::Stack(kids) => kids.iter().map(Node::size).sum(),
|
||||
Node::Pad(_, k)
|
||||
| Node::Aligned(_, _, k)
|
||||
| Node::Sized(_, _, k)
|
||||
| Node::Scroll(_, k) => k.size(),
|
||||
Node::Branch(p, a, b, _) => p.size() + a.size() + b.size(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Every one-step simplification: a wrapper replaced by what it wrapped, a
|
||||
/// child dropped, a length or a word count reduced. Ordered cheapest-first
|
||||
/// so the greedy walk takes the biggest bites early.
|
||||
fn smaller(&self) -> Vec<Node> {
|
||||
let mut out = Vec::new();
|
||||
let leaf = Node::Rect;
|
||||
match self {
|
||||
Node::Text(words, wrap) => {
|
||||
if *words > 1 {
|
||||
out.push(Node::Text(words / 2, *wrap));
|
||||
out.push(Node::Text(words - 1, *wrap));
|
||||
}
|
||||
if *wrap {
|
||||
out.push(Node::Text(*words, false));
|
||||
}
|
||||
out.push(leaf);
|
||||
}
|
||||
Node::OneLine => out.push(Node::Rect),
|
||||
Node::Rect => {}
|
||||
Node::Span(down, gap, kids, order) => {
|
||||
out.extend(order.iter().map(|&i| kids[i].clone()));
|
||||
for i in 0..kids.len() {
|
||||
if kids.len() > 1 {
|
||||
let mut less = kids.clone();
|
||||
less.remove(i);
|
||||
let order = (0..less.len()).collect();
|
||||
out.push(Node::Span(*down, *gap, less, order));
|
||||
}
|
||||
}
|
||||
if *gap != 0.0 {
|
||||
out.push(Node::Span(*down, 0.0, kids.clone(), order.clone()));
|
||||
}
|
||||
for (i, kid) in kids.iter().enumerate() {
|
||||
for small in kid.smaller() {
|
||||
let mut next = kids.clone();
|
||||
next[i] = small;
|
||||
out.push(Node::Span(*down, *gap, next, order.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::Stack(kids) => {
|
||||
out.extend(kids.iter().cloned());
|
||||
for i in 0..kids.len() {
|
||||
if kids.len() > 1 {
|
||||
let mut less = kids.clone();
|
||||
less.remove(i);
|
||||
out.push(Node::Stack(less));
|
||||
}
|
||||
}
|
||||
for (i, kid) in kids.iter().enumerate() {
|
||||
for small in kid.smaller() {
|
||||
let mut next = kids.clone();
|
||||
next[i] = small;
|
||||
out.push(Node::Stack(next));
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::Pad(p, kid) => {
|
||||
out.push((**kid).clone());
|
||||
if *p != 0.0 {
|
||||
out.push(Node::Pad(0.0, kid.clone()));
|
||||
}
|
||||
out.extend(
|
||||
kid.smaller()
|
||||
.into_iter()
|
||||
.map(|k| Node::Pad(*p, Box::new(k))),
|
||||
);
|
||||
}
|
||||
Node::Aligned(x, y, kid) => {
|
||||
out.push((**kid).clone());
|
||||
for (nx, ny) in [(0, *y), (*x, 0)] {
|
||||
if (nx, ny) != (*x, *y) {
|
||||
out.push(Node::Aligned(nx, ny, kid.clone()));
|
||||
}
|
||||
}
|
||||
out.extend(
|
||||
kid.smaller()
|
||||
.into_iter()
|
||||
.map(|k| Node::Aligned(*x, *y, Box::new(k))),
|
||||
);
|
||||
}
|
||||
Node::Sized(x, y, kid) => {
|
||||
out.push((**kid).clone());
|
||||
if x.is_some() {
|
||||
out.push(Node::Sized(None, *y, kid.clone()));
|
||||
}
|
||||
if y.is_some() {
|
||||
out.push(Node::Sized(*x, None, kid.clone()));
|
||||
}
|
||||
out.extend(
|
||||
kid.smaller()
|
||||
.into_iter()
|
||||
.map(|k| Node::Sized(*x, *y, Box::new(k))),
|
||||
);
|
||||
}
|
||||
Node::Scroll(down, kid) => {
|
||||
out.push((**kid).clone());
|
||||
out.extend(
|
||||
kid.smaller()
|
||||
.into_iter()
|
||||
.map(|k| Node::Scroll(*down, Box::new(k))),
|
||||
);
|
||||
}
|
||||
Node::Branch(p, a, b, at) => {
|
||||
out.push((**p).clone());
|
||||
out.push((**a).clone());
|
||||
out.push((**b).clone());
|
||||
for small in p.smaller() {
|
||||
out.push(Node::Branch(Box::new(small), a.clone(), b.clone(), *at));
|
||||
}
|
||||
for small in a.smaller() {
|
||||
out.push(Node::Branch(p.clone(), Box::new(small), b.clone(), *at));
|
||||
}
|
||||
for small in b.smaller() {
|
||||
out.push(Node::Branch(p.clone(), a.clone(), Box::new(small), *at));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// A declared size over about half the tree, the way `iris::random` puts them
|
||||
/// in: on the way into every child rather than as a node kind of its own, so
|
||||
/// readers of a size are dense rather than occasional.
|
||||
fn sized(rng: &mut Rng, inner: Node) -> Node {
|
||||
if !rng.chance() {
|
||||
return inner;
|
||||
}
|
||||
let len = |rng: &mut Rng| match rng.below(4) {
|
||||
0 => Some(LayoutLen::px(20.0 + rng.below(180) as f32)),
|
||||
1 => Some(LayoutLen::LEFTOVER),
|
||||
_ => None,
|
||||
};
|
||||
Node::Sized(len(rng), len(rng), Box::new(inner))
|
||||
}
|
||||
|
||||
fn grow(rng: &mut Rng, depth: usize) -> Node {
|
||||
if depth == 0 {
|
||||
return match rng.below(4) {
|
||||
0 => Node::Text(1 + rng.below(WORDS.len()), true),
|
||||
1 => Node::OneLine,
|
||||
_ => Node::Rect,
|
||||
};
|
||||
}
|
||||
let len = |rng: &mut Rng| match rng.below(4) {
|
||||
0 => Some(LayoutLen::px(20.0 + rng.below(180) as f32)),
|
||||
1 => Some(LayoutLen::LEFTOVER),
|
||||
2 => Some(LayoutLen::rel(0.25 + rng.below(3) as f32 * 0.25)),
|
||||
_ => None,
|
||||
};
|
||||
let kid = |rng: &mut Rng| {
|
||||
let inner = grow(rng, depth - 1);
|
||||
sized(rng, inner)
|
||||
};
|
||||
match rng.below(8) {
|
||||
0 => Node::Scroll(rng.chance(), Box::new(kid(rng))),
|
||||
1 => Node::Aligned(rng.below(4) as u8, rng.below(4) as u8, Box::new(kid(rng))),
|
||||
2 => Node::Pad(rng.below(24) as f32, Box::new(kid(rng))),
|
||||
3 => Node::Sized(len(rng), len(rng), Box::new(kid(rng))),
|
||||
4 => Node::Branch(
|
||||
Box::new(kid(rng)),
|
||||
Box::new(kid(rng)),
|
||||
Box::new(kid(rng)),
|
||||
rng.below(500) as f32,
|
||||
),
|
||||
5 => Node::Stack((0..2 + rng.below(2)).map(|_| kid(rng)).collect()),
|
||||
_ => {
|
||||
let kids: Vec<_> = (0..2 + rng.below(3)).map(|_| kid(rng)).collect();
|
||||
let order = (0..kids.len()).collect();
|
||||
Node::Span(rng.chance(), rng.below(3) as f32 * 4.0, kids, order)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Case {
|
||||
Resize,
|
||||
Repaint,
|
||||
ResizeRepaint,
|
||||
Reorder,
|
||||
SizeChange,
|
||||
}
|
||||
|
||||
/// A different declared length, kept the same kind so the change is to the
|
||||
/// value alone.
|
||||
fn resized_len(len: Option<LayoutLen>) -> Option<LayoutLen> {
|
||||
let half = Rel::from_f32(0.5);
|
||||
len.map(|len| LayoutLen {
|
||||
px: len.px.mul(half) + Px::from_int(13),
|
||||
rel: len.rel.mul(half),
|
||||
leftover: len.leftover,
|
||||
})
|
||||
}
|
||||
|
||||
/// Every declared size changed, as a tree rather than as a change.
|
||||
fn resized(node: &Node) -> Node {
|
||||
match node {
|
||||
Node::Span(down, gap, kids, order) => Node::Span(
|
||||
*down,
|
||||
*gap,
|
||||
kids.iter().map(resized).collect(),
|
||||
order.clone(),
|
||||
),
|
||||
Node::Stack(kids) => Node::Stack(kids.iter().map(resized).collect()),
|
||||
Node::Pad(p, k) => Node::Pad(*p, Box::new(resized(k))),
|
||||
Node::Aligned(x, y, k) => Node::Aligned(*x, *y, Box::new(resized(k))),
|
||||
Node::Sized(x, y, k) => Node::Sized(resized_len(*x), resized_len(*y), Box::new(resized(k))),
|
||||
Node::Scroll(d, k) => Node::Scroll(*d, Box::new(resized(k))),
|
||||
Node::Branch(p, a, b, at) => Node::Branch(
|
||||
Box::new(resized(p)),
|
||||
Box::new(resized(a)),
|
||||
Box::new(resized(b)),
|
||||
*at,
|
||||
),
|
||||
leaf => leaf.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Every span's children rotated by one, as a tree rather than as a change:
|
||||
/// what a warm frame reaches by moving them has to be where growing them that
|
||||
/// way lands.
|
||||
fn reordered(node: &Node) -> Node {
|
||||
match node {
|
||||
Node::Span(down, gap, kids, order) => {
|
||||
let kids = kids.iter().map(reordered).collect::<Vec<_>>();
|
||||
let mut order = order.clone();
|
||||
order.rotate_left(1);
|
||||
Node::Span(*down, *gap, kids, order)
|
||||
}
|
||||
Node::Stack(kids) => Node::Stack(kids.iter().map(reordered).collect()),
|
||||
Node::Pad(p, k) => Node::Pad(*p, Box::new(reordered(k))),
|
||||
Node::Aligned(x, y, k) => Node::Aligned(*x, *y, Box::new(reordered(k))),
|
||||
Node::Sized(x, y, k) => Node::Sized(*x, *y, Box::new(reordered(k))),
|
||||
Node::Scroll(d, k) => Node::Scroll(*d, Box::new(reordered(k))),
|
||||
Node::Branch(p, a, b, at) => Node::Branch(
|
||||
Box::new(reordered(p)),
|
||||
Box::new(reordered(a)),
|
||||
Box::new(reordered(b)),
|
||||
*at,
|
||||
),
|
||||
leaf => leaf.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs one scenario warm and cold and says where they disagree.
|
||||
fn diverges(node: &Node, case: Case) -> Option<String> {
|
||||
let resizes = matches!(case, Case::Resize | Case::ResizeRepaint);
|
||||
let repaints = matches!(case, Case::Repaint | Case::ResizeRepaint);
|
||||
let start = if resizes { OUTER } else { INNER };
|
||||
let mut warm = Harness::new(start);
|
||||
let mut warm_ids = Vec::new();
|
||||
let mut warm_spans = Vec::new();
|
||||
let mut warm_sized = Vec::new();
|
||||
let root = node.build(&mut warm, &mut warm_ids, &mut warm_spans, &mut warm_sized);
|
||||
warm.state.root = Some(root);
|
||||
// The frame that makes it warm: without it there is nothing retained and
|
||||
// the comparison is two cold starts agreeing with each other.
|
||||
warm.frame();
|
||||
if resizes {
|
||||
warm.resize(INNER);
|
||||
warm.frame();
|
||||
}
|
||||
if repaints {
|
||||
for &id in &warm_ids {
|
||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||
}
|
||||
warm.frame();
|
||||
}
|
||||
if case == Case::Reorder {
|
||||
for span in &warm_spans {
|
||||
warm.rsc[*span].children.rotate_left(1);
|
||||
}
|
||||
warm.frame();
|
||||
}
|
||||
if case == Case::SizeChange {
|
||||
let mut lens = Vec::new();
|
||||
node.sized_lens(&mut lens);
|
||||
for (id, (x, y)) in warm_sized.iter().zip(lens) {
|
||||
warm.rsc.widgets_mut().set_size_rules(*id, x, y);
|
||||
}
|
||||
warm.frame();
|
||||
}
|
||||
|
||||
// What the warm tree was moved into, grown that way from the start.
|
||||
let want = match case {
|
||||
Case::Reorder => reordered(node),
|
||||
Case::SizeChange => resized(node),
|
||||
_ => node.clone(),
|
||||
};
|
||||
let mut cold = Harness::new(INNER);
|
||||
let mut cold_ids = Vec::new();
|
||||
let mut cold_spans = Vec::new();
|
||||
let mut cold_sized = Vec::new();
|
||||
let root = want.build(&mut cold, &mut cold_ids, &mut cold_spans, &mut cold_sized);
|
||||
cold.state.root = Some(root);
|
||||
cold.frame();
|
||||
|
||||
for (i, (&w, &c)) in warm_ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
// To a couple of steps of the grid, each a thousandth of a pixel: a
|
||||
// move or a resize lands on the same number now, and a length
|
||||
// measured one way against the same length composed another can
|
||||
// still be a step out per composition between them.
|
||||
let same = match (got, want) {
|
||||
(Some(g), Some(c)) => {
|
||||
let d = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS);
|
||||
d(g.top_left.x, c.top_left.x)
|
||||
&& d(g.top_left.y, c.top_left.y)
|
||||
&& d(g.bot_right.x, c.bot_right.x)
|
||||
&& d(g.bot_right.y, c.bot_right.y)
|
||||
}
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
};
|
||||
if !same {
|
||||
return Some(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Takes the first simplification that still fails, until none does.
|
||||
fn shrink(mut node: Node, case: Case) -> Node {
|
||||
/// 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).is_some())
|
||||
.find(|small| diverges(small, case, seed).is_some())
|
||||
else {
|
||||
return node;
|
||||
};
|
||||
@@ -558,62 +42,58 @@ fn shrink(mut node: Node, case: Case) -> Node {
|
||||
}
|
||||
}
|
||||
|
||||
/// One thread per core but one, each taking a share of the seeds: a tree is
|
||||
/// grown, laid out and dropped within a seed, so nothing is shared. A seed
|
||||
/// that fails shrinks on its own thread and panics there, which brings the
|
||||
/// scope down with it.
|
||||
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)));
|
||||
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(", ")
|
||||
),
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "a fuzzer; run it once the ordinary tests pass"]
|
||||
fn no_grown_tree_lays_out_differently_warm_than_cold() {
|
||||
let seeds: u64 = env("SHRINK_SEEDS", 400);
|
||||
let depth: usize = env("SHRINK_DEPTH", 5);
|
||||
let case = match env("SHRINK_CASE", String::from("resize")).as_str() {
|
||||
"repaint" => Case::Repaint,
|
||||
"resize-repaint" => Case::ResizeRepaint,
|
||||
"reorder" => Case::Reorder,
|
||||
"size-change" => Case::SizeChange,
|
||||
_ => Case::Resize,
|
||||
};
|
||||
let cases = cases();
|
||||
let seeds = rig::seeds("SHRINK_SEED", "SHRINK_SEEDS", 400);
|
||||
let count = seeds.len();
|
||||
|
||||
over_seeds((1..=seeds).collect(), |seed| {
|
||||
let node = grow(&mut Rng::new(seed), depth);
|
||||
let Some(how) = diverges(&node, case) else {
|
||||
return;
|
||||
};
|
||||
let small = shrink(node.clone(), case);
|
||||
over_seeds(seeds, |seed| {
|
||||
let grown = plan(seed, depth, &Edits::default());
|
||||
for &case in &cases {
|
||||
if diverges(&grown, case, seed).is_none() {
|
||||
continue;
|
||||
}
|
||||
let small = shrink(grown.clone(), case, seed);
|
||||
// Described from the shrunk tree: the grown tree's chain names
|
||||
// widgets that are no longer there, and the ancestry of the
|
||||
// failure is what a test is written from.
|
||||
let how = diverges(&small, case, seed).unwrap_or_default();
|
||||
println!(
|
||||
"seed {seed}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
|
||||
node.size(),
|
||||
"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");
|
||||
panic!(
|
||||
"seed {seed} lays out differently warm than cold after {}",
|
||||
case.name()
|
||||
);
|
||||
}
|
||||
});
|
||||
let sizes: Vec<usize> = (1..=seeds)
|
||||
.map(|seed| grow(&mut Rng::new(seed), depth).size())
|
||||
|
||||
let sizes: Vec<usize> = (1..=count as u64)
|
||||
.map(|seed| plan(seed, depth, &Edits::default()).size())
|
||||
.collect();
|
||||
let total: usize = sizes.iter().sum();
|
||||
println!(
|
||||
"{seeds} trees at depth {depth} agree: {} widgets total, largest {}",
|
||||
total,
|
||||
"{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)
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
//! 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/deferred.rs"]
|
||||
mod deferred;
|
||||
#[path = "cases/determinism.rs"]
|
||||
mod determinism;
|
||||
#[path = "cases/drift.rs"]
|
||||
@@ -16,6 +18,8 @@ mod drift;
|
||||
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"]
|
||||
|
||||
+17
-31
@@ -1,5 +1,5 @@
|
||||
//! 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.
|
||||
//! Traces the four-widget trees in `unsettled.rs`, to see what box their text
|
||||
//! is actually drawn in on a first frame against a settled one.
|
||||
|
||||
#![cfg(feature = "layout-diagnostics")]
|
||||
|
||||
@@ -9,30 +9,25 @@ 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;
|
||||
let wrapped = wtext("Wrapping shapes")
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.width(76)
|
||||
.add(&mut h.rsc);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(sized, Axis::X, AxisAlign::POS);
|
||||
.set_alignment(wrapped, Axis::X, AxisAlign::POS);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(sized, Axis::Y, AxisAlign::POS);
|
||||
.set_alignment(wrapped, Axis::Y, AxisAlign::POS);
|
||||
let stack = Stack {
|
||||
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
||||
children: vec![plain.add_strong(&mut h.rsc), wrapped.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(),
|
||||
]
|
||||
vec![plain.id(), wrapped.id(), stack.id(), root.id()]
|
||||
}
|
||||
|
||||
fn dump(label: &str, report: &diag::Report, text: WidgetId) {
|
||||
@@ -42,12 +37,12 @@ fn dump(label: &str, report: &diag::Report, text: WidgetId) {
|
||||
TraceEvent::DrawRequest {
|
||||
id,
|
||||
region,
|
||||
pixel_size,
|
||||
region_px,
|
||||
..
|
||||
} if *id == text => {
|
||||
println!(
|
||||
" draw in {:.2}x{:.2} region {region:?}",
|
||||
pixel_size.x, pixel_size.y
|
||||
region_px.x, region_px.y
|
||||
)
|
||||
}
|
||||
TraceEvent::SizeReported { id, size } if *id == text => {
|
||||
@@ -81,7 +76,7 @@ fn what_box_the_text_is_drawn_in() {
|
||||
|
||||
for _ in 0..2 {
|
||||
for &id in &ids {
|
||||
h.rsc.widgets_mut().get_dyn_mut(id);
|
||||
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
let _ = diag::take();
|
||||
h.frame();
|
||||
@@ -93,23 +88,14 @@ fn what_box_the_text_is_drawn_in() {
|
||||
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 inner = (text,).span(Dir::RIGHT).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);
|
||||
let root = (filler, inner).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(),
|
||||
]
|
||||
vec![text.id(), inner.id(), filler.id(), root.id()]
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user