Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2cb4f05da | ||
|
|
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 | ||
|
|
490918b789 | ||
|
|
a8898aaa54 | ||
|
|
4f5e27cba9 | ||
|
|
11c55bcef9 | ||
|
|
f11f5f4825 | ||
|
|
97cc8b32ed | ||
|
|
95fb4f962c | ||
|
|
bdab55824f | ||
|
|
9d8415d65f | ||
|
|
cb955f1023 | ||
|
|
39e4ca20e6 | ||
|
|
bd6de71a55 | ||
|
|
4e28f1047e | ||
|
|
7548139861 | ||
|
|
5ed9e874a3 | ||
|
|
d3b0ebf90c | ||
|
|
8220a78d4a | ||
|
|
0283c9d6c7 | ||
|
|
71c9c39523 | ||
|
|
f437495309 | ||
|
|
29c7881c8a | ||
|
|
691e3eb23c | ||
|
|
9644971daf | ||
|
|
de9ddc0ad4 |
No files matched your search
@@ -25,6 +25,12 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"]
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["core", "macro", "rig-input"]
|
members = ["core", "macro", "rig-input"]
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
debug = 1
|
||||||
|
|
||||||
|
[profile.test]
|
||||||
|
debug = "line-tables-only"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|||||||
@@ -0,0 +1,574 @@
|
|||||||
|
use crate::{UiNum, util::Vec2};
|
||||||
|
use std::{
|
||||||
|
fmt::{Debug, Display, Formatter},
|
||||||
|
ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A number held as a whole count of `1 / 2^SHIFT`.
|
||||||
|
///
|
||||||
|
/// Layout reaches one place by more than one route -- a box composed down the
|
||||||
|
/// chain, and the same box summed from what its children asked for -- and has
|
||||||
|
/// to decide whether the two are the same place. In floats they land a few
|
||||||
|
/// bits apart, which is a defect wherever the answer changes what is drawn
|
||||||
|
/// rather than where. Here adding and subtracting are exact, a multiply
|
||||||
|
/// drops to the step below, and a conversion between grids takes the nearest
|
||||||
|
/// one, so two routes to one place land on one number and everything
|
||||||
|
/// downstream compares for equality instead of for nearness.
|
||||||
|
///
|
||||||
|
/// `SHIFT` is the number of fractional bits, which is what makes the steps
|
||||||
|
/// divide a whole number: a power of two also converts to `f32` without
|
||||||
|
/// rounding while the value fits in its mantissa.
|
||||||
|
///
|
||||||
|
/// Arithmetic wraps at the ends of the range, the way the `i32` underneath
|
||||||
|
/// does. Saturating instead was measured at a twelfth of layout's
|
||||||
|
/// instructions -- five per add against one -- to keep the ordering of
|
||||||
|
/// coordinates two million pixels out, where nothing draws anyway. A value
|
||||||
|
/// off the end is a defect either way; wrapping makes it an obvious one.
|
||||||
|
/// Only [`Self::from_f32`] clamps, since a float has further to come from.
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, bytemuck::Pod, bytemuck::Zeroable,
|
||||||
|
)]
|
||||||
|
pub struct Fixed<const SHIFT: u32>(i32);
|
||||||
|
|
||||||
|
/// A length or a coordinate in pixels, in steps of `1/1024`. Finer than
|
||||||
|
/// anything a display can show, and exact in `f32` up to 16,384 px, which is
|
||||||
|
/// what lets the same number reach the GPU.
|
||||||
|
pub type Px = Fixed<PX_SHIFT>;
|
||||||
|
|
||||||
|
/// How many bits of a pixel a [`Px`] keeps. One place, because [`PxVec2`]
|
||||||
|
/// and the shader's own decoding are the same grid or nothing lines up.
|
||||||
|
pub const PX_SHIFT: u32 = 10;
|
||||||
|
|
||||||
|
/// A share of what a box has left over, which is a weight beside its
|
||||||
|
/// siblings rather than a fraction of anything: a list divides its room by
|
||||||
|
/// the total of these, so the range has to hold a whole list's worth and the
|
||||||
|
/// precision only has to tell two weights apart.
|
||||||
|
pub type Weight = Fixed<16>;
|
||||||
|
|
||||||
|
/// A fraction of a box. Twenty-four bits of it, which matches `f32` around a
|
||||||
|
/// half and beats it above one -- where anchors actually sit -- and leaves
|
||||||
|
/// +/-128 of range, enough to sum a hundred children each asking for a whole
|
||||||
|
/// box. A `leftover` weight is not one of these: it is a share of what is
|
||||||
|
/// left rather than a fraction of anything, and it sums over a whole list.
|
||||||
|
pub type Rel = Fixed<REL_SHIFT>;
|
||||||
|
|
||||||
|
/// How many bits of a box a [`Rel`] keeps, beside [`PX_SHIFT`] and for the
|
||||||
|
/// same reason.
|
||||||
|
pub const REL_SHIFT: u32 = 24;
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||||
|
pub const ZERO: Self = Self(0);
|
||||||
|
pub const ONE: Self = Self::one();
|
||||||
|
/// The gap between neighbouring values, which is also how far apart two
|
||||||
|
/// numbers can be and still mean the same place.
|
||||||
|
pub const STEP: Self = Self(1);
|
||||||
|
/// Also what stands in for an unbounded end: compared against, never
|
||||||
|
/// added to, since arithmetic wraps past it.
|
||||||
|
pub const MIN: Self = Self(i32::MIN);
|
||||||
|
pub const MAX: Self = Self(i32::MAX);
|
||||||
|
|
||||||
|
const fn one() -> Self {
|
||||||
|
assert!(SHIFT < 31, "a Fixed needs a bit for the whole part");
|
||||||
|
Self(1 << SHIFT)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_raw(raw: i32) -> Self {
|
||||||
|
Self(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The count of steps, for a caller that needs the representation rather
|
||||||
|
/// than the number.
|
||||||
|
pub const fn raw(self) -> i32 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_int(v: i32) -> Self {
|
||||||
|
Self(v.wrapping_mul(Self::one().0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rounds to the nearest step, and clamps to the ends of the grid rather
|
||||||
|
/// than wrapping: this is where a number from outside arrives, and a float
|
||||||
|
/// has the range to be anywhere. A NaN has no nearest step and becomes
|
||||||
|
/// zero, which is a caller's mistake rather than a value worth carrying.
|
||||||
|
///
|
||||||
|
/// Half-away is written out rather than called through `f32::round`,
|
||||||
|
/// which is not `const`: a layout constant has to stay a constant.
|
||||||
|
pub const fn from_f32(v: f32) -> Self {
|
||||||
|
debug_assert!(!v.is_nan(), "a NaN has no place on the grid");
|
||||||
|
let scaled = v * Self::one().0 as f32;
|
||||||
|
// Above 2^23 an `f32` has no fractional part left to round, and
|
||||||
|
// adding a half there rounds the number itself up instead. The cast
|
||||||
|
// saturates at both ends and sends NaN to zero, which is the
|
||||||
|
// behaviour wanted at both.
|
||||||
|
const WHOLE: f32 = (1 << 23) as f32;
|
||||||
|
Self(match (scaled >= WHOLE, scaled <= -WHOLE, scaled < 0.0) {
|
||||||
|
(true, _, _) | (_, true, _) => scaled as i32,
|
||||||
|
(_, _, true) => (scaled - 0.5) as i32,
|
||||||
|
_ => (scaled + 0.5) as i32,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first step at or above `v`, where [`Self::from_f32`] takes the
|
||||||
|
/// nearest one and is below it half the time. For a bound that has to
|
||||||
|
/// admit the value it came from: a measurement rounded down is a bound
|
||||||
|
/// that leaves out the thing it was measured from.
|
||||||
|
pub const fn ceil_from_f32(v: f32) -> Self {
|
||||||
|
let nearest = Self::from_f32(v);
|
||||||
|
match nearest.to_f32() < v {
|
||||||
|
true => nearest.next_up(),
|
||||||
|
false => nearest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From a number as it is written in source -- `16`, `1.5` -- which is
|
||||||
|
/// the other place a value enters the grid.
|
||||||
|
pub fn from_num(v: impl UiNum) -> Self {
|
||||||
|
Self::from_f32(v.to_f32())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn to_f32(self) -> f32 {
|
||||||
|
self.0 as f32 / Self::one().0 as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same value on another grid, rounded where the new one is coarser.
|
||||||
|
pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> {
|
||||||
|
Fixed(match TO >= SHIFT {
|
||||||
|
true => self.0 << (TO - SHIFT),
|
||||||
|
false => shift_round(self.0 as i64, SHIFT - TO) as i32,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn add(self, rhs: Self) -> Self {
|
||||||
|
Self(self.0.wrapping_add(rhs.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn sub(self, rhs: Self) -> Self {
|
||||||
|
Self(self.0.wrapping_sub(rhs.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn neg(self) -> Self {
|
||||||
|
Self(self.0.wrapping_neg())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scaled by a number on any grid, which is how a length takes a fraction
|
||||||
|
/// of itself and keeps being a length: the product is measured in the
|
||||||
|
/// receiver's steps.
|
||||||
|
///
|
||||||
|
/// Dropped to the step below rather than taken to the nearest one
|
||||||
|
/// (Bryan, 2026-09-16), which costs a share a thousandth of a pixel of
|
||||||
|
/// its row -- less than an even number of pixels draws. Toward negative
|
||||||
|
/// infinity on both sides of zero, since that is a shift and nothing
|
||||||
|
/// else: a value and its negation therefore land different distances
|
||||||
|
/// from where they came, so a flipped span can sit a step from its
|
||||||
|
/// mirror image.
|
||||||
|
pub const fn mul<const BY: u32>(self, by: Fixed<BY>) -> Self {
|
||||||
|
Self(((self.0 as i64 * by.0 as i64) >> BY) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repeated a whole number of times, which no grid rounds.
|
||||||
|
pub const fn mul_int(self, by: i32) -> Self {
|
||||||
|
Self(self.0.wrapping_mul(by))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Divided into a whole number of parts, rounded to the nearest step.
|
||||||
|
pub const fn div_int(self, by: i32) -> Self {
|
||||||
|
debug_assert!(by != 0, "no part of nothing");
|
||||||
|
if by == 0 {
|
||||||
|
return Self::ZERO;
|
||||||
|
}
|
||||||
|
Self(div_round(self.0 as i64, by as i64) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Divided by a number on any grid. A zero divisor is a caller bug -- a
|
||||||
|
/// box of no length has no fraction of itself -- and answers with the end
|
||||||
|
/// of the range so that a release build lays out something absurd rather
|
||||||
|
/// than dying.
|
||||||
|
pub const fn div<const BY: u32>(self, by: Fixed<BY>) -> Self {
|
||||||
|
debug_assert!(by.0 != 0, "dividing by a length of zero");
|
||||||
|
if by.0 == 0 {
|
||||||
|
return match self.0 < 0 {
|
||||||
|
true => Self::MIN,
|
||||||
|
false => Self::MAX,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Self(div_round((self.0 as i64) << BY, by.0 as i64) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `num / den` on *this* grid rather than on theirs, for weights coarser
|
||||||
|
/// than the share they divide.
|
||||||
|
pub const fn ratio<const OF: u32>(num: Fixed<OF>, den: Fixed<OF>) -> Self {
|
||||||
|
debug_assert!(den.0 != 0, "no part of a whole of nothing");
|
||||||
|
if den.0 == 0 {
|
||||||
|
return Self::ZERO;
|
||||||
|
}
|
||||||
|
Self(div_round((num.0 as i64) << SHIFT, den.0 as i64) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `from` and `to` a fraction of the way apart, the fraction being the
|
||||||
|
/// receiver -- the argument order [`crate::util::LerpUtil`] already uses.
|
||||||
|
pub const fn lerp<const OF: u32>(self, from: Fixed<OF>, to: Fixed<OF>) -> Fixed<OF> {
|
||||||
|
from.add(to.sub(from).mul(self))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn min(self, other: Self) -> Self {
|
||||||
|
match self.0 < other.0 {
|
||||||
|
true => self,
|
||||||
|
false => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn max(self, other: Self) -> Self {
|
||||||
|
match self.0 > other.0 {
|
||||||
|
true => self,
|
||||||
|
false => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn abs(self) -> Self {
|
||||||
|
Self(self.0.wrapping_abs())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn clamp(self, lo: Self, hi: Self) -> Self {
|
||||||
|
debug_assert!(lo.0 <= hi.0, "an empty clamp has no answer");
|
||||||
|
self.max(lo).min(hi)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The next value along, for an interval that must not admit its own
|
||||||
|
/// boundary. The step is the whole gap, so there is nothing to exclude
|
||||||
|
/// between this and the boundary itself.
|
||||||
|
pub const fn next_up(self) -> Self {
|
||||||
|
Self(self.0.wrapping_add(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn next_down(self) -> Self {
|
||||||
|
Self(self.0.wrapping_sub(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Back to a single step, rounding halves away from zero so that a value and
|
||||||
|
/// its negation round to the same distance.
|
||||||
|
const fn shift_round(v: i64, bits: u32) -> i64 {
|
||||||
|
let half = (1i64 << bits) >> 1;
|
||||||
|
match v < 0 {
|
||||||
|
true => -((-v + half) >> bits),
|
||||||
|
false => (v + half) >> bits,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn div_round(num: i64, den: i64) -> i64 {
|
||||||
|
let (q, rem) = (num / den, num % den);
|
||||||
|
match rem.unsigned_abs() * 2 >= den.unsigned_abs() {
|
||||||
|
true => match (num < 0) == (den < 0) {
|
||||||
|
true => q + 1,
|
||||||
|
false => q - 1,
|
||||||
|
},
|
||||||
|
false => q,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toward positive infinity when `up`, toward negative infinity otherwise.
|
||||||
|
pub(crate) const fn div_toward(num: i64, den: i64, up: bool) -> i64 {
|
||||||
|
let (q, rem) = (num / den, num % den);
|
||||||
|
if rem == 0 {
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
match (rem < 0) == (den < 0) {
|
||||||
|
true => q + up as i64,
|
||||||
|
false => q - !up as i64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clamped to the ends, unlike a [`Fixed`]'s own arithmetic: a range of box
|
||||||
|
/// lengths that runs past `i32` really is unbounded.
|
||||||
|
pub(crate) const fn narrow(v: i64) -> i32 {
|
||||||
|
if v > i32::MAX as i64 {
|
||||||
|
return i32::MAX;
|
||||||
|
}
|
||||||
|
if v < i32::MIN as i64 {
|
||||||
|
return i32::MIN;
|
||||||
|
}
|
||||||
|
v as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> Add for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, rhs: Self) -> Self {
|
||||||
|
Fixed::add(self, rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> Sub for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Self) -> Self {
|
||||||
|
Fixed::sub(self, rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> Neg for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn neg(self) -> Self {
|
||||||
|
Fixed::neg(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> AddAssign for Fixed<SHIFT> {
|
||||||
|
fn add_assign(&mut self, rhs: Self) {
|
||||||
|
*self = Fixed::add(*self, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> SubAssign for Fixed<SHIFT> {
|
||||||
|
fn sub_assign(&mut self, rhs: Self) {
|
||||||
|
*self = Fixed::sub(*self, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32, const BY: u32> Mul<Fixed<BY>> for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn mul(self, rhs: Fixed<BY>) -> Self {
|
||||||
|
Fixed::mul(self, rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32, const BY: u32> Div<Fixed<BY>> for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn div(self, rhs: Fixed<BY>) -> Self {
|
||||||
|
Fixed::div(self, rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> Display for Fixed<SHIFT> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self.to_f32(), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints the number rather than the count of steps: a failing layout test
|
||||||
|
/// reports boxes, and `1126` is not a height anybody can read.
|
||||||
|
impl<const SHIFT: u32> Debug for Fixed<SHIFT> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self.to_f32(), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Two of them, for the places a size or a position needs both axes: a
|
||||||
|
/// window, a box in pixels, a pointer. Held apart from [`crate::util::Vec2`]
|
||||||
|
/// because that one is what the GPU and the platform speak.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub struct FixedVec2<const SHIFT: u32> {
|
||||||
|
pub x: Fixed<SHIFT>,
|
||||||
|
pub y: Fixed<SHIFT>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type PxVec2 = FixedVec2<PX_SHIFT>;
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
||||||
|
pub const ZERO: Self = Self::splat(Fixed::ZERO);
|
||||||
|
|
||||||
|
pub const fn new(x: Fixed<SHIFT>, y: Fixed<SHIFT>) -> Self {
|
||||||
|
Self { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn splat(v: Fixed<SHIFT>) -> Self {
|
||||||
|
Self { x: v, y: v }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_f32(v: Vec2) -> Self {
|
||||||
|
Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first step at or above each part, for a measurement reported as a
|
||||||
|
/// box: what it occupies is not less than what was measured.
|
||||||
|
pub fn ceil_from_f32(v: Vec2) -> Self {
|
||||||
|
Self::new(Fixed::ceil_from_f32(v.x), Fixed::ceil_from_f32(v.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_f32(self) -> Vec2 {
|
||||||
|
Vec2::new(self.x.to_f32(), self.y.to_f32())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn div_int(self, by: i32) -> Self {
|
||||||
|
Self::new(self.x.div_int(by), self.y.div_int(by))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn min(self, other: Self) -> Self {
|
||||||
|
Self::new(self.x.min(other.x), self.y.min(other.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn max(self, other: Self) -> Self {
|
||||||
|
Self::new(self.x.max(other.x), self.y.max(other.y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `impl_op!` names one concrete type, and this one is generic.
|
||||||
|
const impl<const SHIFT: u32> Add for FixedVec2<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, rhs: Self) -> Self {
|
||||||
|
Self::new(self.x.add(rhs.x), self.y.add(rhs.y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> Sub for FixedVec2<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Self) -> Self {
|
||||||
|
Self::new(self.x.sub(rhs.x), self.y.sub(rhs.y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> AddAssign for FixedVec2<SHIFT> {
|
||||||
|
fn add_assign(&mut self, rhs: Self) {
|
||||||
|
*self = Add::add(*self, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> SubAssign for FixedVec2<SHIFT> {
|
||||||
|
fn sub_assign(&mut self, rhs: Self) {
|
||||||
|
*self = Sub::sub(*self, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> Debug for FixedVec2<SHIFT> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "({}, {})", self.x, self.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> Display for FixedVec2<SHIFT> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "({}, {})", self.x, self.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_sum_of_steps_does_not_drift() {
|
||||||
|
let mut at = Px::ZERO;
|
||||||
|
for _ in 0..20_000 {
|
||||||
|
at += Px::from_raw(3);
|
||||||
|
}
|
||||||
|
assert_eq!(at, Px::from_raw(60_000));
|
||||||
|
for _ in 0..20_000 {
|
||||||
|
at -= Px::from_raw(3);
|
||||||
|
}
|
||||||
|
assert_eq!(at, Px::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_pixel_survives_the_trip_through_f32() {
|
||||||
|
for raw in [0, 1, -1, 64, -1000, 16_777_215, -16_777_215] {
|
||||||
|
let px = Px::from_raw(raw);
|
||||||
|
assert_eq!(Px::from_f32(px.to_f32()), px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fraction_of_a_length_is_a_length() {
|
||||||
|
let half = Px::from_int(100) * Rel::from_f32(0.5);
|
||||||
|
assert_eq!(half, Px::from_int(50));
|
||||||
|
assert_eq!(Px::from_int(100) * Rel::ONE, Px::from_int(100));
|
||||||
|
assert_eq!(Px::from_int(100) * Rel::ZERO, Px::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toward negative infinity on both sides of zero, which is what makes
|
||||||
|
/// it a shift rather than a shift and a sign branch -- and what makes a
|
||||||
|
/// value and its negation land different distances from where they came,
|
||||||
|
/// so a flipped span can sit a step from its mirror image.
|
||||||
|
#[test]
|
||||||
|
fn a_multiply_drops_to_the_step_below_on_both_sides_of_zero() {
|
||||||
|
// A step and a half of one, which has no step of its own.
|
||||||
|
let step_and_a_half = Rel::from_f32(1.5).div_int(Px::ONE.raw());
|
||||||
|
assert_eq!(Px::ONE * step_and_a_half, Px::from_raw(1));
|
||||||
|
assert_eq!(Px::ONE.neg() * step_and_a_half, Px::from_raw(-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A division rounds to the nearest step, so it cannot put back the
|
||||||
|
/// steps a truncating multiply dropped: a round trip comes back short,
|
||||||
|
/// never long, and by the few steps the two operations gave up.
|
||||||
|
#[test]
|
||||||
|
fn dividing_by_a_fraction_cannot_undo_a_truncating_multiply() {
|
||||||
|
let third = Rel::ONE / Rel::from_int(3);
|
||||||
|
let len = Px::from_int(300);
|
||||||
|
let back = len * third / third;
|
||||||
|
assert!(back <= len, "{back:?} is longer than {len:?}");
|
||||||
|
assert!(len - back <= Px::from_raw(3), "{back:?} against {len:?}");
|
||||||
|
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bound a greedy line break needs: the width it was measured at is
|
||||||
|
/// not on the grid, and the narrowest box the break still holds for is
|
||||||
|
/// the step at or above it, never the one below.
|
||||||
|
#[test]
|
||||||
|
fn a_ceiling_never_lands_below_the_number_it_came_from() {
|
||||||
|
let step = 1.0 / (1 << PX_SHIFT) as f32;
|
||||||
|
for n in 0..64 {
|
||||||
|
let v = 189.0 + n as f32 * step / 3.0;
|
||||||
|
let up = Px::ceil_from_f32(v);
|
||||||
|
assert!(up.to_f32() >= v, "{up:?} is below {v}");
|
||||||
|
assert!(
|
||||||
|
up.to_f32() - v < step,
|
||||||
|
"{up:?} is more than a step above {v}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// An exact step is its own ceiling.
|
||||||
|
assert_eq!(Px::ceil_from_f32(189.5), Px::from_f32(189.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_number_from_outside_is_clamped_to_the_grid() {
|
||||||
|
assert_eq!(Px::from_f32(1e12), Px::MAX);
|
||||||
|
assert_eq!(Px::from_f32(-1e12), Px::MIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_coarser_grid_rounds_and_a_finer_one_does_not() {
|
||||||
|
// A third, which neither grid holds exactly.
|
||||||
|
let third = Rel::ONE / Rel::from_int(3);
|
||||||
|
assert_eq!(third.to_scale::<6>(), Fixed::<6>::from_raw(21));
|
||||||
|
let coarse = Fixed::<6>::from_raw(21);
|
||||||
|
assert_eq!(coarse.to_scale::<24>().to_scale::<6>(), coarse);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lerp_takes_the_fraction_as_the_receiver() {
|
||||||
|
let (from, to) = (Px::from_int(10), Px::from_int(20));
|
||||||
|
assert_eq!(Rel::ZERO.lerp(from, to), from);
|
||||||
|
assert_eq!(Rel::ONE.lerp(from, to), to);
|
||||||
|
assert_eq!(Rel::from_f32(0.5).lerp(from, to), Px::from_int(15));
|
||||||
|
assert_eq!(Rel::from_f32(0.5).lerp(to, from), Px::from_int(15));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_ratio_is_finer_than_the_weights_it_divides() {
|
||||||
|
let (one, three) = (Weight::ONE, Weight::from_int(3));
|
||||||
|
// A third, which the weights' own grid could only hold to 1/65536.
|
||||||
|
assert_eq!(Rel::ratio(one, three), Rel::from_raw(5592405));
|
||||||
|
assert_eq!(Rel::ratio(three, three), Rel::ONE);
|
||||||
|
assert_eq!(Rel::ratio(Weight::ZERO, three), Rel::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nothing_sits_between_a_value_and_the_next_one() {
|
||||||
|
let at = Px::from_int(3);
|
||||||
|
assert_eq!(at.next_up().next_down(), at);
|
||||||
|
assert_eq!(at.next_up().raw() - at.raw(), 1);
|
||||||
|
assert!(at.next_down() < at && at < at.next_up());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_prints_the_number_rather_than_the_steps() {
|
||||||
|
assert_eq!(format!("{:?}", Px::from_f32(17.59375)), "17.59375");
|
||||||
|
assert_eq!(format!("{}", Px::from_int(-2)), "-2");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
//! reuse, size, placement, and text events for one suspicious widget. The
|
//! reuse, size, placement, and text events for one suspicious widget. The
|
||||||
//! selection is a set and survives [`take`] until cleared.
|
//! selection is a set and survives [`take`] until cleared.
|
||||||
|
|
||||||
use crate::{Axis, Len, Size, UiRegion, WidgetId, util::Vec2};
|
use crate::{Axis, LayoutLen, PxVec2, Size, UiRegion, WidgetId};
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
@@ -26,10 +26,9 @@ use std::{
|
|||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub(crate) enum Counter {
|
pub(crate) enum Counter {
|
||||||
Updates,
|
Updates,
|
||||||
ResizeDependents,
|
|
||||||
DrawRequests,
|
DrawRequests,
|
||||||
WidgetDraws,
|
WidgetDraws,
|
||||||
PlaceCalls,
|
RegionNodeDraws,
|
||||||
SizeReads,
|
SizeReads,
|
||||||
HintHits,
|
HintHits,
|
||||||
HintMisses,
|
HintMisses,
|
||||||
@@ -39,14 +38,13 @@ pub(crate) enum Counter {
|
|||||||
ReuseMoved,
|
ReuseMoved,
|
||||||
ReuseDirty,
|
ReuseDirty,
|
||||||
ReuseWrongParent,
|
ReuseWrongParent,
|
||||||
ReuseUnslotted,
|
ReuseRemapped,
|
||||||
ReuseOwnResize,
|
ReuseOutside,
|
||||||
ReuseDescendantResize,
|
ReuseWrongLayer,
|
||||||
ResizeChecks,
|
ReuseWrongNode,
|
||||||
ResizeCheckChildren,
|
PlaceRedraws,
|
||||||
QueuePops,
|
QueuePops,
|
||||||
DepthReads,
|
DepthReads,
|
||||||
EagerReaderRedraws,
|
|
||||||
LocalRedraws,
|
LocalRedraws,
|
||||||
SizeChanges,
|
SizeChanges,
|
||||||
ReaderEdges,
|
ReaderEdges,
|
||||||
@@ -63,10 +61,9 @@ impl Counter {
|
|||||||
|
|
||||||
const NAMES: [&'static str; Self::COUNT] = [
|
const NAMES: [&'static str; Self::COUNT] = [
|
||||||
"updates",
|
"updates",
|
||||||
"resize dependents",
|
|
||||||
"draw requests",
|
"draw requests",
|
||||||
"widget draws",
|
"widget draws",
|
||||||
"place calls",
|
"region-node draws",
|
||||||
"draw-result size reads",
|
"draw-result size reads",
|
||||||
"hint hits",
|
"hint hits",
|
||||||
"hint misses",
|
"hint misses",
|
||||||
@@ -76,14 +73,13 @@ impl Counter {
|
|||||||
"reuse moved",
|
"reuse moved",
|
||||||
"reuse: dirty",
|
"reuse: dirty",
|
||||||
"reuse: wrong parent",
|
"reuse: wrong parent",
|
||||||
"reuse: unslotted",
|
"reuse remapped",
|
||||||
"reuse: own resize",
|
"reuse: outside what it holds for",
|
||||||
"reuse: descendant resize",
|
"reuse: another layer",
|
||||||
"resize checks",
|
"reuse: region-node choice changed",
|
||||||
"resize children checked",
|
"placed by redrawing",
|
||||||
"redraw queue pops",
|
"redraw queue pops",
|
||||||
"depth reads",
|
"depth reads",
|
||||||
"eager reader redraws",
|
|
||||||
"local redraws",
|
"local redraws",
|
||||||
"size changes",
|
"size changes",
|
||||||
"reader edges",
|
"reader edges",
|
||||||
@@ -100,7 +96,6 @@ impl Counter {
|
|||||||
pub(crate) enum TimerKind {
|
pub(crate) enum TimerKind {
|
||||||
Update,
|
Update,
|
||||||
FullLayout,
|
FullLayout,
|
||||||
ResizeMarking,
|
|
||||||
IncrementalLayout,
|
IncrementalLayout,
|
||||||
TextRender,
|
TextRender,
|
||||||
TextShape,
|
TextShape,
|
||||||
@@ -114,7 +109,6 @@ impl TimerKind {
|
|||||||
const NAMES: [&'static str; Self::COUNT] = [
|
const NAMES: [&'static str; Self::COUNT] = [
|
||||||
"update total",
|
"update total",
|
||||||
"full layout",
|
"full layout",
|
||||||
"resize marking",
|
|
||||||
"incremental layout",
|
"incremental layout",
|
||||||
"text render",
|
"text render",
|
||||||
"text shape",
|
"text shape",
|
||||||
@@ -254,9 +248,10 @@ pub enum ReuseOutcome {
|
|||||||
Moved,
|
Moved,
|
||||||
Dirty,
|
Dirty,
|
||||||
WrongParent,
|
WrongParent,
|
||||||
Unslotted,
|
WrongLayer,
|
||||||
OwnResize,
|
Remapped,
|
||||||
DescendantResize,
|
Outside,
|
||||||
|
Undrawn,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One targeted layout event. Events are retained in execution order, making
|
/// One targeted layout event. Events are retained in execution order, making
|
||||||
@@ -267,8 +262,8 @@ pub enum TraceEvent {
|
|||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
parent: Option<WidgetId>,
|
parent: Option<WidgetId>,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
pixel_size: Vec2,
|
pixel_size: PxVec2,
|
||||||
slotted: bool,
|
region_node: bool,
|
||||||
},
|
},
|
||||||
Reuse {
|
Reuse {
|
||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
@@ -278,7 +273,7 @@ pub enum TraceEvent {
|
|||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
size: Size,
|
size: Size,
|
||||||
},
|
},
|
||||||
Placed {
|
RegionNode {
|
||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
parent: WidgetId,
|
parent: WidgetId,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
@@ -292,7 +287,7 @@ pub enum TraceEvent {
|
|||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
reader: WidgetId,
|
reader: WidgetId,
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
hint: Option<Len>,
|
hint: Option<LayoutLen>,
|
||||||
},
|
},
|
||||||
TextRendered {
|
TextRendered {
|
||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
@@ -363,8 +358,8 @@ pub(crate) fn draw_request(
|
|||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
parent: Option<WidgetId>,
|
parent: Option<WidgetId>,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
pixel_size: Vec2,
|
pixel_size: PxVec2,
|
||||||
slotted: bool,
|
region_node: bool,
|
||||||
) {
|
) {
|
||||||
trace(
|
trace(
|
||||||
id,
|
id,
|
||||||
@@ -373,7 +368,7 @@ pub(crate) fn draw_request(
|
|||||||
parent,
|
parent,
|
||||||
region,
|
region,
|
||||||
pixel_size,
|
pixel_size,
|
||||||
slotted,
|
region_node,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -386,15 +381,15 @@ pub(crate) fn size_reported(id: WidgetId, size: Size) {
|
|||||||
trace(id, TraceEvent::SizeReported { id, size });
|
trace(id, TraceEvent::SizeReported { id, size });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn placed(id: WidgetId, parent: WidgetId, region: UiRegion) {
|
pub(crate) fn region_node(id: WidgetId, parent: WidgetId, region: UiRegion) {
|
||||||
trace(id, TraceEvent::Placed { id, parent, region });
|
trace(id, TraceEvent::RegionNode { id, parent, region });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn size_read(id: WidgetId, reader: WidgetId, size: Size) {
|
pub(crate) fn size_read(id: WidgetId, reader: WidgetId, size: Size) {
|
||||||
trace(id, TraceEvent::SizeRead { id, reader, size });
|
trace(id, TraceEvent::SizeRead { id, reader, size });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn hint_read(id: WidgetId, reader: WidgetId, axis: Axis, hint: Option<Len>) {
|
pub(crate) fn hint_read(id: WidgetId, reader: WidgetId, axis: Axis, hint: Option<LayoutLen>) {
|
||||||
trace(
|
trace(
|
||||||
id,
|
id,
|
||||||
TraceEvent::HintRead {
|
TraceEvent::HintRead {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub mod layout_diagnostics;
|
|||||||
|
|
||||||
mod attr;
|
mod attr;
|
||||||
mod event;
|
mod event;
|
||||||
|
mod fixed;
|
||||||
mod num;
|
mod num;
|
||||||
mod orientation;
|
mod orientation;
|
||||||
mod primitive;
|
mod primitive;
|
||||||
@@ -26,6 +27,7 @@ pub mod util;
|
|||||||
|
|
||||||
pub use attr::*;
|
pub use attr::*;
|
||||||
pub use event::*;
|
pub use event::*;
|
||||||
|
pub use fixed::*;
|
||||||
pub use num::*;
|
pub use num::*;
|
||||||
pub use orientation::*;
|
pub use orientation::*;
|
||||||
pub use primitive::*;
|
pub use primitive::*;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::vec2;
|
use crate::{Px, Rel};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
pub struct Align {
|
pub struct Align {
|
||||||
pub x: Option<AxisAlign>,
|
pub x: Option<AxisAlign>,
|
||||||
pub y: Option<AxisAlign>,
|
pub y: Option<AxisAlign>,
|
||||||
@@ -30,20 +30,32 @@ impl Align {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
/// Where a widget sits in a box longer than it is. The default is the middle,
|
||||||
pub enum AxisAlign {
|
/// because the two edges are the ones that assume a direction: which of them
|
||||||
Neg,
|
/// is the near one depends on the writing system and on which way a container
|
||||||
Center,
|
/// runs, and the middle is the same either way.
|
||||||
Pos,
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
}
|
pub struct AxisAlign(Rel);
|
||||||
|
|
||||||
impl AxisAlign {
|
impl AxisAlign {
|
||||||
pub const fn rel(&self) -> f32 {
|
pub const NEG: Self = Self::new(0.0);
|
||||||
match self {
|
pub const CENTER: Self = Self::new(0.5);
|
||||||
Self::Neg => 0.0,
|
pub const POS: Self = Self::new(1.0);
|
||||||
Self::Center => 0.5,
|
|
||||||
Self::Pos => 1.0,
|
pub const fn new(rel: f32) -> Self {
|
||||||
|
Self(Rel::from_f32(rel))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A fraction of the room left over, which is what the layout reads: the
|
||||||
|
/// three constants are the familiar places along it, not the only ones.
|
||||||
|
pub const fn rel(&self) -> Rel {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AxisAlign {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::CENTER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,41 +65,60 @@ pub struct CardinalAlign {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CardinalAlign {
|
impl CardinalAlign {
|
||||||
pub const LEFT: Self = Self::new(Axis::X, AxisAlign::Neg);
|
pub const LEFT: Self = Self::new(Axis::X, AxisAlign::NEG);
|
||||||
pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::Center);
|
pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::CENTER);
|
||||||
pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::Pos);
|
pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::POS);
|
||||||
pub const TOP: Self = Self::new(Axis::Y, AxisAlign::Neg);
|
pub const TOP: Self = Self::new(Axis::Y, AxisAlign::NEG);
|
||||||
pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::Center);
|
pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::CENTER);
|
||||||
pub const BOT: Self = Self::new(Axis::Y, AxisAlign::Pos);
|
pub const BOT: Self = Self::new(Axis::Y, AxisAlign::POS);
|
||||||
|
|
||||||
pub const fn new(axis: Axis, align: AxisAlign) -> Self {
|
pub const fn new(axis: Axis, align: AxisAlign) -> Self {
|
||||||
Self { axis, align }
|
Self { axis, align }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
pub struct RegionAlign {
|
pub struct RegionAlign {
|
||||||
pub x: AxisAlign,
|
pub x: AxisAlign,
|
||||||
pub y: AxisAlign,
|
pub y: AxisAlign,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegionAlign {
|
impl RegionAlign {
|
||||||
pub const TOP_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Neg);
|
/// Both axes at the near edge: the start of a box in its own orientation.
|
||||||
pub const TOP_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Neg);
|
pub const NEAR: Self = Self {
|
||||||
pub const TOP_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Neg);
|
x: AxisAlign::NEG,
|
||||||
pub const CENTER_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Center);
|
y: AxisAlign::NEG,
|
||||||
pub const CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Center);
|
};
|
||||||
pub const CENTER_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Center);
|
|
||||||
pub const BOT_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Pos);
|
pub fn axis(&self, axis: Axis) -> AxisAlign {
|
||||||
pub const BOT_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Pos);
|
match axis {
|
||||||
pub const BOT_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Pos);
|
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);
|
||||||
|
pub const TOP_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::NEG);
|
||||||
|
pub const CENTER_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::CENTER);
|
||||||
|
pub const CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::CENTER);
|
||||||
|
pub const CENTER_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::CENTER);
|
||||||
|
pub const BOT_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::POS);
|
||||||
|
pub const BOT_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::POS);
|
||||||
|
pub const BOT_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::POS);
|
||||||
|
|
||||||
pub const fn new(x: AxisAlign, y: AxisAlign) -> Self {
|
pub const fn new(x: AxisAlign, y: AxisAlign) -> Self {
|
||||||
Self { x, y }
|
Self { x, y }
|
||||||
}
|
}
|
||||||
pub const fn rel(&self) -> Vec2 {
|
|
||||||
vec2(self.x.rel(), self.y.rel())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiVec2 {
|
impl UiVec2 {
|
||||||
@@ -140,16 +171,15 @@ impl Vec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiScalar {
|
impl Len {
|
||||||
pub const fn align(&self, align: AxisAlign) -> UiSpan {
|
pub const fn align(&self, align: AxisAlign) -> UiSpan {
|
||||||
let rel = align.rel();
|
let rel = align.rel();
|
||||||
let mut start = UiScalar::rel(rel);
|
let rest = Rel::ONE.sub(rel);
|
||||||
start.px -= self.px * rel;
|
let at = Len::from_parts(rel, Px::ZERO);
|
||||||
start.rel -= self.rel * rel;
|
UiSpan {
|
||||||
let mut end = UiScalar::rel(rel);
|
start: Len::from_parts(at.rel.sub(self.rel.mul(rel)), at.px.sub(self.px.mul(rel))),
|
||||||
end.px += self.px * (1.0 - rel);
|
end: Len::from_parts(at.rel.add(self.rel.mul(rest)), at.px.add(self.px.mul(rest))),
|
||||||
end.rel += self.rel * (1.0 - rel);
|
}
|
||||||
UiSpan { start, end }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +195,8 @@ impl From<RegionAlign> for Align {
|
|||||||
impl From<Align> for RegionAlign {
|
impl From<Align> for RegionAlign {
|
||||||
fn from(align: Align) -> Self {
|
fn from(align: Align) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: align.x.unwrap_or(AxisAlign::Center),
|
x: align.x.unwrap_or(AxisAlign::CENTER),
|
||||||
y: align.y.unwrap_or(AxisAlign::Center),
|
y: align.y.unwrap_or(AxisAlign::CENTER),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,7 +219,10 @@ impl From<CardinalAlign> for Align {
|
|||||||
|
|
||||||
const impl From<RegionAlign> for UiVec2 {
|
const impl From<RegionAlign> for UiVec2 {
|
||||||
fn from(align: RegionAlign) -> Self {
|
fn from(align: RegionAlign) -> Self {
|
||||||
Self::rel(align.rel())
|
Self::new(
|
||||||
|
Len::from_parts(align.x.rel(), Px::ZERO),
|
||||||
|
Len::from_parts(align.y.rel(), Px::ZERO),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::{Fixed, FixedVec2};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum Axis {
|
pub enum Axis {
|
||||||
@@ -40,6 +41,29 @@ pub enum Sign {
|
|||||||
Pos,
|
Pos,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
||||||
|
pub const fn axis(&self, axis: Axis) -> Fixed<SHIFT> {
|
||||||
|
match axis {
|
||||||
|
Axis::X => self.x,
|
||||||
|
Axis::Y => self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn axis_mut(&mut self, axis: Axis) -> &mut Fixed<SHIFT> {
|
||||||
|
match axis {
|
||||||
|
Axis::X => &mut self.x,
|
||||||
|
Axis::Y => &mut self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_axis(axis: Axis, aligned: Fixed<SHIFT>, ortho: Fixed<SHIFT>) -> Self {
|
||||||
|
match axis {
|
||||||
|
Axis::X => Self::new(aligned, ortho),
|
||||||
|
Axis::Y => Self::new(ortho, aligned),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Vec2 {
|
impl Vec2 {
|
||||||
pub fn axis(&self, axis: Axis) -> f32 {
|
pub fn axis(&self, axis: Axis) -> f32 {
|
||||||
match axis {
|
match axis {
|
||||||
|
|||||||
+119
-79
@@ -1,22 +1,30 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{UiNum, util::impl_op};
|
use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||||
pub struct Size {
|
pub struct Size {
|
||||||
pub x: Len,
|
pub x: LayoutLen,
|
||||||
pub y: Len,
|
pub y: LayoutLen,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
/// What a widget asks for along one axis: a [`Len`] -- pixels and a fraction
|
||||||
pub struct Len {
|
/// of the box it is given -- plus a share of whatever is left over once
|
||||||
pub px: f32,
|
/// everything fixed has been taken. The parts add up rather than choosing
|
||||||
pub rel: f32,
|
/// between one another.
|
||||||
pub rest: f32,
|
///
|
||||||
|
/// Only a container dividing its room can answer a share, so a length nobody
|
||||||
|
/// divides is a `Len`: a position, a padding, a cap, anything already
|
||||||
|
/// resolved.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct LayoutLen {
|
||||||
|
pub px: Px,
|
||||||
|
pub rel: Rel,
|
||||||
|
pub leftover: Weight,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<N: UiNum> From<N> for Len {
|
impl<N: UiNum> From<N> for LayoutLen {
|
||||||
fn from(value: N) -> Self {
|
fn from(value: N) -> Self {
|
||||||
Len::px(value.to_f32())
|
LayoutLen::px(value.to_f32())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,52 +37,76 @@ impl<Nx: UiNum, Ny: UiNum> From<(Nx, Ny)> for Size {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Len> for Size {
|
/// A length with no share in it is a length a container does not have to
|
||||||
fn from(value: Len) -> Self {
|
/// divide, which is one it can always give.
|
||||||
|
impl From<Len> for LayoutLen {
|
||||||
|
fn from(len: Len) -> Self {
|
||||||
|
Self {
|
||||||
|
px: len.px,
|
||||||
|
rel: len.rel,
|
||||||
|
leftover: Weight::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LayoutLen> for Size {
|
||||||
|
fn from(value: LayoutLen) -> Self {
|
||||||
Self { x: value, y: value }
|
Self { x: value, y: value }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Size {
|
impl Size {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
x: Len::ZERO,
|
x: LayoutLen::ZERO,
|
||||||
y: Len::ZERO,
|
y: LayoutLen::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const REST: Self = Self {
|
pub const LEFTOVER: Self = Self {
|
||||||
x: Len::REST,
|
x: LayoutLen::LEFTOVER,
|
||||||
y: Len::REST,
|
y: LayoutLen::LEFTOVER,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// From something measured outside layout -- a texture, a shaped line --
|
||||||
|
/// which is where a size in floats comes from.
|
||||||
pub fn px(v: Vec2) -> Self {
|
pub fn px(v: Vec2) -> Self {
|
||||||
|
Self::from_px(PxVec2::from_f32(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_px(v: PxVec2) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: Len::px(v.x),
|
x: LayoutLen {
|
||||||
y: Len::px(v.y),
|
px: v.x,
|
||||||
|
..LayoutLen::ZERO
|
||||||
|
},
|
||||||
|
y: LayoutLen {
|
||||||
|
px: v.y,
|
||||||
|
..LayoutLen::ZERO
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rel(v: Vec2) -> Self {
|
pub fn rel(v: Vec2) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: Len::rel(v.x),
|
x: LayoutLen::rel(v.x),
|
||||||
y: Len::rel(v.y),
|
y: LayoutLen::rel(v.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rest(v: Vec2) -> Self {
|
pub fn leftover(v: Vec2) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: Len::rest(v.x),
|
x: LayoutLen::leftover(v.x),
|
||||||
y: Len::rest(v.y),
|
y: LayoutLen::leftover(v.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_uivec2(self) -> UiVec2 {
|
pub fn to_uivec2(self) -> UiVec2 {
|
||||||
UiVec2 {
|
UiVec2 {
|
||||||
x: self.x.apply_rest(),
|
x: self.x.apply_leftover(),
|
||||||
y: self.y.apply_rest(),
|
y: self.y.apply_leftover(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_axis(axis: Axis, aligned: Len, ortho: Len) -> Self {
|
pub fn from_axis(axis: Axis, aligned: LayoutLen, ortho: LayoutLen) -> Self {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => Self {
|
Axis::X => Self {
|
||||||
x: aligned,
|
x: aligned,
|
||||||
@@ -87,53 +119,73 @@ impl Size {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn axis(&self, axis: Axis) -> Len {
|
pub fn axis(&self, axis: Axis) -> LayoutLen {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => self.x,
|
Axis::X => self.x,
|
||||||
Axis::Y => self.y,
|
Axis::Y => self.y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn axis_mut(&mut self, axis: Axis) -> &mut LayoutLen {
|
||||||
|
match axis {
|
||||||
|
Axis::X => &mut self.x,
|
||||||
|
Axis::Y => &mut self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Len {
|
impl LayoutLen {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
px: 0.0,
|
px: Px::ZERO,
|
||||||
rel: 0.0,
|
rel: Rel::ZERO,
|
||||||
rest: 0.0,
|
leftover: Weight::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const REST: Self = Self {
|
pub const LEFTOVER: Self = Self {
|
||||||
px: 0.0,
|
px: Px::ZERO,
|
||||||
rel: 0.0,
|
rel: Rel::ZERO,
|
||||||
rest: 1.0,
|
leftover: Weight::ONE,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn apply_rest(&self) -> UiScalar {
|
/// The whole of what is left over counts as the whole box, which is what
|
||||||
UiScalar {
|
/// a length means to something that is not dividing a box between
|
||||||
rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 },
|
/// siblings -- a scroll asking how long its content is.
|
||||||
px: self.px,
|
pub fn apply_leftover(&self) -> Len {
|
||||||
|
let share = match self.leftover > Weight::ZERO {
|
||||||
|
true => Rel::ONE,
|
||||||
|
false => Rel::ZERO,
|
||||||
|
};
|
||||||
|
Len::from_parts(self.rel.add(share), self.px)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This length, given as a part of a box `len` long, as a part of the
|
||||||
|
/// box `len` is itself a part of. The share is untouched: it is a claim
|
||||||
|
/// on whoever divides the room, not a fraction of anything.
|
||||||
|
pub const fn within_len(self, len: Len) -> Self {
|
||||||
|
let part = Len::from_parts(self.rel, self.px).within_len(len);
|
||||||
|
Self {
|
||||||
|
px: part.px,
|
||||||
|
rel: part.rel,
|
||||||
|
leftover: self.leftover,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn px(px: impl UiNum) -> Self {
|
pub fn px(px: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
px: px.to_f32(),
|
px: Px::from_num(px),
|
||||||
rel: 0.0,
|
..Self::ZERO
|
||||||
rest: 0.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rel(rel: impl UiNum) -> Self {
|
pub fn rel(rel: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
px: 0.0,
|
rel: Rel::from_num(rel),
|
||||||
rel: rel.to_f32(),
|
..Self::ZERO
|
||||||
rest: 0.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rest(ratio: impl UiNum) -> Self {
|
pub fn leftover(ratio: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
px: 0.0,
|
leftover: Weight::from_num(ratio),
|
||||||
rel: 0.0,
|
..Self::ZERO
|
||||||
rest: ratio.to_f32(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,38 +193,26 @@ impl Len {
|
|||||||
pub mod len_fns {
|
pub mod len_fns {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn px(px: impl UiNum) -> Len {
|
pub fn px(px: impl UiNum) -> LayoutLen {
|
||||||
Len {
|
LayoutLen::px(px)
|
||||||
px: px.to_f32(),
|
|
||||||
rel: 0.0,
|
|
||||||
rest: 0.0,
|
|
||||||
}
|
}
|
||||||
|
pub fn rel(rel: impl UiNum) -> LayoutLen {
|
||||||
|
LayoutLen::rel(rel)
|
||||||
}
|
}
|
||||||
pub fn rel(rel: impl UiNum) -> Len {
|
pub fn leftover(ratio: impl UiNum) -> LayoutLen {
|
||||||
Len {
|
LayoutLen::leftover(ratio)
|
||||||
px: 0.0,
|
|
||||||
rel: rel.to_f32(),
|
|
||||||
rest: 0.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn rest(ratio: impl UiNum) -> Len {
|
|
||||||
Len {
|
|
||||||
px: 0.0,
|
|
||||||
rel: 0.0,
|
|
||||||
rest: ratio.to_f32(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_op!(Len Add add; px rel rest);
|
impl_op!(same LayoutLen Add add; px rel leftover);
|
||||||
impl_op!(Len Sub sub; px rel rest);
|
impl_op!(same LayoutLen Sub sub; px rel leftover);
|
||||||
|
|
||||||
impl_op!(Size Add add; x y);
|
impl_op!(same Size Add add; x y);
|
||||||
impl_op!(Size Sub sub; x y);
|
impl_op!(same Size Sub sub; x y);
|
||||||
|
|
||||||
impl Default for Len {
|
impl Default for LayoutLen {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::rest(1.0)
|
Self::leftover(1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,16 +222,16 @@ impl std::fmt::Display for Size {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Len {
|
impl std::fmt::Display for LayoutLen {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
if self.px != 0.0 {
|
if self.px != Px::ZERO {
|
||||||
write!(f, "{} px;", self.px)?;
|
write!(f, "{} px;", self.px)?;
|
||||||
}
|
}
|
||||||
if self.rel != 0.0 {
|
if self.rel != Rel::ZERO {
|
||||||
write!(f, "{} rel;", self.rel)?;
|
write!(f, "{} rel;", self.rel)?;
|
||||||
}
|
}
|
||||||
if self.rest != 0.0 {
|
if self.leftover != Weight::ZERO {
|
||||||
write!(f, "{} rest;", self.rest)?;
|
write!(f, "{} leftover;", self.leftover)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-101
@@ -1,41 +1,46 @@
|
|||||||
use std::{fmt::Display, hash::Hash, marker::Destruct};
|
use std::{fmt::Display, marker::Destruct};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{Px, PxVec2, Rel, UiNum, util::impl_op};
|
||||||
UiNum,
|
|
||||||
util::{LerpUtil, impl_op},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||||
pub struct UiVec2 {
|
pub struct UiVec2 {
|
||||||
pub x: UiScalar,
|
pub x: Len,
|
||||||
pub y: UiScalar,
|
pub y: Len,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiVec2 {
|
impl UiVec2 {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
x: UiScalar::ZERO,
|
x: Len::ZERO,
|
||||||
y: UiScalar::ZERO,
|
y: Len::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const fn new(x: UiScalar, y: UiScalar) -> Self {
|
pub const fn new(x: Len, y: Len) -> Self {
|
||||||
Self { x, y }
|
Self { x, y }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn px(px: impl const Into<Vec2>) -> Self {
|
pub const fn px(px: impl const Into<Vec2>) -> Self {
|
||||||
let px = px.into();
|
let px = px.into();
|
||||||
Self {
|
Self {
|
||||||
x: UiScalar::px(px.x),
|
x: Len::px(px.x),
|
||||||
y: UiScalar::px(px.y),
|
y: Len::px(px.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From lengths already on the grid, with no fraction of a box.
|
||||||
|
pub const fn from_px(px: PxVec2) -> Self {
|
||||||
|
Self {
|
||||||
|
x: Len::from_parts(Rel::ZERO, px.x),
|
||||||
|
y: Len::from_parts(Rel::ZERO, px.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn rel(rel: impl const Into<Vec2>) -> Self {
|
pub const fn rel(rel: impl const Into<Vec2>) -> Self {
|
||||||
let rel = rel.into();
|
let rel = rel.into();
|
||||||
Self {
|
Self {
|
||||||
x: UiScalar::rel(rel.x),
|
x: Len::rel(rel.x),
|
||||||
y: UiScalar::rel(rel.y),
|
y: Len::rel(rel.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,30 +61,29 @@ impl UiVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar {
|
pub fn axis_mut(&mut self, axis: Axis) -> &mut Len {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => &mut self.x,
|
Axis::X => &mut self.x,
|
||||||
Axis::Y => &mut self.y,
|
Axis::Y => &mut self.y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn axis(&self, axis: Axis) -> UiScalar {
|
pub fn axis(&self, axis: Axis) -> Len {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => self.x,
|
Axis::X => self.x,
|
||||||
Axis::Y => self.y,
|
Axis::Y => self.y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_px(&self, rel: Vec2) -> Vec2 {
|
/// Resolved against a box of `size`, which is where a fraction stops
|
||||||
Vec2 {
|
/// being one and becomes a place.
|
||||||
x: self.x.to_px(rel.x),
|
pub fn to_px(&self, size: PxVec2) -> PxVec2 {
|
||||||
y: self.y.to_px(rel.y),
|
PxVec2::new(self.x.to_px(size.x), self.y.to_px(size.y))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const FULL_SIZE: Self = Self::rel(Vec2::ONE);
|
pub const FULL_SIZE: Self = Self::rel(Vec2::ONE);
|
||||||
|
|
||||||
pub const fn from_axis(axis: Axis, aligned: UiScalar, ortho: UiScalar) -> Self {
|
pub const fn from_axis(axis: Axis, aligned: Len, ortho: Len) -> Self {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => Self {
|
Axis::X => Self {
|
||||||
x: aligned,
|
x: aligned,
|
||||||
@@ -93,18 +97,11 @@ impl UiVec2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_px(&self) -> Vec2 {
|
pub fn get_px(&self) -> Vec2 {
|
||||||
(self.x.px, self.y.px).into()
|
(self.x.px.to_f32(), self.y.px.to_f32()).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rel(&self) -> Vec2 {
|
pub fn get_rel(&self) -> Vec2 {
|
||||||
(self.x.rel, self.y.rel).into()
|
(self.x.rel.to_f32(), self.y.rel.to_f32()).into()
|
||||||
}
|
|
||||||
|
|
||||||
pub fn abs_mut(&mut self) -> Vec2View<'_> {
|
|
||||||
Vec2View {
|
|
||||||
x: &mut self.x.px,
|
|
||||||
y: &mut self.y.px,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,8 +111,8 @@ impl Display for UiVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_op!(UiVec2 Add add; x y);
|
impl_op!(same UiVec2 Add add; x y);
|
||||||
impl_op!(UiVec2 Sub sub; x y);
|
impl_op!(same UiVec2 Sub sub; x y);
|
||||||
|
|
||||||
const impl From<Vec2> for UiVec2 {
|
const impl From<Vec2> for UiVec2 {
|
||||||
fn from(px: Vec2) -> Self {
|
fn from(px: Vec2) -> Self {
|
||||||
@@ -132,46 +129,59 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A length along one axis: a fraction of the box it is measured in plus an
|
||||||
|
/// offset, `rel * box + px`. A position is the same number -- the length from
|
||||||
|
/// the start of the box to the point -- which is why a [`UiSpan`] is two of
|
||||||
|
/// these. Both parts are fixed point, so composing one through a chain of
|
||||||
|
/// boxes rounds only where it multiplies, and lands on the same number as any
|
||||||
|
/// other route to the same place.
|
||||||
|
///
|
||||||
|
/// It carries no claim on what a container has left over. That is
|
||||||
|
/// [`crate::LayoutLen`], which is this plus a weight, and which means nothing
|
||||||
|
/// to anyone but whoever divides the room.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, Default, bytemuck::Zeroable)]
|
||||||
pub struct UiScalar {
|
pub struct Len {
|
||||||
pub rel: f32,
|
pub rel: Rel,
|
||||||
pub px: f32,
|
pub px: Px,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for UiScalar {}
|
impl_op!(same Len Add add; rel px);
|
||||||
impl Hash for UiScalar {
|
impl_op!(same Len Sub sub; rel px);
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
||||||
state.write_u32(self.rel.to_bits());
|
|
||||||
state.write_u32(self.px.to_bits());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_op!(UiScalar Add add; rel px);
|
impl Len {
|
||||||
impl_op!(UiScalar Sub sub; rel px);
|
pub const ZERO: Self = Self {
|
||||||
|
rel: Rel::ZERO,
|
||||||
impl UiScalar {
|
px: Px::ZERO,
|
||||||
pub const ZERO: Self = Self { rel: 0.0, px: 0.0 };
|
};
|
||||||
pub const FULL: Self = Self { rel: 1.0, px: 0.0 };
|
pub const FULL: Self = Self {
|
||||||
|
rel: Rel::ONE,
|
||||||
|
px: Px::ZERO,
|
||||||
|
};
|
||||||
|
|
||||||
pub const fn new(rel: f32, px: f32) -> Self {
|
pub const fn new(rel: f32, px: f32) -> Self {
|
||||||
|
Self::from_parts(Rel::from_f32(rel), Px::from_f32(px))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From parts already on the grid, rather than numbers to be put on it.
|
||||||
|
pub const fn from_parts(rel: Rel, px: Px) -> Self {
|
||||||
Self { rel, px }
|
Self { rel, px }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn rel(rel: f32) -> Self {
|
pub const fn rel(rel: f32) -> Self {
|
||||||
Self { rel, px: 0.0 }
|
Self::from_parts(Rel::from_f32(rel), Px::ZERO)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn px(px: f32) -> Self {
|
pub const fn px(px: f32) -> Self {
|
||||||
Self { rel: 0.0, px }
|
Self::from_parts(Rel::ZERO, Px::from_f32(px))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn rel_min() -> Self {
|
pub const fn rel_min() -> Self {
|
||||||
Self::new(0.0, 0.0)
|
Self::ZERO
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn rel_max() -> Self {
|
pub const fn rel_max() -> Self {
|
||||||
Self::new(1.0, 0.0)
|
Self::FULL
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn max(&self, other: Self) -> Self {
|
pub const fn max(&self, other: Self) -> Self {
|
||||||
@@ -188,66 +198,75 @@ impl UiScalar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn offset(mut self, amt: f32) -> Self {
|
/// Both parts by the same fraction, which is what a part of a length
|
||||||
self.px += amt;
|
/// means when the length is part pixels and part a fraction of a box.
|
||||||
|
pub const fn scale(&self, by: Rel) -> Self {
|
||||||
|
Self {
|
||||||
|
rel: self.rel.mul(by),
|
||||||
|
px: self.px.mul(by),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn offset(mut self, amt: Px) -> Self {
|
||||||
|
self.px = self.px.add(amt);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn within(&self, span: &UiSpan) -> Self {
|
pub const fn within(&self, span: &UiSpan) -> Self {
|
||||||
let anchor = self.rel.lerp(span.start.rel, span.end.rel);
|
|
||||||
let offset = self.px + self.rel.lerp(span.start.px, span.end.px);
|
|
||||||
Self {
|
Self {
|
||||||
rel: anchor,
|
rel: self.rel.lerp(span.start.rel, span.end.rel),
|
||||||
px: offset,
|
px: self.px.add(self.rel.lerp(span.start.px, span.end.px)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn within_len(&self, len: UiScalar) -> Self {
|
pub const fn within_len(&self, len: Len) -> Self {
|
||||||
self.within(&UiSpan {
|
self.within(&UiSpan {
|
||||||
start: UiScalar::ZERO,
|
start: Len::ZERO,
|
||||||
end: len,
|
end: len,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_len(&self, len: UiScalar) -> Self {
|
pub fn select_len(&self, len: Len) -> Self {
|
||||||
len.within_len(*self)
|
len.within_len(*self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn flip(&mut self) {
|
pub const fn flip(&mut self) {
|
||||||
self.rel = 1.0 - self.rel;
|
self.rel = Rel::ONE.sub(self.rel);
|
||||||
self.px = -self.px;
|
self.px = self.px.neg();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn to(&self, end: Self) -> UiSpan {
|
pub const fn to(&self, end: Self) -> UiSpan {
|
||||||
UiSpan { start: *self, end }
|
UiSpan { start: *self, end }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn to_px(&self, rel: f32) -> f32 {
|
/// Resolved against a box of `len`, which is the only place a fraction
|
||||||
self.rel * rel + self.px
|
/// becomes a number of pixels.
|
||||||
|
pub const fn to_px(&self, len: Px) -> Px {
|
||||||
|
self.px.add(len.mul(self.rel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct UiSpan {
|
pub struct UiSpan {
|
||||||
pub start: UiScalar,
|
pub start: Len,
|
||||||
pub end: UiScalar,
|
pub end: Len,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiSpan {
|
impl UiSpan {
|
||||||
pub const FULL: Self = Self {
|
pub const FULL: Self = Self {
|
||||||
start: UiScalar::ZERO,
|
start: Len::ZERO,
|
||||||
end: UiScalar::FULL,
|
end: Len::FULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const fn rel(rel: f32) -> Self {
|
pub const fn rel(rel: f32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
start: UiScalar::rel(rel),
|
start: Len::rel(rel),
|
||||||
end: UiScalar::rel(rel),
|
end: Len::rel(rel),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn new(start: UiScalar, end: UiScalar) -> Self {
|
pub const fn new(start: Len, end: Len) -> Self {
|
||||||
Self { start, end }
|
Self { start, end }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,11 +277,16 @@ impl UiSpan {
|
|||||||
std::mem::swap(&mut self.start.px, &mut self.end.px);
|
std::mem::swap(&mut self.start.px, &mut self.end.px);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn shift(&mut self, offset: UiScalar) {
|
pub const fn shift(&mut self, offset: Len) {
|
||||||
self.start += offset;
|
self.start += offset;
|
||||||
self.end += offset;
|
self.end += offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Composing a box through the one it sits in, and the hottest line in
|
||||||
|
/// layout. It used to skip the multiplies where a span was the whole of
|
||||||
|
/// its parent or the parent the whole of its own; both come out of the
|
||||||
|
/// multiply unchanged anyway, and the body those comparisons cost was
|
||||||
|
/// what kept the inliner from taking this at all.
|
||||||
pub const fn within(&self, parent: &Self) -> Self {
|
pub const fn within(&self, parent: &Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
start: self.start.within(parent),
|
start: self.start.within(parent),
|
||||||
@@ -270,9 +294,18 @@ impl UiSpan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn len(&self) -> UiScalar {
|
pub const fn len(&self) -> Len {
|
||||||
self.end - self.start
|
self.end - self.start
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Both ends by the same amount, which is what moving a box without
|
||||||
|
/// changing its length does to every part of it.
|
||||||
|
pub const fn translated(self, by: Len) -> Self {
|
||||||
|
Self {
|
||||||
|
start: self.start + by,
|
||||||
|
end: self.end + by,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -283,6 +316,17 @@ pub struct UiRegion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl UiRegion {
|
impl UiRegion {
|
||||||
|
/// Every part of the box by the same amount on each axis. Done to the
|
||||||
|
/// whole region rather than an end at a time, because that is what it is
|
||||||
|
/// -- and because four adds in a row are four adds, where four asked for
|
||||||
|
/// separately are four sequences.
|
||||||
|
pub const fn translated(self, x: Len, y: Len) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.x.translated(x),
|
||||||
|
y: self.y.translated(y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const FULL: Self = Self {
|
pub const FULL: Self = Self {
|
||||||
x: UiSpan::FULL,
|
x: UiSpan::FULL,
|
||||||
y: UiSpan::FULL,
|
y: UiSpan::FULL,
|
||||||
@@ -336,10 +380,10 @@ impl UiRegion {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_px(&self, size: Vec2) -> PixelRegion {
|
pub fn to_px(&self, size: PxVec2) -> PixelRegion {
|
||||||
PixelRegion {
|
PixelRegion {
|
||||||
top_left: self.top_left().get_rel() * size + self.top_left().get_px(),
|
top_left: self.top_left().to_px(size),
|
||||||
bot_right: self.bot_right().get_rel() * size + self.bot_right().get_px(),
|
bot_right: self.bot_right().to_px(size),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,21 +438,21 @@ impl Display for UiRegion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct PixelRegion {
|
pub struct PixelRegion {
|
||||||
pub top_left: Vec2,
|
pub top_left: PxVec2,
|
||||||
pub bot_right: Vec2,
|
pub bot_right: PxVec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PixelRegion {
|
impl PixelRegion {
|
||||||
pub fn contains(&self, pos: Vec2) -> bool {
|
pub fn contains(&self, pos: PxVec2) -> bool {
|
||||||
pos.x >= self.top_left.x
|
pos.x >= self.top_left.x
|
||||||
&& pos.x <= self.bot_right.x
|
&& pos.x <= self.bot_right.x
|
||||||
&& pos.y >= self.top_left.y
|
&& pos.y >= self.top_left.y
|
||||||
&& pos.y <= self.bot_right.y
|
&& pos.y <= self.bot_right.y
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn size(&self) -> Vec2 {
|
pub fn size(&self) -> PxVec2 {
|
||||||
self.bot_right - self.top_left
|
self.bot_right - self.top_left
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,20 +462,3 @@ impl Display for PixelRegion {
|
|||||||
write!(f, "{} -> {}", self.top_left, self.bot_right)
|
write!(f, "{} -> {}", self.top_left, self.bot_right)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Vec2View<'a> {
|
|
||||||
pub x: &'a mut f32,
|
|
||||||
pub y: &'a mut f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Vec2View<'_> {
|
|
||||||
pub fn set(&mut self, other: Vec2) {
|
|
||||||
*self.x = other.x;
|
|
||||||
*self.y = other.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(&mut self, other: Vec2) {
|
|
||||||
*self.x += other.x;
|
|
||||||
*self.y += other.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -120,6 +120,10 @@ impl<T: Default> Layers<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DrawLayers {
|
impl DrawLayers {
|
||||||
|
/// Inlined on purpose: it is one call per glyph, the innermost thing a
|
||||||
|
/// frame does, and whether the inliner takes it turns out to depend on
|
||||||
|
/// unrelated code elsewhere in the crate -- 12% of a resize frame.
|
||||||
|
#[inline]
|
||||||
pub fn write<P: Primitive>(
|
pub fn write<P: Primitive>(
|
||||||
&mut self,
|
&mut self,
|
||||||
layer: LayerId,
|
layer: LayerId,
|
||||||
|
|||||||
+14
-16
@@ -1,7 +1,8 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
use crate::layout_diagnostics::{self as diag, Counter, TimerKind};
|
use crate::layout_diagnostics::{self as diag, Counter, TimerKind};
|
||||||
use crate::{
|
use crate::{
|
||||||
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
|
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, Px, PxVec2, RegionAlign, UiColor,
|
||||||
|
util::Vec2,
|
||||||
};
|
};
|
||||||
use parley::{
|
use parley::{
|
||||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
||||||
@@ -106,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.
|
/// Keeps text and its corresponding layout from getting out of sync.
|
||||||
pub struct TextBuffer {
|
pub struct TextBuffer {
|
||||||
text: String,
|
text: String,
|
||||||
@@ -199,15 +193,19 @@ impl TextBuffer {
|
|||||||
// A greedy break at one width is the same break at every width down
|
// 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
|
// 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
|
// 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
|
// hand already answers, and re-breaking would only be work.
|
||||||
// disagree with itself -- which is what happens when a parent offers
|
//
|
||||||
// a child the length that child just reported, and the two land
|
// At the longest line exactly, with no margin below it. A narrower
|
||||||
// either side of a float.
|
// width really does break differently, so answering one from the
|
||||||
|
// break in hand is how a warm tree keeps lines a cold tree would
|
||||||
|
// never produce. The margin was here because a text reports the
|
||||||
|
// width it used and a parent hands that back; the report is the step
|
||||||
|
// at or above its longest line now, so what comes back fits.
|
||||||
if let Some(key) = &self.layout_key
|
if let Some(key) = &self.layout_key
|
||||||
&& key.attrs == *attrs
|
&& key.attrs == *attrs
|
||||||
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
|
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
|
||||||
&& want <= broke_at
|
&& want <= broke_at
|
||||||
&& want + BREAK_EPSILON_PX >= self.layout.width()
|
&& want >= self.layout.width()
|
||||||
{
|
{
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::TextShapeHits);
|
diag::bump(Counter::TextShapeHits);
|
||||||
@@ -303,9 +301,9 @@ impl TextData {
|
|||||||
};
|
};
|
||||||
placed.push(PlacedGlyph {
|
placed.push(PlacedGlyph {
|
||||||
entry,
|
entry,
|
||||||
offset: Vec2::new(
|
offset: PxVec2::new(
|
||||||
glyph.x.floor() + entry.left as f32,
|
Px::from_int(glyph.x.floor() as i32 + entry.left),
|
||||||
glyph.y.floor() - entry.top as f32,
|
Px::from_int(glyph.y.floor() as i32 - entry.top),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
PatchRect,
|
PatchRect, PxVec2,
|
||||||
util::{HashMap, Vec2},
|
util::{HashMap, Vec2},
|
||||||
};
|
};
|
||||||
use image::RgbaImage;
|
use image::RgbaImage;
|
||||||
@@ -241,5 +241,7 @@ fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
|
|||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct PlacedGlyph {
|
pub struct PlacedGlyph {
|
||||||
pub entry: GlyphEntry,
|
pub entry: GlyphEntry,
|
||||||
pub offset: Vec2,
|
/// Whole pixels from the origin of the text to this glyph's top-left,
|
||||||
|
/// on the grid once here rather than on every frame that draws it.
|
||||||
|
pub offset: PxVec2,
|
||||||
}
|
}
|
||||||
@@ -16,11 +16,13 @@ pub struct PrimitiveInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PrimitiveInstance {
|
impl PrimitiveInstance {
|
||||||
|
// The region's four scalars, each a `Rel` beside a `Px`: whole counts
|
||||||
|
// that the shader decodes, rather than the numbers themselves.
|
||||||
const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![
|
const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![
|
||||||
0 => Float32x2,
|
0 => Sint32x2,
|
||||||
1 => Float32x2,
|
1 => Sint32x2,
|
||||||
2 => Float32x2,
|
2 => Sint32x2,
|
||||||
3 => Float32x2,
|
3 => Sint32x2,
|
||||||
4 => Uint32,
|
4 => Uint32,
|
||||||
5 => Uint32,
|
5 => Uint32,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -23,7 +23,14 @@ pub use primitive::*;
|
|||||||
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
||||||
|
|
||||||
fn module_source(wgsl: &str) -> String {
|
fn module_source(wgsl: &str) -> String {
|
||||||
format!("{PRELUDE}\n{wgsl}")
|
// The steps come from the same constants the CPU counts in, rather than
|
||||||
|
// a second copy of them written into the shader: a grid the two disagree
|
||||||
|
// about puts every coordinate somewhere else.
|
||||||
|
format!(
|
||||||
|
"const PX_STEP: f32 = 1.0 / {}.0;\nconst REL_STEP: f32 = 1.0 / {}.0;\n{PRELUDE}\n{wgsl}",
|
||||||
|
1u32 << crate::PX_SHIFT,
|
||||||
|
1u32 << crate::REL_SHIFT,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UiRenderNode {
|
pub struct UiRenderNode {
|
||||||
|
|||||||
@@ -15,17 +15,56 @@ struct WindowUniform {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct Mask {
|
struct Mask {
|
||||||
x: UiSpan,
|
x: RawSpan,
|
||||||
y: UiSpan,
|
y: RawSpan,
|
||||||
move_idx: u32,
|
move_idx: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MoveOffset {
|
struct MoveOffset {
|
||||||
x: UiSpan,
|
x: RawSpan,
|
||||||
y: UiSpan,
|
y: RawSpan,
|
||||||
parent: u32,
|
parent: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `PX_STEP` and `REL_STEP` are prepended from `iris_core`'s own constants:
|
||||||
|
// what it stores is a whole count of each, both powers of two, so decoding
|
||||||
|
// is exact and the number here is the number the CPU decided.
|
||||||
|
|
||||||
|
// Every coordinate the CPU decided is a whole count of `PX_STEP`, so one that
|
||||||
|
// composes to within half a step of a pixel boundary is on that boundary and
|
||||||
|
// belongs to the pixel above it. Flooring the product instead drops a pixel
|
||||||
|
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of
|
||||||
|
// `REL_STEP` as 383.99998, and five tabs each lose their last column.
|
||||||
|
//
|
||||||
|
// Taken over the whole coordinate, fraction and pixels summed, since a floor
|
||||||
|
// does not distribute over a sum: floored apart, a half of one and a half of
|
||||||
|
// the other lose the pixel the two together make.
|
||||||
|
fn snap_floor(v: vec2<f32>) -> vec2<f32> {
|
||||||
|
return floor(v + PX_STEP * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RawScalar {
|
||||||
|
rel: i32,
|
||||||
|
px: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RawSpan {
|
||||||
|
start: RawScalar,
|
||||||
|
end: RawScalar,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scalar_of(raw: RawScalar) -> Len {
|
||||||
|
return Len(f32(raw.rel) * REL_STEP, f32(raw.px) * PX_STEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span_of(raw: RawSpan) -> UiSpan {
|
||||||
|
return UiSpan(scalar_of(raw.start), scalar_of(raw.end));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scalar_of_pair(raw: vec2<i32>) -> Len {
|
||||||
|
return Len(f32(raw.x) * REL_STEP, f32(raw.y) * PX_STEP);
|
||||||
|
}
|
||||||
|
|
||||||
struct Region {
|
struct Region {
|
||||||
x: UiSpan,
|
x: UiSpan,
|
||||||
y: UiSpan,
|
y: UiSpan,
|
||||||
@@ -37,11 +76,12 @@ const MOVE_NONE: u32 = 4294967295u;
|
|||||||
// resolve a deep one the same way.
|
// resolve a deep one the same way.
|
||||||
const CHAIN_LIMIT: u32 = 64u;
|
const CHAIN_LIMIT: u32 = 64u;
|
||||||
|
|
||||||
// Written the way `UiScalar::within` writes it rather than as `mix`, so the
|
// The same expression `Len::within` uses, in floats rather than on the
|
||||||
// CPU and the shader compose a position with the same arithmetic and answer
|
// CPU's grid: a move is resolved here so that scrolling a subtree writes one
|
||||||
// the same thing about where a widget is.
|
// entry instead of walking it. What has to hold is that this agrees with
|
||||||
fn scalar_within(s: UiScalar, p: UiSpan) -> UiScalar {
|
// itself frame to frame, not that it matches the CPU to the last bit.
|
||||||
return UiScalar(
|
fn scalar_within(s: Len, p: UiSpan) -> Len {
|
||||||
|
return Len(
|
||||||
p.start.rel + (p.end.rel - p.start.rel) * s.rel,
|
p.start.rel + (p.end.rel - p.start.rel) * s.rel,
|
||||||
s.px + (p.start.px + (p.end.px - p.start.px) * s.rel),
|
s.px + (p.start.px + (p.end.px - p.start.px) * s.rel),
|
||||||
);
|
);
|
||||||
@@ -59,27 +99,27 @@ fn resolve_move(idx: u32, local: Region) -> Region {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let entry = move_offsets[at];
|
let entry = move_offsets[at];
|
||||||
r = Region(span_within(r.x, entry.x), span_within(r.y, entry.y));
|
r = Region(span_within(r.x, span_of(entry.x)), span_within(r.y, span_of(entry.y)));
|
||||||
at = entry.parent;
|
at = entry.parent;
|
||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UiSpan {
|
struct UiSpan {
|
||||||
start: UiScalar,
|
start: Len,
|
||||||
end: UiScalar,
|
end: Len,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UiScalar {
|
struct Len {
|
||||||
rel: f32,
|
rel: f32,
|
||||||
px: f32,
|
px: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InstanceInput {
|
struct InstanceInput {
|
||||||
@location(0) x_start: vec2<f32>,
|
@location(0) x_start: vec2<i32>,
|
||||||
@location(1) x_end: vec2<f32>,
|
@location(1) x_end: vec2<i32>,
|
||||||
@location(2) y_start: vec2<f32>,
|
@location(2) y_start: vec2<i32>,
|
||||||
@location(3) y_end: vec2<f32>,
|
@location(3) y_end: vec2<i32>,
|
||||||
@location(4) mask_idx: u32,
|
@location(4) mask_idx: u32,
|
||||||
@location(5) move_idx: u32,
|
@location(5) move_idx: u32,
|
||||||
}
|
}
|
||||||
@@ -102,8 +142,8 @@ fn vs_main(
|
|||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
|
|
||||||
let local = Region(
|
let local = Region(
|
||||||
UiSpan(UiScalar(in.x_start.x, in.x_start.y), UiScalar(in.x_end.x, in.x_end.y)),
|
UiSpan(scalar_of_pair(in.x_start), scalar_of_pair(in.x_end)),
|
||||||
UiSpan(UiScalar(in.y_start.x, in.y_start.y), UiScalar(in.y_end.x, in.y_end.y)),
|
UiSpan(scalar_of_pair(in.y_start), scalar_of_pair(in.y_end)),
|
||||||
);
|
);
|
||||||
let r = resolve_move(in.move_idx, local);
|
let r = resolve_move(in.move_idx, local);
|
||||||
let top_left_rel = vec2(r.x.start.rel, r.y.start.rel);
|
let top_left_rel = vec2(r.x.start.rel, r.y.start.rel);
|
||||||
@@ -111,8 +151,8 @@ fn vs_main(
|
|||||||
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
|
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 bot_right_px = vec2(r.x.end.px, r.y.end.px);
|
||||||
|
|
||||||
let top_left = floor(top_left_rel * window.dim) + floor(top_left_px);
|
let top_left = snap_floor(top_left_rel * window.dim + top_left_px);
|
||||||
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_px);
|
let bot_right = snap_floor(bot_right_rel * window.dim + bot_right_px);
|
||||||
let size = bot_right - top_left;
|
let size = bot_right - top_left;
|
||||||
|
|
||||||
let uv = vec2<f32>(
|
let uv = vec2<f32>(
|
||||||
@@ -137,14 +177,14 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
|||||||
let mask = masks[in.mask_idx];
|
let mask = masks[in.mask_idx];
|
||||||
// Its own chain, not the drawn primitive's, so a stationary viewport
|
// Its own chain, not the drawn primitive's, so a stationary viewport
|
||||||
// clips content that moves inside it.
|
// clips content that moves inside it.
|
||||||
let m = resolve_move(mask.move_idx, Region(mask.x, mask.y));
|
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 = vec2(m.x.start.rel, m.y.start.rel);
|
||||||
let tl_px = vec2(m.x.start.px, m.y.start.px);
|
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 = vec2(m.x.end.rel, m.y.end.rel);
|
||||||
let br_px = vec2(m.x.end.px, m.y.end.px);
|
let br_px = vec2(m.x.end.px, m.y.end.px);
|
||||||
|
|
||||||
let top_left = floor(tl * window.dim) + floor(tl_px);
|
let top_left = snap_floor(tl * window.dim + tl_px);
|
||||||
let bot_right = floor(br * window.dim) + floor(br_px);
|
let bot_right = snap_floor(br * window.dim + br_px);
|
||||||
let pos = in.clip_position.xy;
|
let pos = in.clip_position.xy;
|
||||||
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
||||||
return color * 0.0;
|
return color * 0.0;
|
||||||
|
|||||||
+54
-22
@@ -1,22 +1,38 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
LayerId, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId, util::Vec2,
|
Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle,
|
||||||
|
UiRegion, UiVec2, WidgetId,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// important non rendering data for retained drawing
|
/// 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)]
|
#[derive(Debug)]
|
||||||
pub struct ActiveData {
|
pub struct ActiveData {
|
||||||
pub id: WidgetId,
|
pub id: WidgetId,
|
||||||
|
/// The box its drawing is in, in `parent_move`'s coordinates.
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
/// What the widget said it used of `region`, the last time it drew.
|
/// The box its parent gave it, in the same coordinates: what it was
|
||||||
|
/// asked about, before its own answer placed its drawing inside it.
|
||||||
|
/// `region` is that placement, and a local redraw asks here.
|
||||||
|
pub given: UiRegion,
|
||||||
|
/// The same box as lengths of its parent's box, which is the one route
|
||||||
|
/// to a box in pixels: a draw threads these down a level at a time, and
|
||||||
|
/// [`crate::UiRenderState::redraw`] takes the same steps back up.
|
||||||
|
pub given_len: UiVec2,
|
||||||
|
/// The lengths of the box its parent first asked about it in, as
|
||||||
|
/// lengths of the box the parent was itself offered. Any later box it
|
||||||
|
/// was given was decided knowing its answer, so this is the question
|
||||||
|
/// asked again -- and a chain of fractions has no frame in it, which is
|
||||||
|
/// why a region node between two widgets cannot break it.
|
||||||
|
pub offer_len: UiVec2,
|
||||||
|
/// 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.
|
||||||
pub size: Size,
|
pub size: Size,
|
||||||
/// The pixel size of the box it drew against. `region` alone cannot say:
|
/// The pixel lengths of `region`, per axis, that its drawing and `size`
|
||||||
/// it is a fraction of a slot's box, and the same fraction of a box that
|
/// hold for.
|
||||||
/// has since changed is a different number of pixels.
|
pub holds: [Holds; 2],
|
||||||
pub px: Vec2,
|
pub drawn: bool,
|
||||||
/// The pixel size of the box its parent first asked about it in, before
|
|
||||||
/// knowing what it came to. `px` may be a box derived from that answer,
|
|
||||||
/// and a size measured there is only the same answer asked again.
|
|
||||||
pub offered_px: Vec2,
|
|
||||||
pub parent: Option<WidgetId>,
|
pub parent: Option<WidgetId>,
|
||||||
/// How far down the tree it was drawn, the root being 1. Carried down a
|
/// How far down the tree it was drawn, the root being 1. Carried down a
|
||||||
/// draw rather than worked out by walking up, so it is right for every
|
/// draw rather than worked out by walking up, so it is right for every
|
||||||
@@ -27,19 +43,35 @@ pub struct ActiveData {
|
|||||||
pub children: Vec<WidgetId>,
|
pub children: Vec<WidgetId>,
|
||||||
/// The children whose size this widget read while drawing.
|
/// The children whose size this widget read while drawing.
|
||||||
pub size_deps: Vec<WidgetId>,
|
pub size_deps: Vec<WidgetId>,
|
||||||
/// Offered pixel axes which flowed into this widget's reported size,
|
/// The movable region its primitives are positioned through: its own when
|
||||||
/// directly or through a child size it read.
|
/// opted in, otherwise the nearest ancestor's.
|
||||||
pub size_box_inputs: [bool; 2],
|
|
||||||
/// Output axes read while producing `size`, distinct from the widget's
|
|
||||||
/// own box when that box has a fixed pixel length.
|
|
||||||
pub size_output_inputs: [bool; 2],
|
|
||||||
/// The output dimensions against which those dependencies were observed.
|
|
||||||
pub output_px: Vec2,
|
|
||||||
/// The slot its primitives are positioned through: its own if its parent
|
|
||||||
/// placed it, otherwise the nearest ancestor that has one.
|
|
||||||
pub move_idx: MoveIdx,
|
pub move_idx: MoveIdx,
|
||||||
/// The slot `region` is given in, which is whatever its parent drew in.
|
/// The declared lengths whoever drew this widget resolved into its box.
|
||||||
|
/// A change to one moves a box this widget cannot fix by drawing again,
|
||||||
|
/// and comparing them is what says so.
|
||||||
|
pub declared: [Option<LayoutLen>; 2],
|
||||||
|
/// The axes along which its parent chose its box from its own answer,
|
||||||
|
/// so a local redraw asks the question its parent asked.
|
||||||
|
pub decided: [bool; 2],
|
||||||
|
/// Its alignment when it was last drawn, which a change to the property
|
||||||
|
/// is found against.
|
||||||
|
pub own_align: RegionAlign,
|
||||||
|
/// The movable region whose coordinates `region` uses.
|
||||||
pub parent_move: MoveIdx,
|
pub parent_move: MoveIdx,
|
||||||
|
/// The mask its drawing is clipped to: one it set itself, or the one it
|
||||||
|
/// inherited from whoever drew it.
|
||||||
pub mask: MaskIdx,
|
pub mask: MaskIdx,
|
||||||
|
/// That inherited one. The two differ exactly where the widget set a
|
||||||
|
/// mask of its own, which is the one it owns and the one a move rewrites
|
||||||
|
/// -- and the one a redraw of it must not be handed back, since setting
|
||||||
|
/// a mask asserts there is none.
|
||||||
|
pub parent_mask: MaskIdx,
|
||||||
pub layer: LayerId,
|
pub layer: LayerId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ActiveData {
|
||||||
|
/// Whether 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
use crate::{Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow};
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
|
/// The lengths of a box, in pixels, that one drawing of a widget holds for:
|
||||||
|
/// give the widget any box in this range and it draws the same thing and
|
||||||
|
/// reports the same size. A widget that never reads its box in pixels holds
|
||||||
|
/// for every length; one that does holds for the one it read unless it says
|
||||||
|
/// otherwise, and a parent holds for whatever keeps every child it asked
|
||||||
|
/// about or drew inside its own range.
|
||||||
|
///
|
||||||
|
/// The ends are lengths on the grid rather than floats with a tolerance
|
||||||
|
/// around them: a box offered back at the length a widget reported comes back
|
||||||
|
/// as the same number, so a range means what it says. The one place a range
|
||||||
|
/// is wider than the length it came from is [`Self::through`], and what it is
|
||||||
|
/// wider by is the floor that inverting a fraction undoes.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Holds {
|
||||||
|
pub lo: Px,
|
||||||
|
pub hi: Px,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Holds {
|
||||||
|
pub const ANY: Self = Self {
|
||||||
|
lo: Px::MIN,
|
||||||
|
hi: Px::MAX,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const fn at(len: Px) -> Self {
|
||||||
|
Self { lo: len, hi: len }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn contains(&self, len: Px) -> bool {
|
||||||
|
len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn and(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
lo: self.lo.max(other.lo),
|
||||||
|
hi: self.hi.min(other.hi),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a box has to be for a part of it, `len` of the box long, to stay
|
||||||
|
/// in this range: the exact preimage of `px + floor(rel * box)`, which is
|
||||||
|
/// the one way a box in pixels is reached. A part with no relative extent
|
||||||
|
/// is a fixed length -- it was drawn at that length and any box keeps it
|
||||||
|
/// there.
|
||||||
|
///
|
||||||
|
/// The answer is an interval even where this range is a single length,
|
||||||
|
/// because the multiply on the way in drops to the step below and many
|
||||||
|
/// boxes therefore give one length. That is a floor rather than an
|
||||||
|
/// allowance: inverting it is two divisions and nothing else, and the
|
||||||
|
/// whole of a box maps back to itself.
|
||||||
|
pub const fn through(self, len: Len) -> Self {
|
||||||
|
let rel = len.rel.raw() as i64;
|
||||||
|
if rel == 0 {
|
||||||
|
return Self::ANY;
|
||||||
|
}
|
||||||
|
let px = len.px.raw() as i64;
|
||||||
|
// `floor(rel * box) >= lo - px` is `rel * box >= (lo - px) << REL`, and
|
||||||
|
// `floor(rel * box) <= hi - px` is `rel * box < (hi - px + 1) << REL`.
|
||||||
|
let lo = (self.lo.raw() as i64 - px) << REL_SHIFT;
|
||||||
|
let hi = (((self.hi.raw() as i64 - px) + 1) << REL_SHIFT) - 1;
|
||||||
|
// Dividing by a negative fraction turns the ends around, so which
|
||||||
|
// bound each comes from is decided before dividing rather than by
|
||||||
|
// taking the min and max of four divisions.
|
||||||
|
match rel > 0 {
|
||||||
|
true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)),
|
||||||
|
false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn raws(lo: i64, hi: i64) -> Self {
|
||||||
|
Self {
|
||||||
|
lo: Px::from_raw(narrow(lo)),
|
||||||
|
hi: Px::from_raw(narrow(hi)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RangeInclusive<Px>> for Holds {
|
||||||
|
fn from(range: RangeInclusive<Px>) -> Self {
|
||||||
|
Self {
|
||||||
|
lo: *range.start(),
|
||||||
|
hi: *range.end(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Rel;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn through_reverses_a_range_for_a_negative_fraction() {
|
||||||
|
// `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
|
||||||
|
let part = Len::from_parts(Rel::from_f32(-0.5), Px::from_int(10));
|
||||||
|
let holds = Holds::from(Px::from_int(20)..=Px::from_int(40)).through(part);
|
||||||
|
assert!(holds.contains(Px::from_int(-60)) && holds.contains(Px::from_int(-20)));
|
||||||
|
assert!(!holds.contains(Px::from_int(-61)) && !holds.contains(Px::from_int(-19)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The case the widening is for: a part that holds only for the length it
|
||||||
|
/// was drawn at has to hold for the box it was drawn in, and a third of a
|
||||||
|
/// box is not a whole number of steps.
|
||||||
|
#[test]
|
||||||
|
fn a_part_maps_back_onto_the_box_it_was_measured_in() {
|
||||||
|
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
|
||||||
|
for box_len in (440..460).map(Px::from_int) {
|
||||||
|
let holds = Holds::at(part.to_px(box_len)).through(part);
|
||||||
|
assert!(holds.contains(box_len), "{box_len:?} left out by {holds:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget handed the whole of its parent's box, with or without pixels
|
||||||
|
/// taken off it, has no fraction to invert: multiplying by one is exact
|
||||||
|
/// and taking the pixels off again is too, so the box maps back to
|
||||||
|
/// itself. Allowing for anything here compounded a step a level down a
|
||||||
|
/// chain of widgets each taking the whole of its parent.
|
||||||
|
#[test]
|
||||||
|
fn the_whole_of_a_box_maps_back_to_itself() {
|
||||||
|
let at = Px::from_int(956);
|
||||||
|
assert_eq!(Holds::at(at).through(Len::FULL), Holds::at(at));
|
||||||
|
let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8));
|
||||||
|
assert_eq!(
|
||||||
|
Holds::at(at).through(less_eight),
|
||||||
|
Holds::at(at + Px::from_int(8))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The range is the exact preimage at both ends, so a box one step
|
||||||
|
/// outside it really does give a length outside this range. What a wider
|
||||||
|
/// range costs is a drawing reused where it does not hold.
|
||||||
|
#[test]
|
||||||
|
fn a_box_one_step_outside_the_range_is_outside_it() {
|
||||||
|
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
|
||||||
|
let at = Px::from_int(300);
|
||||||
|
let holds = Holds::at(at).through(part);
|
||||||
|
for inside in [holds.lo, holds.hi] {
|
||||||
|
assert_eq!(part.to_px(inside), at, "{inside:?} left out of {holds:?}");
|
||||||
|
}
|
||||||
|
for outside in [holds.lo.next_down(), holds.hi.next_up()] {
|
||||||
|
assert_ne!(part.to_px(outside), at, "{outside:?} admitted by {holds:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A truncating multiply only ever drops, so the step it needs allowing
|
||||||
|
/// for on the way in belongs at the top of the range and not the bottom.
|
||||||
|
#[test]
|
||||||
|
fn a_fraction_widens_further_up_than_down() {
|
||||||
|
let half = Len::from_parts(Rel::from_f32(0.5), Px::ZERO);
|
||||||
|
let holds = Holds::at(Px::from_int(100)).through(half);
|
||||||
|
let box_len = Px::from_int(200);
|
||||||
|
assert!(holds.hi - box_len > box_len - holds.lo, "{holds:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_boundary_the_next_step_along_does_not_admit_it() {
|
||||||
|
let boundary = Px::from_int(10);
|
||||||
|
let above = Holds::from(boundary.next_up()..=Px::MAX);
|
||||||
|
assert!(!above.contains(boundary));
|
||||||
|
assert!(above.contains(boundary.next_up()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-6
@@ -10,10 +10,12 @@ use crate::{
|
|||||||
pub const CHAIN_LIMIT: u32 = 64;
|
pub const CHAIN_LIMIT: u32 = 64;
|
||||||
|
|
||||||
mod active;
|
mod active;
|
||||||
|
mod holds;
|
||||||
mod painter;
|
mod painter;
|
||||||
mod render_state;
|
mod render_state;
|
||||||
|
|
||||||
pub use active::*;
|
pub use active::*;
|
||||||
|
pub use holds::*;
|
||||||
pub use painter::{Painter, PrimitiveLike};
|
pub use painter::{Painter, PrimitiveLike};
|
||||||
pub use render_state::*;
|
pub use render_state::*;
|
||||||
|
|
||||||
@@ -66,17 +68,24 @@ impl Moves {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Composes a region held in `idx`'s coordinates down the chain, which is
|
/// The same walk the vertex shader does, in the same `Len` the shader is
|
||||||
/// the same walk the vertex shader does.
|
/// 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 {
|
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
|
||||||
let mut region = local;
|
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;
|
let mut at = idx;
|
||||||
for _ in 0..CHAIN_LIMIT {
|
for _ in 0..CHAIN_LIMIT {
|
||||||
if at == MoveIdx::NONE {
|
if at == MoveIdx::NONE {
|
||||||
return region;
|
return;
|
||||||
}
|
}
|
||||||
let entry = self.arena[at.idx()];
|
let entry = &self.arena[at.idx()];
|
||||||
region = region.within(&entry.region);
|
step(&entry.region);
|
||||||
at = entry.parent;
|
at = entry.parent;
|
||||||
}
|
}
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
@@ -84,7 +93,6 @@ impl Moves {
|
|||||||
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
|
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
|
||||||
and the shader stops at the same depth"
|
and the shader stops at the same depth"
|
||||||
);
|
);
|
||||||
region
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How many slots a region in `idx` is composed through, which is what
|
/// How many slots a region in `idx` is composed through, which is what
|
||||||
|
|||||||
+364
-145
@@ -1,14 +1,16 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
use crate::layout_diagnostics::{self as diag, Counter};
|
use crate::layout_diagnostics::{self as diag, Counter};
|
||||||
use crate::{
|
use crate::{
|
||||||
Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
|
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Size, StrongWidget,
|
||||||
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
|
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight,
|
||||||
|
WidgetId, Widgets,
|
||||||
render::{
|
render::{
|
||||||
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
|
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
|
||||||
PrimitiveKind, TexturePrimitive,
|
PrimitiveKind, TexturePrimitive,
|
||||||
},
|
},
|
||||||
util::Vec2,
|
ui::render_state::DrawInfo,
|
||||||
};
|
};
|
||||||
|
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
||||||
|
|
||||||
/// makes your surfaces look pretty
|
/// makes your surfaces look pretty
|
||||||
pub struct Painter<'a> {
|
pub struct Painter<'a> {
|
||||||
@@ -17,22 +19,39 @@ pub struct Painter<'a> {
|
|||||||
|
|
||||||
/// This widget's box, in the coordinates of `move_idx`.
|
/// This widget's box, in the coordinates of `move_idx`.
|
||||||
pub(super) region: UiRegion,
|
pub(super) region: UiRegion,
|
||||||
|
/// That box in pixels, which its children's are a length of: threaded
|
||||||
|
/// down from the box this widget was given rather than composed back up
|
||||||
|
/// the chain, so every length in layout is one multiply from its
|
||||||
|
/// parent's and [`Holds::through`] inverts exactly that.
|
||||||
|
pub(super) px: PxVec2,
|
||||||
pub(super) mask: MaskIdx,
|
pub(super) mask: MaskIdx,
|
||||||
pub(super) textures: Vec<TextureHandle>,
|
pub(super) textures: Vec<TextureHandle>,
|
||||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||||
pub(super) children: Vec<WidgetId>,
|
pub(super) children: Vec<WidgetId>,
|
||||||
/// The children asked about so far, so the first box each was asked
|
/// The children asked about so far, so the first box each was asked in
|
||||||
/// about is the one recorded as its offer.
|
/// is the one recorded as its offer.
|
||||||
pub(super) offered: Vec<WidgetId>,
|
pub(super) offered: Vec<WidgetId>,
|
||||||
|
/// The lengths of the box this widget was first asked about in, in
|
||||||
|
/// pixels. Its children's offers are a fraction of it.
|
||||||
|
pub(super) offered_px: PxVec2,
|
||||||
|
/// Whether this draw is in a box of those lengths, which makes the
|
||||||
|
/// questions it asks the ones a cold layout asks and their answers the
|
||||||
|
/// ones to keep.
|
||||||
|
pub(super) at_offer: bool,
|
||||||
/// The children whose size this widget read while drawing.
|
/// The children whose size this widget read while drawing.
|
||||||
pub(super) size_deps: Vec<WidgetId>,
|
pub(super) size_deps: Vec<WidgetId>,
|
||||||
/// Offered pixel axes which can affect the size this draw reports.
|
/// What this draw itself read of its box in pixels, per axis: every
|
||||||
pub(super) size_box_inputs: [bool; 2],
|
/// length until it reads one, then that one, unless it says otherwise.
|
||||||
pub(super) size_output_inputs: [bool; 2],
|
pub(super) own: [Holds; 2],
|
||||||
/// The slot this widget's primitives are positioned through: its own if
|
/// What the children it asked about and drew keep it to.
|
||||||
/// its parent placed it, otherwise the nearest ancestor that has one.
|
pub(super) under: [Holds; 2],
|
||||||
|
/// The movable region this widget's primitives are positioned through:
|
||||||
|
/// its own when opted in, otherwise the nearest ancestor's.
|
||||||
pub(super) move_idx: MoveIdx,
|
pub(super) move_idx: MoveIdx,
|
||||||
pub layer: usize,
|
pub layer: usize,
|
||||||
|
/// The layer this widget was entered on, which its children's layers are
|
||||||
|
/// counted from however far `layer` has walked.
|
||||||
|
pub(super) own_layer: usize,
|
||||||
pub(super) depth: usize,
|
pub(super) depth: usize,
|
||||||
pub(super) id: WidgetId,
|
pub(super) id: WidgetId,
|
||||||
}
|
}
|
||||||
@@ -90,83 +109,164 @@ impl<'a> Painter<'a> {
|
|||||||
|
|
||||||
/// Draws a widget within this widget's region.
|
/// Draws a widget within this widget's region.
|
||||||
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
|
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
|
||||||
self.widget_at(id, self.region, false)
|
self.widget_within(id, UiRegion::FULL)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a widget somewhere within this one.
|
/// What a widget's rules declare its lengths to be, which whoever draws
|
||||||
|
/// it resolves into its box. Reading them depends on nothing -- the box
|
||||||
|
/// that comes of them is kept on the child, and `redraw` compares it
|
||||||
|
/// there.
|
||||||
|
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] {
|
||||||
|
declared_lens(self.rsc.widgets(), id.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes back a child that was drawn only to find out how long it is.
|
||||||
|
/// Its drawing is dropped and it is not one of this widget's children
|
||||||
|
/// this frame; what it answered is still something this widget asked.
|
||||||
|
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
|
||||||
|
self.children.retain(|child| *child != id.id());
|
||||||
|
self.state.undraw_rec(id.id(), self.rsc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a widget somewhere within this one. `region` is in this widget's
|
||||||
|
/// own coordinates, and the child's declared lengths are still to be
|
||||||
|
/// taken from it. Where the child's drawing sits inside what it is given
|
||||||
|
/// is the child's alignment, applied where the child is drawn, so a
|
||||||
|
/// container positions a child either by handing it a box of exactly its
|
||||||
|
/// length or by leaving it room and letting its alignment decide.
|
||||||
pub fn widget_within<'s, W: ?Sized>(
|
pub fn widget_within<'s, W: ?Sized>(
|
||||||
&'s mut self,
|
&'s mut self,
|
||||||
id: &'s StrongWidget<W>,
|
id: &'s StrongWidget<W>,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
) -> DrawResult<'s, 'a, W> {
|
) -> DrawResult<'s, 'a, W> {
|
||||||
let region = region.within(&self.region);
|
self.widget_at(id, region, region.size(), [false; 2])
|
||||||
self.widget_at(id, region, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a child this widget decides the box of, and may decide again
|
/// Draws a widget in `region`, saying what the answer means.
|
||||||
/// once it knows what the child came to. The child gets a slot of its
|
///
|
||||||
/// own, so placing it a second time writes one entry however much it
|
/// `reports_of` is what a fraction the child reports is a fraction of, as
|
||||||
/// drew -- moved or resized alike, since everything under the slot is
|
/// lengths of this widget's own box. It is the box the child was given
|
||||||
/// held as a fraction of its box. A child drawn any other way has no slot
|
/// wherever that box is the child's whole area -- a pad's inset, a stack
|
||||||
/// and can only be given a different box by drawing again.
|
/// child, a scroll's content -- and a span passes its own extent along
|
||||||
pub fn place<'s, W: ?Sized>(
|
/// the row instead: it offers each child the room left from its cursor,
|
||||||
|
/// because a text has to wrap at the width actually there, while
|
||||||
|
/// `rel(0.5)` still means half the span wherever the child sits in it.
|
||||||
|
///
|
||||||
|
/// A `decided` axis is one where this box was chosen from the widget's
|
||||||
|
/// own answer. On those the answer is not placed inside the box again: it
|
||||||
|
/// already is the box, and a fraction taken of it a second time would
|
||||||
|
/// shrink it twice. A container uses that where it hands back exactly
|
||||||
|
/// what a child asked for -- a span placing a child at the length it
|
||||||
|
/// reported, a scroll giving its content the content's own length.
|
||||||
|
pub fn widget_at<'s, W: ?Sized>(
|
||||||
&'s mut self,
|
&'s mut self,
|
||||||
id: &'s StrongWidget<W>,
|
id: &'s StrongWidget<W>,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
|
reports_of: UiVec2,
|
||||||
|
decided: [bool; 2],
|
||||||
) -> DrawResult<'s, 'a, W> {
|
) -> DrawResult<'s, 'a, W> {
|
||||||
|
let region_node = self.rsc.widgets().is_region_node(id.id());
|
||||||
|
let declared = self.declared_lens(id);
|
||||||
|
let align = self.rsc.widgets().alignment(id.id());
|
||||||
|
// A rule this box was already chosen from is not resolved into it a
|
||||||
|
// second time. The box is that rule's length already, so resolving
|
||||||
|
// it again takes the fraction twice -- a widget declaring half of a
|
||||||
|
// stack, in the stack its own answer made half a row, is a quarter
|
||||||
|
// of the row. Pixels survive it, being the same length wherever they
|
||||||
|
// are taken from, which is why only a share ever shrank.
|
||||||
|
let resolve = AXES.map(|axis| match decided[axis as usize] {
|
||||||
|
true => None,
|
||||||
|
false => declared[axis as usize],
|
||||||
|
});
|
||||||
|
// Composing `FULL` through a box is not quite the identity in f32,
|
||||||
|
// so a child with nothing declared keeps the box it would have had.
|
||||||
|
let local = match resolve.iter().any(Option::is_some) {
|
||||||
|
true => declared_box(region, resolve, align),
|
||||||
|
false => region,
|
||||||
|
};
|
||||||
|
let within = match local == UiRegion::FULL {
|
||||||
|
true => self.region,
|
||||||
|
false => local.within(&self.region),
|
||||||
|
};
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::PlaceCalls);
|
if region_node {
|
||||||
let region = region.within(&self.region);
|
diag::bump(Counter::RegionNodeDraws);
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
diag::region_node(id.id(), self.id, within);
|
||||||
diag::placed(id.id(), self.id, region);
|
|
||||||
self.widget_at(id, region, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn widget_at<'s, W: ?Sized>(
|
|
||||||
&'s mut self,
|
|
||||||
id: &'s StrongWidget<W>,
|
|
||||||
region: UiRegion,
|
|
||||||
slotted: bool,
|
|
||||||
) -> DrawResult<'s, 'a, W> {
|
|
||||||
// A child listed twice would be moved twice.
|
// A child listed twice would be moved twice.
|
||||||
if !self.children.contains(&id.id()) {
|
if !self.children.contains(&id.id()) {
|
||||||
self.children.push(id.id());
|
self.children.push(id.id());
|
||||||
}
|
}
|
||||||
let size = self.state.draw_inner(
|
let first_ask = self.offer(id.id());
|
||||||
self.layer,
|
let given_len = local.size();
|
||||||
|
let offer_len = match first_ask {
|
||||||
|
true => given_len,
|
||||||
|
false => self
|
||||||
|
.state
|
||||||
|
.active
|
||||||
|
.get(&id.id())
|
||||||
|
.map_or(given_len, |a| a.offer_len),
|
||||||
|
};
|
||||||
|
let px = given_len.to_px(self.px);
|
||||||
|
let offered_px = offer_len.to_px(self.offered_px);
|
||||||
|
// Whether this ask is the child's offer question, which is a question
|
||||||
|
// about lengths: the same lengths somewhere else is the same question.
|
||||||
|
let answers_offer = self.at_offer && px == offered_px;
|
||||||
|
// The answer and what it holds for, both about the box asked in. The
|
||||||
|
// child's record may say something else once its drawing has been
|
||||||
|
// placed: a drawing made again in its placed box holds for that box.
|
||||||
|
let (size, holds) = self.state.draw_inner(
|
||||||
id.id(),
|
id.id(),
|
||||||
region,
|
within,
|
||||||
Some(self.id),
|
DrawInfo {
|
||||||
self.depth + 1,
|
layer: self.layer,
|
||||||
self.move_idx,
|
parent: Some(self.id),
|
||||||
slotted,
|
depth: self.depth + 1,
|
||||||
self.mask,
|
parent_move: self.move_idx,
|
||||||
|
region_node,
|
||||||
|
mask: self.mask,
|
||||||
|
given_len,
|
||||||
|
offer_len,
|
||||||
|
px,
|
||||||
|
offered_px,
|
||||||
|
decided,
|
||||||
|
},
|
||||||
None,
|
None,
|
||||||
self.rsc,
|
self.rsc,
|
||||||
);
|
);
|
||||||
self.offer(id.id(), region);
|
if answers_offer {
|
||||||
|
self.state.active.get_mut(&id.id()).unwrap().answer = (size, holds);
|
||||||
|
}
|
||||||
|
// Whatever the child's answer holds for keeps this one to the boxes
|
||||||
|
// that give the child a length inside it.
|
||||||
|
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
|
||||||
|
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
|
||||||
|
}
|
||||||
DrawResult {
|
DrawResult {
|
||||||
child: id,
|
child: id,
|
||||||
painter: self,
|
painter: self,
|
||||||
size,
|
size: in_parent_frame(size, reports_of, declared),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a child says its length is without being drawn, if it can say.
|
/// What a child says its length is without being drawn, if it can say.
|
||||||
/// Asking counts as reading its size.
|
/// Asking counts as reading its size.
|
||||||
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
|
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
|
||||||
let hint = self
|
let widgets = self.rsc.widgets();
|
||||||
.rsc
|
// A rule is the answer where there is one: it wins over whatever the
|
||||||
.widgets()
|
// widget would draw, so it has to win over what the widget says too.
|
||||||
|
let hint = widgets.size_rules(id.id()).axis(axis).exact().or_else(|| {
|
||||||
|
widgets
|
||||||
.get_dyn(id.id())
|
.get_dyn(id.id())
|
||||||
.and_then(|widget| widget.size_hint(axis));
|
.and_then(|widget| widget.size_hint(axis))
|
||||||
|
});
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::hint_read(id.id(), self.id, axis, hint);
|
diag::hint_read(id.id(), self.id, axis, hint);
|
||||||
match hint {
|
match hint {
|
||||||
Some(hint) => {
|
Some(hint) => {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::HintHits);
|
diag::bump(Counter::HintHits);
|
||||||
self.depend_on_hint(id);
|
self.depend_on(id);
|
||||||
Some(hint)
|
Some(hint)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -177,87 +277,59 @@ impl<'a> Painter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A retained child length valid under the region it is about to be
|
/// A child's length in the box it is about to be offered, if it can be
|
||||||
/// offered. Unlike a hint, this is contextual: it is kept only when none
|
/// had without drawing it: from its hint, or from a drawing it already
|
||||||
/// of the offered pixel axes which produced it changed.
|
/// has that holds for that box. `reports_of` is what a fraction in the
|
||||||
|
/// answer is a fraction of, as it is for [`Self::widget_at`].
|
||||||
pub fn known_len<W: ?Sized>(
|
pub fn known_len<W: ?Sized>(
|
||||||
&mut self,
|
&mut self,
|
||||||
child: &StrongWidget<W>,
|
child: &StrongWidget<W>,
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
) -> Option<Len> {
|
reports_of: UiVec2,
|
||||||
let region = region.within(&self.region);
|
) -> Option<LayoutLen> {
|
||||||
self.offer(child.id(), region);
|
let declared = self.declared_lens(child);
|
||||||
|
let align = self.rsc.widgets().alignment(child.id());
|
||||||
|
let local = declared_box(region, declared, align);
|
||||||
|
let first_ask = self.offer(child.id());
|
||||||
|
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
|
||||||
|
active.offer_len = local.size();
|
||||||
|
}
|
||||||
if let Some(hint) = self.size_hint(child, axis) {
|
if let Some(hint) = self.size_hint(child, axis) {
|
||||||
return Some(hint);
|
return Some(hint);
|
||||||
}
|
}
|
||||||
self.retained_size(child, region)
|
let px = local.size().to_px(self.px);
|
||||||
.map(|size| size.axis(axis))
|
let (size, holds) =
|
||||||
}
|
|
||||||
|
|
||||||
/// `region` in this widget's own coordinates.
|
|
||||||
fn retained_size<W: ?Sized>(
|
|
||||||
&mut self,
|
|
||||||
child: &StrongWidget<W>,
|
|
||||||
region: UiRegion,
|
|
||||||
) -> Option<Size> {
|
|
||||||
let (size, box_inputs, output_inputs) =
|
|
||||||
self.state
|
self.state
|
||||||
.retained_size(child.id(), region, self.move_idx, self.rsc.widgets())?;
|
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?;
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::RetainedSizeHits);
|
diag::bump(Counter::RetainedSizeHits);
|
||||||
self.depend_on_size_inputs(child, box_inputs, output_inputs);
|
self.depend_on(child);
|
||||||
Some(size)
|
if first_ask {
|
||||||
|
let active = self.state.active.get_mut(&child.id()).unwrap();
|
||||||
|
active.answer = (size, holds);
|
||||||
|
}
|
||||||
|
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
|
||||||
|
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
|
||||||
|
}
|
||||||
|
Some(in_parent_frame(size, reports_of, declared).axis(axis))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Records the box a child was first asked about in this draw. Any later
|
/// Whether this is the first box a child is asked about in during a draw
|
||||||
/// box this draw gives it was decided knowing its answer, so a size the
|
/// that is itself in the box it was asked in -- the question a cold
|
||||||
/// child measures there is not an answer to this widget's question.
|
/// layout asks, whose answer is the one to keep.
|
||||||
fn offer(&mut self, child: WidgetId, region: UiRegion) {
|
fn offer(&mut self, child: WidgetId) -> bool {
|
||||||
if self.offered.contains(&child) {
|
if !self.at_offer || self.offered.contains(&child) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
self.offered.push(child);
|
self.offered.push(child);
|
||||||
let px = self.state.px_of(self.move_idx, region);
|
true
|
||||||
if let Some(active) = self.state.active.get_mut(&child) {
|
|
||||||
active.offered_px = px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Depends on a length the child gave without being drawn. A hint is
|
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
||||||
/// context-free, so this depends on the child but on no pixel axis.
|
|
||||||
fn depend_on_hint<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
|
||||||
self.depend_on_size_inputs(child, [false; 2], [false; 2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Depends on a size the child produced by drawing, which carries
|
|
||||||
/// whatever the child read to produce it.
|
|
||||||
fn depend_on_drawn_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
|
||||||
let (box_inputs, output_inputs) = self
|
|
||||||
.state
|
|
||||||
.active
|
|
||||||
.get(&child.id())
|
|
||||||
.map_or(([false; 2], [false; 2]), |active| {
|
|
||||||
(active.size_box_inputs, active.size_output_inputs)
|
|
||||||
});
|
|
||||||
self.depend_on_size_inputs(child, box_inputs, output_inputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn depend_on_size_inputs<W: ?Sized>(
|
|
||||||
&mut self,
|
|
||||||
child: &StrongWidget<W>,
|
|
||||||
box_inputs: [bool; 2],
|
|
||||||
output_inputs: [bool; 2],
|
|
||||||
) {
|
|
||||||
if !self.size_deps.contains(&child.id()) {
|
if !self.size_deps.contains(&child.id()) {
|
||||||
self.size_deps.push(child.id());
|
self.size_deps.push(child.id());
|
||||||
}
|
}
|
||||||
for (own, child) in self.size_box_inputs.iter_mut().zip(box_inputs) {
|
|
||||||
*own |= child;
|
|
||||||
}
|
|
||||||
for (own, child) in self.size_output_inputs.iter_mut().zip(output_inputs) {
|
|
||||||
*own |= child;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_text<'b>(
|
pub fn render_text<'b>(
|
||||||
@@ -279,9 +351,13 @@ impl<'a> Painter<'a> {
|
|||||||
let mut region = origin;
|
let mut region = origin;
|
||||||
region.x.end = region.x.start;
|
region.x.end = region.x.start;
|
||||||
region.y.end = region.y.start;
|
region.y.end = region.y.start;
|
||||||
let mut region = region.offset(UiVec2::px(glyph.offset));
|
let mut region = region.offset(UiVec2::from_px(glyph.offset));
|
||||||
region.x.end = region.x.start + UiScalar::px(glyph.entry.width as f32);
|
let size = PxVec2::new(
|
||||||
region.y.end = region.y.start + UiScalar::px(glyph.entry.height as f32);
|
Px::from_int(glyph.entry.width as i32),
|
||||||
|
Px::from_int(glyph.entry.height as i32),
|
||||||
|
);
|
||||||
|
region.x.end = region.x.start.offset(size.x);
|
||||||
|
region.y.end = region.y.start.offset(size.y);
|
||||||
self.write(
|
self.write(
|
||||||
kind,
|
kind,
|
||||||
GlyphPrimitive {
|
GlyphPrimitive {
|
||||||
@@ -302,45 +378,66 @@ impl<'a> Painter<'a> {
|
|||||||
self.region
|
self.region
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The output's size in pixels. A widget that reads it draws again when
|
/// Where this widget sits in a box longer than the length it takes. A
|
||||||
/// the output changes, since nothing else can put that right.
|
/// widget that positions its own content reads it to place that content
|
||||||
pub fn output_size(&mut self) -> Vec2 {
|
/// the way the box around it would have placed the widget.
|
||||||
self.size_output_inputs = [true; 2];
|
pub fn alignment(&self) -> RegionAlign {
|
||||||
self.state.output_size
|
self.rsc.widgets().alignment(self.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One axis of the output in pixels. Prefer this to [`Self::output_size`]
|
/// Whether a rule beside this widget gives its length on `axis` outright,
|
||||||
/// when the other axis cannot affect the size this widget reports.
|
/// which makes whatever it reports for that axis moot. A rule that only
|
||||||
pub fn output_len(&mut self, axis: Axis) -> f32 {
|
/// bounds the length is not one of these: the answer is still the
|
||||||
self.size_output_inputs[axis as usize] = true;
|
/// widget's to give, and something still has to work it out.
|
||||||
self.state.output_size.axis(axis)
|
///
|
||||||
|
/// The widget under a rule does not otherwise learn of it -- this is for
|
||||||
|
/// a container deciding whether reading its children across an axis is
|
||||||
|
/// worth anything, since reading one is also what makes its own size
|
||||||
|
/// depend on it.
|
||||||
|
pub fn has_exact_size(&self, axis: Axis) -> bool {
|
||||||
|
self.rsc
|
||||||
|
.widgets()
|
||||||
|
.size_rules(self.id)
|
||||||
|
.axis(axis)
|
||||||
|
.exact()
|
||||||
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This widget's box in pixels. Resolved against the output's size and
|
/// This widget's box in pixels. Reading it makes the drawing one that
|
||||||
/// the boxes it sits within, so a widget that reads it draws again when
|
/// holds for this box only, until `holds` says how far it goes.
|
||||||
/// the output changes.
|
pub fn px_size(&mut self) -> PxVec2 {
|
||||||
pub fn px_size(&mut self) -> Vec2 {
|
for (own, len) in self.own.iter_mut().zip([self.px.x, self.px.y]) {
|
||||||
self.size_box_inputs = [true; 2];
|
if *own == Holds::ANY {
|
||||||
let region = self.state.moves.resolve(self.move_idx, self.region);
|
*own = Holds::at(len);
|
||||||
region.size().to_px(self.state.output_size)
|
}
|
||||||
|
}
|
||||||
|
self.px
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One axis of this widget's box in pixels. Prefer this to
|
/// One axis of this widget's box in pixels. Prefer this to
|
||||||
/// [`Self::px_size`] when the other axis cannot affect the reported size.
|
/// [`Self::px_size`] when the other axis cannot affect the drawing.
|
||||||
pub fn px_len(&mut self, axis: Axis) -> f32 {
|
pub fn px_len(&mut self, axis: Axis) -> Px {
|
||||||
self.size_box_inputs[axis as usize] = true;
|
let len = self.px.axis(axis);
|
||||||
self.px_len_for_draw(axis)
|
let own = &mut self.own[axis as usize];
|
||||||
|
if *own == Holds::ANY {
|
||||||
|
*own = Holds::at(len);
|
||||||
|
}
|
||||||
|
len
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One axis of this widget's box in pixels, for a draw whose reported
|
/// The lengths of this widget's box on `axis` that what it is drawing
|
||||||
/// size does not follow from it -- a clamp or a position. Nothing records
|
/// holds for -- the same primitives, in the same fractions and offsets
|
||||||
/// the read, so a size that does depend on it would go stale.
|
/// of the box, and the same reported size. A widget that read its
|
||||||
pub fn px_len_for_draw(&self, axis: Axis) -> f32 {
|
/// length in pixels holds for that one alone until it says otherwise.
|
||||||
let region = self.state.moves.resolve(self.move_idx, self.region);
|
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
|
||||||
region
|
let holds = holds.into();
|
||||||
.size()
|
debug_assert!(
|
||||||
.axis(axis)
|
holds.contains(self.px.axis(axis)),
|
||||||
.to_px(self.state.output_size.axis(axis))
|
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
|
||||||
|
self.label(),
|
||||||
|
self.id
|
||||||
|
);
|
||||||
|
self.own[axis as usize] = holds;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_data(&mut self) -> &mut TextData {
|
pub fn text_data(&mut self) -> &mut TextData {
|
||||||
@@ -351,6 +448,18 @@ impl<'a> Painter<'a> {
|
|||||||
self.layer = self.state.layers.child(self.layer);
|
self.layer = self.state.layers.child(self.layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The layer this widget's `n`th child draws on, addressed rather than
|
||||||
|
/// walked to. A container that measures one child by drawing it can ask
|
||||||
|
/// on the layer that child will end up on, and then the second ask is a
|
||||||
|
/// reuse rather than a second drawing on another layer.
|
||||||
|
pub fn child_layer_at(&mut self, n: usize) {
|
||||||
|
let mut at = self.state.layers.child(self.own_layer);
|
||||||
|
for _ in 0..n {
|
||||||
|
at = self.state.layers.next(at);
|
||||||
|
}
|
||||||
|
self.layer = at;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn next_layer(&mut self) {
|
pub fn next_layer(&mut self) {
|
||||||
self.layer = self.state.layers.next(self.layer);
|
self.layer = self.state.layers.next(self.layer);
|
||||||
}
|
}
|
||||||
@@ -380,11 +489,11 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
|
|||||||
diag::bump(Counter::SizeReads);
|
diag::bump(Counter::SizeReads);
|
||||||
diag::size_read(self.child.id(), self.painter.id, self.size);
|
diag::size_read(self.child.id(), self.painter.id, self.size);
|
||||||
}
|
}
|
||||||
self.painter.depend_on_drawn_size(self.child);
|
self.painter.depend_on(self.child);
|
||||||
self.size
|
self.size
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn len(self, axis: Axis) -> Len {
|
pub fn len(self, axis: Axis) -> LayoutLen {
|
||||||
self.size().axis(axis)
|
self.size().axis(axis)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,3 +522,113 @@ impl PrimitiveLike for &TextureHandle {
|
|||||||
self.into()
|
self.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A child's answer as lengths of the parent's own box. A widget reports a
|
||||||
|
/// fraction, and `reports_of` is the length that fraction is of: the box the
|
||||||
|
/// child was given wherever that is the child's whole area, and the parent's
|
||||||
|
/// own extent wherever the box is a positional remainder, as a span's is
|
||||||
|
/// after an earlier child. Pixels come through untouched either way, being
|
||||||
|
/// that many pixels wherever they end up. A declared axis is already the
|
||||||
|
/// parent's: it resolved the rule in its own box, and the rule is what the
|
||||||
|
/// report says.
|
||||||
|
fn in_parent_frame(size: Size, reports_of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size {
|
||||||
|
let mut size = size;
|
||||||
|
for (axis, declared) in AXES.into_iter().zip(declared) {
|
||||||
|
if declared.is_none() {
|
||||||
|
*size.axis_mut(axis) = size.axis(axis).within_len(reports_of.axis(axis));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a widget declares a length of its box to be. `leftover` is not one: a
|
||||||
|
/// share of what is left over is only a length to the widget dividing one,
|
||||||
|
/// so it passes up in the size instead.
|
||||||
|
pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLen>; 2] {
|
||||||
|
let rules = widgets.size_rules(id);
|
||||||
|
let widget = widgets.get_dyn(id);
|
||||||
|
AXES.map(|axis| {
|
||||||
|
rules.axis(axis).declared().or_else(|| {
|
||||||
|
// A hint still narrows the box where no rule does, which is how a
|
||||||
|
// widget with a natural pixel size -- an image, a gap -- gets that
|
||||||
|
// size rather than the whole offer. That is the offer's business
|
||||||
|
// rather than a declaration's, and this falls away once a widget
|
||||||
|
// occupies its reported size inside the box it was offered.
|
||||||
|
widget
|
||||||
|
.and_then(|widget| widget.size_hint(axis))
|
||||||
|
.filter(|len| len.leftover == Weight::ZERO)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether what a widget reported along an axis is the whole of the box it
|
||||||
|
/// is in rather than a part to be placed inside it. A share fills, because a
|
||||||
|
/// share is a length only to whoever divides one, and whoever did is the one
|
||||||
|
/// that handed down this box. A declared axis does too: `declared_box`
|
||||||
|
/// already placed it, in the parent's box, and the rule's length is what the
|
||||||
|
/// widget reports there. And an axis the parent decided from the answer is
|
||||||
|
/// the answer already.
|
||||||
|
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
|
||||||
|
reported.leftover != Weight::ZERO || declared.is_some() || decided
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What of the box it was given a widget's drawing occupies, as lengths of
|
||||||
|
/// that box: the size it reported wherever that is a part to be placed, and
|
||||||
|
/// the whole of the box wherever the answer fills it.
|
||||||
|
///
|
||||||
|
/// A reported fraction is a fraction of the box the widget drew in, where a
|
||||||
|
/// declared one is a fraction of the box its parent handed down -- a span
|
||||||
|
/// reporting `rel(1.0)` means all of what it was given, whatever that was a
|
||||||
|
/// fraction of. So this is a length of the box rather than a length composed
|
||||||
|
/// into it, and a box in pixels is this step from the given box's pixels.
|
||||||
|
pub(crate) fn placed_lens(
|
||||||
|
size: Size,
|
||||||
|
declared: [Option<LayoutLen>; 2],
|
||||||
|
decided: [bool; 2],
|
||||||
|
) -> UiVec2 {
|
||||||
|
let mut lens = UiVec2::FULL_SIZE;
|
||||||
|
for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) {
|
||||||
|
let reported = size.axis(axis);
|
||||||
|
if !fills(reported, declared, decided) {
|
||||||
|
*lens.axis_mut(axis) = Len::from_parts(reported.rel, reported.px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lens
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where that drawing sits: those lengths taken of the box the widget was
|
||||||
|
/// asked in, on the side of it that the widget's alignment says.
|
||||||
|
pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) -> UiRegion {
|
||||||
|
let mut placed = region;
|
||||||
|
for axis in AXES {
|
||||||
|
// The whole of the box is already where it sits, and the arithmetic
|
||||||
|
// below is the identity for it.
|
||||||
|
if lens.axis(axis) == Len::FULL {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let span = placed.axis_mut(axis);
|
||||||
|
let len = lens.axis(axis).within_len(span.len());
|
||||||
|
span.start += (span.len() - len).scale(align.axis(axis).rel());
|
||||||
|
span.end = span.start + len;
|
||||||
|
}
|
||||||
|
placed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes a widget's declared lengths in the box `region` is given in, since a
|
||||||
|
/// fraction of a length means a fraction of that one, and puts what is left
|
||||||
|
/// over on the side its alignment says. A caller that already reserved the
|
||||||
|
/// space hands back the same length, so this is the identity for it.
|
||||||
|
pub(crate) fn declared_box(
|
||||||
|
mut region: UiRegion,
|
||||||
|
declared: [Option<LayoutLen>; 2],
|
||||||
|
align: RegionAlign,
|
||||||
|
) -> UiRegion {
|
||||||
|
for (axis, len) in AXES.into_iter().zip(declared) {
|
||||||
|
let Some(len) = len else { continue };
|
||||||
|
let span = region.axis_mut(axis);
|
||||||
|
let len = Len::from_parts(len.rel, len.px);
|
||||||
|
span.start += (span.len() - len).scale(align.axis(axis).rel());
|
||||||
|
span.end = span.start + len;
|
||||||
|
}
|
||||||
|
region
|
||||||
|
}
|
||||||
+804
-507
File diff suppressed because it is too large.
Load diff
@@ -35,7 +35,7 @@ impl<T, I: IdNum> Arena<T, I> {
|
|||||||
self.data[i]
|
self.data[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
pub(crate) fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
||||||
&mut self.data[id.idx()]
|
&mut self.data[id.idx()]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,6 +75,11 @@ impl<T, I: IdNum> TrackedArena<T, I> {
|
|||||||
self.refs[i.idx()] += 1;
|
self.refs[i.idx()] += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
||||||
|
self.changed = true;
|
||||||
|
self.inner.get_mut(id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, id: Id<I>) -> T
|
pub fn remove(&mut self, id: Id<I>) -> T
|
||||||
where
|
where
|
||||||
T: Copy,
|
T: Copy,
|
||||||
|
|||||||
@@ -56,6 +56,34 @@ macro_rules! impl_op {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Without the `f32` operations, for a type whose fields are not all the
|
||||||
|
// same kind of number: there is nothing a bare float means to a fraction
|
||||||
|
// and an offset at once.
|
||||||
|
(same $T:ident $op:ident $fn:ident $opa:ident $fna:ident; $($field:ident)*) => {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod ${concat($T, _op_, $fn, _same_impl)} {
|
||||||
|
use super::*;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::ops::*;
|
||||||
|
const impl $op for $T {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn $fn(self, rhs: Self) -> Self::Output {
|
||||||
|
Self {
|
||||||
|
$($field: self.$field.$fn(rhs.$field),)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const impl $opa for $T {
|
||||||
|
fn $fna(&mut self, rhs: Self) {
|
||||||
|
*self = self.$fn(rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(same $T:ident $op:ident $fn:ident; $($field:ident)*) => {
|
||||||
|
impl_op!(same $T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
|
||||||
|
};
|
||||||
($T:ident $op:ident $fn:ident; $($field:ident)*) => {
|
($T:ident $op:ident $fn:ident; $($field:ident)*) => {
|
||||||
impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
|
impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
use crate::Widget;
|
use crate::{RegionAlign, SizeRules, Widget};
|
||||||
|
|
||||||
pub struct WidgetData {
|
pub struct WidgetData {
|
||||||
pub widget: Box<dyn Widget>,
|
pub widget: Box<dyn Widget>,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
|
pub(super) region_node: bool,
|
||||||
|
pub(super) size: SizeRules,
|
||||||
|
pub(super) align: RegionAlign,
|
||||||
/// dynamic borrow checking
|
/// dynamic borrow checking
|
||||||
pub borrowed: bool,
|
pub borrowed: bool,
|
||||||
}
|
}
|
||||||
@@ -16,6 +19,9 @@ impl WidgetData {
|
|||||||
Self {
|
Self {
|
||||||
widget: Box::new(widget),
|
widget: Box::new(widget),
|
||||||
label,
|
label,
|
||||||
|
region_node: false,
|
||||||
|
size: SizeRules::default(),
|
||||||
|
align: RegionAlign::default(),
|
||||||
borrowed: false,
|
borrowed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-26
@@ -1,9 +1,10 @@
|
|||||||
use crate::{Axis, Len, Painter, Size};
|
use crate::{Axis, LayoutLen, Painter, Size};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
mod data;
|
mod data;
|
||||||
mod handle;
|
mod handle;
|
||||||
mod like;
|
mod like;
|
||||||
|
mod size_rule;
|
||||||
mod tag;
|
mod tag;
|
||||||
mod view;
|
mod view;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
@@ -11,24 +12,11 @@ mod widgets;
|
|||||||
pub use data::*;
|
pub use data::*;
|
||||||
pub use handle::*;
|
pub use handle::*;
|
||||||
pub use like::*;
|
pub use like::*;
|
||||||
|
pub use size_rule::*;
|
||||||
pub use tag::*;
|
pub use tag::*;
|
||||||
pub use view::*;
|
pub use view::*;
|
||||||
pub use widgets::*;
|
pub use widgets::*;
|
||||||
|
|
||||||
/// What may be done to a widget's drawing when the box it was given changes
|
|
||||||
/// on this axis, instead of drawing it again. Asked per axis, because wrapped
|
|
||||||
/// text reads the width it is offered and not the height.
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
||||||
pub enum OnResize {
|
|
||||||
Scale,
|
|
||||||
/// Reserved: nothing reads this yet, so a widget saying it is redrawn.
|
|
||||||
/// Keeping an unchanged drawing in a bigger box needs the widget to say
|
|
||||||
/// *where* in that box it should sit, which is the alignment work.
|
|
||||||
Translate,
|
|
||||||
#[default]
|
|
||||||
Redraw,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Widget: Any {
|
pub trait Widget: Any {
|
||||||
/// Draws the widget, and returns what it used of the box it was given.
|
/// Draws the widget, and returns what it used of the box it was given.
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size;
|
fn draw(&mut self, painter: &mut Painter) -> Size;
|
||||||
@@ -36,13 +24,9 @@ pub trait Widget: Any {
|
|||||||
/// An exact length the widget can give without a painter or its children.
|
/// An exact length the widget can give without a painter or its children.
|
||||||
/// Optional, and saves a draw rather than changing one: a hint that
|
/// Optional, and saves a draw rather than changing one: a hint that
|
||||||
/// disagrees with the eventual draw fails a debug assertion.
|
/// disagrees with the eventual draw fails a debug assertion.
|
||||||
fn size_hint(&self, _axis: Axis) -> Option<Len> {
|
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, _axis: Axis) -> OnResize {
|
|
||||||
OnResize::default()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for () {
|
impl Widget for () {
|
||||||
@@ -51,12 +35,8 @@ impl Widget for () {
|
|||||||
Size::default()
|
Size::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size_hint(&self, _axis: Axis) -> Option<Len> {
|
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
|
||||||
Some(Len::default())
|
Some(LayoutLen::default())
|
||||||
}
|
|
||||||
|
|
||||||
fn on_resize(&self, _axis: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
use crate::{Axis, LayoutLen, Weight};
|
||||||
|
|
||||||
|
/// What a widget's length on one axis is, as a rule its parent applies where
|
||||||
|
/// it draws it rather than an answer the widget gives about itself.
|
||||||
|
///
|
||||||
|
/// A rule and a drawn size are not two opinions to reconcile: a rule wins on
|
||||||
|
/// the axis it names, and the `Size` returned by `draw` answers only the axes
|
||||||
|
/// with no rule. That is what lets a span divide its space around a length
|
||||||
|
/// nobody has drawn yet, and it is why a rule lives beside the widget rather
|
||||||
|
/// than inside it -- the widget under the rule never has to know about it.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub enum SizeRule {
|
||||||
|
/// Whatever the widget reports from drawing.
|
||||||
|
#[default]
|
||||||
|
Free,
|
||||||
|
/// This length, whatever the widget reports.
|
||||||
|
Exact(LayoutLen),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SizeRule {
|
||||||
|
/// The length this rule gives without the widget being drawn, if it can
|
||||||
|
/// give one. `leftover` is never among them: a share is a length only to
|
||||||
|
/// whoever divides one, so it passes up in the reported size instead and
|
||||||
|
/// is resolved there.
|
||||||
|
pub fn declared(&self) -> Option<LayoutLen> {
|
||||||
|
match self {
|
||||||
|
Self::Exact(len) if len.leftover == Weight::ZERO => Some(*len),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The length this rule gives outright, whatever the widget reports --
|
||||||
|
/// which makes the widget's answer on that axis moot. A share counts: it
|
||||||
|
/// is a length the widget's parent still has to divide, so it is exact
|
||||||
|
/// here and resolved there, unlike `declared`, which is only the ones
|
||||||
|
/// that give a box directly.
|
||||||
|
pub fn exact(&self) -> Option<LayoutLen> {
|
||||||
|
match self {
|
||||||
|
Self::Free => None,
|
||||||
|
Self::Exact(len) => Some(*len),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The length a widget reporting `reported` ends up with.
|
||||||
|
pub fn apply(&self, reported: LayoutLen) -> LayoutLen {
|
||||||
|
match self {
|
||||||
|
Self::Free => reported,
|
||||||
|
Self::Exact(len) => *len,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LayoutLen> for SizeRule {
|
||||||
|
fn from(len: LayoutLen) -> Self {
|
||||||
|
Self::Exact(len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<LayoutLen>> for SizeRule {
|
||||||
|
fn from(len: Option<LayoutLen>) -> Self {
|
||||||
|
len.map_or(Self::Free, Self::Exact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One rule per axis, which is how a widget carries a length on one axis and
|
||||||
|
/// leaves the other to whatever it draws.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub struct SizeRules {
|
||||||
|
pub x: SizeRule,
|
||||||
|
pub y: SizeRule,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SizeRules {
|
||||||
|
pub fn axis(&self, axis: Axis) -> SizeRule {
|
||||||
|
match axis {
|
||||||
|
Axis::X => self.x,
|
||||||
|
Axis::Y => self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn axis_mut(&mut self, axis: Axis) -> &mut SizeRule {
|
||||||
|
match axis {
|
||||||
|
Axis::X => &mut self.x,
|
||||||
|
Axis::Y => &mut self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
IdLike, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
|
Axis, AxisAlign, IdLike, RegionAlign, SizeRule, SizeRules, StrongWidget, WeakWidget, Widget,
|
||||||
|
WidgetData, WidgetId,
|
||||||
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,6 +101,71 @@ impl Widgets {
|
|||||||
self.data_mut(id.id()).unwrap().label = label;
|
self.data_mut(id.id()).unwrap().label = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this widget owns a movable retained region.
|
||||||
|
pub fn is_region_node(&self, id: impl IdLike) -> bool {
|
||||||
|
self.data(id).unwrap().region_node
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chooses whether this widget's retained drawing has one movable region
|
||||||
|
/// of its own. Changing the boundary redraws the subtree once so every
|
||||||
|
/// primitive names the right coordinate space.
|
||||||
|
pub fn set_region_node(&mut self, id: impl IdLike, region_node: bool) {
|
||||||
|
let id = id.id();
|
||||||
|
let data = self.data_mut(id).unwrap();
|
||||||
|
if data.region_node == region_node {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.region_node = region_node;
|
||||||
|
self.needs_redraw.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The length rules whoever draws this widget applies to its box.
|
||||||
|
pub fn size_rules(&self, id: impl IdLike) -> SizeRules {
|
||||||
|
self.data(id).unwrap().size
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets one axis's rule. The widget is marked rather than its parent
|
||||||
|
/// because the parent is not known here; `redraw` escalates a changed
|
||||||
|
/// declared length to whoever resolves it.
|
||||||
|
pub fn set_size_rule(&mut self, id: impl IdLike, axis: Axis, rule: SizeRule) {
|
||||||
|
let id = id.id();
|
||||||
|
let data = self.data_mut(id).unwrap();
|
||||||
|
if *data.size.axis_mut(axis) == rule {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*data.size.axis_mut(axis) = rule;
|
||||||
|
self.needs_redraw.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where this widget sits in a box longer than the length it takes.
|
||||||
|
pub fn alignment(&self, id: impl IdLike) -> RegionAlign {
|
||||||
|
self.data(id).unwrap().align
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets one axis's alignment. Which box a widget ends up in is its
|
||||||
|
/// parent's to decide, so this is escalated the way a length rule is.
|
||||||
|
pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) {
|
||||||
|
let id = id.id();
|
||||||
|
let data = self.data_mut(id).unwrap();
|
||||||
|
if *data.align.axis_mut(axis) == align {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*data.align.axis_mut(axis) = align;
|
||||||
|
self.needs_redraw.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Both axes at once, for a caller holding a pair.
|
||||||
|
pub fn set_size_rules(
|
||||||
|
&mut self,
|
||||||
|
id: impl IdLike,
|
||||||
|
x: impl Into<SizeRule>,
|
||||||
|
y: impl Into<SizeRule>,
|
||||||
|
) {
|
||||||
|
let id = id.id();
|
||||||
|
self.set_size_rule(id, Axis::X, x.into());
|
||||||
|
self.set_size_rule(id, Axis::Y, y.into());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> {
|
pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> {
|
||||||
self.vec.get_mut(id.id())
|
self.vec.get_mut(id.id())
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-6
@@ -20,22 +20,26 @@ impl DefaultAppState for Client {
|
|||||||
let pad_test = (
|
let pad_test = (
|
||||||
rrect.color(Color::BLUE),
|
rrect.color(Color::BLUE),
|
||||||
(
|
(
|
||||||
|
// The square is one widget and the two shares of the row it
|
||||||
|
// sits centred in are another: a length is a property of a
|
||||||
|
// widget, so `.width` here would overwrite the `.sized`.
|
||||||
rrect
|
rrect
|
||||||
.color(Color::RED)
|
.color(Color::RED)
|
||||||
.sized((100, 100))
|
.sized((100, 100))
|
||||||
.center()
|
.center()
|
||||||
.width(rest(2)),
|
.wrapper()
|
||||||
|
.width(leftover(2)),
|
||||||
(
|
(
|
||||||
rrect.color(Color::ORANGE),
|
rrect.color(Color::ORANGE),
|
||||||
rrect.color(Color::LIME).pad(10.0),
|
rrect.color(Color::LIME).pad(10.0),
|
||||||
)
|
)
|
||||||
.span(Dir::RIGHT)
|
.span(Dir::RIGHT)
|
||||||
.width(rest(2)),
|
.width(leftover(2)),
|
||||||
rrect.color(Color::YELLOW),
|
rrect.color(Color::YELLOW),
|
||||||
)
|
)
|
||||||
.span(Dir::RIGHT)
|
.span(Dir::RIGHT)
|
||||||
.pad(10)
|
.pad(10)
|
||||||
.width(rest(3)),
|
.width(leftover(3)),
|
||||||
)
|
)
|
||||||
.span(Dir::RIGHT)
|
.span(Dir::RIGHT)
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
@@ -121,11 +125,11 @@ impl DefaultAppState for Client {
|
|||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let text_edit_scroll = (
|
let text_edit_scroll = (
|
||||||
msg_area.height(rest(1)),
|
msg_area.height(leftover(1)),
|
||||||
(
|
(
|
||||||
Rect::new(Color::WHITE.darker(0.9)),
|
Rect::new(Color::WHITE.darker(0.9)),
|
||||||
(
|
(
|
||||||
add_text.width(rest(1)),
|
add_text.width(leftover(1)),
|
||||||
Rect::new(Color::GREEN)
|
Rect::new(Color::GREEN)
|
||||||
.on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| {
|
.on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| {
|
||||||
rsc.run_event::<Submit>(add_text, (), ctx.state);
|
rsc.run_event::<Submit>(add_text, (), ctx.state);
|
||||||
@@ -143,7 +147,7 @@ impl DefaultAppState for Client {
|
|||||||
.span(Dir::DOWN)
|
.span(Dir::DOWN)
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let main = WidgetPtr::new().add(rsc);
|
let main = Wrapper::new().add(rsc);
|
||||||
|
|
||||||
let vals = Rc::new(RefCell::new((0, Vec::new())));
|
let vals = Rc::new(RefCell::new((0, Vec::new())));
|
||||||
let mut switch_button = |color, to: WeakWidget, label| {
|
let mut switch_button = |color, to: WeakWidget, label| {
|
||||||
|
|||||||
+8
-3
@@ -26,12 +26,17 @@ impl DefaultAppState for State {
|
|||||||
.wrap(true)
|
.wrap(true)
|
||||||
.text_align(Align::LEFT)
|
.text_align(Align::LEFT)
|
||||||
.pad(16)
|
.pad(16)
|
||||||
|
.width(rel(1.0))
|
||||||
.background(panel());
|
.background(panel());
|
||||||
|
|
||||||
|
// Each one takes the whole width, because `text_align` puts the
|
||||||
|
// glyphs somewhere in the box the text is given and a text that
|
||||||
|
// reports the width of its own glyphs is given exactly that.
|
||||||
|
let label = |text: &str, align| wtext(text).size(24).text_align(align).width(rel(1.0));
|
||||||
let aligned = (
|
let aligned = (
|
||||||
wtext("left").size(24).text_align(Align::LEFT),
|
label("left", Align::LEFT),
|
||||||
wtext("centred").size(24).text_align(Align::CENTER),
|
label("centred", Align::H_CENTER),
|
||||||
wtext("right").size(24).text_align(Align::RIGHT),
|
label("right", Align::RIGHT),
|
||||||
)
|
)
|
||||||
.span(Dir::DOWN)
|
.span(Dir::DOWN)
|
||||||
.gap(8)
|
.gap(8)
|
||||||
|
|||||||
+6
-4
@@ -15,8 +15,10 @@ where
|
|||||||
let region = ctx.data.render.window_region(&id).unwrap();
|
let region = ctx.data.render.window_region(&id).unwrap();
|
||||||
let id_pos = region.top_left;
|
let id_pos = region.top_left;
|
||||||
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
|
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
|
||||||
let pos = ctx.data.pos + container_pos - id_pos;
|
// The pointer arrives from the platform in floats; everything
|
||||||
let size = region.size();
|
// it is compared against is on the grid.
|
||||||
|
let pos = (PxVec2::from_f32(ctx.data.pos) + container_pos - id_pos).to_f32();
|
||||||
|
let size = region.size().to_f32();
|
||||||
select(
|
select(
|
||||||
rsc,
|
rsc,
|
||||||
ctx.data.render,
|
ctx.data.render,
|
||||||
@@ -70,8 +72,8 @@ fn select(
|
|||||||
if let Some(region) = render.window_region(&id) {
|
if let Some(region) = render.window_region(&id) {
|
||||||
state.window.set_ime_allowed(true);
|
state.window.set_ime_allowed(true);
|
||||||
state.window.set_ime_cursor_area(
|
state.window.set_ime_cursor_area(
|
||||||
LogicalPosition::<f32>::from(region.top_left.tuple()),
|
LogicalPosition::<f32>::from(region.top_left.to_f32().tuple()),
|
||||||
LogicalSize::<f32>::from(region.size().tuple()),
|
LogicalSize::<f32>::from(region.size().to_f32().tuple()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
state.focus = Some(id);
|
state.focus = Some(id);
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ impl SensorUi for UiRenderState {
|
|||||||
let Some(region) = region_of(id) else {
|
let Some(region) = region_of(id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !cursor.exists || !region.contains(cursor.pos) {
|
if !cursor.exists || !region.contains(PxVec2::from_f32(cursor.pos)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
hovered.now.push(id);
|
hovered.now.push(id);
|
||||||
@@ -249,8 +249,8 @@ fn deliver<Rsc: HasEvents>(
|
|||||||
region: PixelRegion,
|
region: PixelRegion,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let data = CursorData {
|
let data = CursorData {
|
||||||
pos: cursor.pos - region.top_left,
|
pos: cursor.pos - region.top_left.to_f32(),
|
||||||
size: region.bot_right - region.top_left,
|
size: region.size().to_f32(),
|
||||||
scroll_delta: cursor.scroll_delta,
|
scroll_delta: cursor.scroll_delta,
|
||||||
hover,
|
hover,
|
||||||
cursor: cursor.clone(),
|
cursor: cursor.clone(),
|
||||||
|
|||||||
+16
-3
@@ -29,8 +29,14 @@ macro_rules! assert_corners {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
$harness.region(&$id).expect("widget drew nothing"),
|
$harness.region(&$id).expect("widget drew nothing"),
|
||||||
$crate::core::PixelRegion {
|
$crate::core::PixelRegion {
|
||||||
top_left: $crate::core::util::Vec2::new($x0 as f32, $y0 as f32),
|
top_left: $crate::core::PxVec2::new(
|
||||||
bot_right: $crate::core::util::Vec2::new($x1 as f32, $y1 as f32),
|
$crate::core::Px::from_f32($x0 as f32),
|
||||||
|
$crate::core::Px::from_f32($y0 as f32),
|
||||||
|
),
|
||||||
|
bot_right: $crate::core::PxVec2::new(
|
||||||
|
$crate::core::Px::from_f32($x1 as f32),
|
||||||
|
$crate::core::Px::from_f32($y1 as f32),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -151,13 +157,20 @@ impl Harness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn size(&self) -> Vec2 {
|
pub fn size(&self) -> Vec2 {
|
||||||
self.render.output_size()
|
self.render.output_size().to_f32()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||||
self.render.resize(size);
|
self.render.resize(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Changes a length rule after the fact, the way `.width()` sets one.
|
||||||
|
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<LayoutLen>) {
|
||||||
|
self.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(id, axis, SizeRule::Exact(len.into()));
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the root and lays it out, so a pointer event has something to hit.
|
/// Sets the root and lays it out, so a pointer event has something to hit.
|
||||||
pub fn set_root<T>(&mut self, widget: impl WidgetLike<DefaultRsc<HarnessState>, T>) {
|
pub fn set_root<T>(&mut self, widget: impl WidgetLike<DefaultRsc<HarnessState>, T>) {
|
||||||
widget.set_root(&mut self.rsc, &mut self.state);
|
widget.set_root(&mut self.rsc, &mut self.state);
|
||||||
|
|||||||
+745
-147
@@ -8,17 +8,38 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// The declared lengths of one `SetSize`, by axis.
|
/// The declared lengths of one widget carrying a size rule, by axis.
|
||||||
pub type Lens = [Option<Len>; 2];
|
pub type Lens = [Option<LayoutLen>; 2];
|
||||||
|
|
||||||
|
/// Where one widget carrying an alignment sits, by axis. `None` uses the
|
||||||
|
/// centered default.
|
||||||
|
pub type Aligns = [Option<AxisAlign>; 2];
|
||||||
|
|
||||||
/// What a test changes between two trees grown from the same seed, so the
|
/// What a test changes between two trees grown from the same seed, so the
|
||||||
/// warm one can be mutated and the cold one grown that way to begin with.
|
/// warm one can be mutated and the cold one grown that way to begin with.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Edits {
|
pub struct Edits {
|
||||||
/// Declared sizes, by the order the `SetSize` wrappers were made.
|
/// Declared sizes, by the order the rules were put on.
|
||||||
pub sizes: HashMap<usize, Lens>,
|
pub sizes: HashMap<usize, Lens>,
|
||||||
/// Which children a span has, by the order the spans were made.
|
/// Which children a span has, by the order the spans were made.
|
||||||
pub spans: HashMap<usize, SpanEdit>,
|
pub spans: HashMap<usize, SpanEdit>,
|
||||||
|
/// Alignments, by the order they were put on.
|
||||||
|
pub aligns: HashMap<usize, Aligns>,
|
||||||
|
/// Which widgets own a movable region, by the order they were offered
|
||||||
|
/// one. Region nodes change what a move writes and how deep a primitive's
|
||||||
|
/// chain is, so a tree that never grows one leaves both untested.
|
||||||
|
pub nodes: HashMap<usize, bool>,
|
||||||
|
/// Whether a [`Branch`] takes the side it would take at any measurement,
|
||||||
|
/// rather than the side the one it made says. The oracle wants the
|
||||||
|
/// measured side -- that is the whole point of a branch, and how a widget
|
||||||
|
/// believing a measurement a cold start would not have given it becomes a
|
||||||
|
/// different tree. A rig measuring cost wants this instead: a fixture
|
||||||
|
/// whose shape moves with the thing being measured cannot be compared
|
||||||
|
/// with itself across a change to it, and seed 1 at depth 8 went from 88
|
||||||
|
/// drawn widgets and 2,298 primitive writes a frame to 115 and 8,209
|
||||||
|
/// across fixed point, which is three and a half times the work behind a
|
||||||
|
/// number read as three and a half times the cost.
|
||||||
|
pub fixed_branches: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
@@ -75,13 +96,11 @@ const WORDS: &str = "Wrapping shapes one source into as many lines as the box \
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Tree {
|
pub struct Tree {
|
||||||
pub ids: Vec<WidgetId>,
|
pub ids: Vec<WidgetId>,
|
||||||
pub sized: Vec<WeakWidget<SetSize>>,
|
pub sized: Vec<WidgetId>,
|
||||||
|
pub aligned: Vec<WidgetId>,
|
||||||
|
pub nodes: Vec<WidgetId>,
|
||||||
pub spans: Vec<Spanned>,
|
pub spans: Vec<Spanned>,
|
||||||
pub scrolls: Vec<WeakWidget<Scroll>>,
|
pub scrolls: Vec<WeakWidget<Scroll>>,
|
||||||
/// Children a `SpanEdit` took out, held so that dropping the last share
|
|
||||||
/// of one does not free its id for the next widget to be given -- which
|
|
||||||
/// would put the two trees' `ids` out of step.
|
|
||||||
pub detached: Vec<StrongWidget>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Branches on a child's measured length. Comparing boxes catches a widget
|
/// Branches on a child's measured length. Comparing boxes catches a widget
|
||||||
@@ -99,31 +118,479 @@ pub struct Branch {
|
|||||||
impl Widget for Branch {
|
impl Widget for Branch {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let mut top = UiRegion::FULL;
|
let mut top = UiRegion::FULL;
|
||||||
top.y.end = top.y.start.offset(40.0);
|
top.y.end = top.y.start.offset(Px::from_int(40));
|
||||||
let measured = painter.place(&self.probe, top).len(Axis::X);
|
let measured = painter.widget_within(&self.probe, top).len(Axis::X);
|
||||||
let px = measured.apply_rest().to_px(painter.px_len(Axis::X));
|
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
|
||||||
|
|
||||||
let mut rest = UiRegion::FULL;
|
let mut below = UiRegion::FULL;
|
||||||
rest.y.start = rest.y.start.offset(40.0);
|
below.y.start = below.y.start.offset(Px::from_int(40));
|
||||||
match px > self.threshold {
|
match px > Px::from_f32(self.threshold) {
|
||||||
true => painter.place(&self.wide, rest),
|
true => painter.widget_within(&self.wide, below),
|
||||||
false => painter.place(&self.narrow, rest),
|
false => painter.widget_within(&self.narrow, below),
|
||||||
};
|
};
|
||||||
Size::REST
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Spanned {
|
pub struct Spanned {
|
||||||
pub id: WeakWidget<Span>,
|
pub id: WeakWidget<Span>,
|
||||||
/// Leaves grown with the span whether or not they end up in it, so both
|
/// Everything made for this span that it does not hold -- spares never
|
||||||
/// trees make the same widgets in the same order either way. Attaching
|
/// attached and children detached alike. A widget belongs to one parent,
|
||||||
/// one moves it out of here: a widget belongs to one parent, and one that
|
/// and one that belongs to nobody still has to be held here: dropping
|
||||||
/// belongs to nobody still has to be held or it reads as a leak.
|
/// the last share of it frees its id for the next widget to be given,
|
||||||
|
/// which puts two trees out of step.
|
||||||
pub spares: Vec<StrongWidget>,
|
pub spares: Vec<StrongWidget>,
|
||||||
/// How many children it was grown with, before any edit.
|
/// How many children it was grown with, before any edit.
|
||||||
pub grown: usize,
|
pub grown: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A tree described rather than built: [`plan`] turns a seed into one of
|
||||||
|
/// these and [`build`] turns it into widgets, where growing did both at once.
|
||||||
|
///
|
||||||
|
/// The split is what makes a counterexample readable. A failing seed used to
|
||||||
|
/// be the entire record of one, because a grower that makes widgets as it
|
||||||
|
/// draws leaves nothing to take apart -- a shrinker could only grow its own
|
||||||
|
/// trees and hope to meet the same shape, which in practice it does not. A
|
||||||
|
/// plan is reduced by [`Plan::smaller`] and built again, so any seed that
|
||||||
|
/// fails can be cut down until what is left is small enough to read.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Plan {
|
||||||
|
pub kind: Kind,
|
||||||
|
/// The declared size this widget carries. Whoever grows a widget offers
|
||||||
|
/// it one and the offer is taken or declined; a second offer to the same
|
||||||
|
/// widget is dropped, because two rules on one widget would settle in the
|
||||||
|
/// order they were applied rather than in grow order.
|
||||||
|
pub size: Option<Lens>,
|
||||||
|
/// The alignment it carries, under the same one-offer rule.
|
||||||
|
pub align: Option<Aligns>,
|
||||||
|
/// Whether it was offered a movable region of its own and what it
|
||||||
|
/// answered. `Some(false)` is an offer declined, which still uses up the
|
||||||
|
/// one offer, where `None` is an offer never made.
|
||||||
|
pub region_node: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum Kind {
|
||||||
|
/// Wrapped and unwrapped text, because only one of them reads the width
|
||||||
|
/// it is given and so only one has to be drawn again for a new one.
|
||||||
|
Wrapped,
|
||||||
|
OneLine,
|
||||||
|
Rect {
|
||||||
|
color: usize,
|
||||||
|
alpha: u8,
|
||||||
|
},
|
||||||
|
/// Scrolling reads the pixel length of its box, which nothing else here
|
||||||
|
/// does, and gives its child a box longer than its own.
|
||||||
|
Scroll {
|
||||||
|
axis: Axis,
|
||||||
|
inner: Box<Plan>,
|
||||||
|
},
|
||||||
|
/// All three sides are grown either way, so a tree that draws one has the
|
||||||
|
/// same ids as a tree that draws another.
|
||||||
|
Branch {
|
||||||
|
probe: Box<Plan>,
|
||||||
|
wide: Box<Plan>,
|
||||||
|
narrow: Box<Plan>,
|
||||||
|
threshold: f32,
|
||||||
|
},
|
||||||
|
/// Each side its own, since a padding that is the same all round hides
|
||||||
|
/// anything that treats one edge differently from another.
|
||||||
|
Pad {
|
||||||
|
padding: [i32; 4],
|
||||||
|
inner: Box<Plan>,
|
||||||
|
},
|
||||||
|
Stack {
|
||||||
|
children: Vec<Plan>,
|
||||||
|
},
|
||||||
|
Span {
|
||||||
|
dir: usize,
|
||||||
|
gap: i32,
|
||||||
|
/// Grown for this span, in the order they are made.
|
||||||
|
children: Vec<Plan>,
|
||||||
|
/// Grown beside it whether or not they end up in it, so the widget
|
||||||
|
/// after them has the same id in a tree that leaves them out as in
|
||||||
|
/// one that puts them in.
|
||||||
|
spares: Vec<Plan>,
|
||||||
|
/// Which of `children` then `spares` are actually in the span, and
|
||||||
|
/// in what order -- kept apart from the two lists above so that a
|
||||||
|
/// tree which detaches, attaches or reorders its children still
|
||||||
|
/// makes the same widgets in the same order, and two builds line up
|
||||||
|
/// index for index. Anything not named here is built and held
|
||||||
|
/// rather than dropped, since freeing an id hands it to the next
|
||||||
|
/// widget and puts two trees out of step.
|
||||||
|
order: Vec<usize>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plan {
|
||||||
|
/// A widget carrying nothing anybody has offered it yet.
|
||||||
|
fn bare(kind: Kind) -> Self {
|
||||||
|
Self {
|
||||||
|
kind,
|
||||||
|
size: None,
|
||||||
|
align: None,
|
||||||
|
region_node: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many widgets building it makes, spares and detached children
|
||||||
|
/// included, since those are made either way.
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
1 + match &self.kind {
|
||||||
|
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => inner.size(),
|
||||||
|
Kind::Branch {
|
||||||
|
probe,
|
||||||
|
wide,
|
||||||
|
narrow,
|
||||||
|
..
|
||||||
|
} => probe.size() + wide.size() + narrow.size(),
|
||||||
|
Kind::Stack { children } => children.iter().map(Plan::size).sum(),
|
||||||
|
Kind::Span {
|
||||||
|
children, spares, ..
|
||||||
|
} => children.iter().chain(spares).map(Plan::size).sum(),
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The trees to try instead of this one when reducing a counterexample,
|
||||||
|
/// biggest cut first: a shrinker takes the first that still fails, so
|
||||||
|
/// offering "this subtree alone" before "this subtree with one child
|
||||||
|
/// fewer" is what gets from six hundred widgets to six rather than to
|
||||||
|
/// five hundred and ninety.
|
||||||
|
///
|
||||||
|
/// Every one of these is a tree the generator could have grown, so a
|
||||||
|
/// reduced plan is a counterexample in its own right rather than a
|
||||||
|
/// special case only the shrinker can make.
|
||||||
|
pub fn smaller(&self) -> Vec<Plan> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
// Standing in for the whole of it, which is the largest cut there is.
|
||||||
|
for kid in self.kids() {
|
||||||
|
out.push(kid.clone());
|
||||||
|
}
|
||||||
|
// Then what it carries, which costs nothing to put back if it was
|
||||||
|
// not the thing that mattered.
|
||||||
|
for dropped in [
|
||||||
|
self.region_node.map(|_| Plan {
|
||||||
|
region_node: None,
|
||||||
|
..self.clone()
|
||||||
|
}),
|
||||||
|
self.align.map(|_| Plan {
|
||||||
|
align: None,
|
||||||
|
..self.clone()
|
||||||
|
}),
|
||||||
|
self.size.map(|_| Plan {
|
||||||
|
size: None,
|
||||||
|
..self.clone()
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
out.push(dropped);
|
||||||
|
}
|
||||||
|
out.extend(self.kind.smaller().into_iter().map(|kind| Plan {
|
||||||
|
kind,
|
||||||
|
..self.clone()
|
||||||
|
}));
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Visits every widget in the order [`build`] makes them, so a count
|
||||||
|
/// kept by the visitor indexes the same widget as the matching [`Tree`]
|
||||||
|
/// vector does.
|
||||||
|
pub fn walk_mut(&mut self, at: &mut impl FnMut(&mut Plan)) {
|
||||||
|
match &mut self.kind {
|
||||||
|
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => inner.walk_mut(at),
|
||||||
|
Kind::Branch {
|
||||||
|
probe,
|
||||||
|
wide,
|
||||||
|
narrow,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
probe.walk_mut(at);
|
||||||
|
wide.walk_mut(at);
|
||||||
|
narrow.walk_mut(at);
|
||||||
|
}
|
||||||
|
Kind::Stack { children } => {
|
||||||
|
for child in children {
|
||||||
|
child.walk_mut(at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Kind::Span {
|
||||||
|
children, spares, ..
|
||||||
|
} => {
|
||||||
|
for child in children.iter_mut().chain(spares) {
|
||||||
|
child.walk_mut(at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
at(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same tree with `edits` applied, by the indices the generator would
|
||||||
|
/// have used for them.
|
||||||
|
///
|
||||||
|
/// [`plan`] resolves edits while drawing, which needs a seed. A scenario
|
||||||
|
/// needs them applied to a tree that already exists -- one it has built,
|
||||||
|
/// and one a shrinker may already have cut down, where no seed grows it
|
||||||
|
/// any more. Both routes take the same [`Edits`], so a case written
|
||||||
|
/// against one reads the same against the other.
|
||||||
|
pub fn edited(&self, edits: &Edits) -> Plan {
|
||||||
|
let mut out = self.clone();
|
||||||
|
let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0);
|
||||||
|
out.walk_mut(&mut |plan| {
|
||||||
|
if let Kind::Span {
|
||||||
|
children,
|
||||||
|
spares,
|
||||||
|
order,
|
||||||
|
..
|
||||||
|
} = &mut plan.kind
|
||||||
|
{
|
||||||
|
if let Some(edit) = edits.spans.get(&spans) {
|
||||||
|
*order = span_edited(order, children.len(), spares.len(), edit);
|
||||||
|
}
|
||||||
|
spans += 1;
|
||||||
|
}
|
||||||
|
if let Kind::Branch { threshold, .. } = &mut plan.kind
|
||||||
|
&& edits.fixed_branches
|
||||||
|
{
|
||||||
|
*threshold = f32::MIN;
|
||||||
|
}
|
||||||
|
if plan.size.is_some() {
|
||||||
|
if let Some(lens) = edits.sizes.get(&sized) {
|
||||||
|
plan.size = Some(*lens);
|
||||||
|
}
|
||||||
|
sized += 1;
|
||||||
|
}
|
||||||
|
if plan.align.is_some() {
|
||||||
|
if let Some(align) = edits.aligns.get(&aligned) {
|
||||||
|
plan.align = Some(*align);
|
||||||
|
}
|
||||||
|
aligned += 1;
|
||||||
|
}
|
||||||
|
if plan.region_node.is_some() {
|
||||||
|
if let Some(take) = edits.nodes.get(&nodes) {
|
||||||
|
plan.region_node = Some(*take);
|
||||||
|
}
|
||||||
|
nodes += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kids(&self) -> Vec<&Plan> {
|
||||||
|
match &self.kind {
|
||||||
|
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => vec![inner],
|
||||||
|
Kind::Branch {
|
||||||
|
probe,
|
||||||
|
wide,
|
||||||
|
narrow,
|
||||||
|
..
|
||||||
|
} => vec![probe, wide, narrow],
|
||||||
|
Kind::Stack { children } => children.iter().collect(),
|
||||||
|
Kind::Span { children, .. } => children.iter().collect(),
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Kind {
|
||||||
|
/// Simplifications of the shape alone, leaving what the widget carries to
|
||||||
|
/// [`Plan::smaller`]. Replacing a node with one of its children is there
|
||||||
|
/// rather than here, since it answers with a whole `Plan`.
|
||||||
|
fn smaller(&self) -> Vec<Kind> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
/// One child reduced at a time, rebuilt into the same shape. Every
|
||||||
|
/// answer has the same number of children as it was given, so it is
|
||||||
|
/// for the shapes whose child count is part of what they are.
|
||||||
|
fn reduced(kids: &[Plan], rebuild: &dyn Fn(Vec<Plan>) -> Kind) -> Vec<Kind> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for (i, kid) in kids.iter().enumerate() {
|
||||||
|
for small in kid.smaller() {
|
||||||
|
let mut next = kids.to_vec();
|
||||||
|
next[i] = small;
|
||||||
|
out.push(rebuild(next));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One child dropped, then [`reduced`]. For the shapes that hold any
|
||||||
|
/// number of children, where dropping one is the cut that matters.
|
||||||
|
fn each(kids: &[Plan], rebuild: &dyn Fn(Vec<Plan>) -> Kind) -> Vec<Kind> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for i in 0..kids.len() {
|
||||||
|
if kids.len() > 1 {
|
||||||
|
let mut less = kids.to_vec();
|
||||||
|
less.remove(i);
|
||||||
|
out.push(rebuild(less));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.extend(reduced(kids, rebuild));
|
||||||
|
out
|
||||||
|
}
|
||||||
|
match self {
|
||||||
|
// The one leaf that reads the width it is given, then the one
|
||||||
|
// that does not, then the one that measures nothing at all.
|
||||||
|
Kind::Wrapped => out.push(Kind::OneLine),
|
||||||
|
Kind::OneLine => out.push(Kind::Rect {
|
||||||
|
color: 0,
|
||||||
|
alpha: 255,
|
||||||
|
}),
|
||||||
|
Kind::Rect { .. } => {}
|
||||||
|
Kind::Scroll { axis, inner } => {
|
||||||
|
let axis = *axis;
|
||||||
|
out.extend(each(std::slice::from_ref(inner), &|mut k| Kind::Scroll {
|
||||||
|
axis,
|
||||||
|
inner: Box::new(k.remove(0)),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Kind::Branch {
|
||||||
|
probe,
|
||||||
|
wide,
|
||||||
|
narrow,
|
||||||
|
threshold,
|
||||||
|
} => {
|
||||||
|
let threshold = *threshold;
|
||||||
|
// All three sides stay: a branch is the widget that draws
|
||||||
|
// one of two on a measurement, and one with a side missing
|
||||||
|
// is a different widget rather than a smaller one. Dropping
|
||||||
|
// the branch for a side is offered by `Plan::smaller`.
|
||||||
|
let sides = [(**probe).clone(), (**wide).clone(), (**narrow).clone()];
|
||||||
|
out.extend(reduced(&sides, &|k| Kind::Branch {
|
||||||
|
probe: Box::new(k[0].clone()),
|
||||||
|
wide: Box::new(k[1].clone()),
|
||||||
|
narrow: Box::new(k[2].clone()),
|
||||||
|
threshold,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Kind::Pad { padding, inner } => {
|
||||||
|
let padding = *padding;
|
||||||
|
if padding != [0; 4] {
|
||||||
|
out.push(Kind::Pad {
|
||||||
|
padding: [0; 4],
|
||||||
|
inner: inner.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out.extend(each(std::slice::from_ref(inner), &|mut k| Kind::Pad {
|
||||||
|
padding,
|
||||||
|
inner: Box::new(k.remove(0)),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Kind::Stack { children } => {
|
||||||
|
out.extend(each(children, &|children| Kind::Stack { children }))
|
||||||
|
}
|
||||||
|
Kind::Span {
|
||||||
|
dir,
|
||||||
|
gap,
|
||||||
|
children,
|
||||||
|
spares,
|
||||||
|
order,
|
||||||
|
} => {
|
||||||
|
let (dir, gap, n) = (*dir, *gap, children.len());
|
||||||
|
let span = |children: Vec<Plan>, spares: Vec<Plan>, order: Vec<usize>| Kind::Span {
|
||||||
|
dir,
|
||||||
|
gap,
|
||||||
|
children,
|
||||||
|
spares,
|
||||||
|
order,
|
||||||
|
};
|
||||||
|
let identity: Vec<usize> = (0..n).collect();
|
||||||
|
// An order the generator did not choose is part of the tree,
|
||||||
|
// so take that off before taking the tree apart.
|
||||||
|
if *order != identity {
|
||||||
|
out.push(span(children.clone(), spares.clone(), identity));
|
||||||
|
}
|
||||||
|
// Spares exist to be attached; with none attached they are
|
||||||
|
// widgets the span never holds.
|
||||||
|
if !spares.is_empty() && order.iter().all(|&i| i < n) {
|
||||||
|
out.push(span(children.clone(), Vec::new(), order.clone()));
|
||||||
|
}
|
||||||
|
if gap != 0 {
|
||||||
|
out.push(Kind::Span {
|
||||||
|
dir,
|
||||||
|
gap: 0,
|
||||||
|
children: children.clone(),
|
||||||
|
spares: spares.clone(),
|
||||||
|
order: order.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for k in 0..n {
|
||||||
|
if n > 1 {
|
||||||
|
let mut less = children.clone();
|
||||||
|
less.remove(k);
|
||||||
|
// Everything after it shifts down, spares included,
|
||||||
|
// since they are indexed past the children.
|
||||||
|
let order = order
|
||||||
|
.iter()
|
||||||
|
.filter(|&&i| i != k)
|
||||||
|
.map(|&i| if i > k { i - 1 } else { i })
|
||||||
|
.collect();
|
||||||
|
out.push(span(less, spares.clone(), order));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i, kid) in children.iter().enumerate() {
|
||||||
|
for small in kid.smaller() {
|
||||||
|
let mut next = children.clone();
|
||||||
|
next[i] = small;
|
||||||
|
out.push(span(next, spares.clone(), order.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`SpanEdit`] applied to the order a span already holds its children in.
|
||||||
|
///
|
||||||
|
/// `detach` names positions in that order and `attach` takes from the front
|
||||||
|
/// of what the span is not holding, both of which is what a test changing a
|
||||||
|
/// live span does -- so an edit means the same thing said to a tree and said
|
||||||
|
/// to the plan it was built from. On a span nobody has edited the order is
|
||||||
|
/// the children in the order they were grown, and this is then "leave these
|
||||||
|
/// out and put that many spares on the end".
|
||||||
|
fn span_edited(order: &[usize], children: usize, spares: usize, edit: &SpanEdit) -> Vec<usize> {
|
||||||
|
let mut detach = edit.detach.clone();
|
||||||
|
detach.sort_unstable();
|
||||||
|
detach.dedup();
|
||||||
|
let mut next: Vec<usize> = order
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(at, _)| !detach.contains(at))
|
||||||
|
.map(|(_, &which)| which)
|
||||||
|
.collect();
|
||||||
|
// What the span is not holding, in the order it hands them back: what it
|
||||||
|
// was already not holding first, in the order the widgets were made, and
|
||||||
|
// what this edit takes out after that, highest position first. A child
|
||||||
|
// just detached goes to the back rather than straight back in, which is
|
||||||
|
// what makes detaching one and attaching one a trade.
|
||||||
|
let mut free: Vec<usize> = (0..children + spares)
|
||||||
|
.filter(|i| !order.contains(i))
|
||||||
|
.collect();
|
||||||
|
free.extend(detach.iter().rev().filter_map(|&at| order.get(at).copied()));
|
||||||
|
next.extend(free.into_iter().take(edit.attach));
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plans the tree `seed` describes, `edits` replacing what it would otherwise
|
||||||
|
/// have given the widgets that carry them.
|
||||||
|
///
|
||||||
|
/// The edits are resolved here rather than at build time, so that a plan is
|
||||||
|
/// the whole of what a tree is and building one has nothing left to decide.
|
||||||
|
pub fn plan(seed: u64, depth: usize, edits: &Edits) -> Plan {
|
||||||
|
let mut sow = Sow {
|
||||||
|
rng: Rng::new(seed),
|
||||||
|
edits,
|
||||||
|
sized: 0,
|
||||||
|
aligned: 0,
|
||||||
|
nodes: 0,
|
||||||
|
spans: 0,
|
||||||
|
};
|
||||||
|
sow.node(depth)
|
||||||
|
}
|
||||||
|
|
||||||
/// Grows the tree `seed` describes, `edits` replacing the declared sizes it
|
/// Grows the tree `seed` describes, `edits` replacing the declared sizes it
|
||||||
/// would otherwise have given those wrappers.
|
/// would otherwise have given those wrappers.
|
||||||
pub fn grow<Rsc: UiRsc + 'static>(
|
pub fn grow<Rsc: UiRsc + 'static>(
|
||||||
@@ -132,186 +599,317 @@ pub fn grow<Rsc: UiRsc + 'static>(
|
|||||||
depth: usize,
|
depth: usize,
|
||||||
edits: &Edits,
|
edits: &Edits,
|
||||||
) -> (StrongWidget, Tree) {
|
) -> (StrongWidget, Tree) {
|
||||||
let mut grow = Grow {
|
build(rsc, &plan(seed, depth, edits))
|
||||||
rsc,
|
|
||||||
rng: Rng::new(seed),
|
|
||||||
tree: Tree::default(),
|
|
||||||
edits,
|
|
||||||
};
|
|
||||||
let root = grow.node(depth);
|
|
||||||
(root, grow.tree)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Grow<'a, Rsc> {
|
/// Draws a plan out of the random stream. Every draw happens in the order it
|
||||||
rsc: &'a mut Rsc,
|
/// always has and before the decision it feeds, including the decisions that
|
||||||
|
/// are then dropped, because a seed has to keep meaning the same tree.
|
||||||
|
struct Sow<'a> {
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
tree: Tree,
|
|
||||||
edits: &'a Edits,
|
edits: &'a Edits,
|
||||||
|
sized: usize,
|
||||||
|
aligned: usize,
|
||||||
|
nodes: usize,
|
||||||
|
spans: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
|
impl Sow<'_> {
|
||||||
fn leaf(&mut self) -> StrongWidget {
|
fn leaf(&mut self) -> Plan {
|
||||||
let id: StrongWidget = match self.rng.below(4) {
|
Plan::bare(match self.rng.below(4) {
|
||||||
// Wrapped and unwrapped, because only one of them reads the width
|
0 => Kind::Wrapped,
|
||||||
// it is given and so only one has to be drawn again for a new one.
|
1 => Kind::OneLine,
|
||||||
0 => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
|
|
||||||
1 => wtext("one line, overflowing whatever it is given")
|
|
||||||
.size(16)
|
|
||||||
.wrap(false)
|
|
||||||
.add_strong(self.rsc),
|
|
||||||
_ => {
|
_ => {
|
||||||
let color = COLORS[self.rng.below(COLORS.len())];
|
let color = self.rng.below(COLORS.len());
|
||||||
let alpha = (self.rng.below(5) * 63) as u8;
|
let alpha = (self.rng.below(5) * 63) as u8;
|
||||||
rect(color.alpha(alpha)).add_strong(self.rsc)
|
Kind::Rect { color, alpha }
|
||||||
}
|
}
|
||||||
};
|
})
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn len(&mut self) -> Option<Len> {
|
fn len(&mut self) -> Option<LayoutLen> {
|
||||||
match self.rng.below(4) {
|
match self.rng.below(4) {
|
||||||
0 => Some(Len::px(20.0 + self.rng.below(180) as f32)),
|
0 => Some(LayoutLen::px(20.0 + self.rng.below(180) as f32)),
|
||||||
1 => Some(Len::REST),
|
1 => Some(LayoutLen::LEFTOVER),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn align(&mut self) -> Align {
|
fn align(&mut self) -> Aligns {
|
||||||
let mut axis = || match self.rng.below(4) {
|
let axis = |s: &mut Self| match s.rng.below(4) {
|
||||||
0 => None,
|
0 => None,
|
||||||
1 => Some(AxisAlign::Neg),
|
1 => Some(AxisAlign::NEG),
|
||||||
2 => Some(AxisAlign::Center),
|
2 => Some(AxisAlign::CENTER),
|
||||||
_ => Some(AxisAlign::Pos),
|
_ => Some(AxisAlign::POS),
|
||||||
};
|
};
|
||||||
let (mut x, y) = (axis(), axis());
|
let (x, y) = (axis(self), axis(self));
|
||||||
// Aligning on neither axis is just another transparent wrapper and
|
// Aligning on neither axis leaves the branch unexercised.
|
||||||
// would leave this branch unexercised.
|
match x.is_none() && y.is_none() {
|
||||||
if x.is_none() && y.is_none() {
|
true => [Some(AxisAlign::CENTER), y],
|
||||||
x = Some(AxisAlign::Center);
|
false => [x, y],
|
||||||
}
|
}
|
||||||
Align { x, y }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A declared size over half the tree, kept where a test can change it.
|
/// A declared size over half the tree, kept where a test can change it.
|
||||||
fn sized(&mut self, inner: StrongWidget) -> StrongWidget {
|
fn sized(&mut self, inner: &mut Plan) {
|
||||||
if !self.rng.chance() {
|
let take = self.rng.chance();
|
||||||
return inner;
|
|
||||||
}
|
|
||||||
let idx = self.tree.sized.len();
|
|
||||||
let lens = [self.len(), self.len()];
|
let lens = [self.len(), self.len()];
|
||||||
let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens);
|
if !take || inner.size.is_some() {
|
||||||
let id = SetSize {
|
return;
|
||||||
inner,
|
|
||||||
x: lens[0],
|
|
||||||
y: lens[1],
|
|
||||||
}
|
}
|
||||||
.add(self.rsc);
|
let idx = self.sized;
|
||||||
self.tree.sized.push(id);
|
self.sized += 1;
|
||||||
self.tree.ids.push(id.id());
|
inner.size = Some(self.edits.sizes.get(&idx).copied().unwrap_or(lens));
|
||||||
id.add_strong(self.rsc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn node(&mut self, depth: usize) -> StrongWidget {
|
/// An alignment over some of the tree, kept where a test can change it.
|
||||||
|
fn aligned(&mut self, inner: &mut Plan) {
|
||||||
|
let align = self.align();
|
||||||
|
if inner.align.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let idx = self.aligned;
|
||||||
|
self.aligned += 1;
|
||||||
|
inner.align = Some(self.edits.aligns.get(&idx).copied().unwrap_or(align));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A movable region of its own over some of the tree. What it changes is
|
||||||
|
/// how a move is written and how long a primitive's chain is, neither of
|
||||||
|
/// which any other branch here varies.
|
||||||
|
fn noded(&mut self, inner: &mut Plan) {
|
||||||
|
let take = self.rng.below(4) == 0;
|
||||||
|
if inner.region_node.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let idx = self.nodes;
|
||||||
|
self.nodes += 1;
|
||||||
|
inner.region_node = Some(self.edits.nodes.get(&idx).copied().unwrap_or(take));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offered(&mut self, inner: &mut Plan) {
|
||||||
|
self.sized(inner);
|
||||||
|
self.noded(inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node(&mut self, depth: usize) -> Plan {
|
||||||
if depth == 0 {
|
if depth == 0 {
|
||||||
return self.leaf();
|
return self.leaf();
|
||||||
}
|
}
|
||||||
let positioned = self.rng.below(6);
|
let positioned = self.rng.below(6);
|
||||||
if positioned == 0 {
|
if positioned == 0 {
|
||||||
// Scrolling reads the pixel length of its box, which nothing
|
let mut inner = self.node(depth - 1);
|
||||||
// else here does, and gives its child a box longer than its own.
|
self.offered(&mut inner);
|
||||||
let inner = self.node(depth - 1);
|
|
||||||
let inner = self.sized(inner);
|
|
||||||
let axis = if self.rng.chance() { Axis::X } else { Axis::Y };
|
let axis = if self.rng.chance() { Axis::X } else { Axis::Y };
|
||||||
let id = Scroll::new(inner, axis).add(self.rsc);
|
return Plan::bare(Kind::Scroll {
|
||||||
self.tree.scrolls.push(id);
|
axis,
|
||||||
self.tree.ids.push(id.id());
|
inner: Box::new(inner),
|
||||||
return id.add_strong(self.rsc);
|
});
|
||||||
}
|
}
|
||||||
if positioned == 2 {
|
if positioned == 2 {
|
||||||
// Both sides are grown either way, so a tree that draws one has
|
|
||||||
// the same ids as a tree that draws the other.
|
|
||||||
let probe = self.node(depth - 1);
|
let probe = self.node(depth - 1);
|
||||||
let wide = self.node(depth - 1);
|
let wide = self.node(depth - 1);
|
||||||
let narrow = self.node(depth - 1);
|
let narrow = self.node(depth - 1);
|
||||||
let threshold = self.rng.below(500) as f32;
|
// Drawn either way, so the side a fixed branch takes is still a
|
||||||
let id = Branch {
|
// side the generator chose -- and it consumes the same randomness
|
||||||
probe,
|
// as a measured one, so the two grow the same ids.
|
||||||
wide,
|
let measured = self.rng.below(500) as f32;
|
||||||
narrow,
|
let threshold = match self.edits.fixed_branches {
|
||||||
|
true => f32::MIN,
|
||||||
|
false => measured,
|
||||||
|
};
|
||||||
|
return Plan::bare(Kind::Branch {
|
||||||
|
probe: Box::new(probe),
|
||||||
|
wide: Box::new(wide),
|
||||||
|
narrow: Box::new(narrow),
|
||||||
threshold,
|
threshold,
|
||||||
}
|
});
|
||||||
.add(self.rsc);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
return id.add_strong(self.rsc);
|
|
||||||
}
|
}
|
||||||
if positioned == 1 {
|
if positioned == 1 {
|
||||||
let inner = self.node(depth - 1);
|
// Carries an alignment and makes no widget of its own, so the
|
||||||
let inner = self.sized(inner);
|
// plan for it is the child it aligned.
|
||||||
let id = Aligned {
|
let mut inner = self.node(depth - 1);
|
||||||
inner,
|
self.offered(&mut inner);
|
||||||
align: self.align(),
|
self.aligned(&mut inner);
|
||||||
}
|
return inner;
|
||||||
.add_strong(self.rsc);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
return id;
|
|
||||||
}
|
}
|
||||||
if self.rng.below(4) == 0 {
|
if self.rng.below(4) == 0 {
|
||||||
let inner = self.node(depth - 1);
|
let mut inner = self.node(depth - 1);
|
||||||
let inner = self.sized(inner);
|
self.offered(&mut inner);
|
||||||
// Each side its own, since a padding that is the same all round
|
let side = |s: &mut Self| s.rng.below(24) as i32;
|
||||||
// hides anything that treats one edge differently from another.
|
let padding = [side(self), side(self), side(self), side(self)];
|
||||||
let mut side = || self.rng.below(24) as f32;
|
return Plan::bare(Kind::Pad {
|
||||||
let padding = Padding {
|
padding,
|
||||||
left: side(),
|
inner: Box::new(inner),
|
||||||
right: side(),
|
});
|
||||||
top: side(),
|
|
||||||
bottom: side(),
|
|
||||||
};
|
|
||||||
let id = Pad { padding, inner }.add_strong(self.rsc);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
return id;
|
|
||||||
}
|
}
|
||||||
let grown = 2 + self.rng.below(3);
|
let grown = 2 + self.rng.below(3);
|
||||||
let mut children = Vec::with_capacity(grown);
|
let mut children = Vec::with_capacity(grown);
|
||||||
for _ in 0..grown {
|
for _ in 0..grown {
|
||||||
let child = self.node(depth - 1);
|
let mut child = self.node(depth - 1);
|
||||||
children.push(self.sized(child));
|
self.offered(&mut child);
|
||||||
|
children.push(child);
|
||||||
}
|
}
|
||||||
if self.rng.chance() {
|
if self.rng.chance() {
|
||||||
let id = Stack {
|
return Plan::bare(Kind::Stack { children });
|
||||||
children,
|
|
||||||
size: StackSize::Child(0),
|
|
||||||
}
|
}
|
||||||
.add_strong(self.rsc);
|
let spares: Vec<Plan> = (0..SPARES).map(|_| self.leaf()).collect();
|
||||||
self.tree.ids.push(id.id());
|
let idx = self.spans;
|
||||||
return id;
|
self.spans += 1;
|
||||||
}
|
|
||||||
// Grown either way, so the widget after them has the same id in a
|
|
||||||
// tree that leaves them out as in one that puts them in.
|
|
||||||
let mut spares: Vec<StrongWidget> = (0..SPARES).map(|_| self.leaf()).collect();
|
|
||||||
let idx = self.tree.spans.len();
|
|
||||||
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
|
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
|
||||||
// Highest first, so an index means the same child however many of its
|
let dir = self.rng.below(4);
|
||||||
// neighbours are going too.
|
// A row takes the height it is given rather than its tallest child,
|
||||||
let mut detach = edit.detach.clone();
|
// which is a rule beside it. Derived from an existing choice and
|
||||||
detach.sort_unstable();
|
// consuming no randomness: a seed must keep growing the same tree
|
||||||
for j in detach.into_iter().rev() {
|
// when the generator gains another configuration.
|
||||||
if j < children.len() {
|
let gap = self.rng.below(3) as i32 * 4;
|
||||||
self.tree.detached.push(children.remove(j));
|
let grown: Vec<usize> = (0..children.len()).collect();
|
||||||
}
|
let order = span_edited(&grown, children.len(), spares.len(), &edit);
|
||||||
}
|
Plan::bare(Kind::Span {
|
||||||
let attach = edit.attach.min(spares.len());
|
|
||||||
children.extend(spares.drain(..attach));
|
|
||||||
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][self.rng.below(4)];
|
|
||||||
let id = Span {
|
|
||||||
children,
|
|
||||||
dir,
|
dir,
|
||||||
gap: self.rng.below(3) as f32 * 4.0,
|
gap,
|
||||||
|
children,
|
||||||
|
spares,
|
||||||
|
order,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a plan's widgets in the order it describes them, so two builds of
|
||||||
|
/// one plan line up index for index and their boxes can be compared.
|
||||||
|
pub fn build<Rsc: UiRsc + 'static>(rsc: &mut Rsc, plan: &Plan) -> (StrongWidget, Tree) {
|
||||||
|
let mut build = Build {
|
||||||
|
rsc,
|
||||||
|
tree: Tree::default(),
|
||||||
|
};
|
||||||
|
let root = build.node(plan);
|
||||||
|
(root, build.tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Build<'a, Rsc> {
|
||||||
|
rsc: &'a mut Rsc,
|
||||||
|
tree: Tree,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
||||||
|
fn node(&mut self, plan: &Plan) -> StrongWidget {
|
||||||
|
let built = self.kind(&plan.kind);
|
||||||
|
let id = built.id();
|
||||||
|
if let Some(lens) = plan.size {
|
||||||
|
self.rsc
|
||||||
|
.ui_mut()
|
||||||
|
.widgets
|
||||||
|
.set_size_rules(id, lens[0], lens[1]);
|
||||||
|
self.tree.sized.push(id);
|
||||||
|
}
|
||||||
|
if let Some(align) = plan.align {
|
||||||
|
let widgets = &mut self.rsc.ui_mut().widgets;
|
||||||
|
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
|
||||||
|
widgets.set_alignment(id, axis, align.unwrap_or_default());
|
||||||
|
}
|
||||||
|
self.tree.aligned.push(id);
|
||||||
|
}
|
||||||
|
if let Some(take) = plan.region_node {
|
||||||
|
self.rsc.ui_mut().widgets.set_region_node(id, take);
|
||||||
|
self.tree.nodes.push(id);
|
||||||
|
}
|
||||||
|
built
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&mut self, kind: &Kind) -> StrongWidget {
|
||||||
|
let id: StrongWidget = match kind {
|
||||||
|
Kind::Wrapped => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
|
||||||
|
Kind::OneLine => wtext("one line, overflowing whatever it is given")
|
||||||
|
.size(16)
|
||||||
|
.wrap(false)
|
||||||
|
.add_strong(self.rsc),
|
||||||
|
Kind::Rect { color, alpha } => rect(COLORS[*color].alpha(*alpha)).add_strong(self.rsc),
|
||||||
|
Kind::Scroll { axis, inner } => {
|
||||||
|
let inner = self.node(inner);
|
||||||
|
let id = Scroll::new(inner, *axis).add(self.rsc);
|
||||||
|
self.tree.scrolls.push(id);
|
||||||
|
self.tree.ids.push(id.id());
|
||||||
|
return id.add_strong(self.rsc);
|
||||||
|
}
|
||||||
|
Kind::Branch {
|
||||||
|
probe,
|
||||||
|
wide,
|
||||||
|
narrow,
|
||||||
|
threshold,
|
||||||
|
} => {
|
||||||
|
let probe = self.node(probe);
|
||||||
|
let wide = self.node(wide);
|
||||||
|
let narrow = self.node(narrow);
|
||||||
|
let id = Branch {
|
||||||
|
probe,
|
||||||
|
wide,
|
||||||
|
narrow,
|
||||||
|
threshold: *threshold,
|
||||||
}
|
}
|
||||||
.add(self.rsc);
|
.add(self.rsc);
|
||||||
self.tree.ids.push(id.id());
|
self.tree.ids.push(id.id());
|
||||||
|
return id.add_strong(self.rsc);
|
||||||
|
}
|
||||||
|
Kind::Pad { padding, inner } => {
|
||||||
|
let inner = self.node(inner);
|
||||||
|
let [left, right, top, bottom] = padding.map(Px::from_int);
|
||||||
|
let padding = Padding {
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
top,
|
||||||
|
bottom,
|
||||||
|
};
|
||||||
|
Pad { padding, inner }.add_strong(self.rsc)
|
||||||
|
}
|
||||||
|
Kind::Stack { children } => {
|
||||||
|
let children = children.iter().map(|c| self.node(c)).collect();
|
||||||
|
Stack {
|
||||||
|
children,
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
}
|
||||||
|
.add_strong(self.rsc)
|
||||||
|
}
|
||||||
|
Kind::Span {
|
||||||
|
dir,
|
||||||
|
gap,
|
||||||
|
children,
|
||||||
|
spares,
|
||||||
|
order,
|
||||||
|
} => {
|
||||||
|
let grown = children.len();
|
||||||
|
// Every one of them is made, in this order, whether or not
|
||||||
|
// the span ends up holding it.
|
||||||
|
let made: Vec<StrongWidget> = children
|
||||||
|
.iter()
|
||||||
|
.chain(spares)
|
||||||
|
.map(|c| self.node(c))
|
||||||
|
.collect();
|
||||||
|
let mut left: Vec<Option<StrongWidget>> = made.into_iter().map(Some).collect();
|
||||||
|
let children: Vec<StrongWidget> = order
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&i| left.get_mut(i).and_then(Option::take))
|
||||||
|
.collect();
|
||||||
|
// What the span does not hold is still held here: dropping
|
||||||
|
// the last share of a widget frees its id for the next one
|
||||||
|
// to be given, which puts two trees out of step.
|
||||||
|
let spares: Vec<StrongWidget> = left.into_iter().flatten().collect();
|
||||||
|
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][*dir % 4];
|
||||||
|
let id = Span {
|
||||||
|
children,
|
||||||
|
dir,
|
||||||
|
gap: Px::from_int(*gap),
|
||||||
|
}
|
||||||
|
.add(self.rsc);
|
||||||
|
if dir.axis == Axis::X {
|
||||||
|
self.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rules(id, None, Some(LayoutLen::rel(1.0)));
|
||||||
|
}
|
||||||
|
self.tree.ids.push(id.id());
|
||||||
self.tree.spans.push(Spanned { id, spares, grown });
|
self.tree.spans.push(Spanned { id, spares, grown });
|
||||||
id.add_strong(self.rsc)
|
return id.add_strong(self.rsc);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.tree.ids.push(id.id());
|
||||||
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-6
@@ -11,12 +11,8 @@ impl Widget for Image {
|
|||||||
Size::px(self.handle.size())
|
Size::px(self.handle.size())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size_hint(&self, axis: Axis) -> Option<Len> {
|
fn size_hint(&self, axis: Axis) -> Option<LayoutLen> {
|
||||||
Some(Len::px(self.handle.size().axis(axis)))
|
Some(LayoutLen::px(self.handle.size().axis(axis)))
|
||||||
}
|
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-6
@@ -7,11 +7,12 @@ pub struct Masked {
|
|||||||
impl Widget for Masked {
|
impl Widget for Masked {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
painter.set_mask(painter.region());
|
painter.set_mask(painter.region());
|
||||||
painter.widget(&self.inner).size()
|
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
|
||||||
/// It clips to the box it was given, not to the part its child used.
|
// neither take less of one nor honestly ask for more. Passing the
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
// inner size up instead asks to be placed at a length it does not
|
||||||
OnResize::Redraw
|
// draw, and the framework would place the drawing it clipped away.
|
||||||
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,15 +1,15 @@
|
|||||||
mod image;
|
mod image;
|
||||||
mod mask;
|
mod mask;
|
||||||
mod position;
|
mod position;
|
||||||
mod ptr;
|
|
||||||
mod rect;
|
mod rect;
|
||||||
mod text;
|
mod text;
|
||||||
mod trait_fns;
|
mod trait_fns;
|
||||||
|
mod wrapper;
|
||||||
|
|
||||||
pub use image::*;
|
pub use image::*;
|
||||||
pub use mask::*;
|
pub use mask::*;
|
||||||
pub use position::*;
|
pub use position::*;
|
||||||
pub use ptr::*;
|
|
||||||
pub use rect::*;
|
pub use rect::*;
|
||||||
pub use text::*;
|
pub use text::*;
|
||||||
pub use trait_fns::*;
|
pub use trait_fns::*;
|
||||||
|
pub use wrapper::*;
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
pub struct Aligned {
|
|
||||||
pub inner: StrongWidget,
|
|
||||||
pub align: Align,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Aligned {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
||||||
let known = match self.align.tuple() {
|
|
||||||
(Some(_), Some(_)) => painter
|
|
||||||
.known_len(&self.inner, Axis::X, UiRegion::FULL)
|
|
||||||
.zip(painter.known_len(&self.inner, Axis::Y, UiRegion::FULL))
|
|
||||||
.map(|(x, y)| Size { x, y }),
|
|
||||||
(Some(_), None) => painter
|
|
||||||
.known_len(&self.inner, Axis::X, UiRegion::FULL)
|
|
||||||
.map(|x| Size { x, y: Len::REST }),
|
|
||||||
(None, Some(_)) => painter
|
|
||||||
.known_len(&self.inner, Axis::Y, UiRegion::FULL)
|
|
||||||
.map(|y| Size { x: Len::REST, y }),
|
|
||||||
(None, None) => Some(Size::REST),
|
|
||||||
};
|
|
||||||
// Drawn where it may be too big only when the aligned axes are not
|
|
||||||
// already known, then given its aligned box once its size is known.
|
|
||||||
let had_size = known.is_some();
|
|
||||||
let size = known.unwrap_or_else(|| painter.place(&self.inner, UiRegion::FULL).size());
|
|
||||||
let region = match self.align.tuple() {
|
|
||||||
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
|
|
||||||
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL),
|
|
||||||
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
|
|
||||||
(None, None) => UiRegion::FULL,
|
|
||||||
};
|
|
||||||
let placed = painter.place(&self.inner, region).size();
|
|
||||||
if had_size { placed } else { size }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The aligned box is a fraction of its own, so the child keeps its
|
|
||||||
/// length and stays against the edge it was aligned to.
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,8 +12,4 @@ impl Widget for LayerOffset {
|
|||||||
}
|
}
|
||||||
painter.widget(&self.inner).size()
|
painter.widget(&self.inner).size()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
pub struct MaxSize {
|
|
||||||
pub inner: StrongWidget,
|
|
||||||
pub x: Option<Len>,
|
|
||||||
pub y: Option<Len>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for MaxSize {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
||||||
let child = painter.widget(&self.inner).size();
|
|
||||||
let output = painter.output_size();
|
|
||||||
Size {
|
|
||||||
x: capped(child.x, self.x, output.x),
|
|
||||||
y: capped(child.y, self.y, output.y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn capped(len: Len, max: Option<Len>, output: f32) -> Len {
|
|
||||||
match max {
|
|
||||||
Some(max) if len.apply_rest().to_px(output) > max.apply_rest().to_px(output) => max,
|
|
||||||
_ => len,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,13 @@
|
|||||||
mod align;
|
|
||||||
mod layer;
|
mod layer;
|
||||||
mod max_size;
|
|
||||||
mod offset;
|
mod offset;
|
||||||
mod pad;
|
mod pad;
|
||||||
mod scroll;
|
mod scroll;
|
||||||
mod set_size;
|
|
||||||
mod span;
|
mod span;
|
||||||
mod stack;
|
mod stack;
|
||||||
|
|
||||||
pub use align::*;
|
|
||||||
pub use layer::*;
|
pub use layer::*;
|
||||||
pub use max_size::*;
|
|
||||||
pub use offset::*;
|
pub use offset::*;
|
||||||
pub use pad::*;
|
pub use pad::*;
|
||||||
pub use scroll::*;
|
pub use scroll::*;
|
||||||
pub use set_size::*;
|
|
||||||
pub use span::*;
|
pub use span::*;
|
||||||
pub use stack::*;
|
pub use stack::*;
|
||||||
@@ -10,8 +10,4 @@ impl Widget for Offset {
|
|||||||
let region = UiRegion::FULL.offset(self.amt);
|
let region = UiRegion::FULL.offset(self.amt);
|
||||||
painter.widget_within(&self.inner, region).size()
|
painter.widget_within(&self.inner, region).size()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+30
-32
@@ -7,45 +7,45 @@ pub struct Pad {
|
|||||||
|
|
||||||
impl Widget for Pad {
|
impl Widget for Pad {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
// The inner's own alignment, not the near edge. This reports the
|
||||||
|
// 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.
|
||||||
let inner = painter
|
let inner = painter
|
||||||
.widget_within(&self.inner, self.padding.region())
|
.widget_within(&self.inner, self.padding.region())
|
||||||
.size();
|
.size();
|
||||||
Size {
|
Size {
|
||||||
x: Len {
|
x: LayoutLen {
|
||||||
px: inner.x.px + self.padding.left + self.padding.right,
|
px: inner.x.px + self.padding.left + self.padding.right,
|
||||||
..inner.x
|
..inner.x
|
||||||
},
|
},
|
||||||
y: Len {
|
y: LayoutLen {
|
||||||
px: inner.y.px + self.padding.top + self.padding.bottom,
|
px: inner.y.px + self.padding.top + self.padding.bottom,
|
||||||
..inner.y
|
..inner.y
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The padding is an offset from each edge, so a longer box pads the same
|
|
||||||
/// amount and the child takes the rest.
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Padding {
|
pub struct Padding {
|
||||||
pub left: f32,
|
pub left: Px,
|
||||||
pub right: f32,
|
pub right: Px,
|
||||||
pub top: f32,
|
pub top: Px,
|
||||||
pub bottom: f32,
|
pub bottom: Px,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Padding {
|
impl Padding {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
left: 0.0,
|
left: Px::ZERO,
|
||||||
right: 0.0,
|
right: Px::ZERO,
|
||||||
top: 0.0,
|
top: Px::ZERO,
|
||||||
bottom: 0.0,
|
bottom: Px::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn uniform(amt: impl UiNum) -> Self {
|
pub fn uniform(amt: impl UiNum) -> Self {
|
||||||
let amt = amt.to_f32();
|
let amt = Px::from_num(amt);
|
||||||
Self {
|
Self {
|
||||||
left: amt,
|
left: amt,
|
||||||
right: amt,
|
right: amt,
|
||||||
@@ -62,71 +62,69 @@ impl Padding {
|
|||||||
region
|
region
|
||||||
}
|
}
|
||||||
pub fn x(amt: impl UiNum) -> Self {
|
pub fn x(amt: impl UiNum) -> Self {
|
||||||
let amt = amt.to_f32();
|
let amt = Px::from_num(amt);
|
||||||
Self {
|
Self {
|
||||||
left: amt,
|
left: amt,
|
||||||
right: amt,
|
right: amt,
|
||||||
top: 0.0,
|
..Self::ZERO
|
||||||
bottom: 0.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn y(amt: impl UiNum) -> Self {
|
pub fn y(amt: impl UiNum) -> Self {
|
||||||
let amt = amt.to_f32();
|
let amt = Px::from_num(amt);
|
||||||
Self {
|
Self {
|
||||||
left: 0.0,
|
|
||||||
right: 0.0,
|
|
||||||
top: amt,
|
top: amt,
|
||||||
bottom: amt,
|
bottom: amt,
|
||||||
|
..Self::ZERO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn top(amt: impl UiNum) -> Self {
|
pub fn top(amt: impl UiNum) -> Self {
|
||||||
let mut s = Self::ZERO;
|
let mut s = Self::ZERO;
|
||||||
s.top = amt.to_f32();
|
s.top = Px::from_num(amt);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bottom(amt: impl UiNum) -> Self {
|
pub fn bottom(amt: impl UiNum) -> Self {
|
||||||
let mut s = Self::ZERO;
|
let mut s = Self::ZERO;
|
||||||
s.bottom = amt.to_f32();
|
s.bottom = Px::from_num(amt);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left(amt: impl UiNum) -> Self {
|
pub fn left(amt: impl UiNum) -> Self {
|
||||||
let mut s = Self::ZERO;
|
let mut s = Self::ZERO;
|
||||||
s.left = amt.to_f32();
|
s.left = Px::from_num(amt);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn right(amt: impl UiNum) -> Self {
|
pub fn right(amt: impl UiNum) -> Self {
|
||||||
let mut s = Self::ZERO;
|
let mut s = Self::ZERO;
|
||||||
s.right = amt.to_f32();
|
s.right = Px::from_num(amt);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_top(mut self, amt: impl UiNum) -> Self {
|
pub fn with_top(mut self, amt: impl UiNum) -> Self {
|
||||||
self.top = amt.to_f32();
|
self.top = Px::from_num(amt);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_bottom(mut self, amt: impl UiNum) -> Self {
|
pub fn with_bottom(mut self, amt: impl UiNum) -> Self {
|
||||||
self.bottom = amt.to_f32();
|
self.bottom = Px::from_num(amt);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_left(mut self, amt: impl UiNum) -> Self {
|
pub fn with_left(mut self, amt: impl UiNum) -> Self {
|
||||||
self.left = amt.to_f32();
|
self.left = Px::from_num(amt);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_right(mut self, amt: impl UiNum) -> Self {
|
pub fn with_right(mut self, amt: impl UiNum) -> Self {
|
||||||
self.right = amt.to_f32();
|
self.right = Px::from_num(amt);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: UiNum> From<T> for Padding {
|
impl<T: UiNum> From<T> for Padding {
|
||||||
fn from(amt: T) -> Self {
|
fn from(amt: T) -> Self {
|
||||||
Self::uniform(amt.to_f32())
|
Self::uniform(amt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,39 +3,73 @@ use crate::prelude::*;
|
|||||||
pub struct Scroll {
|
pub struct Scroll {
|
||||||
inner: StrongWidget,
|
inner: StrongWidget,
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
amt: f32,
|
amt: Px,
|
||||||
snap_end: bool,
|
snap_end: bool,
|
||||||
container_len: f32,
|
container_len: Px,
|
||||||
content_len: f32,
|
content_len: Px,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Scroll {
|
impl Widget for Scroll {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let output_len = painter.output_len(self.axis);
|
let container_len = painter.px_len(self.axis);
|
||||||
// Its size is its content's, whatever box that is scrolled within.
|
|
||||||
let container_len = UiScalar::px(painter.px_len_for_draw(self.axis));
|
|
||||||
// Draw in the whole container only when its scrolling-axis length is
|
// Draw in the whole container only when its scrolling-axis length is
|
||||||
// not already known, then place it at the scrolled offset.
|
// not already known, then draw it at the scrolled offset.
|
||||||
let known_len = painter.known_len(&self.inner, self.axis, UiRegion::FULL);
|
let whole = UiRegion::FULL;
|
||||||
let measured = known_len.is_none();
|
let answer_len = match painter.known_len(&self.inner, self.axis, whole, whole.size()) {
|
||||||
let child = measured.then(|| painter.place(&self.inner, UiRegion::FULL).size());
|
Some(len) => len,
|
||||||
let content_len = known_len
|
None => painter.widget(&self.inner).size().axis(self.axis),
|
||||||
.unwrap_or_else(|| child.unwrap().axis(self.axis))
|
};
|
||||||
.apply_rest()
|
let content = answer_len.apply_leftover();
|
||||||
.within_len(container_len)
|
self.container_len = container_len;
|
||||||
.to_px(output_len);
|
self.content_len = content.to_px(container_len);
|
||||||
self.container_len = container_len.to_px(output_len);
|
|
||||||
self.content_len = content_len;
|
|
||||||
|
|
||||||
if self.snap_end {
|
if self.snap_end {
|
||||||
self.amt = self.content_len - self.container_len;
|
self.amt = self.content_len - self.container_len;
|
||||||
}
|
}
|
||||||
self.update_amt();
|
self.update_amt();
|
||||||
|
let align = painter.alignment().axis(self.axis);
|
||||||
|
// Content of a fixed length that fits sits at the start of any box it
|
||||||
|
// fits in -- but only anchored there. Anywhere else it is a part of
|
||||||
|
// the room left over, so it moves with every length the box takes and
|
||||||
|
// the drawing holds for that length alone. One scrolled part way sits
|
||||||
|
// where it is until the box shrinks past what is left of it. Kept to
|
||||||
|
// the end, it moves with every length.
|
||||||
|
let fixed_len = content.rel == Rel::ZERO;
|
||||||
|
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG {
|
||||||
|
painter.holds(self.axis, self.content_len..=Px::MAX);
|
||||||
|
} else if fixed_len && !self.snap_end {
|
||||||
|
let left = self.content_len - self.amt;
|
||||||
|
painter.holds(self.axis, Px::MIN..=left);
|
||||||
|
}
|
||||||
|
|
||||||
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
|
// 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);
|
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
||||||
let placed = painter.place(&self.inner, region).size();
|
}
|
||||||
child.unwrap_or(placed)
|
painter.widget_at(&self.inner, region, region.size(), [true; 2]);
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,22 +78,24 @@ impl Scroll {
|
|||||||
Self {
|
Self {
|
||||||
inner,
|
inner,
|
||||||
axis,
|
axis,
|
||||||
amt: 0.0,
|
amt: Px::ZERO,
|
||||||
snap_end: true,
|
snap_end: true,
|
||||||
container_len: 0.0,
|
container_len: Px::ZERO,
|
||||||
content_len: 0.0,
|
content_len: Px::ZERO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_amt(&mut self) {
|
pub fn update_amt(&mut self) {
|
||||||
self.amt = self.amt.max(0.0);
|
self.amt = self.amt.max(Px::ZERO);
|
||||||
let len = (self.content_len - self.container_len).max(0.0);
|
let len = (self.content_len - self.container_len).max(Px::ZERO);
|
||||||
self.amt = self.amt.min(len);
|
self.amt = self.amt.min(len);
|
||||||
self.snap_end = self.amt == len;
|
self.snap_end = self.amt == len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scrolled by a distance the platform measures, which is the last place
|
||||||
|
/// a wheel notch or a finger is a float.
|
||||||
pub fn scroll(&mut self, amt: f32) {
|
pub fn scroll(&mut self, amt: f32) {
|
||||||
self.amt -= amt;
|
self.amt -= Px::from_f32(amt);
|
||||||
self.update_amt();
|
self.update_amt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
pub struct SetSize {
|
|
||||||
pub inner: StrongWidget,
|
|
||||||
pub x: Option<Len>,
|
|
||||||
pub y: Option<Len>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for SetSize {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
||||||
// A declared length is what the child gets, whatever box this widget
|
|
||||||
// was offered before its parent knew that. Measuring it anywhere else
|
|
||||||
// asks about a box it will not have, and the answer on the other axis
|
|
||||||
// is taken under that: a wrapping text measured in the whole width
|
|
||||||
// reports one line, and nothing revisits it once the real width
|
|
||||||
// arrives.
|
|
||||||
let mut region = UiRegion::FULL;
|
|
||||||
for (axis, len) in [(Axis::X, self.x), (Axis::Y, self.y)] {
|
|
||||||
if let Some(len) = len {
|
|
||||||
let span = region.axis_mut(axis);
|
|
||||||
span.end = span.start + len.apply_rest();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let child = painter.widget_within(&self.inner, region).size();
|
|
||||||
Size {
|
|
||||||
x: self.x.unwrap_or(child.x),
|
|
||||||
y: self.y.unwrap_or(child.y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A declared axis is known without looking at the child, which is what
|
|
||||||
/// lets a span lay out around `.height(rest(1))` without drawing it.
|
|
||||||
fn size_hint(&self, axis: Axis) -> Option<Len> {
|
|
||||||
match axis {
|
|
||||||
Axis::X => self.x,
|
|
||||||
Axis::Y => self.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+134
-36
@@ -4,78 +4,176 @@ use std::marker::PhantomData;
|
|||||||
pub struct Span {
|
pub struct Span {
|
||||||
pub children: Vec<StrongWidget>,
|
pub children: Vec<StrongWidget>,
|
||||||
pub dir: Dir,
|
pub dir: Dir,
|
||||||
pub gap: f32,
|
pub gap: Px,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Span {
|
impl Widget for Span {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let axis = self.dir.axis;
|
let axis = self.dir.axis;
|
||||||
// A length for every child before any is placed: from its own hint
|
// A length for every child before their final boxes are chosen: from
|
||||||
// where it has one, and from drawing it where it does not.
|
// a hint where one exists, and from drawing otherwise.
|
||||||
let mut cursor = UiScalar::rel_min();
|
let mut cursor = Len::rel_min();
|
||||||
let mut lens = Vec::with_capacity(self.children.len());
|
let mut lens = Vec::with_capacity(self.children.len());
|
||||||
for child in &self.children {
|
for child in &self.children {
|
||||||
let mut span = UiSpan::new(cursor, UiScalar::rel_max());
|
let mut span = UiSpan::new(cursor, Len::rel_max());
|
||||||
if self.dir.sign == Sign::Neg {
|
if self.dir.sign == Sign::Neg {
|
||||||
span.flip();
|
span.flip();
|
||||||
}
|
}
|
||||||
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
||||||
let len = match painter.known_len(child, axis, region) {
|
// Offered the room left from the cursor, because a text has to
|
||||||
|
// wrap at the width actually there, but reporting a fraction of
|
||||||
|
// the whole row: `rel(0.5)` is half the span whatever else is in
|
||||||
|
// it and wherever this child sits among them.
|
||||||
|
let len = match painter.known_len(child, axis, region, UiVec2::FULL_SIZE) {
|
||||||
Some(len) => len,
|
Some(len) => len,
|
||||||
None => painter.place(child, region).len(axis),
|
None => painter
|
||||||
|
.widget_at(child, region, UiVec2::FULL_SIZE, [false; 2])
|
||||||
|
.len(axis),
|
||||||
};
|
};
|
||||||
cursor.px += len.px + self.gap;
|
cursor.px += len.px + self.gap;
|
||||||
cursor.rel += len.rel;
|
cursor.rel += len.rel;
|
||||||
lens.push(len);
|
lens.push(len);
|
||||||
}
|
}
|
||||||
|
|
||||||
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
|
let gaps = self
|
||||||
let total = lens.iter().fold(Len::px(gap), |sum, len| sum + *len);
|
.gap
|
||||||
|
.mul_int(self.children.len().saturating_sub(1) as i32);
|
||||||
|
let total = lens.iter().fold(
|
||||||
|
LayoutLen {
|
||||||
|
px: gaps,
|
||||||
|
..LayoutLen::ZERO
|
||||||
|
},
|
||||||
|
|sum, len| sum + *len,
|
||||||
|
);
|
||||||
|
|
||||||
let mut start = UiScalar::rel_min();
|
// Whether anything is left over is a question in pixels: `rel(0.5)`
|
||||||
let mut ortho = Len::ZERO;
|
// 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
|
||||||
|
};
|
||||||
|
painter.holds(axis, holds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Across itself a span is as long as its longest child -- unless a
|
||||||
|
// rule beside it gives that length outright, and then reading them
|
||||||
|
// answers nothing and makes its size depend on theirs for it. A rule
|
||||||
|
// that only bounds the length does not count: the answer is still
|
||||||
|
// this span's to give.
|
||||||
|
let shrinks = !painter.has_exact_size(!axis);
|
||||||
|
// What the fixed parts and the gaps before here take, which is a sum
|
||||||
|
// of lengths and exact, and how much of the leftover weight is
|
||||||
|
// spoken for. A position is one from the other rather than a step
|
||||||
|
// from the last child: the share of the room is rounded, and taking
|
||||||
|
// each from the one before it would carry every rounding along the
|
||||||
|
// row.
|
||||||
|
let mut fixed = Len::rel_min();
|
||||||
|
let mut taken = Weight::ZERO;
|
||||||
|
let 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) {
|
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
|
||||||
|
{
|
||||||
|
painter.undraw(child);
|
||||||
|
fixed.px += self.gap;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let mut span = UiSpan::FULL;
|
let mut span = UiSpan::FULL;
|
||||||
span.start = start;
|
span.start = start;
|
||||||
if len.rest > 0.0 {
|
if len.leftover > Weight::ZERO && shares {
|
||||||
let offset = UiScalar::new(total.rel, total.px);
|
taken += len.leftover;
|
||||||
let rel_end = UiScalar::rel(len.rest / total.rest);
|
|
||||||
let end = (UiScalar::rel_max() + start) - offset;
|
|
||||||
start = rel_end.within(&start.to(end));
|
|
||||||
}
|
}
|
||||||
start.px += len.px;
|
fixed.px += len.px;
|
||||||
start.rel += len.rel;
|
fixed.rel += len.rel;
|
||||||
|
start = shared(fixed, taken, total.leftover, room);
|
||||||
span.end = start;
|
span.end = start;
|
||||||
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
||||||
if self.dir.sign == Sign::Neg {
|
if self.dir.sign == Sign::Neg {
|
||||||
region.flip(axis);
|
region.flip(axis);
|
||||||
}
|
}
|
||||||
let used = painter.place(child, region).size().axis(!axis);
|
// Along the row this box is the child's own answer, so the answer
|
||||||
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
|
// is not placed in it again; across it the child sits where its
|
||||||
if used.rel > 0.0 || used.rest > 0.0 {
|
// alignment says.
|
||||||
ortho = Len::REST;
|
let placed = painter.widget_at(
|
||||||
} else if ortho.rest == 0.0 {
|
child,
|
||||||
|
region,
|
||||||
|
UiVec2::FULL_SIZE,
|
||||||
|
[axis == Axis::X, axis == Axis::Y],
|
||||||
|
);
|
||||||
|
if shrinks {
|
||||||
|
let used = placed.len(!axis);
|
||||||
|
// Choosing between a fixed and a relative length from the
|
||||||
|
// span's own eventual width admits multiple fixed points.
|
||||||
|
// A scalable child therefore makes Children scalable too;
|
||||||
|
// only fixed children are compared with one another.
|
||||||
|
if used.rel != Rel::ZERO || used.leftover != Weight::ZERO {
|
||||||
|
ortho = LayoutLen::LEFTOVER;
|
||||||
|
} else if ortho.leftover == Weight::ZERO {
|
||||||
ortho.px = ortho.px.max(used.px);
|
ortho.px = ortho.px.max(used.px);
|
||||||
}
|
}
|
||||||
start.px += self.gap;
|
}
|
||||||
|
fixed.px += self.gap;
|
||||||
|
start = shared(fixed, taken, total.leftover, room);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carried whole rather than collapsed to one share: a span that sizes
|
// Carried whole rather than collapsed to one share: a span that sizes
|
||||||
// from its children does not resolve `rest`, it passes the weight up,
|
// from its children does not resolve `leftover`, it passes the weight up,
|
||||||
// so nesting spans divides the same space rather than re-dividing a
|
// so nesting spans divides the same space rather than re-dividing a
|
||||||
// share of it. Four `rest(1)` children under two spans under one span
|
// share of it. Four `leftover(1)` children under two spans under one span
|
||||||
// get a quarter each, which collapsing to `rest(1)` per level does
|
// get a quarter each, which collapsing to `leftover(1)` per level does
|
||||||
// not give. Resolution happens at the nearest ancestor with a length,
|
// not give. Resolution happens at the nearest ancestor with a length,
|
||||||
// and the root always has one.
|
// and the root always has one.
|
||||||
let along = total;
|
let along = total;
|
||||||
|
let ortho = match shrinks {
|
||||||
|
true => ortho,
|
||||||
|
false => LayoutLen::rel(1.0),
|
||||||
|
};
|
||||||
Size::from_axis(axis, along, ortho)
|
Size::from_axis(axis, along, ortho)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Every child is placed in fractions and offsets of the span's own box,
|
/// Where a row has reached: everything fixed before this point, which is a
|
||||||
/// so a longer box holds the same layout and the children follow it.
|
/// sum and exact, plus the share of the room the weights so far are worth,
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
/// which is one rounding wherever it is asked for.
|
||||||
OnResize::Scale
|
fn shared(fixed: Len, taken: Weight, weight: Weight, room: Len) -> Len {
|
||||||
|
if taken == Weight::ZERO {
|
||||||
|
return fixed;
|
||||||
}
|
}
|
||||||
|
fixed + room.scale(Rel::ratio(taken, weight))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Span {
|
impl Span {
|
||||||
@@ -83,12 +181,12 @@ impl Span {
|
|||||||
Self {
|
Self {
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
dir,
|
dir,
|
||||||
gap: 0.0,
|
gap: Px::ZERO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
||||||
self.gap = gap.to_f32();
|
self.gap = Px::from_num(gap);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +202,7 @@ impl Span {
|
|||||||
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
|
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
|
||||||
pub children: Wa,
|
pub children: Wa,
|
||||||
pub dir: Dir,
|
pub dir: Dir,
|
||||||
pub gap: f32,
|
pub gap: Px,
|
||||||
_pd: PhantomData<(State, Tag)>,
|
_pd: PhantomData<(State, Tag)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,13 +228,13 @@ impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
|
|||||||
Self {
|
Self {
|
||||||
children,
|
children,
|
||||||
dir,
|
dir,
|
||||||
gap: 0.0,
|
gap: Px::ZERO,
|
||||||
_pd: PhantomData,
|
_pd: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
||||||
self.gap = gap.to_f32();
|
self.gap = Px::from_num(gap);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,25 +13,45 @@ impl Widget for Stack {
|
|||||||
StackSize::Default => None,
|
StackSize::Default => None,
|
||||||
StackSize::Child(i) => Some(i),
|
StackSize::Child(i) => Some(i),
|
||||||
};
|
};
|
||||||
let mut size = Size::default();
|
// Every child gets the whole of this stack's box, the sizing one
|
||||||
|
// included, and the stack is then handed a box of the length that
|
||||||
|
// child asked for. Not the part of the box that length takes: the
|
||||||
|
// stack's own box becomes that length, and taking the fraction of it
|
||||||
|
// again is the fraction twice -- a child asking for half of a stack
|
||||||
|
// that is already half a row would have a quarter of the row.
|
||||||
|
//
|
||||||
|
// It cannot be told apart by asking whether this box is the answer
|
||||||
|
// yet, either. A drawing has to be a function of the box alone, since
|
||||||
|
// moving the stack into the box it asked for reuses the drawing by
|
||||||
|
// scaling it, and a drawing made a fraction of one box is right in
|
||||||
|
// any other. So: fractions of this box throughout, and the move is
|
||||||
|
// the whole of the difference.
|
||||||
|
let region = UiRegion::FULL;
|
||||||
|
// Whichever child sizes the stack is asked here and not again below,
|
||||||
|
// on the layer it ends up on: a retained drawing belongs to the layer
|
||||||
|
// it was made on, so measuring it anywhere else costs a second
|
||||||
|
// drawing of it. Its box is its own answer, so the answer is not
|
||||||
|
// placed inside it again.
|
||||||
|
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) {
|
||||||
|
Some((i, child)) => {
|
||||||
|
painter.child_layer_at(i);
|
||||||
|
painter
|
||||||
|
.widget_at(child, region, region.size(), [true; 2])
|
||||||
|
.size()
|
||||||
|
}
|
||||||
|
None => Size::LEFTOVER,
|
||||||
|
};
|
||||||
for (i, child) in self.children.iter().enumerate() {
|
for (i, child) in self.children.iter().enumerate() {
|
||||||
match i {
|
|
||||||
0 => painter.child_layer(),
|
|
||||||
_ => painter.next_layer(),
|
|
||||||
}
|
|
||||||
let drawn = painter.widget(child);
|
|
||||||
// Only the child that sizes the stack is read, so the others
|
|
||||||
// changing size does not redraw it.
|
|
||||||
if sizing == Some(i) {
|
if sizing == Some(i) {
|
||||||
size = drawn.size();
|
continue;
|
||||||
}
|
}
|
||||||
|
painter.child_layer_at(i);
|
||||||
|
// A box that owes nothing to this child's own answer: where it
|
||||||
|
// sits in one bigger than itself is its own business.
|
||||||
|
painter.widget_within(child, region);
|
||||||
}
|
}
|
||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
|
|||||||
+3
-8
@@ -35,16 +35,11 @@ impl Widget for Rect {
|
|||||||
thickness: self.thickness,
|
thickness: self.thickness,
|
||||||
inner_radius: self.inner_radius,
|
inner_radius: self.inner_radius,
|
||||||
});
|
});
|
||||||
Size::REST
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size_hint(&self, _: Axis) -> Option<Len> {
|
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||||
Some(Len::REST)
|
Some(LayoutLen::LEFTOVER)
|
||||||
}
|
|
||||||
|
|
||||||
/// Its box is its primitive's own region, so a new one is written there.
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,10 +93,6 @@ impl Widget for TextEdit {
|
|||||||
);
|
);
|
||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, axis: Axis) -> OnResize {
|
|
||||||
self.view.on_resize(axis)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CARET_WIDTH: f32 = 1.0;
|
const CARET_WIDTH: f32 = 1.0;
|
||||||
@@ -280,7 +276,13 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
||||||
let pos = pos - self.text.region().top_left().to_px(size);
|
let pos = pos
|
||||||
|
- self
|
||||||
|
.text
|
||||||
|
.region()
|
||||||
|
.top_left()
|
||||||
|
.to_px(PxVec2::from_f32(size))
|
||||||
|
.to_f32();
|
||||||
let prev_sel = self.text.selection;
|
let prev_sel = self.text.selection;
|
||||||
let prev_hit = self.text.double_hit;
|
let prev_hit = self.text.double_hit;
|
||||||
|
|
||||||
|
|||||||
+23
-27
@@ -47,12 +47,23 @@ impl TextView {
|
|||||||
/// answers under the attrs too, so changing those asks a new question
|
/// answers under the attrs too, so changing those asks a new question
|
||||||
/// rather than invalidating anything.
|
/// rather than invalidating anything.
|
||||||
fn render(&mut self, painter: &mut Painter) -> &RenderedText {
|
fn render(&mut self, painter: &mut Painter) -> &RenderedText {
|
||||||
let width = if self.attrs.wrap {
|
let width = self.attrs.wrap.then(|| painter.px_len(Axis::X));
|
||||||
Some(painter.px_len(Axis::X))
|
// The shaper measures in floats, which is where a glyph advance comes
|
||||||
} else {
|
// from; what it answers goes back on the grid.
|
||||||
None
|
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
|
||||||
painter.render_text(&mut self.buf, &self.attrs, width)
|
// 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.
|
||||||
|
//
|
||||||
|
// The step at or above that longest line rather than the nearest
|
||||||
|
// one, since the shaper measures in floats: the nearest step is
|
||||||
|
// under the line half the time, and a range starting there admits a
|
||||||
|
// box the line does not fit in, where the break is not this one.
|
||||||
|
if let Some(width) = width {
|
||||||
|
painter.holds(Axis::X, Px::ceil_from_f32(text.size.x).min(width)..=width);
|
||||||
|
}
|
||||||
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tex(&self) -> Option<&RenderedText> {
|
pub fn tex(&self) -> Option<&RenderedText> {
|
||||||
@@ -72,28 +83,17 @@ impl TextView {
|
|||||||
|
|
||||||
let tex = self.render(painter);
|
let tex = self.render(painter);
|
||||||
let region = tex.size.align(align);
|
let region = tex.size.align(align);
|
||||||
let size = Size::px(tex.size);
|
// 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));
|
||||||
let within = region.within(&painter.region());
|
let within = region.within(&painter.region());
|
||||||
painter.glyphs(tex, within);
|
painter.glyphs(tex, within);
|
||||||
(region, size)
|
(region, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapping reads the width it is offered, so a wider box reshapes it and
|
|
||||||
/// a taller one does not. Alignment matters too, and separately: glyphs
|
|
||||||
/// anchored to the start of an axis stay put when that extent changes,
|
|
||||||
/// but centred or end-aligned ones move even though the shaping stands.
|
|
||||||
pub fn on_resize(&self, axis: Axis) -> OnResize {
|
|
||||||
let reshapes = axis == Axis::X && self.attrs.wrap;
|
|
||||||
let anchored = match axis {
|
|
||||||
Axis::X => self.align.x,
|
|
||||||
Axis::Y => self.align.y,
|
|
||||||
} == AxisAlign::Neg;
|
|
||||||
match reshapes || !anchored {
|
|
||||||
true => OnResize::Redraw,
|
|
||||||
false => OnResize::Translate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn content(&self) -> String {
|
pub fn content(&self) -> String {
|
||||||
self.buf.text().to_string()
|
self.buf.text().to_string()
|
||||||
}
|
}
|
||||||
@@ -120,10 +120,6 @@ impl Widget for Text {
|
|||||||
self.update_buf();
|
self.update_buf();
|
||||||
self.view.draw(painter).1
|
self.view.draw(painter).1
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, axis: Axis) -> OnResize {
|
|
||||||
self.view.on_resize(axis)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for Text {
|
impl Deref for Text {
|
||||||
|
|||||||
+55
-42
@@ -12,14 +12,23 @@ widget_trait! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn align(self, align: impl Into<Align>) -> impl WidgetFn<Rsc, Aligned> {
|
fn align(self, align: impl Into<Align>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
move |state| Aligned {
|
// An axis left out keeps whatever it had, which is centered unless
|
||||||
inner: self.add_strong(state),
|
// something else set it.
|
||||||
align: align.into(),
|
let align = align.into();
|
||||||
|
move |state| {
|
||||||
|
let id = self.add(state);
|
||||||
|
let widgets = &mut state.ui_mut().widgets;
|
||||||
|
for (axis, align) in [(Axis::X, align.x), (Axis::Y, align.y)] {
|
||||||
|
if let Some(align) = align {
|
||||||
|
widgets.set_alignment(id, axis, align);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn center(self) -> impl WidgetFn<Rsc, Aligned> {
|
fn center(self) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
self.align(Align::CENTER)
|
self.align(Align::CENTER)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,48 +40,46 @@ widget_trait! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, SetSize> {
|
fn region_node(self) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
|
|state| {
|
||||||
|
let id = self.add(state);
|
||||||
|
state.ui_mut().widgets.set_region_node(id, true);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sized(self, size: impl Into<Size>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
let size = size.into();
|
let size = size.into();
|
||||||
move |state| SetSize {
|
move |state| {
|
||||||
inner: self.add_strong(state),
|
let id = self.add(state);
|
||||||
x: Some(size.x),
|
let widgets = &mut state.ui_mut().widgets;
|
||||||
y: Some(size.y),
|
widgets.set_size_rule(id, Axis::X, SizeRule::Exact(size.x));
|
||||||
|
widgets.set_size_rule(id, Axis::Y, SizeRule::Exact(size.y));
|
||||||
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn max_width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
|
fn width(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
let len = len.into();
|
let len = len.into();
|
||||||
move |state| MaxSize {
|
move |state| {
|
||||||
inner: self.add_strong(state),
|
let id = self.add(state);
|
||||||
x: Some(len),
|
state
|
||||||
y: None,
|
.ui_mut()
|
||||||
|
.widgets
|
||||||
|
.set_size_rule(id, Axis::X, SizeRule::Exact(len));
|
||||||
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn max_height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
|
fn height(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
let len = len.into();
|
let len = len.into();
|
||||||
move |state| MaxSize {
|
move |state| {
|
||||||
inner: self.add_strong(state),
|
let id = self.add(state);
|
||||||
x: None,
|
state
|
||||||
y: Some(len),
|
.ui_mut()
|
||||||
}
|
.widgets
|
||||||
}
|
.set_size_rule(id, Axis::Y, SizeRule::Exact(len));
|
||||||
|
id
|
||||||
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
|
|
||||||
let len = len.into();
|
|
||||||
move |state| SetSize {
|
|
||||||
inner: self.add_strong(state),
|
|
||||||
x: Some(len),
|
|
||||||
y: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
|
|
||||||
let len = len.into();
|
|
||||||
move |state| SetSize {
|
|
||||||
inner: self.add_strong(state),
|
|
||||||
x: None,
|
|
||||||
y: Some(len),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +92,9 @@ widget_trait! {
|
|||||||
|
|
||||||
fn scrollable(self) -> impl WidgetIdFn<Rsc, Scroll> where Rsc: HasEvents {
|
fn scrollable(self) -> impl WidgetIdFn<Rsc, Scroll> where Rsc: HasEvents {
|
||||||
move |state| {
|
move |state| {
|
||||||
Scroll::new(self.add_strong(state), Axis::Y)
|
let inner = self.add(state);
|
||||||
|
state.ui_mut().widgets.set_region_node(inner, true);
|
||||||
|
Scroll::new(inner.upgrade(state), Axis::Y)
|
||||||
.on(CursorSense::Scroll, |ctx, rsc| {
|
.on(CursorSense::Scroll, |ctx, rsc| {
|
||||||
let delta = ctx.data.scroll_delta.y * 50.0;
|
let delta = ctx.data.scroll_delta.y * 50.0;
|
||||||
ctx.widget(rsc).scroll(delta);
|
ctx.widget(rsc).scroll(delta);
|
||||||
@@ -125,9 +134,13 @@ widget_trait! {
|
|||||||
|state| self.add(state)
|
|state| self.add(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_ptr(self, ptr: WeakWidget<WidgetPtr>, state: &mut Rsc) {
|
// Named for the type it makes rather than as `wrapped`, which would read
|
||||||
let id = self.add_strong(state);
|
// as the text setting. `widget_trait!` takes no attributes, so what it is
|
||||||
state.ui_mut().widgets[ptr].inner = Some(id);
|
// for is on `Wrapper` itself.
|
||||||
|
fn wrapper(self) -> impl WidgetFn<Rsc, Wrapper> {
|
||||||
|
|state| Wrapper {
|
||||||
|
inner: Some(self.add_strong(state)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use std::marker::Unsize;
|
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.
|
||||||
|
pub struct Wrapper {
|
||||||
pub inner: Option<StrongWidget>,
|
pub inner: Option<StrongWidget>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for WidgetPtr {
|
impl Widget for Wrapper {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
match &self.inner {
|
match &self.inner {
|
||||||
Some(id) => painter.widget(id).size(),
|
Some(id) => painter.widget(id).size(),
|
||||||
@@ -14,7 +22,7 @@ impl Widget for WidgetPtr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetPtr {
|
impl Wrapper {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
@@ -35,7 +43,7 @@ impl WidgetPtr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for WidgetPtr {
|
impl Default for Wrapper {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::empty()
|
Self::empty()
|
||||||
}
|
}
|
||||||
@@ -22,17 +22,17 @@ struct BranchesOnMeasurement {
|
|||||||
impl Widget for BranchesOnMeasurement {
|
impl Widget for BranchesOnMeasurement {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let mut top = UiRegion::FULL;
|
let mut top = UiRegion::FULL;
|
||||||
top.y.end = top.y.start.offset(40.0);
|
top.y.end = top.y.start.offset(Px::from_int(40));
|
||||||
let measured = painter.place(&self.probe, top).len(Axis::X);
|
let measured = painter.widget_within(&self.probe, top).len(Axis::X);
|
||||||
let px = measured.apply_rest().to_px(painter.px_len(Axis::X));
|
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
|
||||||
|
|
||||||
let mut rest = UiRegion::FULL;
|
let mut below = UiRegion::FULL;
|
||||||
rest.y.start = rest.y.start.offset(40.0);
|
below.y.start = below.y.start.offset(Px::from_int(40));
|
||||||
match px > self.threshold {
|
match px > Px::from_f32(self.threshold) {
|
||||||
true => painter.place(&self.wide, rest),
|
true => painter.widget_within(&self.wide, below),
|
||||||
false => painter.place(&self.narrow, rest),
|
false => painter.widget_within(&self.narrow, below),
|
||||||
};
|
};
|
||||||
Size::REST
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//! What a retained drawing costs in accuracy when it is moved instead of made
|
||||||
|
//! again. A subtree's stored regions are the only record of where it is, so a
|
||||||
|
//! move that works from the last answer rather than from the box it is now in
|
||||||
|
//! integrates its own rounding, and nothing later recomputes it. Re-expressing
|
||||||
|
//! each part as the same fraction of the new box is what keeps a long-lived
|
||||||
|
//! layout on the one a cold start produces.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
/// A row of a fixed height under a bar, so changing the bar's height moves the
|
||||||
|
/// row without changing the box it is given: the move path, repeatedly.
|
||||||
|
fn plant(h: &mut Harness, bar_height: f32) -> (WeakWidget<Rect>, WeakWidget<Rect>) {
|
||||||
|
let bar = rect(Color::RED).height(bar_height).add(&mut h.rsc);
|
||||||
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let row = (inner, rect(Color::GREEN)).span(Dir::RIGHT).height(100);
|
||||||
|
h.set_root((bar, row).span(Dir::DOWN));
|
||||||
|
(bar, inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enough moves to pass the 0.05 physical pixels layout treats as the same
|
||||||
|
/// place, for a move that adds an offset to the last answer. Measured on this
|
||||||
|
/// fixture on 2026-09-15: adding the offset to both ends of a span shortened
|
||||||
|
/// the row by 0.071 over this many moves and by 0.712 over ten times as many,
|
||||||
|
/// growing with the count rather than settling. Placing the far end from the
|
||||||
|
/// near one instead left 0.069, because the length is re-derived either way.
|
||||||
|
const MOVES: usize = 20_000;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_subtree_moved_many_times_stays_where_a_cold_layout_puts_it() {
|
||||||
|
let mut warm = Harness::new((640, 900));
|
||||||
|
let (bar, inner) = plant(&mut warm, 40.0);
|
||||||
|
let mut height = 40.0;
|
||||||
|
for step in 0..MOVES {
|
||||||
|
height = 40.0 + (step % 300) as f32 * 0.37;
|
||||||
|
warm.set_len(bar, Axis::Y, height);
|
||||||
|
warm.frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let (_, cold_inner) = plant(&mut cold, height);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
assert_eq!(warm.region(&inner), cold.region(&cold_inner));
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -0,0 +1,725 @@
|
|||||||
|
//! Where a frame puts things, with no window to put them in.
|
||||||
|
|
||||||
|
use iris::harness::{Harness, assert_corners};
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
/// A fixed 100 wide, and the rest of the 400 to its neighbour.
|
||||||
|
fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) {
|
||||||
|
let left = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
let right = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((left, right).span(Dir::RIGHT));
|
||||||
|
(left.id(), right.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_span_gives_each_child_the_width_it_asked_for() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (left, right) = two_rects(&mut h);
|
||||||
|
|
||||||
|
assert_corners!(h, left, (0, 0), (100, 200));
|
||||||
|
assert_corners!(h, right, (100, 0), (400, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A span offers each child the room left after the one before, because a
|
||||||
|
/// text has to wrap at the width actually there, but reads what the child
|
||||||
|
/// reports as a fraction of 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.
|
||||||
|
#[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 drawn there
|
||||||
|
// once more; half of that final box is what its own child takes.
|
||||||
|
assert_corners!(h, nested, (200, 0), (400, 100));
|
||||||
|
assert_corners!(h, inner, (200, 0), (300, 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}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A stack takes its size from one child and gives every child that size, so
|
||||||
|
/// a child asking for half of it is asking for half of what it is itself the
|
||||||
|
/// size of. Once the stack has been placed at the length it reported that
|
||||||
|
/// length is the box, and taking the fraction of it again takes it twice:
|
||||||
|
/// half a row became a quarter, and a further stack around it a further half.
|
||||||
|
/// Nothing pinned it because a pixel is the same length wherever it is taken
|
||||||
|
/// from, so only a share ever shrank -- and warm and cold shrink alike, so no
|
||||||
|
/// oracle saw it either.
|
||||||
|
#[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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same reading through a pad: its inset is the whole box less the
|
||||||
|
/// padding, so half of the inset plus the padding is half the box plus one
|
||||||
|
/// padding, not two.
|
||||||
|
#[test]
|
||||||
|
fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
|
||||||
|
let mut h = Harness::new((400, 100));
|
||||||
|
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
||||||
|
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
|
||||||
|
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
|
||||||
|
// Ruled to the window: a root reporting a fraction of it is otherwise
|
||||||
|
// placed inside it by its own alignment, which is not what is under test.
|
||||||
|
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
|
||||||
|
assert_corners!(h, padded, (0, 0), (210, 100));
|
||||||
|
assert_corners!(h, tail, (210, 0), (310, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let child = rect(Color::RED).height(40).add(&mut h.rsc);
|
||||||
|
let span = (child,).span(Dir::RIGHT).height(rel(1.0)).add(&mut h.rsc);
|
||||||
|
h.set_root(span);
|
||||||
|
|
||||||
|
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_span_reports_its_tallest_fixed_child() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let short = rect(Color::RED).height(40).add(&mut h.rsc);
|
||||||
|
let tall = rect(Color::BLUE).height(70).add(&mut h.rsc);
|
||||||
|
let span = (short, tall).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.set_root(span);
|
||||||
|
|
||||||
|
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::px(70.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resizing_relays_out_against_the_new_output() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (left, right) = two_rects(&mut h);
|
||||||
|
|
||||||
|
h.resize((800, 100));
|
||||||
|
assert!(h.needs_redraw());
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_corners!(h, left, (0, 0), (100, 100));
|
||||||
|
assert_corners!(h, right, (100, 0), (800, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_empty_widget_takes_a_share_of_a_span() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let gap = ().add(&mut h.rsc);
|
||||||
|
let right = rect(Color::BLUE).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((gap, right).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
assert_corners!(h, gap, (0, 0), (300, 200));
|
||||||
|
assert_corners!(h, right, (300, 0), (400, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_child_drawn_twice_moves_once() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
// The span measures a child and then places it; listing it twice would
|
||||||
|
// move it twice. The span's own fixed total is shorter than the window,
|
||||||
|
// so the span is centred in it and everything under it carries that.
|
||||||
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let centered = inner.center().width(200).add(&mut h.rsc);
|
||||||
|
let left = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((left, centered).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, inner, (150, 0), (350, 200));
|
||||||
|
|
||||||
|
h.set_len(left, Axis::X, 150);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_corners!(h, inner, (175, 0), (375, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alignment_accepts_an_arbitrary_fraction_and_changes_at_runtime() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let fixed = rect(Color::BLUE).sized((100, 100)).add(&mut h.rsc);
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_alignment(fixed, Axis::X, AxisAlign::new(0.25));
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_alignment(fixed, Axis::Y, AxisAlign::NEG);
|
||||||
|
h.set_root(fixed);
|
||||||
|
assert_corners!(h, fixed, (75, 0), (175, 100));
|
||||||
|
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_alignment(fixed, Axis::X, AxisAlign::new(0.75));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, fixed, (225, 0), (325, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_resize_lands_where_a_cold_start_would() {
|
||||||
|
let build = |h: &mut Harness| {
|
||||||
|
let para = wtext(
|
||||||
|
"Wrapping shapes one source into as many lines as its container leaves room \
|
||||||
|
for, so the height of a paragraph is an answer rather than a setting.",
|
||||||
|
)
|
||||||
|
.size(20)
|
||||||
|
.wrap(true)
|
||||||
|
.pad(16)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let below = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let root = (para, below).span(Dir::DOWN).pad(12);
|
||||||
|
h.set_root(root);
|
||||||
|
(para, below)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let (cold_para, cold_below) = build(&mut cold);
|
||||||
|
|
||||||
|
let mut resized = Harness::new((1920, 1200));
|
||||||
|
let (para, below) = build(&mut resized);
|
||||||
|
resized.resize((900, 1200));
|
||||||
|
resized.frame();
|
||||||
|
|
||||||
|
assert_eq!(resized.region(¶), cold.region(&cold_para), "paragraph");
|
||||||
|
assert_eq!(resized.region(&below), cold.region(&cold_below), "below");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fixed_box_is_drawn_again_rather_than_stretched() {
|
||||||
|
let mut h = Harness::new((400, 400));
|
||||||
|
// The panel fills a stack sized by its sibling, so it is first asked in
|
||||||
|
// the whole box and then given the shorter one. Reusing it in that fixed
|
||||||
|
// box afterwards would leave it whatever height it happened to have.
|
||||||
|
let panel = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let leaf = rect(Color::RED).height(100).add(&mut h.rsc);
|
||||||
|
let stack = (panel, leaf)
|
||||||
|
.stack()
|
||||||
|
.size(StackSize::Child(1))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(stack.align(Align::TOP));
|
||||||
|
assert_corners!(h, panel, (0, 0), (400, 100));
|
||||||
|
|
||||||
|
h.set_len(leaf, Axis::Y, 250);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_corners!(h, panel, (0, 0), (400, 250));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_moved_subtree_takes_its_children_with_it() {
|
||||||
|
let mut h = Harness::new((400, 400));
|
||||||
|
let first = rect(Color::RED).height(40).add(&mut h.rsc);
|
||||||
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let row = inner.pad(10).height(40).region_node().add(&mut h.rsc);
|
||||||
|
// 80 of fixed rows in a 400 window, so the span takes 80 and sits in the
|
||||||
|
// middle of what it was given.
|
||||||
|
h.set_root((first, row).span(Dir::DOWN));
|
||||||
|
assert_corners!(h, inner, (10, 210), (390, 230));
|
||||||
|
|
||||||
|
h.set_len(first, Axis::Y, 80);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
// The row opted into one movable region, so its descendants follow one
|
||||||
|
// entry rather than having their primitive regions rewritten.
|
||||||
|
assert_corners!(h, inner, (10, 230), (390, 250));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let fixed = rect(Color::BLUE).width(50).add(&mut h.rsc);
|
||||||
|
let leftover = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
|
let panel = (fixed, leftover).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
// Changing the bar's width is the only thing that changes the box the
|
||||||
|
// panel and everything under it was drawn for.
|
||||||
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((bar, panel).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, fixed, (100, 0), (150, 200));
|
||||||
|
assert_corners!(h, leftover, (150, 0), (400, 200));
|
||||||
|
|
||||||
|
h.set_len(bar, Axis::X, 200);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
// The panel's box is 100 shorter, so the fixed child is the same 50 wide
|
||||||
|
// against its new start and the one taking what is left absorbs the change.
|
||||||
|
assert_corners!(h, fixed, (200, 0), (250, 200));
|
||||||
|
assert_corners!(h, leftover, (250, 0), (400, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
// The row is 40 tall whatever happens, which used to make its drawing
|
||||||
|
// impossible to take out of: recovering a fraction of a box needs a
|
||||||
|
// relative extent, and it has none on that axis.
|
||||||
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let row = inner.pad(10).height(40).add(&mut h.rsc);
|
||||||
|
let filler = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
|
// This column is an item in a row, so it takes the width left for it
|
||||||
|
// rather than asking for a full row-width in addition to the bar.
|
||||||
|
let column = (row, filler).span(Dir::DOWN).add(&mut h.rsc);
|
||||||
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((bar, column).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, inner, (110, 10), (390, 30));
|
||||||
|
|
||||||
|
h.set_len(bar, Axis::X, 200);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_corners!(h, inner, (210, 10), (390, 30));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_a_region_node_lengthens_the_chain_and_it_can_be_removed() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let leaf = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let buried = leaf.pad(4).pad(4).pad(4).pad(4).add(&mut h.rsc);
|
||||||
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((bar, buried).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
let move_idx = h.render.active[&leaf.id()].parent_move;
|
||||||
|
assert_eq!(h.render.moves.depth(move_idx), 0, "the window is no entry");
|
||||||
|
|
||||||
|
h.rsc.widgets_mut().set_region_node(buried, true);
|
||||||
|
h.frame();
|
||||||
|
let move_idx = h.render.active[&leaf.id()].parent_move;
|
||||||
|
assert_eq!(
|
||||||
|
h.render.moves.depth(move_idx),
|
||||||
|
1,
|
||||||
|
"the opted-in widget's region alone"
|
||||||
|
);
|
||||||
|
|
||||||
|
h.rsc.widgets_mut().set_region_node(buried, false);
|
||||||
|
h.frame();
|
||||||
|
let move_idx = h.render.active[&leaf.id()].parent_move;
|
||||||
|
assert_eq!(h.render.moves.depth(move_idx), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A span that sizes from its children passes their `leftover` weight up
|
||||||
|
/// than collapsing it to one share, so nesting divides the same space instead
|
||||||
|
/// of re-dividing a share of it.
|
||||||
|
#[test]
|
||||||
|
fn nested_spans_divide_the_space_once_however_deep_the_nesting_is() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (a, b, c, d) = (
|
||||||
|
rect(Color::RED).add(&mut h.rsc),
|
||||||
|
rect(Color::BLUE).add(&mut h.rsc),
|
||||||
|
rect(Color::GREEN).add(&mut h.rsc),
|
||||||
|
rect(Color::WHITE).add(&mut h.rsc),
|
||||||
|
);
|
||||||
|
let left = (a, b).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let right = (c, d).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.set_root((left, right).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
for (i, id) in [a, b, c, d].into_iter().enumerate() {
|
||||||
|
let x = i as f32 * 100.0;
|
||||||
|
assert_corners!(h, id, (x, 0), (x + 100.0, 200));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same space, unevenly nested: weights carried up mean a share is a
|
||||||
|
/// share of the whole, not of whatever branch a widget happens to sit in.
|
||||||
|
///
|
||||||
|
/// Each edge lands on the even division or one step below it, since a share
|
||||||
|
/// is a fraction of the room and a truncating multiply gives up what that
|
||||||
|
/// fraction does not divide. What stays exact is that each share starts
|
||||||
|
/// where the last one ended and the row ends at its own edge.
|
||||||
|
#[test]
|
||||||
|
fn an_uneven_nesting_still_gives_every_share_the_same_length() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (a, b, c, d) = (
|
||||||
|
rect(Color::RED).add(&mut h.rsc),
|
||||||
|
rect(Color::BLUE).add(&mut h.rsc),
|
||||||
|
rect(Color::GREEN).add(&mut h.rsc),
|
||||||
|
rect(Color::WHITE).add(&mut h.rsc),
|
||||||
|
);
|
||||||
|
let one = (a,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let three = (b, c, d).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.set_root((one, three).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
let mut start = Px::ZERO;
|
||||||
|
for (i, id) in [a, b, c, d].into_iter().enumerate() {
|
||||||
|
let got = h.region(&id).expect("widget drew nothing");
|
||||||
|
let even = Px::from_int((i as i32 + 1) * 100);
|
||||||
|
assert_eq!(got.top_left, PxVec2::new(start, Px::ZERO), "share {i}");
|
||||||
|
assert_eq!(got.bot_right.y, Px::from_int(200), "share {i}");
|
||||||
|
assert!(
|
||||||
|
got.bot_right.x == even || got.bot_right.x == even.next_down(),
|
||||||
|
"share {i} ends at {:?}, not {even:?}",
|
||||||
|
got.bot_right.x
|
||||||
|
);
|
||||||
|
start = got.bot_right.x;
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
start,
|
||||||
|
Px::from_int(400),
|
||||||
|
"the row stopped short of its edge"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// However many ways a row is divided, the shares add up to the row: each
|
||||||
|
/// one is the fixed parts before it plus a share of the room, rather than a
|
||||||
|
/// step from where the last one ended, so the roundings do not accumulate
|
||||||
|
/// along it. Chained, two hundred of them ended a step short of the edge.
|
||||||
|
#[test]
|
||||||
|
fn a_row_of_equal_shares_fills_it_exactly() {
|
||||||
|
for n in [2usize, 3, 7, 64, 200] {
|
||||||
|
let mut h = Harness::new((1000, 100));
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
let mut kids: Vec<StrongWidget> = Vec::new();
|
||||||
|
for _ in 0..n {
|
||||||
|
let kid = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
ids.push(kid.id());
|
||||||
|
kids.push(kid.add_strong(&mut h.rsc));
|
||||||
|
}
|
||||||
|
let span = Span {
|
||||||
|
children: kids,
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(span);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
for (i, id) in ids.iter().enumerate() {
|
||||||
|
let at = h.region(id).expect("a share drew nothing").top_left.x;
|
||||||
|
let want = Px::from_f32(1000.0 * (i as f32) / (n as f32));
|
||||||
|
assert!(
|
||||||
|
(at - want).abs() <= Px::STEP,
|
||||||
|
"{n} shares: the {i}th starts at {at:?}, not {want:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let end = h.region(ids.last().unwrap()).unwrap().bot_right.x;
|
||||||
|
assert_eq!(end, Px::from_int(1000), "{n} shares do not reach the edge");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the shader puts an edge: the fraction resolved against the window
|
||||||
|
/// plus the pixel offset, taken to the boundary it composes to within half
|
||||||
|
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
|
||||||
|
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
|
||||||
|
let active = &h.render.active[&id];
|
||||||
|
let region = h.render.moves.resolve(active.parent_move, active.region);
|
||||||
|
let dim = h.size().axis(axis);
|
||||||
|
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
|
||||||
|
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
|
||||||
|
let span = region.axis(axis);
|
||||||
|
(edge(span.start), edge(span.end))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hairline(h: &mut Harness, marks: &mut Vec<WidgetId>) -> StrongWidget {
|
||||||
|
let mark = rect(Color::RED).width(1).add_strong(&mut h.rsc);
|
||||||
|
marks.push(mark.id());
|
||||||
|
mark
|
||||||
|
}
|
||||||
|
|
||||||
|
fn share(h: &mut Harness, inner: StrongWidget, ratio: f32) -> StrongWidget {
|
||||||
|
h.set_len(&inner, Axis::X, LayoutLen::leftover(ratio));
|
||||||
|
inner
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shares in weights no binary fraction lands on, a padding on one branch
|
||||||
|
/// and not the other, so an edge falls near an integer as often as it can.
|
||||||
|
fn hairlines(h: &mut Harness, depth: usize, marks: &mut Vec<WidgetId>) -> StrongWidget {
|
||||||
|
let mut span = Span::empty(Dir::RIGHT);
|
||||||
|
if depth == 0 {
|
||||||
|
let left = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let left = share(h, left, 3.0);
|
||||||
|
span.push(left);
|
||||||
|
let mark = hairline(h, marks);
|
||||||
|
span.push(mark);
|
||||||
|
let right = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let right = share(h, right, 7.0);
|
||||||
|
span.push(right);
|
||||||
|
return span.add_strong(&mut h.rsc);
|
||||||
|
}
|
||||||
|
let first = hairlines(h, depth - 1, marks);
|
||||||
|
let first = share(h, first, 3.0);
|
||||||
|
span.push(first);
|
||||||
|
let second = hairlines(h, depth - 1, marks);
|
||||||
|
let second = Pad {
|
||||||
|
padding: Padding {
|
||||||
|
left: Px::from_int(3),
|
||||||
|
right: Px::from_int(7),
|
||||||
|
top: Px::ZERO,
|
||||||
|
bottom: Px::ZERO,
|
||||||
|
},
|
||||||
|
inner: second,
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
let second = share(h, second, 5.0);
|
||||||
|
span.push(second);
|
||||||
|
span.add_strong(&mut h.rsc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A one-pixel line is a pixel wherever it is drawn. Both edges of a fixed
|
||||||
|
/// length share their box's fraction, so composing the chain moves them
|
||||||
|
/// together and the shader's `floor` cannot round the pixel between them
|
||||||
|
/// away -- only shift it. A separator that disappeared at one window size
|
||||||
|
/// would be a defect no size comparison catches.
|
||||||
|
#[test]
|
||||||
|
fn a_one_pixel_line_keeps_its_pixel_through_a_chain() {
|
||||||
|
let mut h = Harness::new((1920, 1200));
|
||||||
|
let mut marks = Vec::new();
|
||||||
|
let root = hairlines(&mut h, 4, &mut marks);
|
||||||
|
h.state.set_root(root);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(marks.len(), 16);
|
||||||
|
|
||||||
|
for size in [(1920, 1200), (1919, 1201), (997, 1003), (1367, 733)] {
|
||||||
|
h.resize(size);
|
||||||
|
h.frame();
|
||||||
|
for mark in &marks {
|
||||||
|
let (start, end) = drawn_edges(&h, *mark, Axis::X);
|
||||||
|
assert_eq!(end - start, 1.0, "at {size:?}, mark {mark:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A span short of room takes it from its shares, which go to nothing and
|
||||||
|
/// then to nothing wider; the fixed lengths between them keep their pixels.
|
||||||
|
/// Collapsing those to make room would delete a separator the caller asked
|
||||||
|
/// for, which is worse than overflowing.
|
||||||
|
#[test]
|
||||||
|
fn a_span_out_of_room_shrinks_its_shares_and_not_its_fixed_lengths() {
|
||||||
|
let mut h = Harness::new((400, 20));
|
||||||
|
let mut marks = Vec::new();
|
||||||
|
let mut span = Span::empty(Dir::RIGHT);
|
||||||
|
for _ in 0..3 {
|
||||||
|
let share_of = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let share_of = share(&mut h, share_of, 1.0);
|
||||||
|
span.push(share_of);
|
||||||
|
let mark = hairline(&mut h, &mut marks);
|
||||||
|
span.push(mark);
|
||||||
|
}
|
||||||
|
let root = span.add_strong(&mut h.rsc);
|
||||||
|
h.state.set_root(root);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
for width in [400, 10, 3, 1] {
|
||||||
|
h.resize((width, 20));
|
||||||
|
h.frame();
|
||||||
|
for mark in &marks {
|
||||||
|
let (start, end) = drawn_edges(&h, *mark, Axis::X);
|
||||||
|
assert_eq!(end - start, 1.0, "at {width} wide, mark {mark:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_a_pure_leftover_child_disappears_when_nothing_is_left() {
|
||||||
|
let mut h = Harness::new((100, 20));
|
||||||
|
let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
let leftover = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((fixed, leftover).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
assert_corners!(h, fixed, (0, 0), (100, 20));
|
||||||
|
assert_eq!(h.region(&leftover), None);
|
||||||
|
|
||||||
|
// An undrawn child remains a dependency of the span, so making room for
|
||||||
|
// it draws it without rebuilding the tree.
|
||||||
|
h.set_len(fixed, Axis::X, 60);
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, leftover, (60, 0), (100, 20));
|
||||||
|
|
||||||
|
let mut h = Harness::new((100, 20));
|
||||||
|
let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
let mixed = rect(Color::BLUE)
|
||||||
|
.width(LayoutLen::px(20) + LayoutLen::LEFTOVER)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root((fixed, mixed).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
// Pixels and fractions still overflow; only a child whose entire length
|
||||||
|
// is leftover is omitted.
|
||||||
|
assert_corners!(h, mixed, (100, 0), (120, 20));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leftover_children_disappear_at_the_exact_fixed_content_boundary() {
|
||||||
|
let mut h = Harness::new((100, 100));
|
||||||
|
let first = rect(Color::RED).height(90).add(&mut h.rsc);
|
||||||
|
let a = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
|
let b = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let inner = (a, b).span(Dir::DOWN).gap(4).add(&mut h.rsc);
|
||||||
|
h.set_root((first, inner).span(Dir::DOWN));
|
||||||
|
assert!(h.region(&a).is_some());
|
||||||
|
assert!(h.region(&b).is_some());
|
||||||
|
|
||||||
|
h.set_len(first, Axis::Y, 96.0);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert!(h.region(&a).is_none());
|
||||||
|
assert!(h.region(&b).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **A stack child smaller than the stack sits where its own alignment
|
||||||
|
/// says.** `Stack` gives every child the box its sizing child defines and
|
||||||
|
/// used to force the near edge on all of them; that override is owed only to
|
||||||
|
/// the sizing child, which has already placed its own content in the box the
|
||||||
|
/// stack derived from its answer. Every other child is handed a box that owes
|
||||||
|
/// nothing to it, so where it sits in one bigger than itself is its own
|
||||||
|
/// business -- and with the override it could not be aligned at all, which is
|
||||||
|
/// what moved the `tabs` example's counters to the wrong corner.
|
||||||
|
#[test]
|
||||||
|
fn a_stack_child_smaller_than_the_stack_keeps_its_own_alignment() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let big = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let small = rect(Color::RED).sized((50, 50)).add(&mut h.rsc);
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_alignment(small.id(), Axis::X, AxisAlign::POS);
|
||||||
|
let (a, b) = (big.add_strong(&mut h.rsc), small.add_strong(&mut h.rsc));
|
||||||
|
let children: Vec<StrongWidget> = vec![a, b];
|
||||||
|
h.set_root(Stack {
|
||||||
|
children,
|
||||||
|
size: StackSize::Default,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_corners!(h, big, (0, 0), (400, 200));
|
||||||
|
// The far edge on X because it asked for it, the middle on Y because
|
||||||
|
// that is the default.
|
||||||
|
assert_corners!(h, small, (350, 75), (400, 125));
|
||||||
|
}
|
||||||
|
/// Five children of one span, buried under three containers that are each a
|
||||||
|
/// fraction of their parent so no length reaches the window without being
|
||||||
|
/// composed and rounded on the way. Returns each child's drawn width and
|
||||||
|
/// each gap between them, in pixels.
|
||||||
|
fn row_under_fractions(kid: Option<LayoutLen>, gap: f32, box_w: f32) -> (Vec<Px>, Vec<Px>) {
|
||||||
|
let mut h = Harness::new((box_w, 400.0));
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
let mut kids: Vec<StrongWidget> = Vec::new();
|
||||||
|
for _ in 0..5 {
|
||||||
|
let r = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
if let Some(len) = kid {
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(r.id(), Axis::X, SizeRule::Exact(len));
|
||||||
|
}
|
||||||
|
ids.push(r.id());
|
||||||
|
kids.push(r.add_strong(&mut h.rsc));
|
||||||
|
}
|
||||||
|
let span = Span {
|
||||||
|
children: kids,
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::from_f32(gap),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let a = (span.width(rel(0.9)),).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let b = (a.width(rel(0.8)),).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.set_root((b.width(rel(0.7)),).span(Dir::RIGHT));
|
||||||
|
let boxes: Vec<_> = ids
|
||||||
|
.iter()
|
||||||
|
.map(|id| h.region(id).expect("a child drew nothing"))
|
||||||
|
.collect();
|
||||||
|
(
|
||||||
|
boxes.iter().map(|b| b.bot_right.x - b.top_left.x).collect(),
|
||||||
|
boxes
|
||||||
|
.windows(2)
|
||||||
|
.map(|p| p[1].top_left.x - p[0].bot_right.x)
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **A length given in pixels is that many pixels, wherever it ends up.** A
|
||||||
|
/// gap and a declared width compose additively -- `Len::within` adds a part's
|
||||||
|
/// own pixels rather than scaling them, and both ends of a gap carry the same
|
||||||
|
/// fraction, so the multiply that rounds is the same on each -- which is why
|
||||||
|
/// nesting the row inside fractions of fractions cannot move them. Swept over
|
||||||
|
/// 2,100 box widths when this was written and exact at every one; five here,
|
||||||
|
/// including widths that divide badly by five.
|
||||||
|
#[test]
|
||||||
|
fn a_length_in_pixels_is_that_many_pixels_however_it_is_nested() {
|
||||||
|
for box_w in [300.0, 1000.0, 1001.0, 1003.0, 1920.0] {
|
||||||
|
let want = Px::from_int(7);
|
||||||
|
let (_, gaps) = row_under_fractions(None, 7.0, box_w);
|
||||||
|
assert!(
|
||||||
|
gaps.iter().all(|g| *g == want),
|
||||||
|
"box {box_w}: gaps between leftover children are {gaps:?}"
|
||||||
|
);
|
||||||
|
let (widths, gaps) = row_under_fractions(Some(LayoutLen::px(100.0)), 7.0, box_w);
|
||||||
|
assert!(
|
||||||
|
gaps.iter().all(|g| *g == want),
|
||||||
|
"box {box_w}: gaps between fixed children are {gaps:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
widths.iter().all(|w| *w == Px::from_int(100)),
|
||||||
|
"box {box_w}: declared widths came out {widths:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **Children asking for the same share of a row are not the same length**,
|
||||||
|
/// and this pins by how much rather than claiming they are equal. A position
|
||||||
|
/// is the quantity that gets rounded, so the row fills exactly and no two
|
||||||
|
/// children leave a seam; what that costs is a step or two between lengths
|
||||||
|
/// that were asked for identically. Exact composition would shrink the
|
||||||
|
/// spread, not remove it: five equal lengths cannot fill a row whose step
|
||||||
|
/// count is not a multiple of five.
|
||||||
|
#[test]
|
||||||
|
fn equal_shares_differ_by_at_most_two_steps_and_fill_the_row() {
|
||||||
|
for kid in [None, Some(LayoutLen::rel(0.2))] {
|
||||||
|
for box_w in [300.0, 1000.0, 1001.0, 1003.0, 1920.0] {
|
||||||
|
let (widths, gaps) = row_under_fractions(kid, 0.0, box_w);
|
||||||
|
let spread = *widths.iter().max().unwrap() - *widths.iter().min().unwrap();
|
||||||
|
assert!(
|
||||||
|
spread <= Px::from_raw(2),
|
||||||
|
"box {box_w}, {kid:?}: widths {widths:?} spread {spread:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
gaps.iter().all(|g| *g == Px::ZERO),
|
||||||
|
"box {box_w}, {kid:?}: children left seams {gaps:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
//! The tree a seed describes, as a value rather than as widgets.
|
||||||
|
//!
|
||||||
|
//! Two things have to hold for a plan to be worth having. Editing a plan has
|
||||||
|
//! to mean what growing with those edits means, or a scenario reads one thing
|
||||||
|
//! and the oracle another. And reducing a plan has to end, or a shrinker
|
||||||
|
//! searching for the smallest counterexample never returns.
|
||||||
|
|
||||||
|
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, plan};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
fn some_edits(seed: u64, of: &Plan) -> Edits {
|
||||||
|
let mut rng = Rng::new(seed);
|
||||||
|
let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0);
|
||||||
|
let mut of = of.clone();
|
||||||
|
of.walk_mut(&mut |p| {
|
||||||
|
if matches!(p.kind, Kind::Span { .. }) {
|
||||||
|
spans += 1;
|
||||||
|
}
|
||||||
|
sized += p.size.is_some() as usize;
|
||||||
|
aligned += p.align.is_some() as usize;
|
||||||
|
nodes += p.region_node.is_some() as usize;
|
||||||
|
});
|
||||||
|
let pick =
|
||||||
|
|n: usize, rng: &mut Rng| -> Vec<usize> { (0..n).filter(|_| rng.chance()).collect() };
|
||||||
|
Edits {
|
||||||
|
sizes: pick(sized, &mut rng)
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| (i, [Some(LayoutLen::LEFTOVER), None]))
|
||||||
|
.collect(),
|
||||||
|
aligns: pick(aligned, &mut rng)
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| (i, [Some(AxisAlign::POS), None]))
|
||||||
|
.collect(),
|
||||||
|
nodes: pick(nodes, &mut rng)
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| (i, true))
|
||||||
|
.collect(),
|
||||||
|
spans: pick(spans, &mut rng)
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| {
|
||||||
|
(
|
||||||
|
i,
|
||||||
|
SpanEdit {
|
||||||
|
detach: vec![0],
|
||||||
|
attach: 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<HashMap<_, _>>(),
|
||||||
|
fixed_branches: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
/// The two routes to an edited tree are one tree. `plan` resolves edits out
|
||||||
|
/// of the random stream as it draws; `edited` puts them on a tree that
|
||||||
|
/// already exists, which is the only route a shrunk plan has, since no seed
|
||||||
|
/// grows one. A scenario written against either has to read the same.
|
||||||
|
#[test]
|
||||||
|
fn editing_a_plan_is_growing_one_with_those_edits() {
|
||||||
|
for seed in 1..=60 {
|
||||||
|
let bare = plan(seed, 5, &Edits::default());
|
||||||
|
let edits = some_edits(seed, &bare);
|
||||||
|
assert_eq!(
|
||||||
|
bare.edited(&edits),
|
||||||
|
plan(seed, 5, &edits),
|
||||||
|
"seed {seed}: edited and grown-with-edits disagree"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every simplification is strictly smaller, so taking them in turn reaches a
|
||||||
|
/// fixed point instead of circling. A shrinker that can return to a tree it
|
||||||
|
/// has already tried does not stop.
|
||||||
|
#[test]
|
||||||
|
fn every_simplification_of_a_plan_is_smaller_than_it() {
|
||||||
|
for seed in 1..=60 {
|
||||||
|
let tree = plan(seed, 4, &Edits::default());
|
||||||
|
let mut queue = vec![tree];
|
||||||
|
let mut seen = 0;
|
||||||
|
while let Some(node) = queue.pop() {
|
||||||
|
seen += 1;
|
||||||
|
if seen > 400 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for small in node.smaller() {
|
||||||
|
assert!(
|
||||||
|
small.size() <= node.size(),
|
||||||
|
"seed {seed}: a simplification grew from {} to {}",
|
||||||
|
node.size(),
|
||||||
|
small.size()
|
||||||
|
);
|
||||||
|
if small.size() < node.size() {
|
||||||
|
queue.push(small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reducing until nothing reduces ends, and ends at something small enough to
|
||||||
|
/// read rather than at the tree it started from.
|
||||||
|
#[test]
|
||||||
|
fn reducing_a_plan_all_the_way_ends() {
|
||||||
|
for seed in 1..=30 {
|
||||||
|
let mut node = plan(seed, 5, &Edits::default());
|
||||||
|
let grown = node.size();
|
||||||
|
let mut steps = 0;
|
||||||
|
while let Some(next) = node.smaller().into_iter().next() {
|
||||||
|
node = next;
|
||||||
|
steps += 1;
|
||||||
|
assert!(steps < 10_000, "seed {seed}: reducing did not end");
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
node.size() < grown.max(2),
|
||||||
|
"seed {seed}: reduced {grown} widgets to {}",
|
||||||
|
node.size()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
File renamed without changes.
@@ -6,21 +6,21 @@ use iris::harness::{Harness, assert_corners};
|
|||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
|
|
||||||
/// A leaf that counts its draws and reports whatever size it is given, so a
|
/// A leaf that counts its draws and reports whatever size it is given, so a
|
||||||
/// test can see what the retained path skipped.
|
/// test can see what the retained path skipped. One that reads its box in
|
||||||
|
/// pixels has a drawing that holds for that box alone.
|
||||||
struct Counted {
|
struct Counted {
|
||||||
draws: Rc<Cell<usize>>,
|
draws: Rc<Cell<usize>>,
|
||||||
size: Size,
|
size: Size,
|
||||||
dependence: OnResize,
|
reads_box: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Counted {
|
impl Widget for Counted {
|
||||||
fn draw(&mut self, _: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
self.draws.set(self.draws.get() + 1);
|
self.draws.set(self.draws.get() + 1);
|
||||||
self.size
|
if self.reads_box {
|
||||||
|
painter.px_size();
|
||||||
}
|
}
|
||||||
|
self.size
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
self.dependence
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,22 +32,63 @@ impl Counts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn counted(h: &mut Harness, size: Size, dependence: OnResize) -> (WeakWidget<Counted>, Counts) {
|
fn counted(h: &mut Harness, size: Size, reads_box: bool) -> (WeakWidget<Counted>, Counts) {
|
||||||
let draws = Rc::new(Cell::new(0));
|
let draws = Rc::new(Cell::new(0));
|
||||||
let id = Counted {
|
let id = Counted {
|
||||||
draws: draws.clone(),
|
draws: draws.clone(),
|
||||||
size,
|
size,
|
||||||
dependence,
|
reads_box,
|
||||||
}
|
}
|
||||||
.add(&mut h.rsc);
|
.add(&mut h.rsc);
|
||||||
(id, Counts(draws))
|
(id, Counts(draws))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A fixed-width leaf beside one that takes the rest, so changing the first
|
struct Layered {
|
||||||
/// hands the second a different box without the output changing.
|
children: [StrongWidget<Rect>; 2],
|
||||||
fn pair(h: &mut Harness, rest: OnResize) -> (WeakWidget<Counted>, Counts, WidgetId) {
|
_revision: usize,
|
||||||
let (first, _) = counted(h, Size::from((100, 200)), OnResize::Translate);
|
}
|
||||||
let (second, draws) = counted(h, Size::REST, rest);
|
|
||||||
|
impl Widget for Layered {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
painter.child_layer();
|
||||||
|
painter.widget(&self.children[0]);
|
||||||
|
painter.next_layer();
|
||||||
|
painter.widget(&self.children[1]);
|
||||||
|
Size::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_redrawn_layered_widget_keeps_the_layer_it_was_entered_on() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let children = [
|
||||||
|
rect(Color::RED).add_strong(&mut h.rsc),
|
||||||
|
rect(Color::BLUE).add_strong(&mut h.rsc),
|
||||||
|
];
|
||||||
|
let root = Layered {
|
||||||
|
children,
|
||||||
|
_revision: 0,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
|
||||||
|
h.rsc[root]._revision += 1;
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
let label = h.rsc.widgets().label(root.id());
|
||||||
|
let active = h
|
||||||
|
.render
|
||||||
|
.debug(h.rsc.widgets(), label)
|
||||||
|
.find(|active| active.id == root.id())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(active.layer, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A fixed-width leaf beside one that takes what is left over, so changing
|
||||||
|
/// the first hands the second a different box without the output changing.
|
||||||
|
fn pair(h: &mut Harness, reads_box: bool) -> (WeakWidget<Counted>, Counts, WidgetId) {
|
||||||
|
let (first, _) = counted(h, Size::from((100, 200)), false);
|
||||||
|
let (second, draws) = counted(h, Size::LEFTOVER, reads_box);
|
||||||
h.set_root((first, second).span(Dir::RIGHT));
|
h.set_root((first, second).span(Dir::RIGHT));
|
||||||
(first, draws, second.id())
|
(first, draws, second.id())
|
||||||
}
|
}
|
||||||
@@ -55,7 +96,7 @@ fn pair(h: &mut Harness, rest: OnResize) -> (WeakWidget<Counted>, Counts, Widget
|
|||||||
#[test]
|
#[test]
|
||||||
fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
|
fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
let (first, draws, second) = pair(&mut h, OnResize::Scale);
|
let (first, draws, second) = pair(&mut h, false);
|
||||||
let settled = draws.get();
|
let settled = draws.get();
|
||||||
assert_corners!(h, second, (100, 0), (400, 200));
|
assert_corners!(h, second, (100, 0), (400, 200));
|
||||||
|
|
||||||
@@ -70,17 +111,36 @@ fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
|
|||||||
assert_corners!(h, second, (150, 0), (400, 200));
|
assert_corners!(h, second, (150, 0), (400, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn moving_an_ordinary_subtree_remaps_its_mask() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (first, _) = counted(&mut h, Size::from((100, 200)), false);
|
||||||
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let masked = inner.masked().add(&mut h.rsc);
|
||||||
|
h.set_root((first, masked).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
h.rsc[first].size = Size::from((150, 200));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
let active = &h.render.active[&masked.id()];
|
||||||
|
assert_eq!(
|
||||||
|
h.rsc.ui().masks[active.mask.idx()].region,
|
||||||
|
UiRegion::new(UiSpan::new(Len::px(150.0), Len::rel_max()), UiSpan::FULL,)
|
||||||
|
);
|
||||||
|
assert_corners!(h, inner, (150, 0), (400, 200));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
|
fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
let (first, draws, second) = pair(&mut h, OnResize::Redraw);
|
let (first, draws, second) = pair(&mut h, true);
|
||||||
let settled = draws.get();
|
let settled = draws.get();
|
||||||
|
|
||||||
h.rsc[first].size = Size::from((150, 200));
|
h.rsc[first].size = Size::from((150, 200));
|
||||||
h.frame();
|
h.frame();
|
||||||
|
|
||||||
// The preceding fixed child makes the remaining box this child's real
|
// The preceding fixed child makes the remaining box this child's real
|
||||||
// box, so measuring it also draws it in its final place.
|
// box, so measuring it also draws it in its final box.
|
||||||
assert_eq!(draws.get(), settled + 1);
|
assert_eq!(draws.get(), settled + 1);
|
||||||
assert_corners!(h, second, (150, 0), (400, 200));
|
assert_corners!(h, second, (150, 0), (400, 200));
|
||||||
}
|
}
|
||||||
@@ -88,25 +148,30 @@ fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn a_span_child_that_declares_its_length_is_drawn_once() {
|
fn a_span_child_that_declares_its_length_is_drawn_once() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
let (told, told_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
|
let (told, told_draws) = counted(&mut h, Size::from((100, 200)), false);
|
||||||
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
|
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), true);
|
||||||
// The span takes one child's length from its hint and has to draw the
|
// The span takes one child's length from its hint and has to draw the
|
||||||
// other to find out, so only the second is drawn before it is placed.
|
// other to find out, so only the second is drawn before its final box.
|
||||||
let hinted = told.width(100).add(&mut h.rsc);
|
let hinted = told.width(100).add(&mut h.rsc);
|
||||||
h.set_root((hinted, asked).span(Dir::RIGHT));
|
h.set_root((hinted, asked).span(Dir::RIGHT));
|
||||||
|
|
||||||
assert_eq!(told_draws.get(), 1);
|
assert_eq!(told_draws.get(), 1);
|
||||||
|
// Reading its box makes its drawing hold for the measuring box alone,
|
||||||
|
// and it reports less than that box: so it is drawn again in the box its
|
||||||
|
// answer places it in, and once more in the final box the span chooses.
|
||||||
|
// A widget that says what it holds for, as text does, skips the middle
|
||||||
|
// one.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
asked_draws.get(),
|
asked_draws.get(),
|
||||||
2,
|
3,
|
||||||
"drawn to be measured, then again to be placed"
|
"drawn to be measured, in its placed box, then in its final box"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_span_relays_out_when_a_child_it_measured_changes() {
|
fn a_span_relays_out_when_a_child_it_measured_changes() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
let (first, _, second) = pair(&mut h, OnResize::Translate);
|
let (first, _, second) = pair(&mut h, false);
|
||||||
|
|
||||||
h.rsc[first].size = Size::from((250, 200));
|
h.rsc[first].size = Size::from((250, 200));
|
||||||
h.frame();
|
h.frame();
|
||||||
@@ -118,8 +183,8 @@ fn a_span_relays_out_when_a_child_it_measured_changes() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn a_repaint_that_keeps_its_size_does_not_relay_out() {
|
fn a_repaint_that_keeps_its_size_does_not_relay_out() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
let (first, draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
|
let (first, draws) = counted(&mut h, Size::from((100, 200)), false);
|
||||||
let (second, _) = counted(&mut h, Size::REST, OnResize::Translate);
|
let (second, _) = counted(&mut h, Size::LEFTOVER, false);
|
||||||
h.set_root((first, second).span(Dir::RIGHT));
|
h.set_root((first, second).span(Dir::RIGHT));
|
||||||
let settled = draws.get();
|
let settled = draws.get();
|
||||||
|
|
||||||
@@ -132,10 +197,10 @@ fn a_repaint_that_keeps_its_size_does_not_relay_out() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_placed_child_survives_the_next_frame() {
|
fn a_span_child_survives_the_next_frame() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
// Both children declare a length, so the span places them from their hints
|
// Both children declare a length, so the span chooses their boxes from
|
||||||
// rather than drawing them to find out.
|
// hints rather than drawing them to find out.
|
||||||
let top = rect(Color::RED).height(80).add(&mut h.rsc);
|
let top = rect(Color::RED).height(80).add(&mut h.rsc);
|
||||||
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
|
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
|
||||||
h.set_root((top, bottom).span(Dir::DOWN));
|
h.set_root((top, bottom).span(Dir::DOWN));
|
||||||
@@ -158,7 +223,7 @@ impl Widget for FromHint {
|
|||||||
let mut region = UiRegion::FULL;
|
let mut region = UiRegion::FULL;
|
||||||
region.y.end = region.y.start.offset(len.px);
|
region.y.end = region.y.start.offset(len.px);
|
||||||
painter.widget_within(&self.inner, region);
|
painter.widget_within(&self.inner, region);
|
||||||
Size::REST
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,26 +238,31 @@ fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
|
|||||||
h.set_root(parent);
|
h.set_root(parent);
|
||||||
assert_corners!(h, inner, (0, 0), (400, 80));
|
assert_corners!(h, inner, (0, 0), (400, 80));
|
||||||
|
|
||||||
h.rsc[inner].y = Some(Len::px(120));
|
h.set_len(inner, Axis::Y, 120);
|
||||||
h.frame();
|
h.frame();
|
||||||
|
|
||||||
assert_corners!(h, inner, (0, 0), (400, 120));
|
assert_corners!(h, inner, (0, 0), (400, 120));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the output's size, which nothing but its own draw can put right.
|
/// Reads its box's size, which nothing but its own draw can put right.
|
||||||
struct ReadsOutput {
|
struct ReadsBox {
|
||||||
draws: Rc<Cell<usize>>,
|
draws: Rc<Cell<usize>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for ReadsOutput {
|
impl Widget for ReadsBox {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
self.draws.set(self.draws.get() + 1);
|
self.draws.set(self.draws.get() + 1);
|
||||||
Size::px(painter.output_size() / 4.0)
|
Size::from_px(painter.px_size().div_int(4))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the output across one axis only, and says so: its drawing follows
|
/// Reads its box across one axis only, so its drawing holds for a taller
|
||||||
/// a taller box on its own, so only a wider one is worth a draw.
|
/// box on its own and only a wider one is worth a draw.
|
||||||
|
///
|
||||||
|
/// Both of these report a quarter of what they read, without saying that the
|
||||||
|
/// drawing holds there too, so each length they are asked at costs two draws:
|
||||||
|
/// one to answer, and one in the quarter-sized box that answer places them
|
||||||
|
/// in. The counts below are in those pairs.
|
||||||
struct ReadsWidth {
|
struct ReadsWidth {
|
||||||
draws: Rc<Cell<usize>>,
|
draws: Rc<Cell<usize>>,
|
||||||
}
|
}
|
||||||
@@ -200,21 +270,17 @@ struct ReadsWidth {
|
|||||||
impl Widget for ReadsWidth {
|
impl Widget for ReadsWidth {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
self.draws.set(self.draws.get() + 1);
|
self.draws.set(self.draws.get() + 1);
|
||||||
Size::px((painter.output_len(Axis::X) / 4.0, 20.0).into())
|
Size::from_px(PxVec2::new(
|
||||||
}
|
painter.px_len(Axis::X).div_int(4),
|
||||||
|
Px::from_int(20),
|
||||||
fn on_resize(&self, axis: Axis) -> OnResize {
|
))
|
||||||
match axis {
|
|
||||||
Axis::X => OnResize::Redraw,
|
|
||||||
Axis::Y => OnResize::Scale,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_resize_does_not_redraw_what_the_shader_can_move() {
|
fn a_resize_does_not_redraw_what_the_shader_can_move() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Scale);
|
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
|
||||||
h.set_root(leaf);
|
h.set_root(leaf);
|
||||||
let settled = draws.get();
|
let settled = draws.get();
|
||||||
|
|
||||||
@@ -230,13 +296,29 @@ fn a_resize_does_not_redraw_what_the_shader_can_move() {
|
|||||||
assert_corners!(h, leaf, (0, 0), (800, 100));
|
assert_corners!(h, leaf, (0, 0), (800, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_span_ruled_across_itself_moves_its_child_without_redrawing_it() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
|
||||||
|
let span = (leaf,).span(Dir::RIGHT).height(rel(1.0)).add(&mut h.rsc);
|
||||||
|
h.set_root(span);
|
||||||
|
let settled = draws.get();
|
||||||
|
|
||||||
|
h.resize((400, 100));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_eq!(draws.get(), settled);
|
||||||
|
assert_corners!(h, leaf, (0, 0), (400, 100));
|
||||||
|
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
/// The output is the root of the box chain, so a resize is a box that changed
|
/// The output is the root of the box chain, so a resize is a box that changed
|
||||||
/// length and `OnResize` answers for it -- there is not a second rule for the
|
/// length like any other -- there is not a second rule for the window. A
|
||||||
/// window. A drawing that does not scale is redrawn whichever box moved.
|
/// drawing that holds for one length is drawn again whichever box moved.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_resize_redraws_what_does_not_scale() {
|
fn a_resize_redraws_what_does_not_scale() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw);
|
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, true);
|
||||||
h.set_root(leaf);
|
h.set_root(leaf);
|
||||||
let settled = draws.get();
|
let settled = draws.get();
|
||||||
|
|
||||||
@@ -248,10 +330,10 @@ fn a_resize_redraws_what_does_not_scale() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_resize_redraws_what_read_the_output() {
|
fn a_resize_redraws_what_read_its_box() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
let draws = Rc::new(Cell::new(0));
|
let draws = Rc::new(Cell::new(0));
|
||||||
let leaf = ReadsOutput {
|
let leaf = ReadsBox {
|
||||||
draws: draws.clone(),
|
draws: draws.clone(),
|
||||||
}
|
}
|
||||||
.add(&mut h.rsc);
|
.add(&mut h.rsc);
|
||||||
@@ -261,11 +343,11 @@ fn a_resize_redraws_what_read_the_output() {
|
|||||||
h.resize((800, 100));
|
h.resize((800, 100));
|
||||||
h.frame();
|
h.frame();
|
||||||
|
|
||||||
assert_eq!(draws.get(), settled + 1);
|
assert_eq!(draws.get(), settled + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_resize_only_redraws_read_output_axes() {
|
fn a_resize_only_redraws_read_axes() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
let draws = Rc::new(Cell::new(0));
|
let draws = Rc::new(Cell::new(0));
|
||||||
let leaf = ReadsWidth {
|
let leaf = ReadsWidth {
|
||||||
@@ -281,11 +363,14 @@ fn a_resize_only_redraws_read_output_axes() {
|
|||||||
|
|
||||||
h.resize((800, 300));
|
h.resize((800, 300));
|
||||||
h.frame();
|
h.frame();
|
||||||
assert_eq!(draws.get(), settled + 1, "width changes its answer");
|
assert_eq!(draws.get(), settled + 2, "width changes its answer");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A window is measured onto the grid like everything else, so a resize too
|
||||||
|
/// small to reach the next step is not a resize at all -- and one that does
|
||||||
|
/// reach it is, however little of a pixel it is worth.
|
||||||
#[test]
|
#[test]
|
||||||
fn subpixel_resize_changes_accumulate_from_the_last_layout() {
|
fn a_resize_within_one_step_is_not_a_resize() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
let draws = Rc::new(Cell::new(0));
|
let draws = Rc::new(Cell::new(0));
|
||||||
let leaf = ReadsWidth {
|
let leaf = ReadsWidth {
|
||||||
@@ -295,30 +380,36 @@ fn subpixel_resize_changes_accumulate_from_the_last_layout() {
|
|||||||
h.set_root(leaf);
|
h.set_root(leaf);
|
||||||
let settled = draws.get();
|
let settled = draws.get();
|
||||||
|
|
||||||
for width in [400.02, 400.04, 400.05] {
|
// All of these are 400 px to the nearest step.
|
||||||
h.resize((width, 200.0));
|
let step = Px::STEP.to_f32();
|
||||||
|
for part in [0.1, 0.2, 0.3] {
|
||||||
|
h.resize((400.0 + step * part, 200.0));
|
||||||
h.frame();
|
h.frame();
|
||||||
assert_eq!(draws.get(), settled);
|
assert_eq!(draws.get(), settled);
|
||||||
}
|
}
|
||||||
|
|
||||||
h.resize((400.06, 200.0));
|
h.resize((400.0 + step, 200.0));
|
||||||
h.frame();
|
h.frame();
|
||||||
assert_eq!(draws.get(), settled + 1);
|
assert_eq!(draws.get(), settled + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The same for a box that changes because a sibling did: what is compared
|
||||||
|
/// is the length on the grid, and three lengths that land on one step are
|
||||||
|
/// one length.
|
||||||
#[test]
|
#[test]
|
||||||
fn subpixel_box_changes_accumulate_from_the_last_draw() {
|
fn a_box_change_within_one_step_is_not_a_change() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
let (first, draws, _) = pair(&mut h, OnResize::Redraw);
|
let (first, draws, _) = pair(&mut h, true);
|
||||||
let settled = draws.get();
|
let settled = draws.get();
|
||||||
|
|
||||||
for width in [100.02, 100.04, 100.05] {
|
let step = Px::STEP.to_f32();
|
||||||
h.rsc[first].size.x = Len::px(width);
|
for part in [0.1, 0.2, 0.3] {
|
||||||
|
h.rsc[first].size.x = LayoutLen::px(100.0 + step * part);
|
||||||
h.frame();
|
h.frame();
|
||||||
assert_eq!(draws.get(), settled);
|
assert_eq!(draws.get(), settled);
|
||||||
}
|
}
|
||||||
|
|
||||||
h.rsc[first].size.x = Len::px(100.06);
|
h.rsc[first].size.x = LayoutLen::px(100.0 + step);
|
||||||
h.frame();
|
h.frame();
|
||||||
assert_eq!(draws.get(), settled + 1);
|
assert_eq!(draws.get(), settled + 1);
|
||||||
}
|
}
|
||||||
@@ -327,7 +418,7 @@ fn subpixel_box_changes_accumulate_from_the_last_draw() {
|
|||||||
fn reporting_the_same_output_size_does_not_start_a_resize() {
|
fn reporting_the_same_output_size_does_not_start_a_resize() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
let draws = Rc::new(Cell::new(0));
|
let draws = Rc::new(Cell::new(0));
|
||||||
let leaf = ReadsOutput {
|
let leaf = ReadsBox {
|
||||||
draws: draws.clone(),
|
draws: draws.clone(),
|
||||||
}
|
}
|
||||||
.add(&mut h.rsc);
|
.add(&mut h.rsc);
|
||||||
@@ -368,7 +459,7 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
|
|||||||
// Every wrapper up to the outer pad read the size below it, so the outer
|
// Every wrapper up to the outer pad read the size below it, so the outer
|
||||||
// pad is what draws again -- and the span it hands the box to is the same
|
// pad is what draws again -- and the span it hands the box to is the same
|
||||||
// size as before, which is what lets a draw reuse its way past the leaf.
|
// size as before, which is what lets a draw reuse its way past the leaf.
|
||||||
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), OnResize::Redraw);
|
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), true);
|
||||||
let padded = leaf.pad(10).add(&mut h.rsc);
|
let padded = leaf.pad(10).add(&mut h.rsc);
|
||||||
let below = rect(Color::RED).add(&mut h.rsc);
|
let below = rect(Color::RED).add(&mut h.rsc);
|
||||||
h.set_root((padded, below).span(Dir::DOWN).pad(12));
|
h.set_root((padded, below).span(Dir::DOWN).pad(12));
|
||||||
@@ -380,8 +471,8 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
|
|||||||
assert_corners!(h, below, (12, 232), (388, 388));
|
assert_corners!(h, below, (12, 232), (388, 388));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Claims its drawing survives its box changing length, and has a child so
|
/// Reads nothing of its box, so its drawing holds for any length, and has a
|
||||||
/// that the walk looking for what does not has one to reach.
|
/// child so that whatever asks about the subtree has one to reach.
|
||||||
struct Stretchy {
|
struct Stretchy {
|
||||||
inner: StrongWidget,
|
inner: StrongWidget,
|
||||||
draws: Rc<Cell<usize>>,
|
draws: Rc<Cell<usize>>,
|
||||||
@@ -392,10 +483,6 @@ impl Widget for Stretchy {
|
|||||||
self.draws.set(self.draws.get() + 1);
|
self.draws.set(self.draws.get() + 1);
|
||||||
painter.widget(&self.inner).size()
|
painter.widget(&self.inner).size()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -413,7 +500,7 @@ fn stretching_a_subtree_carries_the_children_in_it() {
|
|||||||
let settled = draws.get();
|
let settled = draws.get();
|
||||||
assert_corners!(h, inner, (0, 40), (400, 400));
|
assert_corners!(h, inner, (0, 40), (400, 400));
|
||||||
|
|
||||||
h.rsc[first].y = Some(Len::px(80));
|
h.set_len(first, Axis::Y, 80);
|
||||||
h.frame();
|
h.frame();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -430,14 +517,14 @@ fn a_widened_row_redraws_what_reads_its_length_and_nothing_else() {
|
|||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
// What a transcript row is: something whose shaping depends on the width
|
// What a transcript row is: something whose shaping depends on the width
|
||||||
// it is given, beside something that only has to be the right shape.
|
// it is given, beside something that only has to be the right shape.
|
||||||
let (wraps, wrap_draws) = counted(&mut h, Size::REST, OnResize::Redraw);
|
let (wraps, wrap_draws) = counted(&mut h, Size::LEFTOVER, true);
|
||||||
let (backing, back_draws) = counted(&mut h, Size::REST, OnResize::Scale);
|
let (backing, back_draws) = counted(&mut h, Size::LEFTOVER, false);
|
||||||
let row = (backing, wraps).span(Dir::RIGHT).add(&mut h.rsc);
|
let row = (backing, wraps).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
h.set_root((bar, row).span(Dir::RIGHT));
|
h.set_root((bar, row).span(Dir::RIGHT));
|
||||||
let (settled_wrap, settled_back) = (wrap_draws.get(), back_draws.get());
|
let (settled_wrap, settled_back) = (wrap_draws.get(), back_draws.get());
|
||||||
|
|
||||||
h.rsc[bar].x = Some(Len::px(200));
|
h.set_len(bar, Axis::X, 200);
|
||||||
h.frame();
|
h.frame();
|
||||||
|
|
||||||
// The span reads every child's size, so redrawing one takes the span
|
// The span reads every child's size, so redrawing one takes the span
|
||||||
@@ -455,17 +542,89 @@ fn a_declared_length_child_is_not_redrawn_when_the_box_around_it_grows() {
|
|||||||
// again would be for a width it does not have. The declared width is what
|
// again would be for a width it does not have. The declared width is what
|
||||||
// lets the span say that without drawing it: a width the span learnt by
|
// lets the span say that without drawing it: a width the span learnt by
|
||||||
// drawing the child in its own box is only an answer for that box.
|
// drawing the child in its own box is only an answer for that box.
|
||||||
let (counter, draws) = counted(&mut h, Size::from((80, 200)), OnResize::Redraw);
|
let (counter, draws) = counted(&mut h, Size::from((80, 200)), true);
|
||||||
let fixed = counter.width(80).add(&mut h.rsc);
|
let fixed = counter.width(80).add(&mut h.rsc);
|
||||||
let (rest, _) = counted(&mut h, Size::REST, OnResize::Scale);
|
let (leftover, _) = counted(&mut h, Size::LEFTOVER, false);
|
||||||
let row = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc);
|
let row = (fixed, leftover).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
h.set_root((bar, row).span(Dir::RIGHT));
|
h.set_root((bar, row).span(Dir::RIGHT));
|
||||||
let settled = draws.get();
|
let settled = draws.get();
|
||||||
|
|
||||||
h.rsc[bar].x = Some(Len::px(200));
|
h.set_len(bar, Axis::X, 200);
|
||||||
h.frame();
|
h.frame();
|
||||||
|
|
||||||
assert_eq!(draws.get(), settled, "its own length did not change");
|
assert_eq!(draws.get(), settled, "its own length did not change");
|
||||||
assert_corners!(h, fixed, (200, 0), (280, 200));
|
assert_corners!(h, fixed, (200, 0), (280, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A retained drawing belongs to the layer it was made on: asked for again
|
||||||
|
/// on another one it has to be drawn there, since nothing about its geometry
|
||||||
|
/// says it is in a list that paints at a different moment.
|
||||||
|
#[test]
|
||||||
|
fn a_widget_asked_again_on_another_layer_is_drawn_there() {
|
||||||
|
/// Draws its child on its own layer, then again one layer in -- which is
|
||||||
|
/// what a container measuring a child by drawing it used to do.
|
||||||
|
struct Twice(StrongWidget);
|
||||||
|
|
||||||
|
impl Widget for Twice {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
let size = painter.widget(&self.0).size();
|
||||||
|
painter.child_layer();
|
||||||
|
painter.widget(&self.0);
|
||||||
|
size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (front, draws) = counted(&mut h, Size::from((100, 50)), false);
|
||||||
|
let outer = Twice(front.add_strong(&mut h.rsc)).add(&mut h.rsc);
|
||||||
|
h.set_root(outer);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_ne!(
|
||||||
|
h.render.active[&front.id()].layer,
|
||||||
|
h.render.active[&outer.id()].layer,
|
||||||
|
"the first drawing was kept, on the layer it was measured on"
|
||||||
|
);
|
||||||
|
assert_eq!(draws.get(), 2, "the second ask could not reuse the first");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which is why `Stack` measures the child that sizes it on the layer that
|
||||||
|
/// child draws on: one drawing, above the background it stacks over, rather
|
||||||
|
/// than one on each layer and the wrong one kept.
|
||||||
|
#[test]
|
||||||
|
fn a_stacks_sizing_child_is_drawn_once_where_it_belongs() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let background = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let (front, draws) = counted(&mut h, Size::from((100, 50)), false);
|
||||||
|
let stack = Stack {
|
||||||
|
children: vec![
|
||||||
|
background.add_strong(&mut h.rsc),
|
||||||
|
front.add_strong(&mut h.rsc),
|
||||||
|
],
|
||||||
|
size: StackSize::Child(1),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(stack);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
let layer = |id| h.render.active[&id].layer;
|
||||||
|
assert_ne!(layer(front.id()), layer(stack.id()));
|
||||||
|
assert_ne!(layer(front.id()), layer(background.id()));
|
||||||
|
assert_eq!(draws.get(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget's own mask is not the one it inherited, and a redraw of it
|
||||||
|
/// inherits the second: handing back the first is handing it its own mask to
|
||||||
|
/// set a second time, which `set_mask` asserts against.
|
||||||
|
#[test]
|
||||||
|
fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let masked = inner.masked().add(&mut h.rsc);
|
||||||
|
let other = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((other, masked).span(Dir::RIGHT));
|
||||||
|
h.rsc.widgets_mut().get_dyn_mut(masked.id());
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, inner, (100, 0), (400, 200));
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
//! Scrolling moves content and stops at its ends.
|
||||||
|
|
||||||
|
use iris::harness::{Harness, assert_corners};
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scrollable_enables_a_region_node_but_raw_scroll_does_not() {
|
||||||
|
let mut h = Harness::new((100, 100));
|
||||||
|
let default_child = ().add(&mut h.rsc);
|
||||||
|
let _default = default_child.scrollable().add(&mut h.rsc);
|
||||||
|
assert!(h.rsc.widgets().is_region_node(default_child));
|
||||||
|
h.rsc.widgets_mut().set_region_node(default_child, false);
|
||||||
|
assert!(!h.rsc.widgets().is_region_node(default_child));
|
||||||
|
|
||||||
|
let raw_child = ().add(&mut h.rsc);
|
||||||
|
let _raw = Scroll::new(raw_child.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
||||||
|
assert!(!h.rsc.widgets().is_region_node(raw_child));
|
||||||
|
|
||||||
|
let explicit = ().region_node().add(&mut h.rsc);
|
||||||
|
assert!(h.rsc.widgets().is_region_node(explicit));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_scrollable_child_can_drop_its_region_node() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let top = rect(Color::RED).height(200).add(&mut h.rsc);
|
||||||
|
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
|
||||||
|
let content = (top, bottom).span(Dir::DOWN).add(&mut h.rsc);
|
||||||
|
h.set_root(content.scrollable());
|
||||||
|
h.rsc.widgets_mut().set_region_node(content, false);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
h.move_to((200, 100));
|
||||||
|
h.scroll((0, 1));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert!(!h.rsc.widgets().is_region_node(content));
|
||||||
|
assert_corners!(h, top, (0, -150), (400, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
// Twice the window's height, so there is 200 to scroll.
|
||||||
|
let top = rect(Color::RED).height(200).add(&mut h.rsc);
|
||||||
|
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
|
||||||
|
h.set_root((top, bottom).span(Dir::DOWN).scrollable());
|
||||||
|
h.move_to((200, 100));
|
||||||
|
|
||||||
|
// `Scroll` starts snapped to the end.
|
||||||
|
assert_corners!(h, top, (0, -200), (400, 0));
|
||||||
|
|
||||||
|
// The handler scales a wheel line by 50.
|
||||||
|
h.scroll((0, 1));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, top, (0, -150), (400, 50));
|
||||||
|
|
||||||
|
h.scroll((0, 10));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, top, (0, 0), (400, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that clips to its box may not report more than the box: its
|
||||||
|
/// parent would place the part it cut off, and the framework would put a
|
||||||
|
/// drawing longer than its box somewhere. `Masked` is the second of these
|
||||||
|
/// after `Scroll`, and the assertion in `draw_at` is what says so.
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "clips to"]
|
||||||
|
fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
|
||||||
|
struct Clipper(StrongWidget);
|
||||||
|
|
||||||
|
impl Widget for Clipper {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
painter.set_mask(painter.region());
|
||||||
|
painter.widget(&self.0).size()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut h = Harness::new((100, 100));
|
||||||
|
let tall = rect(Color::RED).height(400).add_strong(&mut h.rsc);
|
||||||
|
let clipper = Clipper(tall).add(&mut h.rsc);
|
||||||
|
h.set_root(clipper);
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,616 @@
|
|||||||
|
//! The smallest trees that laid out differently warm than cold, each shrunk
|
||||||
|
//! by `tests/shrink.rs` from hundreds of widgets. The first two are a cold
|
||||||
|
//! frame that had not settled: a wrapping text shaped at a width it was
|
||||||
|
//! measured in rather than the one it was given. The rest are a widget
|
||||||
|
//! measured again in a box its own answer had decided, where the old answer
|
||||||
|
//! is a fixed point whatever the content now says. The last three are
|
||||||
|
//! neither: one box length, composed two ways, landing either side of the
|
||||||
|
//! boundary that decided whether a child was drawn at all, and two boxes
|
||||||
|
//! reached through a region node's own entry rather than through the offer
|
||||||
|
//! that node was given. The last is a wrapping text handed back the width
|
||||||
|
//! it measured, rounded to a step below the line it measured there.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris::random::Branch;
|
||||||
|
|
||||||
|
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
||||||
|
/// the tree changes -- every widget is marked for redraw and the frame is
|
||||||
|
/// taken again -- so no box may move, and a warm frame has to land where a
|
||||||
|
/// cold one does.
|
||||||
|
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
|
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
||||||
|
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let sized = wrapped.width(76).add(&mut h.rsc);
|
||||||
|
let aligned = sized;
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_alignment(sized, Axis::X, AxisAlign::POS);
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_alignment(sized, Axis::Y, AxisAlign::POS);
|
||||||
|
let stack = Stack {
|
||||||
|
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
vec![
|
||||||
|
plain.id(),
|
||||||
|
wrapped.id(),
|
||||||
|
sized.id(),
|
||||||
|
aligned.id(),
|
||||||
|
stack.id(),
|
||||||
|
root.id(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first frame does not reach the layout a second one does, so "cold" is
|
||||||
|
/// not a fixed point and comparing against it compares against a tree that
|
||||||
|
/// has not settled.
|
||||||
|
#[test]
|
||||||
|
fn one_frame_is_enough() {
|
||||||
|
let mut h = Harness::new((640, 900));
|
||||||
|
let ids = plant(&mut h);
|
||||||
|
let first = h.region(&ids[1]).unwrap();
|
||||||
|
for _ in 0..3 {
|
||||||
|
for &id in &ids {
|
||||||
|
h.rsc.widgets_mut().get_dyn_mut(id);
|
||||||
|
}
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
|
let settled = h.region(&ids[1]).unwrap();
|
||||||
|
println!(
|
||||||
|
"first frame {} tall, settled {} tall",
|
||||||
|
first.bot_right.y - first.top_left.y,
|
||||||
|
settled.bot_right.y - settled.top_left.y
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
first.bot_right.y - first.top_left.y,
|
||||||
|
settled.bot_right.y - settled.top_left.y,
|
||||||
|
"the first frame had not finished laying out"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repainting_everything_moves_nothing() {
|
||||||
|
let mut warm = Harness::new((640, 900));
|
||||||
|
let ids = plant(&mut warm);
|
||||||
|
for &id in &ids {
|
||||||
|
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||||
|
}
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let cold_ids = plant(&mut cold);
|
||||||
|
|
||||||
|
let mut wrong = Vec::new();
|
||||||
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||||
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
|
if got != want {
|
||||||
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box
|
||||||
|
/// is the same size whatever the output is, so a resize may not change any of
|
||||||
|
/// it -- but the text comes out 3.92px narrower warm than cold.
|
||||||
|
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
|
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
||||||
|
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let aligned = text;
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_alignment(text, Axis::X, AxisAlign::NEG);
|
||||||
|
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let sized = inner.sized((189, 176)).add(&mut h.rsc);
|
||||||
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||||
|
vec![
|
||||||
|
text.id(),
|
||||||
|
aligned.id(),
|
||||||
|
inner.id(),
|
||||||
|
sized.id(),
|
||||||
|
filler.id(),
|
||||||
|
root.id(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
|
||||||
|
let mut warm = Harness::new((1920, 1200));
|
||||||
|
let ids = plant_fixed(&mut warm);
|
||||||
|
warm.frame();
|
||||||
|
warm.resize((640, 900));
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let cold_ids = plant_fixed(&mut cold);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
let mut wrong = Vec::new();
|
||||||
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||||
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
|
if got != want {
|
||||||
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by
|
||||||
|
/// moving them, cold by growing them that way. Same widgets, same sizes, one
|
||||||
|
/// ends up 29.9px from where the other does.
|
||||||
|
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
|
||||||
|
let wrapped = wtext("Wrapping shapes one source into as many lines")
|
||||||
|
.size(16)
|
||||||
|
.wrap(true)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let plain = wtext("one line, overflowing whatever it is given")
|
||||||
|
.size(16)
|
||||||
|
.wrap(false)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let first: StrongWidget = wrapped.add_strong(&mut h.rsc);
|
||||||
|
let second: StrongWidget = plain.add_strong(&mut h.rsc);
|
||||||
|
let children = match swapped {
|
||||||
|
true => vec![second, first],
|
||||||
|
false => vec![first, second],
|
||||||
|
};
|
||||||
|
let span = Span {
|
||||||
|
children,
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let span_handle = span;
|
||||||
|
let aligned = span;
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_alignment(span, Axis::X, AxisAlign::CENTER);
|
||||||
|
h.state.root = Some(aligned.add_strong(&mut h.rsc));
|
||||||
|
(
|
||||||
|
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
|
||||||
|
span_handle,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swapping_two_children_lands_where_growing_them_that_way_does() {
|
||||||
|
let mut warm = Harness::new((640, 900));
|
||||||
|
let (ids, span) = plant_pair(&mut warm, false);
|
||||||
|
warm.frame();
|
||||||
|
warm.rsc[span].children.rotate_left(1);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let (cold_ids, _) = plant_pair(&mut cold, true);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
let mut wrong = Vec::new();
|
||||||
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||||
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
|
if got != want {
|
||||||
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its
|
||||||
|
/// content from what the content says, and hands that box down through a
|
||||||
|
/// pass-through; the span under it was given that box once, so nothing at its
|
||||||
|
/// own edge says the box was its own answer.
|
||||||
|
fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
|
||||||
|
let words = "Wrapping shapes one source into as many lines as the box leaves room for,";
|
||||||
|
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let mut inner_children: Vec<StrongWidget> =
|
||||||
|
vec![text.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)];
|
||||||
|
if swapped {
|
||||||
|
inner_children.rotate_left(1);
|
||||||
|
}
|
||||||
|
let inner = Span {
|
||||||
|
children: inner_children,
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let block = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let fixed = block.width(87).add(&mut h.rsc);
|
||||||
|
let mut outer_children: Vec<StrongWidget> =
|
||||||
|
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
|
||||||
|
if swapped {
|
||||||
|
outer_children.rotate_left(1);
|
||||||
|
}
|
||||||
|
let outer = Span {
|
||||||
|
children: outer_children,
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
// Carried no rule even before rules were a property: it is here to be a
|
||||||
|
// widget between the span and the scroll, not to declare anything.
|
||||||
|
let through = (outer,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(through.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||||
|
h.state.root = Some(scroll.add_strong(&mut h.rsc));
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
text.id(),
|
||||||
|
filler.id(),
|
||||||
|
inner.id(),
|
||||||
|
block.id(),
|
||||||
|
fixed.id(),
|
||||||
|
outer.id(),
|
||||||
|
through.id(),
|
||||||
|
scroll.id(),
|
||||||
|
],
|
||||||
|
[inner, outer],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_span_given_the_box_its_answer_decided_matches_a_cold_layout() {
|
||||||
|
let mut warm = Harness::new((640, 900));
|
||||||
|
let (ids, spans) = plant_scrolled(&mut warm, false);
|
||||||
|
warm.frame();
|
||||||
|
for span in spans {
|
||||||
|
warm.rsc[span].children.rotate_left(1);
|
||||||
|
}
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let (cold_ids, _) = plant_scrolled(&mut cold, true);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
let mut wrong = Vec::new();
|
||||||
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||||
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
|
if got != want {
|
||||||
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reports a width derived from the box it is asked in. Reading through the
|
||||||
|
/// painter is its declaration that the answer holds for that width only.
|
||||||
|
struct Wider {
|
||||||
|
extra: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Wider {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
Size {
|
||||||
|
x: LayoutLen {
|
||||||
|
px: painter.px_len(Axis::X) + Px::from_f32(self.extra),
|
||||||
|
..LayoutLen::ZERO
|
||||||
|
},
|
||||||
|
y: LayoutLen::LEFTOVER,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) {
|
||||||
|
let content = Wider { extra }.add(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||||
|
let root = scroll;
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_alignment(scroll, Axis::X, AxisAlign::NEG);
|
||||||
|
h.set_root(root);
|
||||||
|
(content, scroll.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_scrolls_retained_answer_is_the_one_a_cold_layout_asks_for() {
|
||||||
|
let mut warm = Harness::new((100, 100));
|
||||||
|
let (content, scroll) = plant_wider(&mut warm, 50.0);
|
||||||
|
warm.rsc[content].extra = 70.0;
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((100, 100));
|
||||||
|
let (_, cold_scroll) = plant_wider(&mut cold, 70.0);
|
||||||
|
|
||||||
|
assert_eq!(warm.region(&scroll), cold.region(&cold_scroll));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Six widgets, shrunk from 266. `measured`'s box is exactly the height of its
|
||||||
|
/// one fixed child, which is the box a parent sizing itself from that answer
|
||||||
|
/// hands back -- so whether its leftover-only child was drawn at all came down
|
||||||
|
/// to the 0.00003 px the composed length differs by, one way warm and the
|
||||||
|
/// other cold.
|
||||||
|
fn plant_boundary(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
|
||||||
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let plain = wtext("one line, overflowing whatever it is given")
|
||||||
|
.size(16)
|
||||||
|
.wrap(false)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let mut pair: Vec<StrongWidget> =
|
||||||
|
vec![filler.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)];
|
||||||
|
if swapped {
|
||||||
|
pair.rotate_left(1);
|
||||||
|
}
|
||||||
|
let measured = Span {
|
||||||
|
children: pair,
|
||||||
|
dir: Dir::DOWN,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
// Takes the whole box on its own, so the span above has nothing left to
|
||||||
|
// divide and `measured` is given exactly the text's height.
|
||||||
|
let whole = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rules(whole, None, Some(LayoutLen::rel(1.0)));
|
||||||
|
let mut inner_children: Vec<StrongWidget> = vec![
|
||||||
|
measured.add_strong(&mut h.rsc),
|
||||||
|
whole.add_strong(&mut h.rsc),
|
||||||
|
];
|
||||||
|
if swapped {
|
||||||
|
inner_children.rotate_left(1);
|
||||||
|
}
|
||||||
|
let inner = Span {
|
||||||
|
children: inner_children,
|
||||||
|
dir: Dir::DOWN,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rules(inner, None, Some(LayoutLen::px(198.0)));
|
||||||
|
// One more span above it: without a box composed through it, both trees
|
||||||
|
// round the same way and the boundary is never crossed.
|
||||||
|
let outer = (inner,).span(Dir::DOWN).add(&mut h.rsc);
|
||||||
|
h.set_root(outer);
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
filler.id(),
|
||||||
|
plain.id(),
|
||||||
|
measured.id(),
|
||||||
|
whole.id(),
|
||||||
|
inner.id(),
|
||||||
|
outer.id(),
|
||||||
|
],
|
||||||
|
[measured, inner],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() {
|
||||||
|
let mut warm = Harness::new((640, 900));
|
||||||
|
let (ids, spans) = plant_boundary(&mut warm, false);
|
||||||
|
warm.frame();
|
||||||
|
for span in spans {
|
||||||
|
warm.rsc[span].children.rotate_left(1);
|
||||||
|
}
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let (cold_ids, _) = plant_boundary(&mut cold, true);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
let mut wrong = Vec::new();
|
||||||
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||||
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
|
if got != want {
|
||||||
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed
|
||||||
|
/// 18 grows at depth 6. A scroll inside a scroll, the inner one owning a
|
||||||
|
/// movable region of its own, and only its text marked for redraw. Nothing
|
||||||
|
/// about the tree changes, so no box may.
|
||||||
|
fn plant_nested_scrolls(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
|
let text = wtext("one line, overflowing whatever it is given")
|
||||||
|
.size(16)
|
||||||
|
.wrap(false)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let inner = Scroll::new(text.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||||
|
h.rsc.widgets_mut().set_region_node(inner.id(), true);
|
||||||
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
h.rsc.widgets_mut().set_size_rules(
|
||||||
|
filler.id(),
|
||||||
|
Some(LayoutLen::px(87.0)),
|
||||||
|
Some(LayoutLen::px(24.0)),
|
||||||
|
);
|
||||||
|
let span = Span {
|
||||||
|
children: vec![inner.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)],
|
||||||
|
dir: Dir::DOWN,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let root = Scroll::new(span.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
vec![text.id(), inner.id(), filler.id(), span.id(), root.id()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A local redraw asks a dirty widget in the box its parent gave it, and only
|
||||||
|
/// where that box is as long as the one it was offered; anything else is a
|
||||||
|
/// question its parent has to ask. This inner scroll's offer is the outer
|
||||||
|
/// scroll's whole viewport and the box it was given is 24px shorter -- the
|
||||||
|
/// height of the sized child the outer scroll snaps to the end of -- so what
|
||||||
|
/// it must not do is settle itself. It was drawn at its offer once, and the
|
||||||
|
/// inner scroll and its text stayed 24px too low.
|
||||||
|
#[test]
|
||||||
|
fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let ids = plant_nested_scrolls(&mut warm);
|
||||||
|
warm.rsc.widgets_mut().get_dyn_mut(ids[0]);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let cold_ids = plant_nested_scrolls(&mut cold);
|
||||||
|
|
||||||
|
let mut wrong = Vec::new();
|
||||||
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||||
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
|
if got != want {
|
||||||
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220
|
||||||
|
/// to. The pad owns a movable region and is the scroll's content, so the box
|
||||||
|
/// the scroll places it in is as long as that content while the box it was
|
||||||
|
/// offered is the viewport -- and with no padding to tell those two apart,
|
||||||
|
/// the span inside it looked like it was still at its offer. So everything
|
||||||
|
/// under the pad was asked again in the *placed* box, the offer resolving
|
||||||
|
/// against the node's own entry, which holds that box: the texts kept the
|
||||||
|
/// widths they had, the content stayed the length those widths make, and the
|
||||||
|
/// old answer confirmed itself. What the branch adds is a tree that differs
|
||||||
|
/// rather than a box that moved, since a probe measured at the wrong width
|
||||||
|
/// takes the other side.
|
||||||
|
fn plant_under_a_node(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
|
||||||
|
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let wide = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
|
let narrow = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let branch = Branch {
|
||||||
|
probe: probe.add_strong(&mut h.rsc),
|
||||||
|
wide: wide.add_strong(&mut h.rsc),
|
||||||
|
narrow: narrow.add_strong(&mut h.rsc),
|
||||||
|
threshold: 213.0,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let wrapped = wtext(
|
||||||
|
"Wrapping shapes one source into as many lines as the box \
|
||||||
|
leaves room for, so a paragraph's height is an answer and not a setting.",
|
||||||
|
)
|
||||||
|
.size(16)
|
||||||
|
.wrap(true)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let plain = wtext("one line, overflowing whatever it is given")
|
||||||
|
.size(16)
|
||||||
|
.wrap(false)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let row = |h: &mut Harness, mut children: Vec<StrongWidget>| {
|
||||||
|
if swapped {
|
||||||
|
children.rotate_left(1);
|
||||||
|
}
|
||||||
|
Span {
|
||||||
|
children,
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc)
|
||||||
|
};
|
||||||
|
let texts: Vec<StrongWidget> =
|
||||||
|
vec![wrapped.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)];
|
||||||
|
let inner = row(h, texts);
|
||||||
|
let pair: Vec<StrongWidget> = vec![branch.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
|
||||||
|
let outer = row(h, pair);
|
||||||
|
let pad = Pad {
|
||||||
|
padding: Padding::ZERO,
|
||||||
|
inner: outer.add_strong(&mut h.rsc),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.rsc.widgets_mut().set_region_node(pad.id(), true);
|
||||||
|
let root = Scroll::new(pad.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
probe.id(),
|
||||||
|
wide.id(),
|
||||||
|
narrow.id(),
|
||||||
|
branch.id(),
|
||||||
|
wrapped.id(),
|
||||||
|
plain.id(),
|
||||||
|
inner.id(),
|
||||||
|
outer.id(),
|
||||||
|
pad.id(),
|
||||||
|
root.id(),
|
||||||
|
],
|
||||||
|
[outer, inner],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let (ids, spans) = plant_under_a_node(&mut warm, false);
|
||||||
|
warm.frame();
|
||||||
|
for span in spans {
|
||||||
|
warm.rsc[span].children.rotate_left(1);
|
||||||
|
}
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let (cold_ids, _) = plant_under_a_node(&mut cold, true);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
let mut wrong = Vec::new();
|
||||||
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||||
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
|
if got != want {
|
||||||
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \
|
||||||
|
box leaves room for, so a paragraph's height is an answer and not a setting.";
|
||||||
|
|
||||||
|
/// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4,
|
||||||
|
/// `shuffle-swap-for-three`). The stack takes its size from the span above,
|
||||||
|
/// the span takes its width from the longest line of the texts in it, and
|
||||||
|
/// the text below the span is then wrapped at that width -- so a width the
|
||||||
|
/// shaper measured comes back to it as the box to break in.
|
||||||
|
fn plant_a_measured_width(h: &mut Harness, swapped: bool) -> (WeakWidget<Span>, WidgetId) {
|
||||||
|
let first: StrongWidget = rect(Color::YELLOW).add_strong(&mut h.rsc);
|
||||||
|
let mut inner = Span::empty(Dir::UP);
|
||||||
|
inner.children = match swapped {
|
||||||
|
true => swapped_in(h),
|
||||||
|
false => vec![first],
|
||||||
|
};
|
||||||
|
let inner = inner.height(142).add(&mut h.rsc);
|
||||||
|
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let stack = Stack {
|
||||||
|
children: vec![inner.add_strong(&mut h.rsc), text.add_strong(&mut h.rsc)],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root((stack,).span(Dir::DOWN).width(195));
|
||||||
|
(inner, text.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the span holds once its children have been swapped, which is what
|
||||||
|
/// the warm tree is changed to and what the cold one is grown with.
|
||||||
|
fn swapped_in(h: &mut Harness) -> Vec<StrongWidget> {
|
||||||
|
let paragraph = |h: &mut Harness| -> StrongWidget {
|
||||||
|
wtext(PARAGRAPH).size(16).wrap(true).add_strong(&mut h.rsc)
|
||||||
|
};
|
||||||
|
vec![
|
||||||
|
paragraph(h),
|
||||||
|
rect(Color::YELLOW).add_strong(&mut h.rsc),
|
||||||
|
paragraph(h),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A text handed back the width it measured breaks there the way it broke
|
||||||
|
/// when it measured it. The width the shaper answers is not on the grid, and
|
||||||
|
/// a report rounded to the nearest step is under the longest line half the
|
||||||
|
/// time: a warm tree then keeps a break made in a wider box while a cold one
|
||||||
|
/// makes a narrower break in the same box, and the paragraph gains a line.
|
||||||
|
#[test]
|
||||||
|
fn a_text_is_given_back_a_box_the_line_it_measured_fits_in() {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let (inner, text) = plant_a_measured_width(&mut warm, false);
|
||||||
|
warm.frame();
|
||||||
|
warm.rsc[inner].children = swapped_in(&mut warm);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let (_, cold_text) = plant_a_measured_width(&mut cold, true);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
assert_eq!(warm.region(&text), cold.region(&cold_text));
|
||||||
|
}
|
||||||
+5
-6
@@ -1,6 +1,5 @@
|
|||||||
//! What the vertex shader's move-chain walk costs, against how deep the chain
|
//! What the vertex shader's move-chain walk costs, against how many nested
|
||||||
//! is. Every active widget owns a slot, so the depth a primitive resolves
|
//! region nodes a primitive resolves through.
|
||||||
//! through is its depth in the widget tree.
|
|
||||||
//!
|
//!
|
||||||
//! cargo test --release --test chain_cost -- --ignored --nocapture
|
//! cargo test --release --test chain_cost -- --ignored --nocapture
|
||||||
//!
|
//!
|
||||||
@@ -17,8 +16,8 @@
|
|||||||
|
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use iris_core::{
|
use iris_core::{
|
||||||
MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode, UiRenderState,
|
Len, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
|
||||||
UiScalar, UiSpan,
|
UiRenderState, UiSpan,
|
||||||
};
|
};
|
||||||
use wgpu::{Color as GpuColor, *};
|
use wgpu::{Color as GpuColor, *};
|
||||||
|
|
||||||
@@ -80,7 +79,7 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
|||||||
slot = render.moves.push(slot, UiRegion::FULL);
|
slot = render.moves.push(slot, UiRegion::FULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
let px = |v: f32| UiScalar { rel: 0.0, px: v };
|
let px = |v: f32| Len::px(v);
|
||||||
for i in 0..INSTANCES {
|
for i in 0..INSTANCES {
|
||||||
let x = (i % (SIZE as usize / 2)) as f32 * 2.0;
|
let x = (i % (SIZE as usize / 2)) as f32 * 2.0;
|
||||||
let y = (i / (SIZE as usize / 2)) as f32;
|
let y = (i / (SIZE as usize / 2)) as f32;
|
||||||
|
|||||||
+91
-433
@@ -1,20 +1,21 @@
|
|||||||
//! 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: slots
|
//! Every case is one of `scenario`'s, over the trees `iris::random` grows
|
||||||
//! rewritten, some widgets drawn again, the rest untouched. The property here
|
//! from a seed. The fast test takes a handful of seeds and the ignored one
|
||||||
//! is that what comes out is the tree a cold start would have produced, so
|
//! takes as many as it is asked for; both run the same cases the shrinker
|
||||||
//! anything the retained path carried over that it should not have shows up
|
//! does over the same trees, so a seed that fails here is reduced by
|
||||||
//! as a difference in somebody's box.
|
|
||||||
//!
|
//!
|
||||||
//! `iris::random` grows the tree and `examples/random.rs` draws one. A seed is
|
//! SHRINK_SEED=<seed> SHRINK_DEPTH=<depth> SHRINK_CASE=<case> \
|
||||||
//! the whole reproduction; `a_long_run_of_seeds_agrees` is the ignored sweep
|
//! cargo test --release --test shrink -- --ignored --nocapture
|
||||||
//! for when it is worth spending the time.
|
//!
|
||||||
|
//! `IRIS_GENERATED_SEED`, `IRIS_GENERATED_SEEDS` and `IRIS_GENERATED_DEPTH`
|
||||||
|
//! select what the long run covers.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
#[path = "scenario/mod.rs"]
|
||||||
|
mod scenario;
|
||||||
|
|
||||||
use iris::harness::Harness;
|
use iris::random::{Edits, plan};
|
||||||
use iris::prelude::*;
|
use scenario::{ALL, Case, diverges, env, over_seeds};
|
||||||
use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow};
|
|
||||||
|
|
||||||
/// How deep the generator branches. The generator widens two to four ways per
|
/// 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
|
/// level, so depth is exponential in width and a deep narrow tree is not
|
||||||
@@ -24,445 +25,102 @@ fn depth() -> usize {
|
|||||||
env("IRIS_GENERATED_DEPTH", 4)
|
env("IRIS_GENERATED_DEPTH", 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
/// The seeds the ordinary tests take. Seven that have never failed; 86,
|
||||||
std::env::var(name)
|
/// which a `Scroll` fixed point once settled differently on; and 20, which
|
||||||
.ok()
|
/// caught a locally redrawn widget being placed twice in the box its parent
|
||||||
.and_then(|value| value.parse().ok())
|
/// had already placed it in.
|
||||||
.unwrap_or(fallback)
|
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
|
||||||
}
|
|
||||||
const SEEDS: [u64; 7] = [1, 2, 3, 5, 8, 13, 98];
|
|
||||||
const REGION_EPSILON_PX: f32 = 0.05;
|
|
||||||
|
|
||||||
fn same_coordinate(got: f32, want: f32) -> bool {
|
fn check(seed: u64, depth: usize, case: Case) {
|
||||||
(got - want).abs() <= REGION_EPSILON_PX
|
let grown = plan(seed, depth, &Edits::default());
|
||||||
}
|
if let Some(how) = diverges(&grown, case, seed) {
|
||||||
|
panic!(
|
||||||
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
|
"seed {seed} at depth {depth} differs after {}: {how}\n\
|
||||||
match (got, want) {
|
reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \
|
||||||
(Some(got), Some(want)) => {
|
SHRINK_CASE={} cargo test --release --test shrink -- --ignored --nocapture",
|
||||||
same_coordinate(got.top_left.x, want.top_left.x)
|
case.name(),
|
||||||
&& same_coordinate(got.top_left.y, want.top_left.y)
|
case.name(),
|
||||||
&& same_coordinate(got.bot_right.x, want.bot_right.x)
|
);
|
||||||
&& same_coordinate(got.bot_right.y, want.bot_right.y)
|
|
||||||
}
|
|
||||||
(None, None) => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree {
|
macro_rules! case {
|
||||||
let (root, tree) = grow(&mut h.rsc, seed, depth(), edits);
|
($name:ident, $case:expr) => {
|
||||||
h.state.root = Some(root);
|
#[test]
|
||||||
h.frame();
|
fn $name() {
|
||||||
tree
|
for seed in SEEDS {
|
||||||
}
|
check(seed, depth(), $case);
|
||||||
|
|
||||||
fn resize_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens {
|
|
||||||
let lens = [
|
|
||||||
Some(Len::px(20.0 + rng.below(180) as f32)),
|
|
||||||
Some(Len::px(20.0 + rng.below(180) as f32)),
|
|
||||||
];
|
|
||||||
let sized = &mut h.rsc[tree.sized[idx]];
|
|
||||||
sized.x = lens[0];
|
|
||||||
sized.y = 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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 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;
|
|
||||||
let len = |l: &Option<Len>| match l {
|
|
||||||
Some(l) => format!("{l}"),
|
|
||||||
None => "-".into(),
|
|
||||||
};
|
|
||||||
if let Some(w) = any.downcast_ref::<SetSize>() {
|
|
||||||
return format!("SetSize{{x:{},y:{}}}", len(&w.x), len(&w.y));
|
|
||||||
}
|
|
||||||
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::<Aligned>() {
|
|
||||||
let a = |v: Option<AxisAlign>| match v {
|
|
||||||
None => "-",
|
|
||||||
Some(AxisAlign::Neg) => "neg",
|
|
||||||
Some(AxisAlign::Center) => "mid",
|
|
||||||
Some(AxisAlign::Pos) => "pos",
|
|
||||||
};
|
|
||||||
return format!("Aligned{{x:{},y:{}}}", a(w.align.x), a(w.align.y));
|
|
||||||
}
|
|
||||||
if let Some(w) = any.downcast_ref::<Stack>() {
|
|
||||||
return format!("Stack{{n:{}}}", w.children.len());
|
|
||||||
}
|
|
||||||
label
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every widget in one tree against the matching widget in the other. A
|
case!(
|
||||||
/// mismatch prints the widget's ancestry, marking the ones that own a slot,
|
many_widgets_redrawing_at_once_leaves_every_box_where_it_was,
|
||||||
/// since where two trees disagree is rarely where the cause is.
|
Case::RepaintSome
|
||||||
fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness, &Tree)) {
|
);
|
||||||
let ((wh, wt), (ch, ct)) = (warm, cold);
|
case!(
|
||||||
assert_eq!(wt.ids.len(), ct.ids.len(), "seed {seed}: different trees");
|
everything_redrawing_at_once_leaves_every_box_where_it_was,
|
||||||
let mut drawn = 0;
|
Case::Repaint
|
||||||
let mut wrong = 0;
|
);
|
||||||
for (i, (&w, &c)) in wt.ids.iter().zip(&ct.ids).enumerate() {
|
case!(
|
||||||
let (got, want) = (wh.region(&w), ch.region(&c));
|
a_resize_lands_where_starting_at_that_size_would,
|
||||||
drawn += usize::from(got.is_some());
|
Case::Resize
|
||||||
// This oracle cares where rasterization lands, not whether equivalent
|
);
|
||||||
// arithmetic produced the same f32. Keep the tolerance to one
|
case!(
|
||||||
// twentieth of a physical pixel, while whether a widget drew remains
|
a_resize_and_a_repaint_land_where_starting_that_way_would,
|
||||||
// exact.
|
Case::ResizeRepaint
|
||||||
if same_region(got, want) {
|
);
|
||||||
continue;
|
case!(
|
||||||
}
|
a_size_change_after_a_resize_lands_the_same_way,
|
||||||
wrong += 1;
|
Case::ResizeSize
|
||||||
if wrong <= 3 {
|
);
|
||||||
let mut chain = Vec::new();
|
case!(
|
||||||
let mut at = Some(w);
|
a_size_change_lands_where_growing_it_that_way_would,
|
||||||
while let Some(id) = at {
|
Case::Size
|
||||||
let active = &wh.render.active[&id];
|
);
|
||||||
let slot = match active.move_idx == active.parent_move {
|
case!(
|
||||||
true => "",
|
every_size_changing_at_once_lands_where_growing_it_that_way_would,
|
||||||
false => "*",
|
Case::EverySize
|
||||||
};
|
);
|
||||||
chain.push(format!("{}{slot}", describe(id, wh)));
|
case!(
|
||||||
at = active.parent;
|
an_alignment_change_lands_where_growing_it_that_way_would,
|
||||||
}
|
Case::Align
|
||||||
println!(
|
);
|
||||||
"seed {seed} after {what}: widget {i}\n warm {got:?}\n cold {want:?}\n {}",
|
case!(
|
||||||
chain.join(" < ")
|
giving_and_taking_a_movable_region_rebuilds_what_resolves_it,
|
||||||
);
|
Case::RegionNode
|
||||||
}
|
);
|
||||||
}
|
case!(
|
||||||
assert!(drawn > 0, "seed {seed}: nothing was drawn");
|
reordering_a_span_lands_where_growing_it_that_way_would,
|
||||||
assert_eq!(wrong, 0, "seed {seed}: {wrong} widgets differ after {what}");
|
Case::Reorder
|
||||||
}
|
);
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 before: Vec<_> = grown.ids.iter().map(|id| warm.region(id)).collect();
|
|
||||||
|
|
||||||
let (spans, _held) = reshuffle(&mut warm, &mut grown, shuffle);
|
|
||||||
warm.frame();
|
|
||||||
|
|
||||||
// Or the two trees would agree for want of anything having happened.
|
|
||||||
let after = grown.ids.iter().map(|id| warm.region(id));
|
|
||||||
let moved = before.iter().zip(after).filter(|(a, b)| *a != b).count();
|
|
||||||
assert!(moved > 0, "seed {seed}: {shuffle:?} changed nothing");
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
assert!(
|
|
||||||
!warm.rsc.widgets().needs_redraw.is_empty(),
|
|
||||||
"seed {seed}: nothing was marked"
|
|
||||||
);
|
|
||||||
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 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]
|
#[test]
|
||||||
fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
|
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 {
|
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]
|
#[test]
|
||||||
#[ignore = "a hundred seeds, rather than the seven the others check"]
|
#[ignore = "as many seeds as it is asked for, rather than the nine the others check"]
|
||||||
fn a_long_run_of_seeds_agrees() {
|
fn a_long_run_of_seeds_agrees() {
|
||||||
let seeds = std::env::var("IRIS_GENERATED_SEED")
|
let depth = depth();
|
||||||
|
let seeds: Vec<u64> = match std::env::var("IRIS_GENERATED_SEED")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|seed| seed.parse().ok())
|
.and_then(|v| v.parse().ok())
|
||||||
.map(|seed| seed..=seed)
|
{
|
||||||
.unwrap_or_else(|| 1..=env("IRIS_GENERATED_SEEDS", 100));
|
Some(seed) => vec![seed],
|
||||||
for seed in seeds {
|
None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(),
|
||||||
changed_size(seed);
|
};
|
||||||
changed_every_size(seed);
|
over_seeds(seeds, |seed| {
|
||||||
repainted_together(seed);
|
for case in ALL {
|
||||||
resized(seed);
|
check(seed, depth, case);
|
||||||
resized_then_changed(seed);
|
|
||||||
for shuffle in SHUFFLES {
|
|
||||||
reshuffled(seed, shuffle);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
-351
@@ -1,351 +0,0 @@
|
|||||||
//! Where a frame puts things, with no window to put them in.
|
|
||||||
|
|
||||||
use iris::harness::{Harness, assert_corners};
|
|
||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
/// A fixed 100 wide, and the rest of the 400 to its neighbour.
|
|
||||||
fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) {
|
|
||||||
let left = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
||||||
let right = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
h.set_root((left, right).span(Dir::RIGHT));
|
|
||||||
(left.id(), right.id())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_span_gives_each_child_the_width_it_asked_for() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (left, right) = two_rects(&mut h);
|
|
||||||
|
|
||||||
assert_corners!(h, left, (0, 0), (100, 200));
|
|
||||||
assert_corners!(h, right, (100, 0), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resizing_relays_out_against_the_new_output() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (left, right) = two_rects(&mut h);
|
|
||||||
|
|
||||||
h.resize((800, 100));
|
|
||||||
assert!(h.needs_redraw());
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, left, (0, 0), (100, 100));
|
|
||||||
assert_corners!(h, right, (100, 0), (800, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn an_empty_widget_takes_a_share_of_a_span() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let gap = ().add(&mut h.rsc);
|
|
||||||
let right = rect(Color::BLUE).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((gap, right).span(Dir::RIGHT));
|
|
||||||
|
|
||||||
assert_corners!(h, gap, (0, 0), (300, 200));
|
|
||||||
assert_corners!(h, right, (300, 0), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_child_drawn_twice_moves_once() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
// `Aligned` draws its child twice; listing it twice would move it twice.
|
|
||||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
let centered = inner.center().width(200).add(&mut h.rsc);
|
|
||||||
let left = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((left, centered).span(Dir::RIGHT));
|
|
||||||
assert_corners!(h, inner, (100, 0), (300, 200));
|
|
||||||
|
|
||||||
h.rsc[left].x = Some(Len::px(150));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, inner, (150, 0), (350, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_resize_lands_where_a_cold_start_would() {
|
|
||||||
let build = |h: &mut Harness| {
|
|
||||||
let para = wtext(
|
|
||||||
"Wrapping shapes one source into as many lines as its container leaves room \
|
|
||||||
for, so the height of a paragraph is an answer rather than a setting.",
|
|
||||||
)
|
|
||||||
.size(20)
|
|
||||||
.wrap(true)
|
|
||||||
.pad(16)
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let below = rect(Color::RED).add(&mut h.rsc);
|
|
||||||
let root = (para, below).span(Dir::DOWN).pad(12);
|
|
||||||
h.set_root(root);
|
|
||||||
(para, below)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut cold = Harness::new((900, 1200));
|
|
||||||
let (cold_para, cold_below) = build(&mut cold);
|
|
||||||
|
|
||||||
let mut resized = Harness::new((1920, 1200));
|
|
||||||
let (para, below) = build(&mut resized);
|
|
||||||
resized.resize((900, 1200));
|
|
||||||
resized.frame();
|
|
||||||
|
|
||||||
assert_eq!(resized.region(¶), cold.region(&cold_para), "paragraph");
|
|
||||||
assert_eq!(resized.region(&below), cold.region(&cold_below), "below");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_fixed_box_is_drawn_again_rather_than_stretched() {
|
|
||||||
let mut h = Harness::new((400, 400));
|
|
||||||
// The panel fills a stack sized by its sibling, so it is drawn in the
|
|
||||||
// whole box and then placed in the shorter one. Reusing it in that fixed
|
|
||||||
// box afterwards would leave it whatever height it happened to have.
|
|
||||||
let panel = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
let leaf = rect(Color::RED).height(100).add(&mut h.rsc);
|
|
||||||
let stack = (panel, leaf)
|
|
||||||
.stack()
|
|
||||||
.size(StackSize::Child(1))
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(stack.align(Align::TOP));
|
|
||||||
assert_corners!(h, panel, (0, 0), (400, 100));
|
|
||||||
|
|
||||||
h.rsc[leaf].y = Some(Len::px(250));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, panel, (0, 0), (400, 250));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_moved_subtree_takes_its_children_with_it() {
|
|
||||||
let mut h = Harness::new((400, 400));
|
|
||||||
let first = rect(Color::RED).height(40).add(&mut h.rsc);
|
|
||||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
let row = inner.pad(10).height(40).add(&mut h.rsc);
|
|
||||||
h.set_root((first, row).span(Dir::DOWN));
|
|
||||||
assert_corners!(h, inner, (10, 50), (390, 70));
|
|
||||||
|
|
||||||
h.rsc[first].y = Some(Len::px(80));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
// The row is the same shape somewhere else, so one slot moved it and
|
|
||||||
// `inner`'s own region was never rewritten.
|
|
||||||
assert_corners!(h, inner, (10, 90), (390, 110));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let fixed = rect(Color::BLUE).width(50).add(&mut h.rsc);
|
|
||||||
let rest = rect(Color::GREEN).add(&mut h.rsc);
|
|
||||||
let panel = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
// Changing the bar's width is the only thing that changes the box the
|
|
||||||
// panel and everything under it was drawn for.
|
|
||||||
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((bar, panel).span(Dir::RIGHT));
|
|
||||||
assert_corners!(h, fixed, (100, 0), (150, 200));
|
|
||||||
assert_corners!(h, rest, (150, 0), (400, 200));
|
|
||||||
|
|
||||||
h.rsc[bar].x = Some(Len::px(200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
// The panel's box is 100 shorter, so the fixed child is the same 50 wide
|
|
||||||
// against its new start and the one taking the rest absorbs the change.
|
|
||||||
assert_corners!(h, fixed, (200, 0), (250, 200));
|
|
||||||
assert_corners!(h, rest, (250, 0), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
// The row is 40 tall whatever happens, which used to make its drawing
|
|
||||||
// impossible to take out of: recovering a fraction of a box needs a
|
|
||||||
// relative extent, and it has none on that axis.
|
|
||||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
let row = inner.pad(10).height(40).add(&mut h.rsc);
|
|
||||||
let filler = rect(Color::GREEN).add(&mut h.rsc);
|
|
||||||
let column = (row, filler).span(Dir::DOWN).add(&mut h.rsc);
|
|
||||||
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((bar, column).span(Dir::RIGHT));
|
|
||||||
assert_corners!(h, inner, (110, 10), (390, 30));
|
|
||||||
|
|
||||||
h.rsc[bar].x = Some(Len::px(200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, inner, (210, 10), (390, 30));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn only_a_container_that_places_its_children_lengthens_the_chain() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let leaf = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
// Four widgets between the span and the leaf, none of which places what
|
|
||||||
// it draws, so all of them share the span's slot.
|
|
||||||
let buried = leaf.pad(4).pad(4).pad(4).pad(4).add(&mut h.rsc);
|
|
||||||
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((bar, buried).span(Dir::RIGHT));
|
|
||||||
|
|
||||||
let slot = h.render.active[&leaf.id()].parent_move;
|
|
||||||
assert_eq!(
|
|
||||||
h.render.moves.depth(slot),
|
|
||||||
2,
|
|
||||||
"the span above the leaf, and the root the window is held in"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A span that sizes from its children passes their `rest` weight up rather
|
|
||||||
/// than collapsing it to one share, so nesting divides the same space instead
|
|
||||||
/// of re-dividing a share of it.
|
|
||||||
#[test]
|
|
||||||
fn nested_spans_divide_the_space_once_however_deep_the_nesting_is() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (a, b, c, d) = (
|
|
||||||
rect(Color::RED).add(&mut h.rsc),
|
|
||||||
rect(Color::BLUE).add(&mut h.rsc),
|
|
||||||
rect(Color::GREEN).add(&mut h.rsc),
|
|
||||||
rect(Color::WHITE).add(&mut h.rsc),
|
|
||||||
);
|
|
||||||
let left = (a, b).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
let right = (c, d).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
h.set_root((left, right).span(Dir::RIGHT));
|
|
||||||
|
|
||||||
for (i, id) in [a, b, c, d].into_iter().enumerate() {
|
|
||||||
let x = i as f32 * 100.0;
|
|
||||||
assert_corners!(h, id, (x, 0), (x + 100.0, 200));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The same space, unevenly nested: weights carried up mean a share is a
|
|
||||||
/// share of the whole, not of whatever branch a widget happens to sit in.
|
|
||||||
#[test]
|
|
||||||
fn an_uneven_nesting_still_gives_every_share_the_same_length() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (a, b, c, d) = (
|
|
||||||
rect(Color::RED).add(&mut h.rsc),
|
|
||||||
rect(Color::BLUE).add(&mut h.rsc),
|
|
||||||
rect(Color::GREEN).add(&mut h.rsc),
|
|
||||||
rect(Color::WHITE).add(&mut h.rsc),
|
|
||||||
);
|
|
||||||
let one = (a,).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
let three = (b, c, d).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
h.set_root((one, three).span(Dir::RIGHT));
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Where the shader puts an edge: the two parts of a scalar are floored
|
|
||||||
/// apart, so a fraction and a pixel offset snap independently.
|
|
||||||
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 edge = |s: UiScalar| (s.rel * dim).floor() + s.px.floor();
|
|
||||||
let span = region.axis(axis);
|
|
||||||
(edge(span.start), edge(span.end))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hairline(h: &mut Harness, marks: &mut Vec<WidgetId>) -> StrongWidget {
|
|
||||||
let inner = rect(Color::RED).add_strong(&mut h.rsc);
|
|
||||||
let mark = SetSize {
|
|
||||||
inner,
|
|
||||||
x: Some(Len::px(1.0)),
|
|
||||||
y: None,
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc);
|
|
||||||
marks.push(mark.id());
|
|
||||||
mark
|
|
||||||
}
|
|
||||||
|
|
||||||
fn share(h: &mut Harness, inner: StrongWidget, ratio: f32) -> StrongWidget {
|
|
||||||
SetSize {
|
|
||||||
inner,
|
|
||||||
x: Some(Len::rest(ratio)),
|
|
||||||
y: None,
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shares in weights no binary fraction lands on, a padding on one branch
|
|
||||||
/// and not the other, so an edge falls near an integer as often as it can.
|
|
||||||
fn hairlines(h: &mut Harness, depth: usize, marks: &mut Vec<WidgetId>) -> StrongWidget {
|
|
||||||
let mut span = Span::empty(Dir::RIGHT);
|
|
||||||
if depth == 0 {
|
|
||||||
let left = rect(Color::BLUE).add_strong(&mut h.rsc);
|
|
||||||
let left = share(h, left, 3.0);
|
|
||||||
span.push(left);
|
|
||||||
let mark = hairline(h, marks);
|
|
||||||
span.push(mark);
|
|
||||||
let right = rect(Color::BLUE).add_strong(&mut h.rsc);
|
|
||||||
let right = share(h, right, 7.0);
|
|
||||||
span.push(right);
|
|
||||||
return span.add_strong(&mut h.rsc);
|
|
||||||
}
|
|
||||||
let first = hairlines(h, depth - 1, marks);
|
|
||||||
let first = share(h, first, 3.0);
|
|
||||||
span.push(first);
|
|
||||||
let second = hairlines(h, depth - 1, marks);
|
|
||||||
let second = Pad {
|
|
||||||
padding: Padding {
|
|
||||||
left: 3.0,
|
|
||||||
right: 7.0,
|
|
||||||
top: 0.0,
|
|
||||||
bottom: 0.0,
|
|
||||||
},
|
|
||||||
inner: second,
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc);
|
|
||||||
let second = share(h, second, 5.0);
|
|
||||||
span.push(second);
|
|
||||||
span.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A one-pixel line is a pixel wherever it is drawn. Both edges of a fixed
|
|
||||||
/// length share their box's fraction, so composing the chain moves them
|
|
||||||
/// together and the shader's `floor` cannot round the pixel between them
|
|
||||||
/// away -- only shift it. A separator that disappeared at one window size
|
|
||||||
/// would be a defect no size comparison catches.
|
|
||||||
#[test]
|
|
||||||
fn a_one_pixel_line_keeps_its_pixel_through_a_chain() {
|
|
||||||
let mut h = Harness::new((1920, 1200));
|
|
||||||
let mut marks = Vec::new();
|
|
||||||
let root = hairlines(&mut h, 4, &mut marks);
|
|
||||||
h.state.set_root(root);
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(marks.len(), 16);
|
|
||||||
|
|
||||||
for size in [(1920, 1200), (1919, 1201), (997, 1003), (1367, 733)] {
|
|
||||||
h.resize(size);
|
|
||||||
h.frame();
|
|
||||||
for mark in &marks {
|
|
||||||
let (start, end) = drawn_edges(&h, *mark, Axis::X);
|
|
||||||
assert_eq!(end - start, 1.0, "at {size:?}, mark {mark:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A span short of room takes it from its shares, which go to nothing and
|
|
||||||
/// then to nothing wider; the fixed lengths between them keep their pixels.
|
|
||||||
/// Collapsing those to make room would delete a separator the caller asked
|
|
||||||
/// for, which is worse than overflowing.
|
|
||||||
#[test]
|
|
||||||
fn a_span_out_of_room_shrinks_its_shares_and_not_its_fixed_lengths() {
|
|
||||||
let mut h = Harness::new((400, 20));
|
|
||||||
let mut marks = Vec::new();
|
|
||||||
let mut span = Span::empty(Dir::RIGHT);
|
|
||||||
for _ in 0..3 {
|
|
||||||
let share_of = rect(Color::BLUE).add_strong(&mut h.rsc);
|
|
||||||
let share_of = share(&mut h, share_of, 1.0);
|
|
||||||
span.push(share_of);
|
|
||||||
let mark = hairline(&mut h, &mut marks);
|
|
||||||
span.push(mark);
|
|
||||||
}
|
|
||||||
let root = span.add_strong(&mut h.rsc);
|
|
||||||
h.state.set_root(root);
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
for width in [400, 10, 3, 1] {
|
|
||||||
h.resize((width, 20));
|
|
||||||
h.frame();
|
|
||||||
for mark in &marks {
|
|
||||||
let (start, end) = drawn_edges(&h, *mark, Axis::X);
|
|
||||||
assert_eq!(end - start, 1.0, "at {width} wide, mark {mark:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,7 @@ fn a_selected_widget_retains_its_layout_events() {
|
|||||||
diagnostics::clear_traced_widgets();
|
diagnostics::clear_traced_widgets();
|
||||||
let _ = diagnostics::take();
|
let _ = diagnostics::take();
|
||||||
let mut harness = Harness::new((400, 200));
|
let mut harness = Harness::new((400, 200));
|
||||||
let leaf = rect(Color::RED).add(&mut harness.rsc);
|
let leaf = rect(Color::RED).region_node().add(&mut harness.rsc);
|
||||||
let other = rect(Color::BLUE).add(&mut harness.rsc);
|
let other = rect(Color::BLUE).add(&mut harness.rsc);
|
||||||
let root = (leaf, other).span(Dir::RIGHT).add(&mut harness.rsc);
|
let root = (leaf, other).span(Dir::RIGHT).add(&mut harness.rsc);
|
||||||
harness.set_root(root);
|
harness.set_root(root);
|
||||||
@@ -46,7 +46,7 @@ fn a_selected_widget_retains_its_layout_events() {
|
|||||||
report
|
report
|
||||||
.traces()
|
.traces()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|event| matches!(event, TraceEvent::Placed { id, .. } if *id == leaf.id()))
|
.any(|event| matches!(event, TraceEvent::RegionNode { id, .. } if *id == leaf.id()))
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
report
|
report
|
||||||
@@ -92,9 +92,18 @@ fn trace_selected(tree: &Tree) {
|
|||||||
#[cfg(not(feature = "layout-diagnostics"))]
|
#[cfg(not(feature = "layout-diagnostics"))]
|
||||||
fn trace_selected(_: &Tree) {}
|
fn trace_selected(_: &Tree) {}
|
||||||
|
|
||||||
|
/// The shape a cost is measured on must not depend on what layout measured,
|
||||||
|
/// or two commits are compared on two different trees. See `Edits`.
|
||||||
|
fn rig_edits() -> Edits {
|
||||||
|
Edits {
|
||||||
|
fixed_branches: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
||||||
let mut harness = Harness::new(OUTPUT);
|
let mut harness = Harness::new(OUTPUT);
|
||||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits());
|
||||||
harness.state.root = Some(root);
|
harness.state.root = Some(root);
|
||||||
harness.frame();
|
harness.frame();
|
||||||
println!(
|
println!(
|
||||||
@@ -173,7 +182,7 @@ fn layout_cost() {
|
|||||||
|
|
||||||
if selected("cold") {
|
if selected("cold") {
|
||||||
let mut harness = Harness::new(OUTPUT);
|
let mut harness = Harness::new(OUTPUT);
|
||||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits());
|
||||||
harness.state.root = Some(root);
|
harness.state.root = Some(root);
|
||||||
println!(
|
println!(
|
||||||
"fixture: seed {seed}, depth {depth}, {} widgets",
|
"fixture: seed {seed}, depth {depth}, {} widgets",
|
||||||
@@ -216,7 +225,11 @@ fn layout_cost() {
|
|||||||
trace_selected(&tree);
|
trace_selected(&tree);
|
||||||
let sized = tree.sized[0];
|
let sized = tree.sized[0];
|
||||||
run("size", frames, &mut harness, move |harness, frame| {
|
run("size", frames, &mut harness, move |harness, frame| {
|
||||||
harness.rsc[sized].x = Some(Len::px(100.0 + (frame % 2) as f32 * 40.0));
|
let len = LayoutLen::px(100.0 + (frame % 2) as f32 * 40.0);
|
||||||
|
harness
|
||||||
|
.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(sized, Axis::X, SizeRule::Exact(len));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! What re-placing a subtree costs per frame, as a load for a counter rather
|
//! What remapping a subtree costs per frame, as a load for a counter rather
|
||||||
//! than a check. A span of 200 fixed-height rows, five primitives each, with
|
//! than a check. A span of 200 fixed-height rows, five primitives each, with
|
||||||
//! the row above them changing height every frame, so every row below is
|
//! the row above them changing height every frame, so every row below is
|
||||||
//! offered a box the same shape somewhere else.
|
//! offered a box the same shape somewhere else.
|
||||||
@@ -6,9 +6,7 @@
|
|||||||
//! cargo test --release --test replace_cost -- --ignored
|
//! cargo test --release --test replace_cost -- --ignored
|
||||||
//! perf stat -e instructions:u target/release/.../replace_cost-* --ignored
|
//! perf stat -e instructions:u target/release/.../replace_cost-* --ignored
|
||||||
//!
|
//!
|
||||||
//! Wall time is the wrong number here; see `draw_cost.rs`. Measured on
|
//! Wall time is the wrong number here; see `draw_cost.rs`.
|
||||||
//! 2026-09-14 at 1.98M instructions per frame, against 2.38M for rewriting
|
|
||||||
//! each row's regions instead and 7.13M for redrawing them.
|
|
||||||
|
|
||||||
use iris::harness::Harness;
|
use iris::harness::Harness;
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
@@ -18,7 +16,7 @@ const FRAMES: usize = 200;
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "measurement, not a check"]
|
#[ignore = "measurement, not a check"]
|
||||||
fn replacing_rows_every_frame() {
|
fn remapping_rows_every_frame() {
|
||||||
let mut h = Harness::new((1920, 1200));
|
let mut h = Harness::new((1920, 1200));
|
||||||
let first = rect(Color::RED).height(40).add(&mut h.rsc);
|
let first = rect(Color::RED).height(40).add(&mut h.rsc);
|
||||||
let mut span = Span::empty(Dir::DOWN);
|
let mut span = Span::empty(Dir::DOWN);
|
||||||
@@ -37,7 +35,7 @@ fn replacing_rows_every_frame() {
|
|||||||
}
|
}
|
||||||
h.set_root(span);
|
h.set_root(span);
|
||||||
for i in 0..FRAMES {
|
for i in 0..FRAMES {
|
||||||
h.rsc[first].y = Some(Len::px(40.0 + (i % 2) as f32));
|
h.set_len(first, Axis::Y, 40.0 + (i % 2) as f32);
|
||||||
h.frame();
|
h.frame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,11 @@ fn build(h: &mut Harness, rows: usize) -> Vec<WidgetId> {
|
|||||||
let mut col = Span::empty(Dir::DOWN);
|
let mut col = Span::empty(Dir::DOWN);
|
||||||
for _ in 0..rows {
|
for _ in 0..rows {
|
||||||
let mut row = Span::empty(Dir::RIGHT);
|
let mut row = Span::empty(Dir::RIGHT);
|
||||||
row.push(rect(Color::RED).width(Len::px(40.0)).add_strong(&mut h.rsc));
|
row.push(
|
||||||
|
rect(Color::RED)
|
||||||
|
.width(LayoutLen::px(40.0))
|
||||||
|
.add_strong(&mut h.rsc),
|
||||||
|
);
|
||||||
let mut body = Span::empty(Dir::DOWN);
|
let mut body = Span::empty(Dir::DOWN);
|
||||||
let para = wtext(words(&mut rng, 12, 52))
|
let para = wtext(words(&mut rng, 12, 52))
|
||||||
.size(16)
|
.size(16)
|
||||||
|
|||||||
@@ -0,0 +1,467 @@
|
|||||||
|
//! The scenarios both fuzzers run, over the tree a [`Plan`] describes.
|
||||||
|
//!
|
||||||
|
//! One implementation rather than two. The oracle grew its trees from a seed
|
||||||
|
//! and the shrinker grew its own, with every scenario written out on each
|
||||||
|
//! side, so a failure the oracle found could not be handed to the shrinker:
|
||||||
|
//! there was no tree to pass it, only a seed, and a seed cannot be made
|
||||||
|
//! smaller. Both take a plan now, so whatever finds a counterexample can also
|
||||||
|
//! reduce it.
|
||||||
|
//!
|
||||||
|
//! Each target compiles this for itself, so what only one of them calls is
|
||||||
|
//! dead code in the other.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris::random::{Aligns, Edits, Kind, Lens, Plan, Rng, SpanEdit, Tree, build};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// A seed per thread but one, since a seed grows, lays out and drops its tree
|
||||||
|
/// alone. A failing seed still shrinks and panics on its own thread.
|
||||||
|
pub fn over_seeds(seeds: Vec<u64>, run: impl Fn(u64) + Sync) {
|
||||||
|
let threads =
|
||||||
|
std::thread::available_parallelism().map_or(1, |n| n.get().saturating_sub(1).max(1));
|
||||||
|
let chunk = seeds.len().div_ceil(threads).max(1);
|
||||||
|
std::thread::scope(|scope| {
|
||||||
|
for part in seeds.chunks(chunk) {
|
||||||
|
let run = &run;
|
||||||
|
scope.spawn(move || part.iter().for_each(|&seed| run(seed)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||||
|
std::env::var(name)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The window a tree is grown in, and the one a resize takes it to.
|
||||||
|
const OUTER: (f32, f32) = (1920.0, 1200.0);
|
||||||
|
const INNER: (f32, f32) = (640.0, 900.0);
|
||||||
|
const STILL: (f32, f32) = (900.0, 1200.0);
|
||||||
|
|
||||||
|
/// The same box, to two steps of the grid between the two ways of reaching
|
||||||
|
/// it. A move, a repaint, a row of shares and every length in pixels land on
|
||||||
|
/// the same number. What needs the slack is a position: a box centred in a
|
||||||
|
/// fraction of its parent against the same box centred in its own pixels,
|
||||||
|
/// and a box re-expressed as a fraction of a parent that changed length.
|
||||||
|
/// A step is a thousandth of a pixel, where this was a twentieth of one
|
||||||
|
/// before any of it was on a grid.
|
||||||
|
///
|
||||||
|
/// **One step is not enough**, tried 2026-09-17 once a length in pixels
|
||||||
|
/// stopped being composed: it passes the 100-seed oracle and fails the
|
||||||
|
/// 400-seed shrinker on `resize-size`, seeds 384 and 162, by 0.002 px. So
|
||||||
|
/// what is left here is the resize path's own rounding rather than a length
|
||||||
|
/// reached two ways.
|
||||||
|
const AGREE_STEPS: i32 = 2;
|
||||||
|
|
||||||
|
/// A way of changing what a span holds. Each is a shape worth its own case:
|
||||||
|
/// taking a child out of the middle is not the same as emptying a span, and
|
||||||
|
/// adding one is not the same as adding three.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Shuffle {
|
||||||
|
/// Every other child, so what is left is interleaved with what went.
|
||||||
|
EveryOther,
|
||||||
|
/// Everything but the first, which is the last step before empty.
|
||||||
|
AllButFirst,
|
||||||
|
/// Three more on the end at once.
|
||||||
|
AddThree,
|
||||||
|
/// The first out and three more on, so the count moves both ways.
|
||||||
|
SwapForThree,
|
||||||
|
/// One out of the middle and one on the end.
|
||||||
|
TradeOne,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shuffle {
|
||||||
|
fn of(self, grown: usize) -> SpanEdit {
|
||||||
|
let all = |step: usize, from: usize| (from..grown).step_by(step).collect();
|
||||||
|
match self {
|
||||||
|
Self::EveryOther => SpanEdit {
|
||||||
|
detach: all(2, 0),
|
||||||
|
attach: 0,
|
||||||
|
},
|
||||||
|
Self::AllButFirst => SpanEdit {
|
||||||
|
detach: all(1, 1),
|
||||||
|
attach: 0,
|
||||||
|
},
|
||||||
|
Self::AddThree => SpanEdit {
|
||||||
|
detach: Vec::new(),
|
||||||
|
attach: 3,
|
||||||
|
},
|
||||||
|
Self::SwapForThree => SpanEdit {
|
||||||
|
detach: vec![0],
|
||||||
|
attach: 3,
|
||||||
|
},
|
||||||
|
Self::TradeOne => SpanEdit {
|
||||||
|
detach: vec![grown / 2],
|
||||||
|
attach: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a warm tree is put through before it is compared with a cold one
|
||||||
|
/// grown the way it was left.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Case {
|
||||||
|
/// Nothing changes, so no box may either. What this exercises is the
|
||||||
|
/// order a frame settles a dirty set in.
|
||||||
|
Repaint,
|
||||||
|
/// Every fifth widget rather than all of them: marking all of them
|
||||||
|
/// redraws the whole tree, which is a cold start reached the long way,
|
||||||
|
/// where the mixed case leaves a redrawn subtree beside a retained one.
|
||||||
|
RepaintSome,
|
||||||
|
Resize,
|
||||||
|
ResizeRepaint,
|
||||||
|
/// A resize and then a size change, so a retained answer is asked to
|
||||||
|
/// survive two different kinds of invalidation in a row.
|
||||||
|
ResizeSize,
|
||||||
|
/// A few declared sizes.
|
||||||
|
Size,
|
||||||
|
/// Every declared size at once, so every reader of a size has a changed
|
||||||
|
/// descendant in the same frame and the whole dirty set settles together.
|
||||||
|
EverySize,
|
||||||
|
Align,
|
||||||
|
/// Giving a widget a movable region of its own, or taking it away, is a
|
||||||
|
/// structural change: every primitive under it changes which chain
|
||||||
|
/// resolves it.
|
||||||
|
RegionNode,
|
||||||
|
/// The same children in a different order, which moves every one of them
|
||||||
|
/// without changing what any of them is.
|
||||||
|
Reorder,
|
||||||
|
Shuffle(Shuffle),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ALL: [Case; 15] = [
|
||||||
|
Case::Repaint,
|
||||||
|
Case::RepaintSome,
|
||||||
|
Case::Resize,
|
||||||
|
Case::ResizeRepaint,
|
||||||
|
Case::ResizeSize,
|
||||||
|
Case::Size,
|
||||||
|
Case::EverySize,
|
||||||
|
Case::Align,
|
||||||
|
Case::RegionNode,
|
||||||
|
Case::Reorder,
|
||||||
|
Case::Shuffle(Shuffle::EveryOther),
|
||||||
|
Case::Shuffle(Shuffle::AllButFirst),
|
||||||
|
Case::Shuffle(Shuffle::AddThree),
|
||||||
|
Case::Shuffle(Shuffle::SwapForThree),
|
||||||
|
Case::Shuffle(Shuffle::TradeOne),
|
||||||
|
];
|
||||||
|
|
||||||
|
impl Case {
|
||||||
|
/// The name `CASE` selects it by, and the one a failure prints.
|
||||||
|
pub fn name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Repaint => "repaint",
|
||||||
|
Self::RepaintSome => "repaint-some",
|
||||||
|
Self::Resize => "resize",
|
||||||
|
Self::ResizeRepaint => "resize-repaint",
|
||||||
|
Self::ResizeSize => "resize-size",
|
||||||
|
Self::Size => "size",
|
||||||
|
Self::EverySize => "every-size",
|
||||||
|
Self::Align => "align",
|
||||||
|
Self::RegionNode => "region-node",
|
||||||
|
Self::Reorder => "reorder",
|
||||||
|
Self::Shuffle(Shuffle::EveryOther) => "shuffle-every-other",
|
||||||
|
Self::Shuffle(Shuffle::AllButFirst) => "shuffle-all-but-first",
|
||||||
|
Self::Shuffle(Shuffle::AddThree) => "shuffle-add-three",
|
||||||
|
Self::Shuffle(Shuffle::SwapForThree) => "shuffle-swap-for-three",
|
||||||
|
Self::Shuffle(Shuffle::TradeOne) => "shuffle-trade-one",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn named(name: &str) -> Option<Self> {
|
||||||
|
ALL.into_iter().find(|case| case.name() == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grown in the first, compared in the second.
|
||||||
|
fn window(self) -> ((f32, f32), (f32, f32)) {
|
||||||
|
match self {
|
||||||
|
Self::Resize | Self::ResizeRepaint | Self::ResizeSize => (OUTER, INNER),
|
||||||
|
_ => (STILL, STILL),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
|
||||||
|
for &id in tree.ids.iter().step_by(step) {
|
||||||
|
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn a_len(rng: &mut Rng) -> Option<LayoutLen> {
|
||||||
|
Some(LayoutLen::px(20.0 + rng.below(180) as f32))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens {
|
||||||
|
let lens = [a_len(rng), a_len(rng)];
|
||||||
|
warm.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rules(tree.sized[idx], lens[0], lens[1]);
|
||||||
|
lens
|
||||||
|
}
|
||||||
|
|
||||||
|
fn realign_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns {
|
||||||
|
let side = |rng: &mut Rng| match rng.below(4) {
|
||||||
|
0 => None,
|
||||||
|
1 => Some(AxisAlign::NEG),
|
||||||
|
2 => Some(AxisAlign::CENTER),
|
||||||
|
_ => Some(AxisAlign::POS),
|
||||||
|
};
|
||||||
|
let align = [side(rng), side(rng)];
|
||||||
|
let id = tree.aligned[idx];
|
||||||
|
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
|
||||||
|
warm.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_alignment(id, axis, align.unwrap_or_default());
|
||||||
|
}
|
||||||
|
align
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every span's children in a different order, said both to the warm tree and
|
||||||
|
/// to the plan the cold one is grown from.
|
||||||
|
fn reorder(warm: &mut Harness, tree: &Tree, plan: &Plan) -> Plan {
|
||||||
|
for span in &tree.spans {
|
||||||
|
let children = &mut warm.rsc[span.id].children;
|
||||||
|
if !children.is_empty() {
|
||||||
|
children.rotate_left(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut out = plan.clone();
|
||||||
|
out.walk_mut(&mut |node| {
|
||||||
|
if let Kind::Span { order, .. } = &mut node.kind
|
||||||
|
&& !order.is_empty()
|
||||||
|
{
|
||||||
|
order.rotate_left(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies `shuffle` to every third span. What it takes out is given back to
|
||||||
|
/// the span's spares: the last share of a widget must outlive the comparison,
|
||||||
|
/// or its id is handed to something else and the two trees stop lining up.
|
||||||
|
fn reshuffle(warm: &mut Harness, tree: &mut Tree, shuffle: Shuffle) -> HashMap<usize, SpanEdit> {
|
||||||
|
let mut edits = HashMap::new();
|
||||||
|
for (idx, span) in tree.spans.iter_mut().enumerate().step_by(3) {
|
||||||
|
let edit = shuffle.of(span.grown);
|
||||||
|
let mut take = edit.detach.clone();
|
||||||
|
take.sort_unstable();
|
||||||
|
let children = &mut warm.rsc[span.id].children;
|
||||||
|
// Highest first, so an index means the same child however many of its
|
||||||
|
// neighbours are going too.
|
||||||
|
for j in take.into_iter().rev() {
|
||||||
|
if j < children.len() {
|
||||||
|
span.spares.push(children.remove(j));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let attach = edit.attach.min(span.spares.len());
|
||||||
|
let moved: Vec<_> = span.spares.drain(..attach).collect();
|
||||||
|
warm.rsc[span.id].children.extend(moved);
|
||||||
|
edits.insert(idx, edit);
|
||||||
|
}
|
||||||
|
edits
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes the warm tree and answers with the plan a cold tree grown that way
|
||||||
|
/// comes from. Each arm settles its own frame, so a case that changes nothing
|
||||||
|
/// does not get a second one that could settle what the first left.
|
||||||
|
fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mut Rng) -> Plan {
|
||||||
|
let some_sizes = |warm: &mut Harness, tree: &Tree, rng: &mut Rng| {
|
||||||
|
let mut sizes = HashMap::new();
|
||||||
|
for _ in 0..4 {
|
||||||
|
if tree.sized.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let idx = rng.below(tree.sized.len());
|
||||||
|
sizes.insert(idx, resize_one(warm, tree, idx, rng));
|
||||||
|
}
|
||||||
|
sizes
|
||||||
|
};
|
||||||
|
let edits = match case {
|
||||||
|
Case::Resize => return plan.clone(),
|
||||||
|
Case::Repaint | Case::ResizeRepaint => {
|
||||||
|
mark(warm, tree, 1);
|
||||||
|
warm.frame();
|
||||||
|
return plan.clone();
|
||||||
|
}
|
||||||
|
Case::RepaintSome => {
|
||||||
|
mark(warm, tree, 5);
|
||||||
|
warm.frame();
|
||||||
|
return plan.clone();
|
||||||
|
}
|
||||||
|
Case::Reorder => {
|
||||||
|
let out = reorder(warm, tree, plan);
|
||||||
|
warm.frame();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
Case::Size | Case::ResizeSize => Edits {
|
||||||
|
sizes: some_sizes(warm, tree, rng),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Case::EverySize => Edits {
|
||||||
|
sizes: (0..tree.sized.len())
|
||||||
|
.map(|idx| (idx, resize_one(warm, tree, idx, rng)))
|
||||||
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Case::Align => Edits {
|
||||||
|
aligns: (0..tree.aligned.len())
|
||||||
|
.step_by(3)
|
||||||
|
.map(|idx| (idx, realign_one(warm, tree, idx, rng)))
|
||||||
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Case::RegionNode => {
|
||||||
|
let mut nodes = HashMap::new();
|
||||||
|
for idx in (0..tree.nodes.len()).step_by(2) {
|
||||||
|
let id = tree.nodes[idx];
|
||||||
|
let take = !warm.rsc.widgets().is_region_node(id);
|
||||||
|
warm.rsc.widgets_mut().set_region_node(id, take);
|
||||||
|
nodes.insert(idx, take);
|
||||||
|
}
|
||||||
|
Edits {
|
||||||
|
nodes,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Case::Shuffle(shuffle) => Edits {
|
||||||
|
spans: reshuffle(warm, tree, shuffle),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
warm.frame();
|
||||||
|
plan.edited(&edits)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a widget was configured with, so a tree a fuzzer found can be written
|
||||||
|
/// out by hand. A failure is a lead; the fast test that replaces it has to be
|
||||||
|
/// buildable from what the failure printed.
|
||||||
|
fn describe(id: WidgetId, h: &Harness) -> String {
|
||||||
|
let rules = h.rsc.widgets().size_rules(id);
|
||||||
|
let rule = |r: SizeRule| match r.exact() {
|
||||||
|
Some(len) => format!("{len}"),
|
||||||
|
None => "-".into(),
|
||||||
|
};
|
||||||
|
let align = h.rsc.widgets().alignment(id);
|
||||||
|
let side = |a: AxisAlign| {
|
||||||
|
if a == AxisAlign::NEG {
|
||||||
|
"neg".into()
|
||||||
|
} else if a == AxisAlign::CENTER {
|
||||||
|
"mid".into()
|
||||||
|
} else if a == AxisAlign::POS {
|
||||||
|
"pos".into()
|
||||||
|
} else {
|
||||||
|
format!("{:.2}", a.rel())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// A rule and an alignment are properties of whatever carries them, so
|
||||||
|
// they print with that widget rather than as widgets of their own.
|
||||||
|
let mut out = describe_widget(id, h);
|
||||||
|
if (rules.x, rules.y) != (SizeRule::Free, SizeRule::Free) {
|
||||||
|
out += &format!("[x:{},y:{}]", rule(rules.x), rule(rules.y));
|
||||||
|
}
|
||||||
|
if align != RegionAlign::default() {
|
||||||
|
out += &format!("@{},{}", side(align.x), side(align.y));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn describe_widget(id: WidgetId, h: &Harness) -> String {
|
||||||
|
let label = h.rsc.widgets().label(id).to_string();
|
||||||
|
let Some(widget) = h.rsc.widgets().get_dyn(id) else {
|
||||||
|
return label;
|
||||||
|
};
|
||||||
|
let any: &dyn std::any::Any = widget;
|
||||||
|
if let Some(w) = any.downcast_ref::<Span>() {
|
||||||
|
let sign = if w.dir.sign == Sign::Neg { "-" } else { "+" };
|
||||||
|
return format!(
|
||||||
|
"Span{{dir:{:?}{sign},gap:{},n:{}}}",
|
||||||
|
w.dir.axis,
|
||||||
|
w.gap,
|
||||||
|
w.children.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(w) = any.downcast_ref::<Pad>() {
|
||||||
|
let p = &w.padding;
|
||||||
|
return format!(
|
||||||
|
"Pad{{l:{},r:{},t:{},b:{}}}",
|
||||||
|
p.left, p.right, p.top, p.bottom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(w) = any.downcast_ref::<Stack>() {
|
||||||
|
return format!("Stack{{n:{}}}", w.children.len());
|
||||||
|
}
|
||||||
|
label
|
||||||
|
}
|
||||||
|
|
||||||
|
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
|
||||||
|
match (got, want) {
|
||||||
|
(Some(got), Some(want)) => {
|
||||||
|
let same = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS);
|
||||||
|
same(got.top_left.x, want.top_left.x)
|
||||||
|
&& same(got.top_left.y, want.top_left.y)
|
||||||
|
&& same(got.bot_right.x, want.bot_right.x)
|
||||||
|
&& same(got.bot_right.y, want.bot_right.y)
|
||||||
|
}
|
||||||
|
(None, None) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs `case` on the tree `plan` describes, warm and cold, and says where
|
||||||
|
/// the two disagree. `seed` chooses only the values a case picks at random,
|
||||||
|
/// so one plan under one case is one comparison however it was reached.
|
||||||
|
pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
|
||||||
|
let (start, end) = case.window();
|
||||||
|
let mut warm = Harness::new(start);
|
||||||
|
let (root, mut tree) = build(&mut warm.rsc, plan);
|
||||||
|
warm.state.root = Some(root);
|
||||||
|
// The frame that makes it warm: without it nothing is retained and the
|
||||||
|
// comparison is two cold starts agreeing with each other.
|
||||||
|
warm.frame();
|
||||||
|
if start != end {
|
||||||
|
warm.resize(end);
|
||||||
|
warm.frame();
|
||||||
|
}
|
||||||
|
let cold_plan = change(case, &mut warm, &mut tree, plan, &mut Rng::new(seed));
|
||||||
|
|
||||||
|
let mut cold = Harness::new(end);
|
||||||
|
let (root, cold_tree) = build(&mut cold.rsc, &cold_plan);
|
||||||
|
cold.state.root = Some(root);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
let mut drawn = 0;
|
||||||
|
for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() {
|
||||||
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
|
drawn += got.is_some() as usize;
|
||||||
|
if same_region(got, want) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Where two trees disagree is rarely where the cause is, so the
|
||||||
|
// ancestry comes with it, marking the widgets that own a region.
|
||||||
|
let mut chain = Vec::new();
|
||||||
|
let mut at = Some(w);
|
||||||
|
while let Some(id) = at {
|
||||||
|
let active = &warm.render.active[&id];
|
||||||
|
let node = match active.move_idx == active.parent_move {
|
||||||
|
true => "",
|
||||||
|
false => "*",
|
||||||
|
};
|
||||||
|
chain.push(format!("{}{node}", describe(id, &warm)));
|
||||||
|
at = active.parent;
|
||||||
|
}
|
||||||
|
return Some(format!(
|
||||||
|
"widget {i}\n warm {got:?}\n cold {want:?}\n {}",
|
||||||
|
chain.join(" < ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
match drawn {
|
||||||
|
0 => Some("nothing was drawn".into()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
//! Scrolling moves content and stops at its ends.
|
|
||||||
|
|
||||||
use iris::harness::{Harness, assert_corners};
|
|
||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
// Twice the window's height, so there is 200 to scroll.
|
|
||||||
let top = rect(Color::RED).height(200).add(&mut h.rsc);
|
|
||||||
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
|
|
||||||
h.set_root((top, bottom).span(Dir::DOWN).scrollable());
|
|
||||||
h.move_to((200, 100));
|
|
||||||
|
|
||||||
// `Scroll` starts snapped to the end.
|
|
||||||
assert_corners!(h, top, (0, -200), (400, 0));
|
|
||||||
|
|
||||||
// The handler scales a wheel line by 50.
|
|
||||||
h.scroll((0, 1));
|
|
||||||
h.frame();
|
|
||||||
assert_corners!(h, top, (0, -150), (400, 50));
|
|
||||||
|
|
||||||
h.scroll((0, 10));
|
|
||||||
h.frame();
|
|
||||||
assert_corners!(h, top, (0, 0), (400, 200));
|
|
||||||
}
|
|
||||||
+61
-487
@@ -1,479 +1,38 @@
|
|||||||
//! 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
|
//! A seed is not a lead anybody can read: the tree is hundreds of widgets,
|
||||||
//! anybody can read: the tree is hundreds of widgets, and reconstructing the
|
//! and reconstructing the part that matters by hand has failed every time it
|
||||||
//! part that matters by hand has failed every time it has been tried. This
|
//! has been tried. This grows the trees `iris::random` describes, takes them
|
||||||
//! grows trees it can take apart, so a failure is reduced to the smallest
|
//! apart, and prints the smallest one that still fails as something to write
|
||||||
//! tree that still shows it and printed as something to write a fast test
|
//! a fast test from.
|
||||||
//! from.
|
|
||||||
//!
|
//!
|
||||||
//! cargo test --release --test shrink -- --ignored --nocapture
|
//! cargo test --release --test shrink -- --ignored --nocapture
|
||||||
//!
|
//!
|
||||||
//! `SHRINK_SEEDS` how many trees to try, `SHRINK_DEPTH` how deep to grow
|
//! `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
|
//! them, `SHRINK_CASE` which scenario or `all` for every one. `SHRINK_SEED`
|
||||||
//! ordinary tests pass, and turn what it finds into a test of its own rather
|
//! takes a single seed, which is how a failure `generated` printed is handed
|
||||||
//! than leaving a seed as the record.
|
//! 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;
|
#[path = "scenario/mod.rs"]
|
||||||
use iris::prelude::*;
|
mod scenario;
|
||||||
use iris::random::{Branch, Rng};
|
|
||||||
|
|
||||||
/// The same two leaves `iris::random` grows, since only one of them reads the
|
use iris::random::{Edits, Plan, plan};
|
||||||
/// width it is given and that is the difference that matters.
|
use scenario::{ALL, Case, diverges, env, over_seeds};
|
||||||
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.",
|
|
||||||
];
|
|
||||||
|
|
||||||
const ONE_LINE: &str = "one line, overflowing whatever it is given";
|
/// Takes the first simplification that still fails, until none does. The
|
||||||
|
/// simplifications come biggest first, so this walks down rather than
|
||||||
const OUTER: (f32, f32) = (1920.0, 1200.0);
|
/// nibbling: a six-hundred-widget tree reaches single figures in a few
|
||||||
const INNER: (f32, f32) = (640.0, 900.0);
|
/// hundred builds.
|
||||||
|
fn shrink(mut node: Plan, case: Case, seed: u64) -> Plan {
|
||||||
#[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<Len>, Option<Len>, 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>>,
|
|
||||||
) -> 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))).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: *gap,
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
spans.push(handle);
|
|
||||||
handle.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
Node::Stack(kids) => {
|
|
||||||
let children = kids.iter().map(|k| k.build(h, out, spans)).collect();
|
|
||||||
Stack {
|
|
||||||
children,
|
|
||||||
size: StackSize::Child(0),
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
Node::Pad(p, kid) => {
|
|
||||||
let inner = kid.build(h, out, spans);
|
|
||||||
Pad {
|
|
||||||
padding: Padding {
|
|
||||||
left: *p,
|
|
||||||
right: *p,
|
|
||||||
top: *p,
|
|
||||||
bottom: *p,
|
|
||||||
},
|
|
||||||
inner,
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
Node::Aligned(x, y, kid) => {
|
|
||||||
let inner = kid.build(h, out, spans);
|
|
||||||
Aligned {
|
|
||||||
inner,
|
|
||||||
align: Align {
|
|
||||||
x: axis_align(*x),
|
|
||||||
y: axis_align(*y),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
Node::Sized(x, y, kid) => {
|
|
||||||
let inner = kid.build(h, out, spans);
|
|
||||||
SetSize {
|
|
||||||
inner,
|
|
||||||
x: *x,
|
|
||||||
y: *y,
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
Node::Scroll(down, kid) => {
|
|
||||||
let inner = kid.build(h, out, spans);
|
|
||||||
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);
|
|
||||||
let wide = a.build(h, out, spans);
|
|
||||||
let narrow = b.build(h, out, spans);
|
|
||||||
Branch {
|
|
||||||
probe,
|
|
||||||
wide,
|
|
||||||
narrow,
|
|
||||||
threshold: *at,
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
out.push(id.id());
|
|
||||||
id
|
|
||||||
}
|
|
||||||
|
|
||||||
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(Len::px(20.0 + rng.below(180) as f32)),
|
|
||||||
1 => Some(Len::REST),
|
|
||||||
_ => 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(Len::px(20.0 + rng.below(180) as f32)),
|
|
||||||
1 => Some(Len::REST),
|
|
||||||
2 => Some(Len::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,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 root = node.build(&mut warm, &mut warm_ids, &mut warm_spans);
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// What the warm tree was moved into, grown that way from the start.
|
|
||||||
let want = match case {
|
|
||||||
Case::Reorder => reordered(node),
|
|
||||||
_ => node.clone(),
|
|
||||||
};
|
|
||||||
let mut cold = Harness::new(INNER);
|
|
||||||
let mut cold_ids = Vec::new();
|
|
||||||
let mut cold_spans = Vec::new();
|
|
||||||
let root = want.build(&mut cold, &mut cold_ids, &mut cold_spans);
|
|
||||||
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));
|
|
||||||
let same = match (got, want) {
|
|
||||||
(Some(g), Some(c)) => {
|
|
||||||
let d = |a: f32, b: f32| (a - b).abs() <= 0.05;
|
|
||||||
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 {
|
|
||||||
loop {
|
loop {
|
||||||
let Some(next) = node
|
let Some(next) = node
|
||||||
.smaller()
|
.smaller()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|small| diverges(small, case).is_some())
|
.find(|small| diverges(small, case, seed).is_some())
|
||||||
else {
|
else {
|
||||||
return node;
|
return node;
|
||||||
};
|
};
|
||||||
@@ -481,45 +40,60 @@ fn shrink(mut node: Node, case: Case) -> Node {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
fn cases() -> Vec<Case> {
|
||||||
std::env::var(name)
|
match env("SHRINK_CASE", String::from("all")).as_str() {
|
||||||
.ok()
|
"all" => ALL.to_vec(),
|
||||||
.and_then(|v| v.parse().ok())
|
name => match Case::named(name) {
|
||||||
.unwrap_or(fallback)
|
Some(case) => vec![case],
|
||||||
|
None => panic!(
|
||||||
|
"unknown SHRINK_CASE {name:?}; one of all, {}",
|
||||||
|
ALL.map(Case::name).join(", ")
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "a fuzzer; run it once the ordinary tests pass"]
|
#[ignore = "a fuzzer; run it once the ordinary tests pass"]
|
||||||
fn no_grown_tree_lays_out_differently_warm_than_cold() {
|
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 depth: usize = env("SHRINK_DEPTH", 5);
|
||||||
let case = match env("SHRINK_CASE", String::from("resize")).as_str() {
|
let cases = cases();
|
||||||
"repaint" => Case::Repaint,
|
let seeds: Vec<u64> = match std::env::var("SHRINK_SEED")
|
||||||
"resize-repaint" => Case::ResizeRepaint,
|
.ok()
|
||||||
"reorder" => Case::Reorder,
|
.and_then(|v| v.parse().ok())
|
||||||
_ => Case::Resize,
|
{
|
||||||
|
Some(seed) => vec![seed],
|
||||||
|
None => (1..=env("SHRINK_SEEDS", 400_u64)).collect(),
|
||||||
};
|
};
|
||||||
|
let count = seeds.len();
|
||||||
|
|
||||||
for seed in 1..=seeds {
|
over_seeds(seeds, |seed| {
|
||||||
let node = grow(&mut Rng::new(seed), depth);
|
let grown = plan(seed, depth, &Edits::default());
|
||||||
let Some(how) = diverges(&node, case) else {
|
for &case in &cases {
|
||||||
|
let Some(how) = diverges(&grown, case, seed) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let small = shrink(node.clone(), case);
|
let small = shrink(grown.clone(), case, seed);
|
||||||
println!(
|
println!(
|
||||||
"seed {seed}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
|
"seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
|
||||||
node.size(),
|
case.name(),
|
||||||
|
grown.size(),
|
||||||
small.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();
|
.collect();
|
||||||
let total: usize = sizes.iter().sum();
|
|
||||||
println!(
|
println!(
|
||||||
"{seeds} trees at depth {depth} agree: {} widgets total, largest {}",
|
"{count} trees at depth {depth} agree over {} case(s): {} widgets total, largest {}",
|
||||||
total,
|
cases.len(),
|
||||||
|
sizes.iter().sum::<usize>(),
|
||||||
sizes.iter().max().copied().unwrap_or(0)
|
sizes.iter().max().copied().unwrap_or(0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//! Every ordinary correctness test, as modules of one target.
|
||||||
|
//!
|
||||||
|
//! One binary rather than a dozen: each `tests/*.rs` links the whole
|
||||||
|
//! dependency graph again, which is most of what `cargo test` spends its time
|
||||||
|
//! on here. Libtest still runs the cases in parallel, and a filter still
|
||||||
|
//! selects them -- `cargo test --test suite layout::` for one module.
|
||||||
|
//!
|
||||||
|
//! The rigs stay their own targets: `shrink` and `generated` are fuzzers run
|
||||||
|
//! on their own, and the `*_cost` and `*_diagnostics` ones are measurements.
|
||||||
|
|
||||||
|
#[path = "cases/determinism.rs"]
|
||||||
|
mod determinism;
|
||||||
|
#[path = "cases/drift.rs"]
|
||||||
|
mod drift;
|
||||||
|
#[path = "cases/idempotence.rs"]
|
||||||
|
mod idempotence;
|
||||||
|
#[path = "cases/layout.rs"]
|
||||||
|
mod layout;
|
||||||
|
#[path = "cases/plan.rs"]
|
||||||
|
mod plan;
|
||||||
|
#[path = "cases/pointer.rs"]
|
||||||
|
mod pointer;
|
||||||
|
#[path = "cases/pointer_routing.rs"]
|
||||||
|
mod pointer_routing;
|
||||||
|
#[path = "cases/retained.rs"]
|
||||||
|
mod retained;
|
||||||
|
#[path = "cases/scroll.rs"]
|
||||||
|
mod scroll;
|
||||||
|
#[path = "cases/tasks.rs"]
|
||||||
|
mod tasks;
|
||||||
|
#[path = "cases/text_edit.rs"]
|
||||||
|
mod text_edit;
|
||||||
|
#[path = "cases/unsettled.rs"]
|
||||||
|
mod unsettled;
|
||||||
+15
-30
@@ -10,20 +10,14 @@ use iris::prelude::*;
|
|||||||
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
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 wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
||||||
let sized = SetSize {
|
let sized = wrapped.width(76).add(&mut h.rsc);
|
||||||
inner: wrapped.add_strong(&mut h.rsc),
|
let aligned = sized;
|
||||||
x: Some(Len::px(76.0)),
|
h.rsc
|
||||||
y: None,
|
.widgets_mut()
|
||||||
}
|
.set_alignment(sized, Axis::X, AxisAlign::POS);
|
||||||
.add(&mut h.rsc);
|
h.rsc
|
||||||
let aligned = Aligned {
|
.widgets_mut()
|
||||||
inner: sized.add_strong(&mut h.rsc),
|
.set_alignment(sized, Axis::Y, AxisAlign::POS);
|
||||||
align: Align {
|
|
||||||
x: Some(AxisAlign::Pos),
|
|
||||||
y: Some(AxisAlign::Pos),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let stack = Stack {
|
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), aligned.add_strong(&mut h.rsc)],
|
||||||
size: StackSize::Child(0),
|
size: StackSize::Child(0),
|
||||||
@@ -62,8 +56,8 @@ fn dump(label: &str, report: &diag::Report, text: WidgetId) {
|
|||||||
TraceEvent::SizeRead { id, reader, size } if *id == text => {
|
TraceEvent::SizeRead { id, reader, size } if *id == text => {
|
||||||
println!(" size read by {reader:?}: {size}")
|
println!(" size read by {reader:?}: {size}")
|
||||||
}
|
}
|
||||||
TraceEvent::Placed { id, parent, region } if *id == text => {
|
TraceEvent::RegionNode { id, parent, region } if *id == text => {
|
||||||
println!(" placed by {parent:?} at {region:?}")
|
println!(" region node under {parent:?} at {region:?}")
|
||||||
}
|
}
|
||||||
TraceEvent::Reuse { id, outcome } if *id == text => println!(" reuse: {outcome:?}"),
|
TraceEvent::Reuse { id, outcome } if *id == text => println!(" reuse: {outcome:?}"),
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -99,21 +93,12 @@ fn what_box_the_text_is_drawn_in() {
|
|||||||
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
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 text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||||
let aligned = Aligned {
|
let aligned = text;
|
||||||
inner: text.add_strong(&mut h.rsc),
|
h.rsc
|
||||||
align: Align {
|
.widgets_mut()
|
||||||
x: Some(AxisAlign::Neg),
|
.set_alignment(text, Axis::X, AxisAlign::NEG);
|
||||||
y: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
let sized = SetSize {
|
let sized = inner.sized((189, 176)).add(&mut h.rsc);
|
||||||
inner: inner.add_strong(&mut h.rsc),
|
|
||||||
x: Some(Len::px(189.0)),
|
|
||||||
y: Some(Len::px(176.0)),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let filler = rect(Color::RED).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, sized).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
h.state.root = Some(root.add_strong(&mut h.rsc));
|
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||||
|
|||||||
@@ -1,298 +0,0 @@
|
|||||||
//! The smallest trees that laid out differently warm than cold, each shrunk
|
|
||||||
//! by `tests/shrink.rs` from hundreds of widgets. The first two are a cold
|
|
||||||
//! frame that had not settled: a wrapping text shaped at a width it was
|
|
||||||
//! measured in rather than the one it was given. The rest are a widget
|
|
||||||
//! measured again in a box its own answer had decided, where the old answer
|
|
||||||
//! is a fixed point whatever the content now says.
|
|
||||||
|
|
||||||
use iris::harness::Harness;
|
|
||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
|
||||||
/// the tree changes -- every widget is marked for redraw and the frame is
|
|
||||||
/// taken again -- so no box may move, and a warm frame has to land where a
|
|
||||||
/// cold one does.
|
|
||||||
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
|
||||||
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
|
||||||
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
|
||||||
let sized = SetSize {
|
|
||||||
inner: wrapped.add_strong(&mut h.rsc),
|
|
||||||
x: Some(Len::px(76.0)),
|
|
||||||
y: None,
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let aligned = Aligned {
|
|
||||||
inner: sized.add_strong(&mut h.rsc),
|
|
||||||
align: Align {
|
|
||||||
x: Some(AxisAlign::Pos),
|
|
||||||
y: Some(AxisAlign::Pos),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let stack = Stack {
|
|
||||||
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
|
||||||
size: StackSize::Child(0),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
h.set_root(root);
|
|
||||||
vec![
|
|
||||||
plain.id(),
|
|
||||||
wrapped.id(),
|
|
||||||
sized.id(),
|
|
||||||
aligned.id(),
|
|
||||||
stack.id(),
|
|
||||||
root.id(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The first frame does not reach the layout a second one does, so "cold" is
|
|
||||||
/// not a fixed point and comparing against it compares against a tree that
|
|
||||||
/// has not settled.
|
|
||||||
#[test]
|
|
||||||
fn one_frame_is_enough() {
|
|
||||||
let mut h = Harness::new((640, 900));
|
|
||||||
let ids = plant(&mut h);
|
|
||||||
let first = h.region(&ids[1]).unwrap();
|
|
||||||
for _ in 0..3 {
|
|
||||||
for &id in &ids {
|
|
||||||
h.rsc.widgets_mut().get_dyn_mut(id);
|
|
||||||
}
|
|
||||||
h.frame();
|
|
||||||
}
|
|
||||||
let settled = h.region(&ids[1]).unwrap();
|
|
||||||
println!(
|
|
||||||
"first frame {} tall, settled {} tall",
|
|
||||||
first.bot_right.y - first.top_left.y,
|
|
||||||
settled.bot_right.y - settled.top_left.y
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
first.bot_right.y - first.top_left.y,
|
|
||||||
settled.bot_right.y - settled.top_left.y,
|
|
||||||
"the first frame had not finished laying out"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn repainting_everything_moves_nothing() {
|
|
||||||
let mut warm = Harness::new((640, 900));
|
|
||||||
let ids = plant(&mut warm);
|
|
||||||
for &id in &ids {
|
|
||||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
|
||||||
}
|
|
||||||
warm.frame();
|
|
||||||
|
|
||||||
let mut cold = Harness::new((640, 900));
|
|
||||||
let cold_ids = plant(&mut cold);
|
|
||||||
|
|
||||||
let mut wrong = Vec::new();
|
|
||||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
||||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
||||||
if got != want {
|
|
||||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box
|
|
||||||
/// is the same size whatever the output is, so a resize may not change any of
|
|
||||||
/// it -- but the text comes out 3.92px narrower warm than cold.
|
|
||||||
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
|
||||||
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
|
||||||
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
|
||||||
let aligned = Aligned {
|
|
||||||
inner: text.add_strong(&mut h.rsc),
|
|
||||||
align: Align {
|
|
||||||
x: Some(AxisAlign::Neg),
|
|
||||||
y: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
let sized = SetSize {
|
|
||||||
inner: inner.add_strong(&mut h.rsc),
|
|
||||||
x: Some(Len::px(189.0)),
|
|
||||||
y: Some(Len::px(176.0)),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
|
||||||
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
h.state.root = Some(root.add_strong(&mut h.rsc));
|
|
||||||
vec![
|
|
||||||
text.id(),
|
|
||||||
aligned.id(),
|
|
||||||
inner.id(),
|
|
||||||
sized.id(),
|
|
||||||
filler.id(),
|
|
||||||
root.id(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
|
|
||||||
let mut warm = Harness::new((1920, 1200));
|
|
||||||
let ids = plant_fixed(&mut warm);
|
|
||||||
warm.frame();
|
|
||||||
warm.resize((640, 900));
|
|
||||||
warm.frame();
|
|
||||||
|
|
||||||
let mut cold = Harness::new((640, 900));
|
|
||||||
let cold_ids = plant_fixed(&mut cold);
|
|
||||||
cold.frame();
|
|
||||||
|
|
||||||
let mut wrong = Vec::new();
|
|
||||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
||||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
||||||
if got != want {
|
|
||||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by
|
|
||||||
/// moving them, cold by growing them that way. Same widgets, same sizes, one
|
|
||||||
/// ends up 29.9px from where the other does.
|
|
||||||
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
|
|
||||||
let wrapped = wtext("Wrapping shapes one source into as many lines")
|
|
||||||
.size(16)
|
|
||||||
.wrap(true)
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let plain = wtext("one line, overflowing whatever it is given")
|
|
||||||
.size(16)
|
|
||||||
.wrap(false)
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let first: StrongWidget = wrapped.add_strong(&mut h.rsc);
|
|
||||||
let second: StrongWidget = plain.add_strong(&mut h.rsc);
|
|
||||||
let children = match swapped {
|
|
||||||
true => vec![second, first],
|
|
||||||
false => vec![first, second],
|
|
||||||
};
|
|
||||||
let span = Span {
|
|
||||||
children,
|
|
||||||
dir: Dir::RIGHT,
|
|
||||||
gap: 0.0,
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let span_handle = span;
|
|
||||||
let aligned = Aligned {
|
|
||||||
inner: span.add_strong(&mut h.rsc),
|
|
||||||
align: Align {
|
|
||||||
x: Some(AxisAlign::Center),
|
|
||||||
y: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.state.root = Some(aligned.add_strong(&mut h.rsc));
|
|
||||||
(
|
|
||||||
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
|
|
||||||
span_handle,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn swapping_two_children_lands_where_growing_them_that_way_does() {
|
|
||||||
let mut warm = Harness::new((640, 900));
|
|
||||||
let (ids, span) = plant_pair(&mut warm, false);
|
|
||||||
warm.frame();
|
|
||||||
warm.rsc[span].children.rotate_left(1);
|
|
||||||
warm.frame();
|
|
||||||
|
|
||||||
let mut cold = Harness::new((640, 900));
|
|
||||||
let (cold_ids, _) = plant_pair(&mut cold, true);
|
|
||||||
cold.frame();
|
|
||||||
|
|
||||||
let mut wrong = Vec::new();
|
|
||||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
||||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
||||||
if got != want {
|
|
||||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its
|
|
||||||
/// content from what the content says, and hands that box down through a
|
|
||||||
/// pass-through; the span under it was placed once, in that box, so nothing
|
|
||||||
/// at its own edge says the box was its own answer.
|
|
||||||
fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
|
|
||||||
let words = "Wrapping shapes one source into as many lines as the box leaves room for,";
|
|
||||||
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
|
||||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
|
||||||
let mut inner_children: Vec<StrongWidget> =
|
|
||||||
vec![text.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)];
|
|
||||||
if swapped {
|
|
||||||
inner_children.rotate_left(1);
|
|
||||||
}
|
|
||||||
let inner = Span {
|
|
||||||
children: inner_children,
|
|
||||||
dir: Dir::RIGHT,
|
|
||||||
gap: 0.0,
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let block = rect(Color::RED).add(&mut h.rsc);
|
|
||||||
let fixed = SetSize {
|
|
||||||
inner: block.add_strong(&mut h.rsc),
|
|
||||||
x: Some(Len::px(87.0)),
|
|
||||||
y: None,
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let mut outer_children: Vec<StrongWidget> =
|
|
||||||
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
|
|
||||||
if swapped {
|
|
||||||
outer_children.rotate_left(1);
|
|
||||||
}
|
|
||||||
let outer = Span {
|
|
||||||
children: outer_children,
|
|
||||||
dir: Dir::RIGHT,
|
|
||||||
gap: 0.0,
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let through = SetSize {
|
|
||||||
inner: outer.add_strong(&mut h.rsc),
|
|
||||||
x: None,
|
|
||||||
y: None,
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let scroll = Scroll::new(through.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
|
||||||
h.state.root = Some(scroll.add_strong(&mut h.rsc));
|
|
||||||
(
|
|
||||||
vec![
|
|
||||||
text.id(),
|
|
||||||
filler.id(),
|
|
||||||
inner.id(),
|
|
||||||
block.id(),
|
|
||||||
fixed.id(),
|
|
||||||
outer.id(),
|
|
||||||
through.id(),
|
|
||||||
scroll.id(),
|
|
||||||
],
|
|
||||||
[inner, outer],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_span_placed_once_in_a_box_its_answer_decided() {
|
|
||||||
let mut warm = Harness::new((640, 900));
|
|
||||||
let (ids, spans) = plant_scrolled(&mut warm, false);
|
|
||||||
warm.frame();
|
|
||||||
for span in spans {
|
|
||||||
warm.rsc[span].children.rotate_left(1);
|
|
||||||
}
|
|
||||||
warm.frame();
|
|
||||||
|
|
||||||
let mut cold = Harness::new((640, 900));
|
|
||||||
let (cold_ids, _) = plant_scrolled(&mut cold, true);
|
|
||||||
cold.frame();
|
|
||||||
|
|
||||||
let mut wrong = Vec::new();
|
|
||||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
||||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
||||||
if got != want {
|
|
||||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user