Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d0326769c | ||
|
|
2dba90bd0f | ||
|
|
b295c8b97a | ||
|
|
c2b8bf83de | ||
|
|
77ed7a24c0 | ||
|
|
f8aa0c5cdf | ||
|
|
b7b8d09e40 | ||
|
|
1096c3167a | ||
|
|
445287c95c | ||
|
|
750217631d | ||
|
|
9b4cc329ce | ||
|
|
02048eab77 | ||
|
|
d8d51221ee | ||
|
|
781199a7c9 | ||
|
|
69ba91588a | ||
|
|
8088a1fa59 | ||
|
|
713e3e747b | ||
|
|
aea0387567 | ||
|
|
1ebd4d3a05 | ||
|
|
7e2b4cd9db | ||
|
|
3da1c71870 | ||
|
|
6c84b6f2cb | ||
|
|
8d2b7a512b | ||
|
|
40b89c1f79 | ||
|
|
4d42f1c8ca | ||
|
|
2807a925af | ||
|
|
55df32a33c | ||
|
|
58ce74dd7d | ||
|
|
c55be21761 | ||
|
|
beb138632a | ||
|
|
aeb60e50f5 | ||
|
|
a904cf4f36 | ||
|
|
5642f2010a | ||
|
|
84dad211f5 | ||
|
|
add6774980 | ||
|
|
cadfba05dd | ||
|
|
38b3a81053 |
No files matched your search
@@ -25,6 +25,12 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"]
|
||||
[workspace]
|
||||
members = ["core", "macro", "rig-input"]
|
||||
|
||||
# Full debug info was the bulk of what the linker wrote here and almost none of
|
||||
# what anything read. `dev` keeps line tables and scopes, which is what stepping
|
||||
# through an example wants; the tests keep the line tables alone, which is what
|
||||
# a backtrace reads. Measured when the tests became one target: relinking them
|
||||
# went from 9.8 s to 7.7 s with these, and target/ from 45 GB to 13 GB with the
|
||||
# two changes together.
|
||||
[profile.dev]
|
||||
debug = 1
|
||||
|
||||
|
||||
@@ -14,3 +14,27 @@ WidgetRef<W> or smth instead of Id
|
||||
vecs for each widget type?
|
||||
|
||||
POTENTIAL BUG: closures that store IDs will not decrement the id!!! need to not increment id if moved into closure somehow??? wait no, need to decrement ID every time an event fn is added...... only if the id is used in it..??
|
||||
|
||||
transforms on a move entry (scale + rotation)
|
||||
an entry is a translation today; composing through one scales the rel
|
||||
part and passes px through untouched, so fixed-size content and glyphs
|
||||
do not follow a shortened entry
|
||||
want a real transform per entry, resolved in resolve_move the way the
|
||||
translation already is, so a whole subtree transforms with one buffer
|
||||
write and no redraw
|
||||
wanted for compose-style stretch at the end of a scroll area, and for
|
||||
rotation generally
|
||||
|
||||
a prepare stage on Event, so Data has no placeholder field
|
||||
run_sensors builds one CursorData per widget and has to put something in
|
||||
`sense` before anything knows which sense matched, so it writes
|
||||
CursorSense::Hovering and says in place that it means nothing;
|
||||
should_run then clones the whole thing to overwrite that one field
|
||||
the state is representable only because the type lets the caller say it:
|
||||
what the caller supplies and what matching adds are two different things
|
||||
wearing one struct
|
||||
the awkward part is doing it without the generics getting annoying --
|
||||
Data<'a> is already a GAT with a default, and splitting it in two adds
|
||||
another associated type to every Event impl for the sake of one field
|
||||
(Bryan, 2026-09-20; low priority, he wants a good answer rather than a
|
||||
quick one)
|
||||
@@ -131,14 +131,6 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
self.0 as f32 / Self::one().0 as f32
|
||||
}
|
||||
|
||||
/// The same value on another grid, rounded where the new one is coarser.
|
||||
pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> {
|
||||
Fixed(match TO >= SHIFT {
|
||||
true => 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))
|
||||
}
|
||||
@@ -246,16 +238,6 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
@@ -531,15 +513,6 @@ mod tests {
|
||||
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));
|
||||
|
||||
+102
-94
@@ -15,7 +15,7 @@
|
||||
//! reuse, size, placement, and text events for one suspicious widget. The
|
||||
//! selection is a set and survives [`take`] until cleared.
|
||||
|
||||
use crate::{Axis, LayoutLen, PxVec2, Size, UiRegion, WidgetId};
|
||||
use crate::{Axis, LayoutHolds, LayoutLen, PxVec2, Size, UiRegion, UiVec2, WidgetId};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
@@ -23,100 +23,71 @@ use std::{
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum Counter {
|
||||
Updates,
|
||||
DrawRequests,
|
||||
WidgetDraws,
|
||||
RegionNodeDraws,
|
||||
SizeReads,
|
||||
HintHits,
|
||||
HintMisses,
|
||||
ReuseAttempts,
|
||||
ReuseExact,
|
||||
ReuseMoved,
|
||||
ReuseDirty,
|
||||
ReuseWrongParent,
|
||||
ReuseRemapped,
|
||||
ReuseOutside,
|
||||
ReuseWrongLayer,
|
||||
ReuseWrongNode,
|
||||
QueuePops,
|
||||
DepthReads,
|
||||
LocalRedraws,
|
||||
SizeChanges,
|
||||
ReaderEdges,
|
||||
PrimitiveWrites,
|
||||
TextRenders,
|
||||
TextShapeHits,
|
||||
TextShapes,
|
||||
TextBreaks,
|
||||
GlyphPlacements,
|
||||
OutsidePinnedLen,
|
||||
OutsideFrame,
|
||||
OutsideExtent,
|
||||
/// Declares a counter or timer kind beside the name its report prints. Two
|
||||
/// lists in the same order was one list too many: a variant inserted without
|
||||
/// its label moving with it renames every total after it, and nothing says
|
||||
/// so.
|
||||
macro_rules! labelled {
|
||||
($(#[$meta:meta])* $vis:vis enum $Name:ident { $($variant:ident = $label:literal,)* }) => {
|
||||
$(#[$meta])*
|
||||
#[derive(Clone, Copy)]
|
||||
$vis enum $Name { $($variant,)* }
|
||||
|
||||
impl $Name {
|
||||
const COUNT: usize = [$($label,)*].len();
|
||||
const NAMES: [&'static str; Self::COUNT] = [$($label,)*];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
const COUNT: usize = Self::OutsideExtent as usize + 1;
|
||||
|
||||
const NAMES: [&'static str; Self::COUNT] = [
|
||||
"updates",
|
||||
"draw requests",
|
||||
"widget draws",
|
||||
"region-node draws",
|
||||
"draw-result size reads",
|
||||
"hint hits",
|
||||
"hint misses",
|
||||
"reuse attempts",
|
||||
"reuse exact",
|
||||
"reuse moved",
|
||||
"reuse: dirty",
|
||||
"reuse: wrong parent",
|
||||
"reuse remapped",
|
||||
"reuse: outside what it holds for",
|
||||
"reuse: another layer",
|
||||
"reuse: region-node choice changed",
|
||||
"redraw queue pops",
|
||||
"depth reads",
|
||||
"local redraws",
|
||||
"size changes",
|
||||
"reader edges",
|
||||
"primitive writes",
|
||||
"text renders",
|
||||
"text shape hits",
|
||||
"text shapes",
|
||||
"text line breaks",
|
||||
"glyph placements",
|
||||
"reuse outside: the length it was pinned to",
|
||||
"reuse outside: a frame length",
|
||||
"reuse outside: an extent length",
|
||||
];
|
||||
labelled! {
|
||||
pub(crate) enum Counter {
|
||||
Updates = "updates",
|
||||
DrawRequests = "draw requests",
|
||||
WidgetDraws = "widget draws",
|
||||
RegionNodeDraws = "region-node draws",
|
||||
SizeReads = "draw-result size reads",
|
||||
HintHits = "hint hits",
|
||||
HintMisses = "hint misses",
|
||||
ReuseAttempts = "reuse attempts",
|
||||
ReuseExact = "reuse exact",
|
||||
ReuseMoved = "reuse moved",
|
||||
ReuseDirty = "reuse: dirty",
|
||||
ReuseUndrawn = "reuse: nothing drawn to keep",
|
||||
ReuseWrongParent = "reuse: wrong parent",
|
||||
ReuseRemapped = "reuse remapped",
|
||||
ReuseOutside = "reuse: outside what it holds for",
|
||||
ReuseWrongLayer = "reuse: another layer",
|
||||
ReuseWrongNode = "reuse: region-node choice changed",
|
||||
ReuseWrongMask = "reuse: a different inherited mask",
|
||||
QueuePops = "redraw queue pops",
|
||||
DepthReads = "depth reads",
|
||||
LocalRedraws = "local redraws",
|
||||
SizeChanges = "size changes",
|
||||
ReaderEdges = "reader edges",
|
||||
PrimitiveWrites = "primitive writes",
|
||||
TextRenders = "text renders",
|
||||
TextShapeHits = "text shape hits",
|
||||
TextShapes = "text shapes",
|
||||
TextBreaks = "text line breaks",
|
||||
GlyphPlacements = "glyph placements",
|
||||
OutsidePinnedLen = "reuse outside: the length it was pinned to",
|
||||
OutsideWindow = "reuse outside: this window",
|
||||
OutsideRelBase = "reuse outside: a rel base",
|
||||
OutsideRegion = "reuse outside: a region length",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum TimerKind {
|
||||
Update,
|
||||
FullLayout,
|
||||
IncrementalLayout,
|
||||
TextRender,
|
||||
TextShape,
|
||||
TextBreak,
|
||||
GlyphPlacement,
|
||||
}
|
||||
|
||||
impl TimerKind {
|
||||
const COUNT: usize = Self::GlyphPlacement as usize + 1;
|
||||
|
||||
const NAMES: [&'static str; Self::COUNT] = [
|
||||
"update total",
|
||||
"full layout",
|
||||
"incremental layout",
|
||||
"text render",
|
||||
"text shape",
|
||||
"text line break",
|
||||
"glyph placement",
|
||||
];
|
||||
labelled! {
|
||||
pub(crate) enum TimerKind {
|
||||
Update = "update total",
|
||||
FullLayout = "full layout",
|
||||
IncrementalLayout = "incremental layout",
|
||||
TextRender = "text render",
|
||||
TextShape = "text shape",
|
||||
TextBreak = "text line break",
|
||||
GlyphPlacement = "glyph placement",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -251,6 +222,8 @@ pub enum ReuseOutcome {
|
||||
Dirty,
|
||||
WrongParent,
|
||||
WrongLayer,
|
||||
WrongMask,
|
||||
WrongNode,
|
||||
Remapped,
|
||||
Outside,
|
||||
Undrawn,
|
||||
@@ -264,7 +237,7 @@ pub enum TraceEvent {
|
||||
id: WidgetId,
|
||||
parent: Option<WidgetId>,
|
||||
region: UiRegion,
|
||||
pixel_size: PxVec2,
|
||||
region_px: PxVec2,
|
||||
region_node: bool,
|
||||
},
|
||||
Reuse {
|
||||
@@ -360,7 +333,7 @@ pub(crate) fn draw_request(
|
||||
id: WidgetId,
|
||||
parent: Option<WidgetId>,
|
||||
region: UiRegion,
|
||||
pixel_size: PxVec2,
|
||||
region_px: PxVec2,
|
||||
region_node: bool,
|
||||
) {
|
||||
trace(
|
||||
@@ -369,7 +342,7 @@ pub(crate) fn draw_request(
|
||||
id,
|
||||
parent,
|
||||
region,
|
||||
pixel_size,
|
||||
region_px,
|
||||
region_node,
|
||||
},
|
||||
);
|
||||
@@ -379,6 +352,41 @@ pub(crate) fn reuse(id: WidgetId, outcome: ReuseOutcome) {
|
||||
trace(id, TraceEvent::Reuse { id, outcome });
|
||||
}
|
||||
|
||||
/// A drawing that cannot be reused because the box on offer is outside what
|
||||
/// it holds for, and which of the four contracts said so. They overlap: a
|
||||
/// drawing can be outside two of them at once, and counting each is what
|
||||
/// says where a rel base redrawing more than it should is coming from.
|
||||
pub(crate) fn outside(
|
||||
id: WidgetId,
|
||||
holds: LayoutHolds,
|
||||
region: UiRegion,
|
||||
rel_base: UiVec2,
|
||||
window: PxVec2,
|
||||
) {
|
||||
for axis in Axis::BOTH {
|
||||
let holds = holds[axis];
|
||||
let len = region[axis].len();
|
||||
let window = window[axis];
|
||||
if holds.region_len.is_some_and(|pinned| pinned != len) {
|
||||
bump(Counter::OutsidePinnedLen);
|
||||
}
|
||||
if !holds.window.contains(window) {
|
||||
bump(Counter::OutsideWindow);
|
||||
}
|
||||
if holds
|
||||
.rel_base
|
||||
.is_some_and(|pinned| pinned != rel_base[axis])
|
||||
{
|
||||
bump(Counter::OutsideRelBase);
|
||||
}
|
||||
if !holds.region.contains(len.to_px(window)) {
|
||||
bump(Counter::OutsideRegion);
|
||||
}
|
||||
}
|
||||
bump(Counter::ReuseOutside);
|
||||
reuse(id, ReuseOutcome::Outside);
|
||||
}
|
||||
|
||||
pub(crate) fn size_reported(id: WidgetId, size: Size) {
|
||||
trace(id, TraceEvent::SizeReported { id, size });
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#![feature(unsize)]
|
||||
#![feature(coerce_unsized)]
|
||||
#![feature(option_into_flat_iter)]
|
||||
#![feature(const_index)]
|
||||
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
pub mod layout_diagnostics;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::util::impl_axis_index;
|
||||
use crate::{Px, Rel};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Align {
|
||||
pub x: Option<AxisAlign>,
|
||||
pub y: Option<AxisAlign>,
|
||||
@@ -83,28 +84,6 @@ pub struct RegionAlign {
|
||||
pub y: AxisAlign,
|
||||
}
|
||||
|
||||
impl RegionAlign {
|
||||
/// Both axes at the near edge: the start of a box in its own orientation.
|
||||
pub const NEAR: Self = Self {
|
||||
x: AxisAlign::NEG,
|
||||
y: AxisAlign::NEG,
|
||||
};
|
||||
|
||||
pub fn axis(&self, axis: Axis) -> AxisAlign {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut AxisAlign {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RegionAlign {
|
||||
pub const TOP_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::NEG);
|
||||
pub const TOP_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::NEG);
|
||||
@@ -172,13 +151,15 @@ impl Vec2 {
|
||||
}
|
||||
|
||||
impl Len {
|
||||
/// This length placed in the box it is measured in: the alignment names a
|
||||
/// point along that box, and the two ends are that point less the part of
|
||||
/// the length falling before it and plus the part falling after.
|
||||
pub const fn align(&self, align: AxisAlign) -> UiSpan {
|
||||
let rel = align.rel();
|
||||
let rest = Rel::ONE.sub(rel);
|
||||
let at = Len::from_parts(rel, Px::ZERO);
|
||||
UiSpan {
|
||||
start: Len::from_parts(at.rel.sub(self.rel.mul(rel)), at.px.sub(self.px.mul(rel))),
|
||||
end: Len::from_parts(at.rel.add(self.rel.mul(rest)), at.px.add(self.px.mul(rest))),
|
||||
start: at - self.scale(rel),
|
||||
end: at + self.scale(Rel::ONE.sub(rel)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,3 +212,6 @@ impl RegionAlign {
|
||||
UiVec2::from(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl_axis_index!(RegionAlign => AxisAlign);
|
||||
impl_axis_index!(Align => Option<AxisAlign>);
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::util::impl_axis_index;
|
||||
use crate::{Fixed, FixedVec2};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
@@ -8,14 +9,8 @@ pub enum Axis {
|
||||
}
|
||||
|
||||
impl Axis {
|
||||
/// A per-axis pair with `aligned` on this axis and `ortho` on the other,
|
||||
/// which is what `from_axis` does for a vector.
|
||||
pub fn pair<T>(self, aligned: T, ortho: T) -> [T; 2] {
|
||||
match self {
|
||||
Self::X => [aligned, ortho],
|
||||
Self::Y => [ortho, aligned],
|
||||
}
|
||||
}
|
||||
/// Both of them, for the layout code that asks the same question of each.
|
||||
pub const BOTH: [Self; 2] = [Self::X, Self::Y];
|
||||
}
|
||||
|
||||
impl std::ops::Not for Axis {
|
||||
@@ -53,20 +48,6 @@ pub enum Sign {
|
||||
}
|
||||
|
||||
impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
||||
pub const fn axis(&self, axis: Axis) -> Fixed<SHIFT> {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn axis_mut(&mut self, axis: Axis) -> &mut Fixed<SHIFT> {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_axis(axis: Axis, aligned: Fixed<SHIFT>, ortho: Fixed<SHIFT>) -> Self {
|
||||
match axis {
|
||||
Axis::X => Self::new(aligned, ortho),
|
||||
@@ -76,20 +57,6 @@ impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
pub fn axis(&self, axis: Axis) -> f32 {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut f32 {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_axis(axis: Axis, aligned: f32, ortho: f32) -> Self {
|
||||
Self {
|
||||
x: match axis {
|
||||
@@ -104,47 +71,5 @@ impl Vec2 {
|
||||
}
|
||||
}
|
||||
|
||||
pub const trait AxisT {
|
||||
fn get() -> Axis;
|
||||
}
|
||||
|
||||
pub struct XAxis;
|
||||
const impl AxisT for XAxis {
|
||||
fn get() -> Axis {
|
||||
Axis::X
|
||||
}
|
||||
}
|
||||
|
||||
pub struct YAxis;
|
||||
const impl AxisT for YAxis {
|
||||
fn get() -> Axis {
|
||||
Axis::Y
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct BothAxis<T> {
|
||||
pub x: T,
|
||||
pub y: T,
|
||||
}
|
||||
|
||||
impl<T> BothAxis<T> {
|
||||
pub const fn axis<A: const AxisT>(&mut self) -> &mut T {
|
||||
match A::get() {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
pub fn take_axis<A: const AxisT>(self) -> T {
|
||||
match A::get() {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
pub fn axis_dyn(&mut self, axis: Axis) -> &mut T {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl_axis_index!({const SHIFT: u32} FixedVec2<SHIFT> => Fixed<SHIFT>);
|
||||
impl_axis_index!(Vec2 => f32);
|
||||
+32
-15
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::util::impl_axis_index;
|
||||
use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
@@ -118,20 +119,6 @@ impl Size {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis(&self, axis: Axis) -> LayoutLen {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut LayoutLen {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutLen {
|
||||
@@ -158,11 +145,39 @@ impl LayoutLen {
|
||||
Len::from_parts(self.rel.add(share), self.px)
|
||||
}
|
||||
|
||||
/// Only pixels: the same number of them whatever box it lands in, and
|
||||
/// whatever anyone else in the row asks for. A length that is any part
|
||||
/// of a box or of what is left over is not one.
|
||||
pub fn is_px(&self) -> bool {
|
||||
self.rel == Rel::ZERO && self.leftover == Weight::ZERO
|
||||
}
|
||||
|
||||
/// Nothing but a claim on what is left over, so there is no length here
|
||||
/// at all where nothing is.
|
||||
pub fn is_only_leftover(&self) -> bool {
|
||||
self.leftover > Weight::ZERO && self.without_leftover() == Len::ZERO
|
||||
}
|
||||
|
||||
/// This as a length of a box, where it is one. `leftover` is not: a
|
||||
/// share of what is left over is a length only to whoever divides one,
|
||||
/// so it passes up in the reported size instead and is resolved there.
|
||||
pub fn declared(&self) -> Option<Len> {
|
||||
(self.leftover == Weight::ZERO).then(|| self.without_leftover())
|
||||
}
|
||||
|
||||
/// What this takes whatever is left over: the reading of a length for
|
||||
/// anyone not dividing a box between siblings, where a share is a claim
|
||||
/// on someone else's room rather than a length of its own.
|
||||
/// [`Self::apply_leftover`] is the opposite reading of the same value.
|
||||
pub const fn without_leftover(&self) -> Len {
|
||||
Len::from_parts(self.rel, self.px)
|
||||
}
|
||||
|
||||
/// This length, given as a part of a box `len` long, as a part of the
|
||||
/// box `len` is itself a part of. The share is untouched: it is a claim
|
||||
/// on whoever divides the room, not a fraction of anything.
|
||||
pub const fn within_len(self, len: Len) -> Self {
|
||||
let part = Len::from_parts(self.rel, self.px).within_len(len);
|
||||
let part = self.without_leftover().within_len(len);
|
||||
Self {
|
||||
px: part.px,
|
||||
rel: part.rel,
|
||||
@@ -236,3 +251,5 @@ impl std::fmt::Display for LayoutLen {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl_axis_index!(Size => LayoutLen);
|
||||
+13
-60
@@ -1,3 +1,4 @@
|
||||
use crate::util::impl_axis_index;
|
||||
use std::{fmt::Display, marker::Destruct};
|
||||
|
||||
use super::*;
|
||||
@@ -61,20 +62,6 @@ impl UiVec2 {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut Len {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis(&self, axis: Axis) -> Len {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolved against a box of `size`, which is where a fraction stops
|
||||
/// being one and becomes a place.
|
||||
pub fn to_px(&self, size: PxVec2) -> PxVec2 {
|
||||
@@ -176,14 +163,6 @@ impl Len {
|
||||
Self::from_parts(Rel::ZERO, Px::from_f32(px))
|
||||
}
|
||||
|
||||
pub const fn rel_min() -> Self {
|
||||
Self::ZERO
|
||||
}
|
||||
|
||||
pub const fn rel_max() -> Self {
|
||||
Self::FULL
|
||||
}
|
||||
|
||||
pub const fn max(&self, other: Self) -> Self {
|
||||
Self {
|
||||
rel: self.rel.max(other.rel),
|
||||
@@ -226,10 +205,6 @@ impl Len {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_len(&self, len: Len) -> Self {
|
||||
len.within_len(*self)
|
||||
}
|
||||
|
||||
pub const fn flip(&mut self) {
|
||||
self.rel = Rel::ONE.sub(self.rel);
|
||||
self.px = self.px.neg();
|
||||
@@ -294,17 +269,17 @@ impl UiSpan {
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn len(&self) -> Len {
|
||||
self.end - self.start
|
||||
/// A box `len` long inside this one, on the side `align` says. Both must
|
||||
/// be lengths of the same rel base: it subtracts one from the other
|
||||
/// rather than composing it in, which is what keeps a fraction the same
|
||||
/// fraction however long this box turns out to be.
|
||||
pub const fn place(self, len: Len, align: AxisAlign) -> Self {
|
||||
let start = self.start + (self.len() - len).scale(align.rel());
|
||||
Self::new(start, start + len)
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
pub const fn len(&self) -> Len {
|
||||
self.end - self.start
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,17 +291,6 @@ pub struct 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 {
|
||||
x: UiSpan::FULL,
|
||||
y: UiSpan::FULL,
|
||||
@@ -348,20 +312,6 @@ impl UiRegion {
|
||||
y: self.y.within(&parent.y),
|
||||
}
|
||||
}
|
||||
pub const fn axis(&self, axis: Axis) -> &UiSpan {
|
||||
match axis {
|
||||
Axis::X => &self.x,
|
||||
Axis::Y => &self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn axis_mut(&mut self, axis: Axis) -> &mut UiSpan {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn flip(&mut self, axis: Axis) {
|
||||
match axis {
|
||||
Axis::X => self.x.flip(),
|
||||
@@ -462,3 +412,6 @@ impl Display for PixelRegion {
|
||||
write!(f, "{} -> {}", self.top_left, self.bot_right)
|
||||
}
|
||||
}
|
||||
|
||||
impl_axis_index!(UiVec2 => Len);
|
||||
impl_axis_index!(UiRegion => UiSpan);
|
||||
@@ -133,10 +133,6 @@ impl TextBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self::new("")
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
@@ -167,10 +167,6 @@ impl GlyphAtlas {
|
||||
pub fn page_count(&self) -> u32 {
|
||||
self.pages.len() as u32
|
||||
}
|
||||
|
||||
pub fn glyph_count(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
|
||||
+13
-4
@@ -23,13 +23,22 @@ pub use primitive::*;
|
||||
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
||||
|
||||
fn module_source(wgsl: &str) -> String {
|
||||
// The steps come from the same constants the CPU counts in, rather than
|
||||
// a second copy of them written into the shader: a grid the two disagree
|
||||
// about puts every coordinate somewhere else.
|
||||
// Every number both sides count in, written once here rather than a
|
||||
// second time in the shader: a grid the two disagree about puts every
|
||||
// coordinate somewhere else, and a sentinel they disagree about makes one
|
||||
// of them walk a chain from a slot the other says is not there.
|
||||
format!(
|
||||
"const PX_STEP: f32 = 1.0 / {}.0;\nconst REL_STEP: f32 = 1.0 / {}.0;\n{PRELUDE}\n{wgsl}",
|
||||
"const PX_STEP: f32 = 1.0 / {}.0;\n\
|
||||
const REL_STEP: f32 = 1.0 / {}.0;\n\
|
||||
const MASK_NONE: u32 = {}u;\n\
|
||||
const MOVE_NONE: u32 = {}u;\n\
|
||||
const CHAIN_LIMIT: u32 = {}u;\n\
|
||||
{PRELUDE}\n{wgsl}",
|
||||
1u32 << crate::PX_SHIFT,
|
||||
1u32 << crate::REL_SHIFT,
|
||||
MaskIdx::NONE.idx(),
|
||||
MoveIdx::NONE.idx(),
|
||||
crate::CHAIN_LIMIT,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,9 +26,11 @@ struct MoveOffset {
|
||||
parent: u32,
|
||||
}
|
||||
|
||||
// `PX_STEP` and `REL_STEP` are prepended from `iris_core`'s own constants:
|
||||
// what it stores is a whole count of each, both powers of two, so decoding
|
||||
// is exact and the number here is the number the CPU decided.
|
||||
// `PX_STEP`, `REL_STEP`, `MASK_NONE`, `MOVE_NONE` and `CHAIN_LIMIT` are
|
||||
// prepended from `iris_core`'s own constants, so none of them is written
|
||||
// twice. What the CPU stores is a whole count of each step, and both steps
|
||||
// are powers of two, so decoding is exact and the number here is the number
|
||||
// the CPU decided.
|
||||
|
||||
// Every coordinate the CPU decided is a whole count of `PX_STEP`, so one that
|
||||
// composes to within half a step of a pixel boundary is on that boundary and
|
||||
@@ -70,12 +72,6 @@ struct Region {
|
||||
y: UiSpan,
|
||||
}
|
||||
|
||||
const MOVE_NONE: u32 = 4294967295u;
|
||||
// Keep in step with `iris_core::CHAIN_LIMIT`. It bounds a malformed cycle
|
||||
// rather than any real tree, and the CPU walk uses the same number so both
|
||||
// resolve a deep one the same way.
|
||||
const CHAIN_LIMIT: u32 = 64u;
|
||||
|
||||
// The same expression `Len::within` uses, in floats rather than on the
|
||||
// CPU's grid: a move is resolved here so that scrolling a subtree writes one
|
||||
// entry instead of walking it. What has to hold is that this agrees with
|
||||
@@ -171,7 +167,7 @@ fn vs_main(
|
||||
}
|
||||
|
||||
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
||||
if in.mask_idx == 4294967295u {
|
||||
if in.mask_idx == MASK_NONE {
|
||||
return color;
|
||||
}
|
||||
let mask = masks[in.mask_idx];
|
||||
|
||||
+35
-27
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
LayerId, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Place, RegionAlign, RetainedPrimitive,
|
||||
Declared, LayerId, LayoutHolds, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, RetainedPrimitive,
|
||||
Size, TextureHandle, UiRegion, UiVec2, WidgetId,
|
||||
};
|
||||
|
||||
@@ -10,29 +10,24 @@ use crate::{
|
||||
pub struct ActiveData {
|
||||
pub id: WidgetId,
|
||||
/// Where its drawing goes, in its region node's coordinates.
|
||||
pub extent: UiRegion,
|
||||
pub placement: UiRegion,
|
||||
/// What a fraction declared or reported under this widget is a fraction
|
||||
/// of, as a length of the window.
|
||||
pub frame: UiVec2,
|
||||
/// A frame its parent decided for it on each axis -- a row's slot, or
|
||||
/// padding's frame less its pixels -- as a length of the window. `None`
|
||||
/// forwards the parent's frame. What it declared is kept separately in
|
||||
/// `declared` and is a fraction of whichever of the two reached it.
|
||||
pub narrow: [Option<Len>; 2],
|
||||
/// Where its drawing was put, and where it was asked, each as a part of
|
||||
/// its parent's box. The two differ where a container asks in one place
|
||||
/// and puts the answer in another -- a row measures from its cursor and
|
||||
/// puts the child in its slot. A part is a length from the box's start,
|
||||
/// so a box that moved re-places every child by re-adding that start.
|
||||
pub placed: [Place; 2],
|
||||
pub asked: [Place; 2],
|
||||
pub rel_base: UiVec2,
|
||||
/// Where its drawing was put, and where it was asked. The two differ
|
||||
/// where a container asks in one place and puts the answer in another --
|
||||
/// a row measures from its cursor and puts the child in its slot. Each
|
||||
/// carries the rel base that ask stated, so asking again from either is
|
||||
/// the same question it was.
|
||||
pub placed: PlaceDesc,
|
||||
pub asked: PlaceDesc,
|
||||
/// The box it was asked in, in the parent's region-node coordinates: the
|
||||
/// box its drawing was made in and the one its contract is about. Its
|
||||
/// drawing is placed elsewhere by re-expression, never by asking again.
|
||||
pub part: UiRegion,
|
||||
pub region: UiRegion,
|
||||
/// The measured answer and its dependencies. A hint-only dependency or
|
||||
/// a widget first encountered during placement has no measurement yet.
|
||||
pub answer: Option<(Size, LayoutHolds)>,
|
||||
pub answer: Option<Answer>,
|
||||
/// Asked more than once in its parent's last draw -- measured in one box
|
||||
/// and then asked in the one the parent decided. The parent's layout
|
||||
/// rests on the first answer and its drawing on the last, so only the
|
||||
@@ -40,8 +35,8 @@ pub struct ActiveData {
|
||||
pub re_asked: bool,
|
||||
/// What the widget reported, in window-unit lengths.
|
||||
pub size: Size,
|
||||
/// The window and extent reads that this drawing holds for, and the
|
||||
/// frame and box it pinned.
|
||||
/// The window and region reads that this drawing holds for, and the
|
||||
/// rel base and region it pinned.
|
||||
pub holds: LayoutHolds,
|
||||
pub drawn: bool,
|
||||
pub parent: Option<WidgetId>,
|
||||
@@ -51,24 +46,23 @@ pub struct ActiveData {
|
||||
pub depth: usize,
|
||||
pub textures: Vec<TextureHandle>,
|
||||
/// Its primitives, each keeping the box it was written in -- in this
|
||||
/// widget's extent coordinates, which is what a move recomposes from.
|
||||
/// widget's placement coordinates, which is what a move recomposes from.
|
||||
pub primitives: Vec<RetainedPrimitive>,
|
||||
/// An owned mask holds one reference independently of its primitives.
|
||||
pub mask_region: Option<UiRegion>,
|
||||
pub children: Vec<WidgetId>,
|
||||
/// The children whose size this widget read while drawing.
|
||||
pub size_deps: Vec<WidgetId>,
|
||||
/// The movable region its primitives are positioned through: its own when
|
||||
/// opted in, otherwise the nearest ancestor's.
|
||||
pub move_idx: MoveIdx,
|
||||
/// The declared lengths whoever drew this widget resolved into its frame.
|
||||
/// The declared lengths whoever drew this widget resolved into its rel base.
|
||||
/// A change to one moves a box this widget cannot fix by drawing again,
|
||||
/// and comparing them is what says so.
|
||||
pub declared: [Option<LayoutLen>; 2],
|
||||
pub declared: Declared,
|
||||
/// 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 `extent` uses when this widget
|
||||
/// does not own a region node.
|
||||
/// The movable region whose coordinates its placement is in when this
|
||||
/// widget does not own a region node.
|
||||
pub parent_move: MoveIdx,
|
||||
/// The mask its drawing is clipped to: one it set itself, or the one it
|
||||
/// inherited from whoever drew it.
|
||||
@@ -86,6 +80,20 @@ impl ActiveData {
|
||||
/// all. Not `size`, which is what its last drawing reported: a drawing
|
||||
/// re-expressed in the box that answer chose is not a second answer.
|
||||
pub fn measured(&self) -> Option<Size> {
|
||||
self.answer.map(|(size, _)| size)
|
||||
self.answer.map(|answer| answer.size)
|
||||
}
|
||||
|
||||
/// Whether it owns a region node rather than sharing the one it was drawn
|
||||
/// under, which is what its two move indices being different says.
|
||||
pub fn is_region_node(&self) -> bool {
|
||||
self.move_idx != self.parent_move
|
||||
}
|
||||
}
|
||||
|
||||
/// What a widget answered when it was asked: the size it reported, and the
|
||||
/// boxes and windows that answer holds for.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Answer {
|
||||
pub size: Size,
|
||||
pub holds: LayoutHolds,
|
||||
}
|
||||
+28
-2
@@ -19,6 +19,26 @@ pub struct Holds {
|
||||
pub hi: Px,
|
||||
}
|
||||
|
||||
impl Len {
|
||||
/// Whether this is longer than `than` in a window this wide, and the
|
||||
/// windows that answer holds for.
|
||||
///
|
||||
/// Which is longer is a question in pixels -- `rel(0.5)` is longer than
|
||||
/// 300 px at a box of 600 and shorter at 400 -- and it is asked of the
|
||||
/// difference and answered back through that same difference, so the
|
||||
/// boundary is the comparison's own rather than a second way of finding
|
||||
/// it.
|
||||
pub fn longer_than(&self, than: Len, window: Px) -> (bool, Holds) {
|
||||
let over = *self - than;
|
||||
let longer = over.to_px(window) > Px::ZERO;
|
||||
let side = match longer {
|
||||
true => Px::STEP..=Px::MAX,
|
||||
false => Px::MIN..=Px::ZERO,
|
||||
};
|
||||
(longer, Holds::from(side).through(over))
|
||||
}
|
||||
}
|
||||
|
||||
impl Holds {
|
||||
pub const ANY: Self = Self {
|
||||
lo: Px::MIN,
|
||||
@@ -33,7 +53,13 @@ impl Holds {
|
||||
len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
|
||||
}
|
||||
|
||||
pub const fn and(self, other: Self) -> Self {
|
||||
/// Every length `other` holds for is one this holds for, so a drawing
|
||||
/// made under this range is still good wherever `other` is.
|
||||
pub const fn covers(&self, other: Self) -> bool {
|
||||
self.lo.raw() <= other.lo.raw() && self.hi.raw() >= other.hi.raw()
|
||||
}
|
||||
|
||||
pub const fn and(&self, other: Self) -> Self {
|
||||
Self {
|
||||
lo: self.lo.max(other.lo),
|
||||
hi: self.hi.min(other.hi),
|
||||
@@ -51,7 +77,7 @@ impl Holds {
|
||||
/// 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 {
|
||||
pub const fn through(&self, len: Len) -> Self {
|
||||
if self.lo.raw() == Px::MIN.raw() && self.hi.raw() == Px::MAX.raw() {
|
||||
return Self::ANY;
|
||||
}
|
||||
|
||||
+84
-55
@@ -1,79 +1,108 @@
|
||||
use crate::{Axis, Holds, Len, PxVec2, UiRegion, UiVec2};
|
||||
use crate::util::impl_axis_index;
|
||||
use crate::{Axis, Holds, Len, Px, PxVec2, UiRegion, UiVec2};
|
||||
|
||||
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
||||
|
||||
/// What one evaluation of a widget depends on: the window lengths its reads
|
||||
/// hold for, the pixel lengths of its own box, and the symbolic lengths of
|
||||
/// that box and of its frame where either one is what it was expressed in.
|
||||
/// What one evaluation of a widget depends on along one axis: the window
|
||||
/// lengths its reads hold for, the pixel lengths of its own box, and the
|
||||
/// symbolic lengths of that box and of its rel base where either one is what
|
||||
/// it was expressed in.
|
||||
///
|
||||
/// The symbolic lengths are pins rather than ranges: a container places its
|
||||
/// children as lengths of its frame measured from where its own box starts,
|
||||
/// children as lengths of its rel base measured from where its own box starts,
|
||||
/// so what it draws turns on that box's length and on nothing about where it
|
||||
/// is. A box pin reaches the parent only where the box it pinned is the
|
||||
/// parent's own; anywhere else the parent chose that length itself, and a
|
||||
/// widget pinned this way is checked when it is re-placed.
|
||||
///
|
||||
/// A frame pin says the answer or the drawing is a fraction of the frame,
|
||||
/// which is a different length wherever the frame is a different one -- at
|
||||
/// A rel base pin says the answer or the drawing is a fraction of the rel base,
|
||||
/// which is a different length wherever the rel base is a different one -- at
|
||||
/// the same window size, so no range of window pixels can say it. A length
|
||||
/// of the frame that is only pixels is not one: it is that many pixels
|
||||
/// whatever the frame turns out to be.
|
||||
/// of the rel base that is only pixels is not one: it is that many pixels
|
||||
/// whatever the rel base turns out to be.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct AxisHolds {
|
||||
pub window: Holds,
|
||||
pub rel_base: Option<Len>,
|
||||
pub region: Holds,
|
||||
pub region_len: Option<Len>,
|
||||
}
|
||||
|
||||
impl AxisHolds {
|
||||
pub const ANY: Self = Self {
|
||||
window: Holds::ANY,
|
||||
rel_base: None,
|
||||
region: Holds::ANY,
|
||||
region_len: None,
|
||||
};
|
||||
|
||||
pub fn and(&self, other: Self) -> Self {
|
||||
// Two pins of the same length disagreeing would mean one drawing was
|
||||
// a fraction of two different lengths at once.
|
||||
debug_assert!(
|
||||
self.region_len.is_none()
|
||||
|| other.region_len.is_none()
|
||||
|| self.region_len == other.region_len
|
||||
);
|
||||
debug_assert!(
|
||||
self.rel_base.is_none() || other.rel_base.is_none() || self.rel_base == other.rel_base
|
||||
);
|
||||
Self {
|
||||
window: self.window.and(other.window),
|
||||
rel_base: self.rel_base.or(other.rel_base),
|
||||
region: self.region.and(other.region),
|
||||
region_len: self.region_len.or(other.region_len),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn covers(&self, other: Self) -> bool {
|
||||
self.window.covers(other.window)
|
||||
&& self.region.covers(other.region)
|
||||
&& self
|
||||
.region_len
|
||||
.is_none_or(|len| other.region_len == Some(len))
|
||||
&& self.rel_base.is_none_or(|len| other.rel_base == Some(len))
|
||||
}
|
||||
|
||||
/// Whether a widget in a box `len` long, with that rel base, in that
|
||||
/// window, is one this drawing holds for.
|
||||
pub fn contains(&self, window: Px, rel_base: Len, len: Len) -> bool {
|
||||
self.window.contains(window)
|
||||
&& self.rel_base.is_none_or(|pinned| pinned == rel_base)
|
||||
&& self.region.contains(len.to_px(window))
|
||||
&& self.region_len.is_none_or(|pinned| pinned == len)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`AxisHolds`] on both axes. Every question asked of it is asked of one
|
||||
/// axis at a time, since a widget that read one length holds for any length
|
||||
/// of the other.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct LayoutHolds {
|
||||
pub window: [Holds; 2],
|
||||
pub frame_len: [Option<Len>; 2],
|
||||
pub extent: [Holds; 2],
|
||||
pub extent_len: [Option<Len>; 2],
|
||||
pub x: AxisHolds,
|
||||
pub y: AxisHolds,
|
||||
}
|
||||
|
||||
impl LayoutHolds {
|
||||
pub const ANY: Self = Self {
|
||||
window: [Holds::ANY; 2],
|
||||
frame_len: [None; 2],
|
||||
extent: [Holds::ANY; 2],
|
||||
extent_len: [None; 2],
|
||||
x: AxisHolds::ANY,
|
||||
y: AxisHolds::ANY,
|
||||
};
|
||||
|
||||
pub fn and(self, other: Self) -> Self {
|
||||
let mut result = Self::ANY;
|
||||
for n in 0..2 {
|
||||
result.window[n] = self.window[n].and(other.window[n]);
|
||||
result.extent[n] = self.extent[n].and(other.extent[n]);
|
||||
debug_assert!(
|
||||
self.extent_len[n].is_none()
|
||||
|| other.extent_len[n].is_none()
|
||||
|| self.extent_len[n] == other.extent_len[n]
|
||||
);
|
||||
debug_assert!(
|
||||
self.frame_len[n].is_none()
|
||||
|| other.frame_len[n].is_none()
|
||||
|| self.frame_len[n] == other.frame_len[n]
|
||||
);
|
||||
result.extent_len[n] = self.extent_len[n].or(other.extent_len[n]);
|
||||
result.frame_len[n] = self.frame_len[n].or(other.frame_len[n]);
|
||||
pub fn and(&self, other: Self) -> Self {
|
||||
Self {
|
||||
x: self.x.and(other.x),
|
||||
y: self.y.and(other.y),
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn covers(self, other: Self) -> bool {
|
||||
(0..2).all(|n| {
|
||||
self.window[n].lo <= other.window[n].lo
|
||||
&& self.window[n].hi >= other.window[n].hi
|
||||
&& self.extent[n].lo <= other.extent[n].lo
|
||||
&& self.extent[n].hi >= other.extent[n].hi
|
||||
&& self.extent_len[n].is_none_or(|len| other.extent_len[n] == Some(len))
|
||||
&& self.frame_len[n].is_none_or(|len| other.frame_len[n] == Some(len))
|
||||
})
|
||||
pub fn covers(&self, other: Self) -> bool {
|
||||
self.x.covers(other.x) && self.y.covers(other.y)
|
||||
}
|
||||
|
||||
pub fn contains(self, window: PxVec2, frame: UiVec2, extent: UiRegion) -> bool {
|
||||
AXES.into_iter().all(|axis| {
|
||||
let n = axis as usize;
|
||||
let len = extent.axis(axis).len();
|
||||
self.window[n].contains(window.axis(axis))
|
||||
&& self.frame_len[n].is_none_or(|pinned| pinned == frame.axis(axis))
|
||||
&& self.extent[n].contains(len.to_px(window.axis(axis)))
|
||||
&& self.extent_len[n].is_none_or(|pinned| pinned == len)
|
||||
})
|
||||
pub fn contains(&self, window: PxVec2, rel_base: UiVec2, region: UiRegion) -> bool {
|
||||
Axis::BOTH
|
||||
.into_iter()
|
||||
.all(|axis| self[axis].contains(window[axis], rel_base[axis], region[axis].len()))
|
||||
}
|
||||
}
|
||||
|
||||
impl_axis_index!(LayoutHolds => AxisHolds);
|
||||
+1
-1
@@ -20,7 +20,7 @@ pub use active::*;
|
||||
pub use holds::*;
|
||||
pub use layout_holds::*;
|
||||
pub use painter::{Painter, PrimitiveLike};
|
||||
pub use place::*;
|
||||
pub use place::{PlaceDesc, PlaceDescAxis, RetainedPrimitive};
|
||||
pub use render_state::*;
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
+407
-289
@@ -1,33 +1,32 @@
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
use crate::layout_diagnostics::{self as diag, Counter};
|
||||
use crate::{
|
||||
Axis, Holds, LayoutHolds, LayoutLen, Len, Part, Place, Px, PxVec2, RegionAlign, Rel,
|
||||
RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
|
||||
TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
|
||||
Axis, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Rel,
|
||||
RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextureHandle,
|
||||
UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
|
||||
render::{
|
||||
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
|
||||
TexturePrimitive,
|
||||
},
|
||||
ui::render_state::{DrawInfo, Placing},
|
||||
ui::{
|
||||
place::{PlaceSpan, RelBase},
|
||||
render_state::{DrawInfo, Placing},
|
||||
},
|
||||
};
|
||||
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
||||
|
||||
/// makes your surfaces look pretty
|
||||
pub struct Painter<'a> {
|
||||
pub(super) state: &'a mut UiRenderState,
|
||||
pub(super) rsc: &'a mut dyn UiRsc,
|
||||
|
||||
/// This widget's frame, per axis: a length of the window, and what a
|
||||
/// This widget's rel base, per axis: a length of the window, and what a
|
||||
/// fraction it or anything under it declares or reports is a fraction
|
||||
/// of. A length rather than a box, so padding can take from both the
|
||||
/// frame and the box without either becoming the other.
|
||||
pub(super) frame: UiVec2,
|
||||
/// Where this widget's drawing goes, in its region node's coordinates.
|
||||
pub(super) extent: UiRegion,
|
||||
/// The extent's symbolic length where this draw read it, which makes the
|
||||
/// drawing one that holds for that length alone -- the way reading a
|
||||
/// length in pixels makes it hold for that number of pixels.
|
||||
pub(super) extent_len: [Option<Len>; 2],
|
||||
/// rel base and the box without either becoming the other.
|
||||
pub(super) rel_base: UiVec2,
|
||||
/// The box this widget was asked in, in its region node's coordinates:
|
||||
/// what it draws in, and what its children's places are parts of.
|
||||
pub(super) region: UiRegion,
|
||||
/// The window in pixels. Frames and boxes become pixels against this one
|
||||
/// unit, regardless of region-node boundaries.
|
||||
pub(super) window: PxVec2,
|
||||
@@ -35,19 +34,19 @@ pub struct Painter<'a> {
|
||||
pub(super) textures: Vec<TextureHandle>,
|
||||
pub(super) primitives: Vec<RetainedPrimitive>,
|
||||
pub(super) mask_region: Option<UiRegion>,
|
||||
/// The previous drawing's owned mask, available for this draw to reclaim.
|
||||
pub(super) mask_slot: Option<MaskIdx>,
|
||||
/// Only children whose answers were read constrain this widget's answer.
|
||||
pub(super) answer_under: LayoutHolds,
|
||||
pub(super) children: Vec<WidgetId>,
|
||||
/// The children whose size this widget read while drawing.
|
||||
pub(super) size_deps: Vec<WidgetId>,
|
||||
/// What this draw itself read of the window in pixels, per axis: every
|
||||
/// window until it reads one, then that one, unless it says otherwise.
|
||||
pub(super) window_own: [Holds; 2],
|
||||
/// Its frame's symbolic length where this draw read it, which makes the
|
||||
/// drawing one that holds for that frame alone.
|
||||
pub(super) frame_own_len: [Option<Len>; 2],
|
||||
/// The window reads' equivalent for its own box.
|
||||
pub(super) extent_own: [Holds; 2],
|
||||
/// What this draw itself reads, as against what its children's drawings
|
||||
/// hold for: every window and every length of its own region until it
|
||||
/// reads one, then that one unless it says otherwise, and the rel base or
|
||||
/// region length it read symbolically, each of which makes the drawing
|
||||
/// hold for that length alone.
|
||||
pub(super) own: LayoutHolds,
|
||||
/// What each child's drawing depends on. Asking a child again replaces
|
||||
/// its drawing, so it replaces this too rather than narrowing it.
|
||||
pub(super) under: Vec<(WidgetId, LayoutHolds)>,
|
||||
@@ -73,10 +72,10 @@ impl<'a> Painter<'a> {
|
||||
self.write_resolved(kind, primitive, region, self.resolve(region));
|
||||
}
|
||||
|
||||
/// A box in this widget's extent coordinates, composed into its region
|
||||
/// node's coordinates.
|
||||
/// A box in this widget's region, composed into its region node's
|
||||
/// coordinates.
|
||||
fn resolve(&self, region: UiRegion) -> UiRegion {
|
||||
region.within(&self.extent)
|
||||
region.within(&self.region)
|
||||
}
|
||||
|
||||
fn write_resolved<P: Primitive>(
|
||||
@@ -104,7 +103,6 @@ impl<'a> Painter<'a> {
|
||||
|
||||
fn push_primitive(&mut self, h: RetainedPrimitive) {
|
||||
if self.mask != MaskIdx::NONE {
|
||||
// TODO: I have no clue if this works at all :joy:
|
||||
self.rsc.ui_mut().masks.push_ref(self.mask);
|
||||
}
|
||||
self.primitives.push(h);
|
||||
@@ -129,59 +127,88 @@ impl<'a> Painter<'a> {
|
||||
assert!(self.mask == MaskIdx::NONE);
|
||||
let resolved = self.resolve(region);
|
||||
let move_idx = self.move_idx;
|
||||
self.mask = self.rsc.ui_mut().masks.push(Mask {
|
||||
let mask = Mask {
|
||||
region: resolved,
|
||||
move_idx,
|
||||
});
|
||||
};
|
||||
let masks = &mut self.rsc.ui_mut().masks;
|
||||
self.mask = match self.mask_slot.take() {
|
||||
Some(idx) => {
|
||||
*masks.get_mut(idx) = mask;
|
||||
idx
|
||||
}
|
||||
None => {
|
||||
let idx = masks.push(mask);
|
||||
// The owner keeps the slot alive even with no primitives.
|
||||
masks.push_ref(idx);
|
||||
idx
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Draws a widget in the whole of this widget's own box, with the frame
|
||||
/// forwarded unchanged: what a container that is only a wrapper around
|
||||
/// one child wants, and what every transparent container passes for the
|
||||
/// frame.
|
||||
/// Draws a widget in the whole of this widget's own box, with the rel
|
||||
/// base forwarded unchanged: what a container that is only a wrapper
|
||||
/// around one child wants.
|
||||
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
|
||||
self.widget_at(id, [None; 2], [Place::Within(Part::All); 2])
|
||||
self.widget_at(id, UiRegion::FULL)
|
||||
}
|
||||
|
||||
/// Resolves what the place says about the child's rel base into a length,
|
||||
/// where that is this widget's own narrowed the way the region is. An
|
||||
/// axis the region leaves whole is not read at all, so a wrapper that
|
||||
/// only moves its child does not pin its drawing to a rel base.
|
||||
fn resolve_rel_base(&mut self, mut place: PlaceDesc) -> PlaceDesc {
|
||||
for axis in Axis::BOTH {
|
||||
let at = &mut place[axis];
|
||||
let (RelBase::WithRegion, PlaceSpan::Within(span)) = (at.rel_base, at.span) else {
|
||||
continue;
|
||||
};
|
||||
let len = span.len();
|
||||
at.rel_base = match len == Len::FULL {
|
||||
true => RelBase::Inherit,
|
||||
false => RelBase::Len(len.within_len(self.rel_base(axis))),
|
||||
};
|
||||
}
|
||||
place
|
||||
}
|
||||
|
||||
/// Asks a child, saying what its fractions are of and where it is asked.
|
||||
///
|
||||
/// `narrow` is a length this widget decided for the child's frame, per
|
||||
/// axis, as a length of this widget's own frame: a resolved share, or a
|
||||
/// box a sibling's answer decided. `None` forwards this widget's frame,
|
||||
/// which is what a container that only divides room passes, so a
|
||||
/// fraction under it means the same wherever it sits and however deeply
|
||||
/// it is nested. A declared length narrows the frame here whatever the
|
||||
/// caller says. A narrowed frame is placed in the part by the child's
|
||||
/// alignment and is the box the child is asked in.
|
||||
/// `place` says where the child goes and what its fractions are of:
|
||||
/// see [`PlaceDesc`]. A `UiRegion` converts into the common case, which
|
||||
/// is a box of this widget's own with the answer placed inside it.
|
||||
///
|
||||
/// `place` is where the child is asked, per axis, as a part of this
|
||||
/// widget's box: see [`Place`]. The child draws once, in that box, and
|
||||
/// its answer is placed inside it by re-expressing the drawing. Nothing
|
||||
/// is drawn again in a box an answer chose; a container that puts the
|
||||
/// The child draws once, in the region that comes of it, and its answer
|
||||
/// is placed inside that region by re-expressing the drawing. Nothing is
|
||||
/// drawn again in a box an answer chose; a container that puts the
|
||||
/// answer somewhere else says so with [`Self::place_at`].
|
||||
pub fn widget_at<'s, W: ?Sized>(
|
||||
&'s mut self,
|
||||
id: &'s StrongWidget<W>,
|
||||
narrow: [Option<Len>; 2],
|
||||
place: [Place; 2],
|
||||
place: impl Into<PlaceDesc>,
|
||||
) -> DrawResult<'s, 'a, W> {
|
||||
let offer = self.resolve_rel_base(place.into());
|
||||
let Ask {
|
||||
rel_base,
|
||||
region,
|
||||
place,
|
||||
declared,
|
||||
holds: ask_holds,
|
||||
} = self
|
||||
.placing()
|
||||
.ask(self.rsc.widgets(), self.window, id.id(), offer);
|
||||
let region_node = self.rsc.widgets().is_region_node(id.id());
|
||||
let declared = self.declared_lens(id);
|
||||
let align = self.rsc.widgets().alignment(id.id());
|
||||
let (frame, extent) =
|
||||
frame_and_extent(self.extent, self.frame, place, narrow, declared, align);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
if region_node {
|
||||
diag::bump(Counter::RegionNodeDraws);
|
||||
diag::region_node(id.id(), self.id, extent);
|
||||
diag::region_node(id.id(), self.id, region);
|
||||
}
|
||||
// A child listed twice would be moved twice.
|
||||
let re_asked = self.children.contains(&id.id());
|
||||
if !re_asked {
|
||||
self.children.push(id.id());
|
||||
}
|
||||
let px = frame.to_px(self.window);
|
||||
let (size, answer_holds, holds) = self.state.draw_inner(
|
||||
let drawn = self.state.draw_inner(
|
||||
id.id(),
|
||||
DrawInfo {
|
||||
layer: self.layer,
|
||||
@@ -190,19 +217,18 @@ impl<'a> Painter<'a> {
|
||||
parent_move: self.move_idx,
|
||||
region_node,
|
||||
mask: self.mask,
|
||||
frame,
|
||||
part: extent,
|
||||
rel_base,
|
||||
region,
|
||||
placed: place,
|
||||
asked: place,
|
||||
narrow,
|
||||
asked: offer,
|
||||
ask_holds,
|
||||
re_asked,
|
||||
px,
|
||||
},
|
||||
None,
|
||||
self.rsc,
|
||||
);
|
||||
let holds = self.in_parent(holds, extent, place, narrow, declared);
|
||||
let answer_holds = self.in_parent(answer_holds, extent, place, narrow, declared);
|
||||
let holds = self.in_parent(drawn.drawing_holds, region, place, declared);
|
||||
let answer_holds = self.in_parent(drawn.answer.holds, region, place, declared);
|
||||
match self.under.iter_mut().find(|(child, _)| *child == id.id()) {
|
||||
Some((_, kept)) => *kept = holds,
|
||||
None => self.under.push((id.id(), holds)),
|
||||
@@ -210,7 +236,7 @@ impl<'a> Painter<'a> {
|
||||
DrawResult {
|
||||
child: id,
|
||||
painter: self,
|
||||
size,
|
||||
size: drawn.answer.size,
|
||||
answer_holds,
|
||||
}
|
||||
}
|
||||
@@ -224,79 +250,80 @@ impl<'a> Painter<'a> {
|
||||
self.state.undraw_rec(id.id(), self.rsc);
|
||||
}
|
||||
|
||||
/// Puts a child asked about in this draw somewhere else in this
|
||||
/// widget's box: its answer, placed in this part instead. The drawing
|
||||
/// is re-expressed there rather than made again -- what a row does once
|
||||
/// it knows every slot, having measured each child from its cursor.
|
||||
pub fn place_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, place: [Place; 2]) {
|
||||
debug_assert!(
|
||||
self.children.contains(&id.id()),
|
||||
"'{}' placed a child it did not ask about in this draw",
|
||||
self.label()
|
||||
);
|
||||
/// Puts a child in `place` of this widget's box, where that box is the
|
||||
/// answer the child already gave: the drawing is re-expressed there
|
||||
/// rather than made again -- what a row does once it knows every slot,
|
||||
/// having measured each child from its cursor.
|
||||
///
|
||||
/// A child this draw has not asked about, and one whose rel base this
|
||||
/// narrows, is asked here instead: there is no answer to re-express, or
|
||||
/// the question has changed. So a container that places every child the
|
||||
/// same way says it once, and which of the two happens is this widget's
|
||||
/// business rather than the caller's.
|
||||
pub fn place_at<'s, W: ?Sized>(
|
||||
&'s mut self,
|
||||
id: &'s StrongWidget<W>,
|
||||
place: impl Into<PlaceDesc>,
|
||||
) -> DrawResult<'s, 'a, W> {
|
||||
let place = self.resolve_rel_base(place.into());
|
||||
let states_rel_base = Axis::BOTH
|
||||
.iter()
|
||||
.any(|&axis| matches!(place[axis].rel_base, RelBase::Len(_)));
|
||||
if states_rel_base || !self.children.contains(&id.id()) {
|
||||
return self.widget_at(id, place);
|
||||
}
|
||||
let at = self.placing();
|
||||
self.state.place_in(id.id(), &at, place, self.rsc);
|
||||
let active = &self.state.active[&id.id()];
|
||||
let size = active.measured().unwrap_or(active.size);
|
||||
DrawResult {
|
||||
child: id,
|
||||
painter: self,
|
||||
size,
|
||||
// Read where it was asked; moving it is not a second answer.
|
||||
answer_holds: LayoutHolds::ANY,
|
||||
}
|
||||
}
|
||||
|
||||
/// This widget as the thing its children are placed within.
|
||||
fn placing(&self) -> Placing {
|
||||
Placing {
|
||||
id: self.id,
|
||||
extent: self.extent,
|
||||
frame: self.frame,
|
||||
window: self.window,
|
||||
id: Some(self.id),
|
||||
region: self.region,
|
||||
rel_base: self.rel_base,
|
||||
depth: self.depth,
|
||||
move_idx: self.move_idx,
|
||||
mask: self.mask,
|
||||
}
|
||||
}
|
||||
|
||||
/// What a widget's rules declare its lengths to be, which whoever draws
|
||||
/// it resolves into its frame. Reading them depends on nothing -- the box
|
||||
/// that comes of them is kept on the child, and `redraw` compares it
|
||||
/// there.
|
||||
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] {
|
||||
declared_lens(self.rsc.widgets(), id.id())
|
||||
}
|
||||
|
||||
/// What a child says its length is without being drawn, if it can say,
|
||||
/// as the length its draw would report: a fraction in it is resolved
|
||||
/// against this widget's frame, which is the frame a child asked with
|
||||
/// against this widget's rel base, which is the rel base a child asked with
|
||||
/// nothing narrowed gets. Asking counts as reading its size.
|
||||
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
|
||||
let widgets = self.rsc.widgets();
|
||||
// A rule is the answer where there is one: it wins over whatever the
|
||||
// widget would draw, so it has to win over what the widget says too.
|
||||
let hint = widgets
|
||||
.size_rules(id.id())
|
||||
.axis(axis)
|
||||
.exact()
|
||||
.or_else(|| {
|
||||
widgets
|
||||
.get_dyn(id.id())
|
||||
.and_then(|widget| widget.size_hint(axis))
|
||||
})
|
||||
.map(|hint| hint.within_len(self.frame.axis(axis)));
|
||||
let hint = self.rsc.widgets().exact_len(id.id(), axis);
|
||||
let rel_base = self.rel_base[axis];
|
||||
let resolved = hint.map(|hint| hint.within_len(rel_base));
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::hint_read(id.id(), self.id, axis, hint);
|
||||
match hint {
|
||||
Some(hint) => {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::HintHits);
|
||||
self.depend_on(id);
|
||||
// A fraction was just resolved against this frame, so what
|
||||
// this draw does with it is a function of the frame's length.
|
||||
if hint.rel != Rel::ZERO {
|
||||
self.frame_own_len[axis as usize] = Some(self.frame.axis(axis));
|
||||
}
|
||||
Some(hint)
|
||||
}
|
||||
None => {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::HintMisses);
|
||||
None
|
||||
{
|
||||
diag::hint_read(id.id(), self.id, axis, resolved);
|
||||
diag::bump(match resolved {
|
||||
Some(_) => Counter::HintHits,
|
||||
None => Counter::HintMisses,
|
||||
});
|
||||
}
|
||||
if let Some(hint) = hint {
|
||||
self.depend_on(id);
|
||||
// Resolving a fraction against this rel base makes this draw a
|
||||
// function of the rel base's length. The fraction to ask about is
|
||||
// the child's own: resolved against a rel base of pixels, none is
|
||||
// left to see it by.
|
||||
if hint.rel != Rel::ZERO {
|
||||
self.own[axis].rel_base = Some(rel_base);
|
||||
}
|
||||
}
|
||||
resolved
|
||||
}
|
||||
|
||||
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
||||
@@ -317,11 +344,11 @@ impl<'a> Painter<'a> {
|
||||
ui.text.render(buffer, attrs, width)
|
||||
}
|
||||
|
||||
/// Writes glyphs in the selected frame or extent coordinates.
|
||||
/// Writes glyphs in the selected rel base or region coordinates.
|
||||
// TODO: merge the text methods into the primitive ones.
|
||||
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
|
||||
// Glyph offsets and sizes are pixels, which compose additively.
|
||||
// Only the shared origin needs composing through the extent.
|
||||
// Only the shared origin needs composing through the region.
|
||||
let resolved = self.resolve(origin);
|
||||
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
|
||||
for glyph in text.glyphs.iter() {
|
||||
@@ -353,25 +380,25 @@ impl<'a> Painter<'a> {
|
||||
}
|
||||
|
||||
/// The symbolic length of this widget's own box along one axis, in the
|
||||
/// lengths of its frame that it places its children in. Reading it pins
|
||||
/// lengths of its rel base that it places its children in. Reading it pins
|
||||
/// the drawing to that length -- and to nothing about where the box
|
||||
/// starts, which is what lets a container move without being drawn
|
||||
/// again. One axis at a time, because a container that divides one axis
|
||||
/// holds for any length of the other.
|
||||
pub fn extent_len(&mut self, axis: Axis) -> Len {
|
||||
let len = self.extent.axis(axis).len();
|
||||
self.extent_len[axis as usize] = Some(len);
|
||||
pub fn region_len(&mut self, axis: Axis) -> Len {
|
||||
let len = self.region[axis].len();
|
||||
self.own[axis].region_len = Some(len);
|
||||
len
|
||||
}
|
||||
|
||||
/// The symbolic length of this widget's frame along one axis: what a
|
||||
/// fraction it or anything under it declares is a fraction of. A
|
||||
/// container reads it to hand a length of it down -- padding, which
|
||||
/// takes its pixels off. Reading it pins the drawing to that frame, the
|
||||
/// way [`Self::extent_len`] pins it to the box.
|
||||
pub fn frame_len(&mut self, axis: Axis) -> Len {
|
||||
let len = self.frame.axis(axis);
|
||||
self.frame_own_len[axis as usize] = Some(len);
|
||||
/// This widget's rel base along one axis: what a fraction it or anything
|
||||
/// under it declares or reports is a fraction of. A container reads it
|
||||
/// to hand a length of it down -- padding, which takes its pixels off.
|
||||
/// Reading it pins the drawing to that rel base, the way
|
||||
/// [`Self::region_len`] pins it to the box.
|
||||
pub fn rel_base(&mut self, axis: Axis) -> Len {
|
||||
let len = self.rel_base[axis];
|
||||
self.own[axis].rel_base = Some(len);
|
||||
len
|
||||
}
|
||||
|
||||
@@ -392,10 +419,7 @@ impl<'a> Painter<'a> {
|
||||
/// 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)
|
||||
self.rsc.widgets().size_rules(self.id)[axis]
|
||||
.exact()
|
||||
.is_some()
|
||||
}
|
||||
@@ -409,13 +433,13 @@ impl<'a> Painter<'a> {
|
||||
/// One axis of this widget's own box in pixels. Prefer this to
|
||||
/// [`Self::px_size`] when the other axis cannot affect the drawing.
|
||||
pub fn px_len(&mut self, axis: Axis) -> Px {
|
||||
let part = self.extent.axis(axis).len();
|
||||
let len = part.to_px(self.window.axis(axis));
|
||||
let own = &mut self.extent_own[axis as usize];
|
||||
let len = self.region[axis].len();
|
||||
let px = len.to_px(self.window[axis]);
|
||||
let own = &mut self.own[axis].region;
|
||||
if *own == Holds::ANY {
|
||||
*own = Holds::at(len);
|
||||
*own = Holds::at(px);
|
||||
}
|
||||
len
|
||||
px
|
||||
}
|
||||
|
||||
/// The lengths of this widget's own box on `axis` that what it is drawing
|
||||
@@ -423,15 +447,15 @@ impl<'a> Painter<'a> {
|
||||
/// of the box, and the same reported size. A widget that read its length
|
||||
/// in pixels holds for that one alone until it says otherwise.
|
||||
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
|
||||
let part = self.extent.axis(axis).len();
|
||||
let len = self.region[axis].len();
|
||||
let holds = holds.into();
|
||||
debug_assert!(
|
||||
holds.contains(part.to_px(self.window.axis(axis))),
|
||||
holds.contains(len.to_px(self.window[axis])),
|
||||
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
|
||||
self.label(),
|
||||
self.id
|
||||
);
|
||||
self.extent_own[axis as usize] = holds;
|
||||
self.own[axis].region = holds;
|
||||
}
|
||||
|
||||
/// A window length in pixels, which is what every length in layout is
|
||||
@@ -439,9 +463,9 @@ impl<'a> Painter<'a> {
|
||||
/// length is a fraction of it; one that is only pixels is that many
|
||||
/// pixels in any window and pins nothing.
|
||||
pub fn to_px(&mut self, len: Len, axis: Axis) -> Px {
|
||||
let window = self.window.axis(axis);
|
||||
let window = self.window[axis];
|
||||
if len.rel != Rel::ZERO {
|
||||
let own = &mut self.window_own[axis as usize];
|
||||
let own = &mut self.own[axis].window;
|
||||
if *own == Holds::ANY {
|
||||
*own = Holds::at(window);
|
||||
}
|
||||
@@ -449,6 +473,26 @@ impl<'a> Painter<'a> {
|
||||
len.to_px(window)
|
||||
}
|
||||
|
||||
/// [`Len::longer_than`], asked on this widget's behalf: the windows the
|
||||
/// comparison comes out the same way on are windows its drawing holds
|
||||
/// for, and nowhere else does it. What a container has left for the
|
||||
/// shares it divides is the one thing that asks.
|
||||
///
|
||||
/// Narrowed rather than stated, because whatever else this widget read
|
||||
/// about the window is a reason its drawing holds where it does too.
|
||||
pub fn longer_than(&mut self, len: Len, than: Len, axis: Axis) -> bool {
|
||||
let window = self.window[axis];
|
||||
let (longer, holds) = len.longer_than(than, window);
|
||||
debug_assert!(
|
||||
holds.contains(window),
|
||||
"'{}' ({:?}) compared two lengths and kept a range without this window",
|
||||
self.label(),
|
||||
self.id
|
||||
);
|
||||
self.own[axis].window = self.own[axis].window.and(holds);
|
||||
longer
|
||||
}
|
||||
|
||||
/// The windows this drawing holds for, stated rather than taken: a
|
||||
/// container that branched on a length in pixels says which side of the
|
||||
/// boundary it was on, which is wider than the one window reading that
|
||||
@@ -456,16 +500,12 @@ impl<'a> Painter<'a> {
|
||||
pub fn window_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
|
||||
let holds = holds.into();
|
||||
debug_assert!(
|
||||
holds.contains(self.window.axis(axis)),
|
||||
holds.contains(self.window[axis]),
|
||||
"'{}' ({:?}) says its drawing holds for windows that leave out this one",
|
||||
self.label(),
|
||||
self.id
|
||||
);
|
||||
self.window_own[axis as usize] = holds;
|
||||
}
|
||||
|
||||
pub fn text_data(&mut self) -> &mut TextData {
|
||||
&mut self.rsc.ui_mut().text
|
||||
self.own[axis].window = holds;
|
||||
}
|
||||
|
||||
pub fn child_layer(&mut self) {
|
||||
@@ -520,7 +560,7 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
|
||||
}
|
||||
|
||||
pub fn len(self, axis: Axis) -> LayoutLen {
|
||||
self.size().axis(axis)
|
||||
self.size()[axis]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,50 +589,46 @@ impl PrimitiveLike for &TextureHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves what a child depends on into this widget's own terms: this
|
||||
/// method's `impl` block is where a `Painter`'s own boxes are, so it takes
|
||||
/// only what the child was asked with.
|
||||
impl Painter<'_> {
|
||||
/// Window ranges are already about the one unit and combine directly.
|
||||
/// A frame pin becomes this widget's own frame wherever a length of it
|
||||
/// is what reached the child; where only pixels did, no length of this
|
||||
/// frame can change the child's and the pin stops here.
|
||||
/// Moves what a child depends on into this widget's own terms, taking
|
||||
/// only what the child was asked with.
|
||||
///
|
||||
/// Extent validity maps back through the part of this widget's box,
|
||||
/// Window ranges are already about the one unit and combine directly.
|
||||
/// A rel base pin becomes this widget's own rel base wherever a length of it
|
||||
/// is what reached the child; where only pixels did, no length of this
|
||||
/// rel base can change the child's and the pin stops here.
|
||||
///
|
||||
/// A child's validity maps back through the part of this widget's box,
|
||||
/// where the box the child was asked in is that part; a declared length
|
||||
/// places the box inside the part instead, and then only that length
|
||||
/// reaches the child. A narrowed frame is not one of these: it decides
|
||||
/// reaches the child. A narrowed rel base is not one of these: it decides
|
||||
/// what fractions under the child mean and leaves the box the part it
|
||||
/// was given.
|
||||
fn in_parent(
|
||||
&self,
|
||||
holds: LayoutHolds,
|
||||
extent: UiRegion,
|
||||
place: [Place; 2],
|
||||
narrow: [Option<Len>; 2],
|
||||
declared: [Option<LayoutLen>; 2],
|
||||
region: UiRegion,
|
||||
place: PlaceDesc,
|
||||
declared: Declared,
|
||||
) -> LayoutHolds {
|
||||
let mut result = LayoutHolds::ANY;
|
||||
for axis in AXES {
|
||||
let n = axis as usize;
|
||||
for axis in Axis::BOTH {
|
||||
let declared = declared[axis];
|
||||
let holds = holds[axis];
|
||||
let at = place[axis];
|
||||
let result = &mut result[axis];
|
||||
// Every read became pixels against the window, so a range on
|
||||
// it is already in this widget's terms.
|
||||
result.window[n] = holds.window[n];
|
||||
let reaches = narrow[n].is_none()
|
||||
&& !matches!(place[n].part(), Part::Sized(_))
|
||||
&& declared[n].is_none_or(|len| len.rel != Rel::ZERO);
|
||||
result.frame_len[n] = holds.frame_len[n].and(reaches.then(|| self.frame.axis(axis)));
|
||||
match (place[n].part(), declared[n].is_some()) {
|
||||
// Its box is this widget's own, or a part of it in that
|
||||
// box's own lengths: so what it holds for is a range on this
|
||||
// widget's own box, which is what lets that box move without
|
||||
// a redraw. A length it pinned is this widget's length
|
||||
// wherever the part is the whole of it, and pins the same
|
||||
// way.
|
||||
(Part::All, false) => {
|
||||
result.extent[n] = holds.extent[n];
|
||||
result.extent_len[n] = holds.extent_len[n];
|
||||
}
|
||||
result.window = holds.window;
|
||||
// A length this widget named -- a resolved share, a box a sibling
|
||||
// decided, a box it sized outright, which is its own base -- is
|
||||
// not a length of this widget's rel base, so a pin on it stops
|
||||
// here. So does a declaration in pixels: no length of either base
|
||||
// is in it to see.
|
||||
let reaches = !matches!(at.rel_base, RelBase::Len(_))
|
||||
&& declared.is_none_or(|len| len.rel != Rel::ZERO);
|
||||
result.rel_base = holds.rel_base.and(reaches.then(|| self.rel_base[axis]));
|
||||
match (at.span, declared) {
|
||||
// Its box is a part of this widget's own box, in that box's
|
||||
// own lengths, so what it holds for maps back through that
|
||||
// part into a range on this widget's box. A length it pinned
|
||||
@@ -600,21 +636,20 @@ impl Painter<'_> {
|
||||
// part is the whole of the box less pixels, which is the one
|
||||
// shape that inverts exactly; any other part pins this
|
||||
// widget's own length.
|
||||
(Part::Of(span), false) => {
|
||||
(PlaceSpan::Within(span), None) => {
|
||||
let part_len = span.len();
|
||||
result.extent[n] = holds.extent[n].through(part_len);
|
||||
result.extent_len[n] = holds.extent_len[n].map(|pinned| match part_len.rel {
|
||||
result.region = holds.region.through(part_len);
|
||||
result.region_len = holds.region_len.map(|pinned| match part_len.rel {
|
||||
Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px),
|
||||
_ => self.extent.axis(axis).len(),
|
||||
_ => self.region[axis].len(),
|
||||
});
|
||||
}
|
||||
// Its box is a length this widget decided, from its own
|
||||
// frame or from a sibling's answer: no length of this
|
||||
// rel base or from a sibling's answer: no length of this
|
||||
// widget's box reaches it, so what it holds for is a range
|
||||
// on the window and none of it on that box.
|
||||
_ => {
|
||||
result.window[n] =
|
||||
result.window[n].and(holds.extent[n].through(extent.axis(axis).len()));
|
||||
result.window = result.window.and(holds.region.through(region[axis].len()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -622,120 +657,203 @@ impl Painter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(|| {
|
||||
impl Widgets {
|
||||
/// What says a widget's length on one axis without drawing it, if anything
|
||||
/// does. A rule is the answer where there is one: it wins over whatever the
|
||||
/// widget would draw, so it has to win over what the widget says too -- a
|
||||
/// share included, since a share is a length only to whoever divides one,
|
||||
/// and that is the parent rather than this widget.
|
||||
fn exact_len(&self, id: WidgetId, axis: Axis) -> Option<LayoutLen> {
|
||||
self.size_rules(id)[axis].exact().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)
|
||||
self.get_dyn(id)?.size_hint(axis)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether what a widget reported along an axis is the whole of the box it
|
||||
/// is in rather than a part to be placed inside it. A share fills, because a
|
||||
/// share is a length only to whoever divides one, and whoever did is the one
|
||||
/// that handed down this box. A declared axis does too: the rule already gave
|
||||
/// the region its length, and the rule's length is what the widget reports
|
||||
/// there. And an axis the parent decided from the answer is
|
||||
/// the answer already.
|
||||
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
|
||||
reported.leftover != Weight::ZERO || declared.is_some() || decided
|
||||
}
|
||||
|
||||
/// Where a widget's drawing goes inside the part its parent gave it: what
|
||||
/// it reported, on the side of the part its alignment says, and the whole
|
||||
/// part wherever the answer fills it.
|
||||
///
|
||||
/// The length it reported is a length of its frame, and the part is one too,
|
||||
/// so this takes one from the other rather than composing it into the part.
|
||||
/// That is what makes a fraction the same fraction wherever the part it is
|
||||
/// placed in sits and however long it is -- the fraction is resolved once,
|
||||
/// here, against the frame it was reported of.
|
||||
pub(crate) fn placed_extent(
|
||||
part: UiRegion,
|
||||
size: Size,
|
||||
declared: [Option<LayoutLen>; 2],
|
||||
fill: [bool; 2],
|
||||
align: RegionAlign,
|
||||
) -> UiRegion {
|
||||
let mut placed = part;
|
||||
for axis in AXES {
|
||||
let n = axis as usize;
|
||||
let reported = size.axis(axis);
|
||||
if fills(reported, declared[n], fill[n]) {
|
||||
continue;
|
||||
}
|
||||
let len = Len::from_parts(reported.rel, reported.px);
|
||||
let span = placed.axis_mut(axis);
|
||||
span.start += (span.len() - len).scale(align.axis(axis).rel());
|
||||
span.end = span.start + len;
|
||||
}
|
||||
placed
|
||||
|
||||
/// What a widget's box is where a rule or its own hint gives one outright,
|
||||
/// rather than a share for whoever draws it to divide.
|
||||
pub(super) fn declared_lens(&self, id: WidgetId) -> Declared {
|
||||
Declared::from_axes(|axis| self.exact_len(id, axis)?.declared())
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame length and the box a child is asked in, in the coordinates the
|
||||
/// widget asking draws in.
|
||||
///
|
||||
/// `own` is that widget's own box, and `place` what of it the child is
|
||||
/// given. `narrow` is a frame the container decided for the child -- a row's
|
||||
/// slot, or padding's frame less its pixels -- and [`Part::Sized`] one a
|
||||
/// sibling's answer decided; both are window lengths, like every other
|
||||
/// length here, since a slot of a row is not a fraction of anything the row
|
||||
/// can name. The child's declaration is a fraction of whichever reached it,
|
||||
/// and is the only one of the three that also places the box: a box the
|
||||
/// caller decided is what `place` names.
|
||||
pub(crate) fn frame_and_extent(
|
||||
own: UiRegion,
|
||||
parent_frame: UiVec2,
|
||||
place: [Place; 2],
|
||||
narrow: [Option<Len>; 2],
|
||||
declared: [Option<LayoutLen>; 2],
|
||||
align: RegionAlign,
|
||||
) -> (UiVec2, UiRegion) {
|
||||
let part = part_of(own, place, align);
|
||||
let mut frame = parent_frame;
|
||||
let mut extent = part;
|
||||
for axis in AXES {
|
||||
let n = axis as usize;
|
||||
let sized = match place[n].part() {
|
||||
Part::Sized(len) => Some(len),
|
||||
_ => None,
|
||||
/// One ask of a widget: the box it draws in, what its fractions are of, and
|
||||
/// what deciding those read.
|
||||
pub(super) struct Ask {
|
||||
pub rel_base: UiVec2,
|
||||
pub region: UiRegion,
|
||||
/// The place the ask came to, which a rule of the widget's own can take
|
||||
/// past the box its parent offered.
|
||||
pub place: PlaceDesc,
|
||||
pub declared: Declared,
|
||||
/// What the ask itself holds for, kept on the widget asked about: a rule
|
||||
/// compared against the offer in pixels holds only for the windows on its
|
||||
/// side of the crossing, and that range reaches whoever asked through the
|
||||
/// drawing it is part of. Kept on the widget asked about rather than on
|
||||
/// the asker because the root has no asker.
|
||||
pub holds: LayoutHolds,
|
||||
}
|
||||
|
||||
impl Placing {
|
||||
/// Asks about a widget at `place` of this box, with the widget's own
|
||||
/// rules applied to what the place offers it. `place` is resolved: what
|
||||
/// a rel base of the asker's is a fraction of, the asker worked out.
|
||||
///
|
||||
/// Every ask is this one, the root's included -- there the box is the
|
||||
/// window and nothing above narrowed it, which is what [`Self::WINDOW`]
|
||||
/// says.
|
||||
pub(super) fn ask(
|
||||
&self,
|
||||
widgets: &Widgets,
|
||||
window: PxVec2,
|
||||
id: WidgetId,
|
||||
mut place: PlaceDesc,
|
||||
) -> Ask {
|
||||
let align = widgets.alignment(id);
|
||||
let mut holds = LayoutHolds::ANY;
|
||||
// A share fills what the pixels and fraction beside it leave of the
|
||||
// box and overflows where they are longer, which is the rule a span
|
||||
// follows with one child. Only the overflow is a box of the child's
|
||||
// own: a share that fits is the box it was given, which is what this
|
||||
// place already says.
|
||||
for axis in Axis::BOTH {
|
||||
let (len, kept) =
|
||||
self.share_past_the_offer(widgets, window[axis], id, place, align, axis);
|
||||
holds[axis].window = holds[axis].window.and(kept);
|
||||
if let Some(len) = len {
|
||||
place[axis] = len.as_desc().fills();
|
||||
}
|
||||
}
|
||||
let declared = widgets.declared_lens(id);
|
||||
let (rel_base, region) =
|
||||
place.rel_base_and_region(self.region, self.rel_base, declared, align);
|
||||
Ask {
|
||||
rel_base,
|
||||
region,
|
||||
place,
|
||||
declared,
|
||||
holds,
|
||||
}
|
||||
}
|
||||
|
||||
/// The box a widget's own share asks for where that is longer than the
|
||||
/// box `place` gives it, and nothing where the share fits -- with the
|
||||
/// windows that answer holds for, which is a range either way.
|
||||
///
|
||||
/// A share is a length only to whoever divides one, and nothing divides a
|
||||
/// box handed to one child: what is left of it after the pixels and the
|
||||
/// fraction beside the share is what the share takes, so the length comes
|
||||
/// to the whole box until those are longer than it and to them once they
|
||||
/// are. Only that second case is a box its parent did not give, and the
|
||||
/// crossing between them is a question in pixels.
|
||||
fn share_past_the_offer(
|
||||
&self,
|
||||
widgets: &Widgets,
|
||||
window: Px,
|
||||
id: WidgetId,
|
||||
place: PlaceDesc,
|
||||
align: RegionAlign,
|
||||
axis: Axis,
|
||||
) -> (Option<Len>, Holds) {
|
||||
// A place that is the widget's placement outright is a box its parent
|
||||
// decided, and a parent that divides one has already given the share
|
||||
// whatever it was owed. Only an offer -- a box with the answer still
|
||||
// to be placed inside it -- is a box a share reads.
|
||||
if place[axis].fills {
|
||||
return (None, Holds::ANY);
|
||||
}
|
||||
// A share with nothing beside it is the box whatever the box is, so
|
||||
// there is no comparison to make and no range to keep for one.
|
||||
let Some(stated) = widgets.exact_len(id, axis) else {
|
||||
return (None, Holds::ANY);
|
||||
};
|
||||
let base = sized
|
||||
.or(narrow[n])
|
||||
.unwrap_or_else(|| parent_frame.axis(axis));
|
||||
let len = declared[n]
|
||||
.map(|len| Len::from_parts(len.rel, len.px).within_len(base))
|
||||
.unwrap_or(base);
|
||||
*frame.axis_mut(axis) = len;
|
||||
if declared[n].is_some() {
|
||||
let slot = part.axis(axis);
|
||||
let start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
|
||||
*extent.axis_mut(axis) = UiSpan::new(start, start + len);
|
||||
if stated.leftover == Weight::ZERO || stated.is_only_leftover() {
|
||||
return (None, Holds::ANY);
|
||||
}
|
||||
let fixed = stated
|
||||
.without_leftover()
|
||||
.within_len(place.base(axis, self.rel_base));
|
||||
let offer = place.of(self.region, align)[axis].len();
|
||||
let (longer, holds) = fixed.longer_than(offer, window);
|
||||
(longer.then_some(fixed), holds)
|
||||
}
|
||||
(frame, extent)
|
||||
}
|
||||
|
||||
/// The part of a widget's own box a `place` names, in the coordinates that
|
||||
/// box is in.
|
||||
fn part_of(extent: UiRegion, place: [Place; 2], align: RegionAlign) -> UiRegion {
|
||||
let mut part = extent;
|
||||
for axis in AXES {
|
||||
*part.axis_mut(axis) = place[axis as usize]
|
||||
.part()
|
||||
.of(*extent.axis(axis), align.axis(axis));
|
||||
impl LayoutLen {
|
||||
/// Whether what a widget reported along an axis is the whole of the box
|
||||
/// it is in rather than a part to be placed inside it. A share fills,
|
||||
/// because a share is a length only to whoever divides one, and whoever
|
||||
/// did is the one that handed down this box. A declared axis does too:
|
||||
/// the rule already gave the region its length, and the rule's length is
|
||||
/// what the widget reports there. And an axis the parent decided from
|
||||
/// the answer is the answer already.
|
||||
pub(super) fn fills(&self, declared: Option<Len>, decided: bool) -> bool {
|
||||
self.leftover != Weight::ZERO || declared.is_some() || decided
|
||||
}
|
||||
}
|
||||
|
||||
impl PlaceDesc {
|
||||
/// Where a widget's drawing goes inside the part its parent gave it: what
|
||||
/// it reported, on the side of the part its alignment says, and the whole
|
||||
/// part wherever the answer fills it.
|
||||
///
|
||||
/// The length it reported is a length of its rel base, and the part is one
|
||||
/// too, so this takes one from the other rather than composing it into the
|
||||
/// part. That is what makes a fraction the same fraction wherever the part
|
||||
/// it is placed in sits and however long it is -- the fraction is resolved
|
||||
/// once, here, against the rel base it was reported of.
|
||||
pub(super) fn placement(
|
||||
self,
|
||||
region: UiRegion,
|
||||
size: Size,
|
||||
declared: Declared,
|
||||
align: RegionAlign,
|
||||
) -> UiRegion {
|
||||
let mut placed = region;
|
||||
for axis in Axis::BOTH {
|
||||
let reported = size[axis];
|
||||
if reported.fills(declared[axis], self[axis].fills) {
|
||||
continue;
|
||||
}
|
||||
placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]);
|
||||
}
|
||||
placed
|
||||
}
|
||||
|
||||
/// The rel base length and the box a child is asked in, in the coordinates the
|
||||
/// widget asking draws in.
|
||||
///
|
||||
/// `own` is that widget's own box, and `place` what of it the child is
|
||||
/// given, including any rel base it states -- a row's slot, or padding's rel
|
||||
/// base less its pixels. That is a window length, like every other length
|
||||
/// here, since a slot of a row is not a fraction of anything the row can
|
||||
/// name. The child's declaration is a fraction of whichever reached it, and
|
||||
/// is the only one that also places the box: a box the caller decided is
|
||||
/// what `place` names.
|
||||
pub(super) fn rel_base_and_region(
|
||||
self,
|
||||
own: UiRegion,
|
||||
parent_rel_base: UiVec2,
|
||||
declared: Declared,
|
||||
align: RegionAlign,
|
||||
) -> (UiVec2, UiRegion) {
|
||||
let given = self.of(own, align);
|
||||
let mut rel_base = parent_rel_base;
|
||||
let mut region = given;
|
||||
for axis in Axis::BOTH {
|
||||
let base = self.base(axis, parent_rel_base);
|
||||
let len = declared[axis]
|
||||
.map(|len| len.within_len(base))
|
||||
.unwrap_or(base);
|
||||
rel_base[axis] = len;
|
||||
if declared[axis].is_some() {
|
||||
region[axis] = given[axis].place(len, align[axis]);
|
||||
}
|
||||
}
|
||||
(rel_base, region)
|
||||
}
|
||||
part
|
||||
}
|
||||
+193
-52
@@ -1,66 +1,205 @@
|
||||
use crate::{AxisAlign, Len, PrimitiveHandle, UiRegion, UiSpan};
|
||||
use crate::util::impl_axis_index;
|
||||
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan, UiVec2};
|
||||
|
||||
/// What of a widget's own box a child is given, along one axis.
|
||||
/// How a child's region along one axis comes from the region of the widget
|
||||
/// asking, and what its fractions are of.
|
||||
///
|
||||
/// The three ways of saying a region are the three the geometry already has:
|
||||
/// a span composed into the caller's box, a span shifted to where that box
|
||||
/// starts, and a length placed in it by alignment. Which one is meant cannot
|
||||
/// be read off the numbers, since two of them take the same span and apply
|
||||
/// it differently, so it is said here.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Part {
|
||||
/// The whole of it.
|
||||
All,
|
||||
/// Window lengths from where the box starts, which is what a container
|
||||
/// dividing room among its children speaks: a child's report is a window
|
||||
/// length, so the cursor that sums those reports is one too. A moved box
|
||||
/// re-places every child by re-adding its start, exactly. A fraction
|
||||
/// here is a fraction of the window and not of the box -- the whole of a
|
||||
/// box is [`Self::All`], not a `rel(1.0)` span.
|
||||
From(UiSpan),
|
||||
/// A part of the box in its own coordinates, which is what a container
|
||||
/// that insets one speaks: taking eleven pixels off the end needs no
|
||||
/// length, where saying the same thing in window lengths would make the
|
||||
/// container read its own box -- and a box chosen from its own answer
|
||||
/// then feeds back into the answer.
|
||||
Of(UiSpan),
|
||||
/// A box of this length, wherever in the parent's box the child's own
|
||||
/// alignment puts it, and that same length as its frame. Unlike `From`,
|
||||
/// it is a length decided from above rather than a place along a
|
||||
/// container's cursor -- what a stack's sizing child decides for the
|
||||
/// rest.
|
||||
pub struct PlaceDescAxis {
|
||||
pub span: PlaceSpan,
|
||||
pub fills: bool,
|
||||
pub rel_base: RelBase,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum PlaceSpan {
|
||||
Within(UiSpan),
|
||||
Shifted(UiSpan),
|
||||
Sized(Len),
|
||||
}
|
||||
|
||||
impl Part {
|
||||
/// Where it lands in the coordinates `extent` is in.
|
||||
pub(crate) fn of(self, extent: UiSpan, align: AxisAlign) -> UiSpan {
|
||||
match self {
|
||||
Self::All => extent,
|
||||
Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end),
|
||||
Self::Of(span) => span.within(&extent),
|
||||
Self::Sized(len) => {
|
||||
let start = extent.start + (extent.len() - len).scale(align.rel());
|
||||
UiSpan::new(start, start + len)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a child goes along one axis, as a part of this widget's box.
|
||||
/// What a child's fractions are of. [`PlaceSpan::Sized`] is a length the
|
||||
/// caller named, which is always its own base, so nothing here constructs one
|
||||
/// beside anything but [`Self::Len`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Place {
|
||||
/// The child's answer, aligned inside the part by the child's alignment.
|
||||
Within(Part),
|
||||
/// Exactly the part; the answer is not placed inside it again.
|
||||
Fill(Part),
|
||||
pub enum RelBase {
|
||||
/// The caller's own, unchanged.
|
||||
Inherit,
|
||||
/// The caller's own, narrowed the way the region is.
|
||||
WithRegion,
|
||||
/// This length of the window.
|
||||
Len(Len),
|
||||
}
|
||||
|
||||
impl Place {
|
||||
pub(crate) fn part(self) -> Part {
|
||||
match self {
|
||||
Self::Within(part) | Self::Fill(part) => part,
|
||||
impl PlaceDescAxis {
|
||||
/// The whole of the caller's box.
|
||||
pub const WHOLE: Self = UiSpan::FULL.within_desc();
|
||||
|
||||
/// This region is the child's placement: its answer is not placed inside
|
||||
/// it again. A container uses it where it hands back exactly what the
|
||||
/// child asked for -- a row placing a child at the length it reported.
|
||||
pub const fn fills(mut self) -> Self {
|
||||
self.fills = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// This along `axis`, and the whole of the caller's box across it: what
|
||||
/// a container dividing one axis says, since nothing divides the other.
|
||||
/// [`PlaceDesc::from_axis`] says the across one where it is not the
|
||||
/// whole.
|
||||
pub const fn on_axis(self, axis: Axis) -> PlaceDesc {
|
||||
PlaceDesc::from_axis(axis, self, Self::WHOLE)
|
||||
}
|
||||
|
||||
/// What the child's fractions are of, as a length of the window: a
|
||||
/// resolved share, or a box a sibling's answer decided.
|
||||
pub const fn rel_base(mut self, len: Len) -> Self {
|
||||
self.rel_base = RelBase::Len(len);
|
||||
self
|
||||
}
|
||||
|
||||
/// Where it lands in the coordinates `own` is in.
|
||||
pub fn of(self, own: UiSpan, align: AxisAlign) -> UiSpan {
|
||||
match self.span {
|
||||
PlaceSpan::Within(span) => span.within(&own),
|
||||
PlaceSpan::Shifted(mut span) => {
|
||||
span.shift(own.start);
|
||||
span
|
||||
}
|
||||
PlaceSpan::Sized(len) => own.place(len, align),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a child is asked, on both axes. A [`UiRegion`] converts into the
|
||||
/// common case: that box of the caller's own, the answer placed inside it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PlaceDesc {
|
||||
pub x: PlaceDescAxis,
|
||||
pub y: PlaceDescAxis,
|
||||
}
|
||||
|
||||
impl PlaceDesc {
|
||||
/// The whole of the caller's box, on both axes.
|
||||
pub const WHOLE: Self = Self::splat(PlaceDescAxis::WHOLE);
|
||||
|
||||
pub const fn new(x: PlaceDescAxis, y: PlaceDescAxis) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
/// The same on both axes.
|
||||
pub const fn splat(place: PlaceDescAxis) -> Self {
|
||||
Self { x: place, y: place }
|
||||
}
|
||||
|
||||
/// A description per axis, where the two differ and neither is the
|
||||
/// axis a container divides.
|
||||
pub fn from_axes(f: impl Fn(Axis) -> PlaceDescAxis) -> Self {
|
||||
Self::new(f(Axis::X), f(Axis::Y))
|
||||
}
|
||||
|
||||
/// `aligned` on `axis` and `ortho` on the other, which is how a
|
||||
/// container that divides one axis says what it is doing.
|
||||
pub const fn from_axis(axis: Axis, aligned: PlaceDescAxis, ortho: PlaceDescAxis) -> Self {
|
||||
match axis {
|
||||
Axis::X => Self::new(aligned, ortho),
|
||||
Axis::Y => Self::new(ortho, aligned),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the part is the drawing's box outright, rather than the box
|
||||
/// the answer is placed inside.
|
||||
pub(crate) fn fills(self) -> bool {
|
||||
matches!(self, Self::Fill(_))
|
||||
/// Both regions are the child's placement. See [`PlaceDescAxis::fills`].
|
||||
pub const fn fills(self) -> Self {
|
||||
Self::new(self.x.fills(), self.y.fills())
|
||||
}
|
||||
|
||||
/// The child's rel base on one axis. See [`PlaceDescAxis::rel_base`].
|
||||
pub const fn rel_base(mut self, axis: Axis, len: Len) -> Self {
|
||||
self[axis] = self[axis].rel_base(len);
|
||||
self
|
||||
}
|
||||
|
||||
/// What a child's fractions on one axis are of, as a length of the
|
||||
/// window: a length this place names, or the rel base of the widget
|
||||
/// giving it, which is `parent_rel_base`.
|
||||
pub(super) fn base(&self, axis: Axis, parent_rel_base: UiVec2) -> Len {
|
||||
match self[axis].rel_base {
|
||||
RelBase::Len(len) => len,
|
||||
RelBase::Inherit | RelBase::WithRegion => parent_rel_base[axis],
|
||||
}
|
||||
}
|
||||
|
||||
/// The box each axis names, in the coordinates `own` is in.
|
||||
pub fn of(self, own: UiRegion, align: RegionAlign) -> UiRegion {
|
||||
UiRegion::new(self.x.of(own.x, align.x), self.y.of(own.y, align.y))
|
||||
}
|
||||
}
|
||||
|
||||
impl UiSpan {
|
||||
/// This span composed into the caller's own box, so it moves and scales
|
||||
/// with it: [`UiSpan::within`], which is what a container that insets
|
||||
/// one speaks. Taking eleven pixels off the end needs no length, where
|
||||
/// saying the same thing in window lengths would make the container read
|
||||
/// its own box -- and a box chosen from its own answer then feeds back
|
||||
/// into the answer.
|
||||
///
|
||||
/// The child's rel base is narrowed the same way, so padding takes its
|
||||
/// pixels off both and `rel(1)` under it fills the caller rather than
|
||||
/// overflowing it.
|
||||
pub const fn within_desc(self) -> PlaceDescAxis {
|
||||
PlaceDescAxis {
|
||||
span: PlaceSpan::Within(self),
|
||||
fills: false,
|
||||
rel_base: RelBase::WithRegion,
|
||||
}
|
||||
}
|
||||
|
||||
/// This span shifted to where the caller's own box starts: window
|
||||
/// lengths along a cursor, which is what a container dividing room among
|
||||
/// its children speaks. A child's report is a window length, so the
|
||||
/// cursor that sums those reports is one too, and a moved box re-places
|
||||
/// every child by re-adding its start, exactly.
|
||||
///
|
||||
/// The child's rel base passes through: how far along the cursor a child
|
||||
/// sits says nothing about what a fraction under it is of. The same span
|
||||
/// says [`Self::within_desc`] as a part of that box instead, and which is
|
||||
/// meant cannot be read off the numbers.
|
||||
pub const fn shifted_desc(self) -> PlaceDescAxis {
|
||||
PlaceDescAxis {
|
||||
span: PlaceSpan::Shifted(self),
|
||||
fills: false,
|
||||
rel_base: RelBase::Inherit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Len {
|
||||
/// A box this long, placed in the caller's own by the child's alignment:
|
||||
/// the rule that places an answer, with the length given from above
|
||||
/// rather than reported. What a stack's sizing child decides for the
|
||||
/// rest. It is the child's rel base too.
|
||||
pub const fn as_desc(self) -> PlaceDescAxis {
|
||||
PlaceDescAxis {
|
||||
span: PlaceSpan::Sized(self),
|
||||
fills: false,
|
||||
rel_base: RelBase::Len(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UiRegion> for PlaceDesc {
|
||||
fn from(region: UiRegion) -> Self {
|
||||
Self::new(region.x.within_desc(), region.y.within_desc())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PlaceDescAxis> for PlaceDesc {
|
||||
fn from(place: PlaceDescAxis) -> Self {
|
||||
Self::splat(place)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,3 +210,5 @@ pub struct RetainedPrimitive {
|
||||
pub handle: PrimitiveHandle,
|
||||
pub region: UiRegion,
|
||||
}
|
||||
|
||||
impl_axis_index!(PlaceDesc => PlaceDescAxis);
|
||||
+344
-356
File diff suppressed because it is too large.
Load diff
@@ -35,7 +35,7 @@ impl<T, I: IdNum> Arena<T, I> {
|
||||
self.data[i]
|
||||
}
|
||||
|
||||
pub(crate) fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
||||
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
||||
&mut self.data[id.idx()]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,3 +93,31 @@ macro_rules! impl_op {
|
||||
}
|
||||
|
||||
pub(crate) use impl_op;
|
||||
|
||||
/// `Index<Axis>` for a pair, which is how every pair here is read by axis.
|
||||
/// The generics clause is given in braces where the type has one.
|
||||
macro_rules! impl_axis_index {
|
||||
($({$($gen:tt)*})? $T:ty => $Out:ty) => {
|
||||
const impl $(<$($gen)*>)? std::ops::Index<crate::Axis> for $T {
|
||||
type Output = $Out;
|
||||
|
||||
fn index(&self, axis: crate::Axis) -> &$Out {
|
||||
match axis {
|
||||
crate::Axis::X => &self.x,
|
||||
crate::Axis::Y => &self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const impl $(<$($gen)*>)? std::ops::IndexMut<crate::Axis> for $T {
|
||||
fn index_mut(&mut self, axis: crate::Axis) -> &mut $Out {
|
||||
match axis {
|
||||
crate::Axis::X => &mut self.x,
|
||||
crate::Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use impl_axis_index;
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{Axis, LayoutLen, Weight};
|
||||
use crate::util::impl_axis_index;
|
||||
use crate::{Axis, LayoutLen, Len};
|
||||
|
||||
/// 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.
|
||||
@@ -19,14 +20,9 @@ pub enum SizeRule {
|
||||
|
||||
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,
|
||||
}
|
||||
/// give one.
|
||||
pub fn declared(&self) -> Option<Len> {
|
||||
self.exact().and_then(|len| len.declared())
|
||||
}
|
||||
|
||||
/// The length this rule gives outright, whatever the widget reports --
|
||||
@@ -40,14 +36,6 @@ impl SizeRule {
|
||||
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 {
|
||||
@@ -70,18 +58,29 @@ pub struct SizeRules {
|
||||
pub y: SizeRule,
|
||||
}
|
||||
|
||||
impl SizeRules {
|
||||
pub fn axis(&self, axis: Axis) -> SizeRule {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
impl_axis_index!(SizeRules => SizeRule);
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut SizeRule {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
/// What a widget's box is on each axis where something says so outright,
|
||||
/// before it is drawn: a rule beside it, or a hint it gives about itself.
|
||||
/// Whoever draws the widget resolves these against its rel base.
|
||||
///
|
||||
/// A [`Len`] rather than a [`LayoutLen`], because a share can never be one
|
||||
/// -- see [`LayoutLen::declared`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Declared {
|
||||
pub x: Option<Len>,
|
||||
pub y: Option<Len>,
|
||||
}
|
||||
|
||||
impl Declared {
|
||||
pub const NONE: Self = Self { x: None, y: None };
|
||||
|
||||
pub fn from_axes(f: impl Fn(Axis) -> Option<Len>) -> Self {
|
||||
Self {
|
||||
x: f(Axis::X),
|
||||
y: f(Axis::Y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_axis_index!(Declared => Option<Len>);
|
||||
@@ -30,6 +30,14 @@ impl Widgets {
|
||||
!self.needs_redraw.is_empty()
|
||||
}
|
||||
|
||||
/// Marks this widget for the next frame to draw again, with nothing about
|
||||
/// it changed. Taking a widget mutably marks it too, which is the ordinary
|
||||
/// content-change signal; this is for a change the borrow cannot express,
|
||||
/// and for asking for the same tree over again.
|
||||
pub fn mark_for_redraw(&mut self, id: impl IdLike) {
|
||||
self.needs_redraw.insert(id.id());
|
||||
}
|
||||
|
||||
pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> {
|
||||
Some(self.vec.get(id)?.widget.as_ref())
|
||||
}
|
||||
@@ -41,14 +49,14 @@ impl Widgets {
|
||||
|
||||
/// get_dyn but dynamic borrow checking of widgets
|
||||
/// lets you do recursive (tree) operations, like the painter does
|
||||
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> {
|
||||
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> DynBorrower<'a, dyn Widget> {
|
||||
// SAFETY: must guarantee no other mutable references to this widget exist
|
||||
// done through the borrow variable
|
||||
let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) };
|
||||
if data.borrowed {
|
||||
panic!("tried to mutably borrow the same widget twice");
|
||||
}
|
||||
WidgetWrapper::new(data.widget.as_mut(), &mut data.borrowed)
|
||||
DynBorrower::new(data.widget.as_mut(), &mut data.borrowed)
|
||||
}
|
||||
|
||||
pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget>
|
||||
@@ -130,10 +138,10 @@ impl Widgets {
|
||||
pub fn set_size_rule(&mut self, id: impl IdLike, axis: Axis, rule: SizeRule) {
|
||||
let id = id.id();
|
||||
let data = self.data_mut(id).unwrap();
|
||||
if *data.size.axis_mut(axis) == rule {
|
||||
if data.size[axis] == rule {
|
||||
return;
|
||||
}
|
||||
*data.size.axis_mut(axis) = rule;
|
||||
data.size[axis] = rule;
|
||||
self.needs_redraw.insert(id);
|
||||
}
|
||||
|
||||
@@ -147,14 +155,14 @@ impl Widgets {
|
||||
pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) {
|
||||
let id = id.id();
|
||||
let data = self.data_mut(id).unwrap();
|
||||
if *data.align.axis_mut(axis) == align {
|
||||
if data.align[axis] == align {
|
||||
return;
|
||||
}
|
||||
*data.align.axis_mut(axis) = align;
|
||||
data.align[axis] = align;
|
||||
self.needs_redraw.insert(id);
|
||||
}
|
||||
|
||||
/// Both axes at once, for a caller holding a pair.
|
||||
/// Both axes at once.
|
||||
pub fn set_size_rules(
|
||||
&mut self,
|
||||
id: impl IdLike,
|
||||
@@ -188,8 +196,6 @@ impl Default for Widgets {
|
||||
}
|
||||
}
|
||||
|
||||
pub type WidgetWrapper<'a> = DynBorrower<'a, dyn Widget>;
|
||||
|
||||
impl<I: IdLike> std::ops::Index<I> for Widgets
|
||||
where
|
||||
I::Widget: Sized + Widget,
|
||||
|
||||
+11
-6
@@ -106,11 +106,16 @@ export WAYLAND_DISPLAY
|
||||
|
||||
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
|
||||
|
||||
swaymsg output HEADLESS-1 mode "$mode" >/dev/null
|
||||
# The extent `replay-touch` positions against, so a script's coordinates
|
||||
# are the output's own pixels.
|
||||
out_w=${mode%x*}
|
||||
out_h=${mode#*x}; out_h=${out_h%@*}
|
||||
# The extent `replay-touch` positions against, so a script's coordinates are
|
||||
# the output's own pixels. Set beside every mode change, since a gesture
|
||||
# scaled against a mode the output no longer has lands somewhere else and
|
||||
# still looks like a run that worked.
|
||||
set_mode() {
|
||||
swaymsg output HEADLESS-1 mode "$1" >/dev/null
|
||||
out_w=${1%x*}
|
||||
out_h=${1#*x}; out_h=${out_h%@*}
|
||||
}
|
||||
set_mode "$mode"
|
||||
|
||||
# Built before the app starts, so a compile error is not reported as a
|
||||
# window that failed to move.
|
||||
@@ -149,7 +154,7 @@ while [ $i -lt "$((seconds * 2))" ]; do
|
||||
done
|
||||
|
||||
if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
swaymsg output HEADLESS-1 mode "$resize" >/dev/null
|
||||
set_mode "$resize"
|
||||
echo "run-headless: resized to $resize" >&2
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 191 B |
+4
-3
@@ -15,9 +15,10 @@ where
|
||||
let region = ctx.data.render.window_region(&id).unwrap();
|
||||
let id_pos = region.top_left;
|
||||
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
|
||||
// The pointer arrives from the platform in floats; everything
|
||||
// it is compared against is on the grid.
|
||||
let pos = (PxVec2::from_f32(ctx.data.pos) + container_pos - id_pos).to_f32();
|
||||
// The two regions are on the grid and the pointer is not, so the
|
||||
// step between them is taken there and the pointer keeps the
|
||||
// precision the platform gave it.
|
||||
let pos = ctx.data.pos + (container_pos - id_pos).to_f32();
|
||||
let size = region.size().to_f32();
|
||||
select(
|
||||
rsc,
|
||||
|
||||
@@ -22,12 +22,12 @@ impl UiRenderer {
|
||||
}
|
||||
|
||||
pub fn draw(&mut self) {
|
||||
let output = match self.surface.get_current_texture() {
|
||||
CurrentSurfaceTexture::Success(texture) => texture,
|
||||
CurrentSurfaceTexture::Suboptimal(texture) => {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
texture
|
||||
}
|
||||
let (output, suboptimal) = match self.surface.get_current_texture() {
|
||||
CurrentSurfaceTexture::Success(texture) => (texture, false),
|
||||
// Used for this frame, and the swapchain rebuilt after it has
|
||||
// been presented: configuring the surface while a texture it
|
||||
// handed out is still alive panics.
|
||||
CurrentSurfaceTexture::Suboptimal(texture) => (texture, true),
|
||||
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
return;
|
||||
@@ -60,6 +60,9 @@ impl UiRenderer {
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.window.pre_present_notify();
|
||||
self.queue.present(output);
|
||||
if suboptimal {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
|
||||
|
||||
+60
-35
@@ -8,23 +8,16 @@
|
||||
use crate::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// The declared lengths of one widget carrying a size rule, by axis.
|
||||
pub type Lens = [Option<LayoutLen>; 2];
|
||||
|
||||
/// Where one widget carrying an alignment sits, by axis. `None` uses the
|
||||
/// centered default.
|
||||
pub type Aligns = [Option<AxisAlign>; 2];
|
||||
|
||||
/// What a test changes between two trees grown from the same seed, so the
|
||||
/// warm one can be mutated and the cold one grown that way to begin with.
|
||||
#[derive(Default)]
|
||||
pub struct Edits {
|
||||
/// Declared sizes, by the order the rules were put on.
|
||||
pub sizes: HashMap<usize, Lens>,
|
||||
pub sizes: HashMap<usize, SizeRules>,
|
||||
/// Which children a span has, by the order the spans were made.
|
||||
pub spans: HashMap<usize, SpanEdit>,
|
||||
/// Alignments, by the order they were put on.
|
||||
pub aligns: HashMap<usize, Aligns>,
|
||||
pub aligns: HashMap<usize, Align>,
|
||||
/// 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.
|
||||
@@ -118,9 +111,9 @@ pub struct Branch {
|
||||
impl Widget for Branch {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
|
||||
let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut)));
|
||||
let top = UiSpan::new(Len::ZERO, cut).shifted_desc();
|
||||
let measured = painter
|
||||
.widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top])
|
||||
.widget_at(&self.probe, top.on_axis(Axis::Y))
|
||||
.len(Axis::X);
|
||||
let len = measured.apply_leftover();
|
||||
let px = painter.to_px(len, Axis::X);
|
||||
@@ -134,11 +127,11 @@ impl Widget for Branch {
|
||||
};
|
||||
painter.window_holds(Axis::X, holds.through(len));
|
||||
|
||||
let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y))));
|
||||
let place = [Place::Within(Part::All), below];
|
||||
let below = UiSpan::new(cut, painter.region_len(Axis::Y)).shifted_desc();
|
||||
let place = below.on_axis(Axis::Y);
|
||||
match px > threshold {
|
||||
true => painter.widget_at(&self.wide, [None; 2], place),
|
||||
false => painter.widget_at(&self.narrow, [None; 2], place),
|
||||
true => painter.widget_at(&self.wide, place),
|
||||
false => painter.widget_at(&self.narrow, place),
|
||||
};
|
||||
Size::LEFTOVER
|
||||
}
|
||||
@@ -176,9 +169,11 @@ pub struct Plan {
|
||||
/// 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>,
|
||||
pub size: Option<SizeRules>,
|
||||
/// The alignment it carries, under the same one-offer rule. An axis left
|
||||
/// out takes the centered default, which is what [`RegionAlign`] reads it
|
||||
/// as.
|
||||
pub align: Option<Align>,
|
||||
/// 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.
|
||||
@@ -195,6 +190,9 @@ pub enum Kind {
|
||||
color: usize,
|
||||
alpha: u8,
|
||||
},
|
||||
/// The one leaf whose own length is a number of pixels it knows before it
|
||||
/// is drawn, which is the hint a rule beside it has to win over.
|
||||
Image,
|
||||
/// Scrolling reads the pixel length of its box, which nothing else here
|
||||
/// does, and gives its child a box longer than its own.
|
||||
Scroll {
|
||||
@@ -448,9 +446,11 @@ impl Kind {
|
||||
}
|
||||
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.
|
||||
// that does not, then the one that measures nothing at all. A
|
||||
// picture measures nothing either, but its length is its own, so
|
||||
// it steps to the leaf that takes whatever it is given.
|
||||
Kind::Wrapped => out.push(Kind::OneLine),
|
||||
Kind::OneLine => out.push(Kind::Rect {
|
||||
Kind::OneLine | Kind::Image => out.push(Kind::Rect {
|
||||
color: 0,
|
||||
alpha: 255,
|
||||
}),
|
||||
@@ -632,9 +632,10 @@ struct Sow<'a> {
|
||||
|
||||
impl Sow<'_> {
|
||||
fn leaf(&mut self) -> Plan {
|
||||
Plan::bare(match self.rng.below(4) {
|
||||
Plan::bare(match self.rng.below(5) {
|
||||
0 => Kind::Wrapped,
|
||||
1 => Kind::OneLine,
|
||||
2 => Kind::Image,
|
||||
_ => {
|
||||
let color = self.rng.below(COLORS.len());
|
||||
let alpha = (self.rng.below(5) * 63) as u8;
|
||||
@@ -651,7 +652,7 @@ impl Sow<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn align(&mut self) -> Aligns {
|
||||
fn align(&mut self) -> Align {
|
||||
let axis = |s: &mut Self| match s.rng.below(4) {
|
||||
0 => None,
|
||||
1 => Some(AxisAlign::NEG),
|
||||
@@ -661,15 +662,21 @@ impl Sow<'_> {
|
||||
let (x, y) = (axis(self), axis(self));
|
||||
// Aligning on neither axis leaves the branch unexercised.
|
||||
match x.is_none() && y.is_none() {
|
||||
true => [Some(AxisAlign::CENTER), y],
|
||||
false => [x, y],
|
||||
true => Align {
|
||||
x: Some(AxisAlign::CENTER),
|
||||
y,
|
||||
},
|
||||
false => Align { x, y },
|
||||
}
|
||||
}
|
||||
|
||||
/// A declared size over half the tree, kept where a test can change it.
|
||||
fn sized(&mut self, inner: &mut Plan) {
|
||||
let take = self.rng.chance();
|
||||
let lens = [self.len(), self.len()];
|
||||
let lens = SizeRules {
|
||||
x: self.len().into(),
|
||||
y: self.len().into(),
|
||||
};
|
||||
if !take || inner.size.is_some() {
|
||||
return;
|
||||
}
|
||||
@@ -773,10 +780,6 @@ impl Sow<'_> {
|
||||
self.spans += 1;
|
||||
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
|
||||
let dir = self.rng.below(4);
|
||||
// A row takes the height it is given rather than its tallest child,
|
||||
// which is a rule beside it. Derived from an existing choice and
|
||||
// consuming no randomness: a seed must keep growing the same tree
|
||||
// when the generator gains another configuration.
|
||||
let gap = self.rng.below(3) as i32 * 4;
|
||||
let grown: Vec<usize> = (0..children.len()).collect();
|
||||
let order = span_edited(&grown, children.len(), spares.len(), &edit);
|
||||
@@ -796,6 +799,7 @@ pub fn build<Rsc: UiRsc + 'static>(rsc: &mut Rsc, plan: &Plan) -> (StrongWidget,
|
||||
let mut build = Build {
|
||||
rsc,
|
||||
tree: Tree::default(),
|
||||
checkerboard: None,
|
||||
};
|
||||
let root = build.node(plan);
|
||||
(root, build.tree)
|
||||
@@ -804,6 +808,10 @@ pub fn build<Rsc: UiRsc + 'static>(rsc: &mut Rsc, plan: &Plan) -> (StrongWidget,
|
||||
struct Build<'a, Rsc> {
|
||||
rsc: &'a mut Rsc,
|
||||
tree: Tree,
|
||||
/// The checkerboard, uploaded when the first image in this tree is built.
|
||||
/// A handle is a reference to the texture, so every image after that one
|
||||
/// clones this rather than uploading the same picture again.
|
||||
checkerboard: Option<TextureHandle>,
|
||||
}
|
||||
|
||||
impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
||||
@@ -811,16 +819,14 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
||||
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.rsc.ui_mut().widgets.set_size_rules(id, lens.x, lens.y);
|
||||
self.tree.sized.push(id);
|
||||
}
|
||||
if let Some(align) = plan.align {
|
||||
let resolved = RegionAlign::from(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());
|
||||
for axis in Axis::BOTH {
|
||||
widgets.set_alignment(id, axis, resolved[axis]);
|
||||
}
|
||||
self.tree.aligned.push(id);
|
||||
}
|
||||
@@ -831,6 +837,20 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
||||
built
|
||||
}
|
||||
|
||||
/// The one picture the generated trees draw: a 64x64 checkerboard of purple
|
||||
/// and black in 8 px cells. Committed rather than drawn here, so that one
|
||||
/// seed is one tree whatever anything else does, and included rather than
|
||||
/// opened, so that growing a tree does not depend on a working directory.
|
||||
fn checkerboard(&mut self) -> TextureHandle {
|
||||
if self.checkerboard.is_none() {
|
||||
let image = include_bytes!("assets/checkerboard.png")
|
||||
.get_image()
|
||||
.expect("the checkerboard is committed beside this file");
|
||||
self.checkerboard = Some(self.rsc.ui_mut().textures.add(image));
|
||||
}
|
||||
self.checkerboard.clone().unwrap()
|
||||
}
|
||||
|
||||
fn kind(&mut self, kind: &Kind) -> StrongWidget {
|
||||
let id: StrongWidget = match kind {
|
||||
Kind::Wrapped => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
|
||||
@@ -839,6 +859,7 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
||||
.wrap(false)
|
||||
.add_strong(self.rsc),
|
||||
Kind::Rect { color, alpha } => rect(COLORS[*color].alpha(*alpha)).add_strong(self.rsc),
|
||||
Kind::Image => Image::new(self.checkerboard()).add_strong(self.rsc),
|
||||
Kind::Scroll { axis, inner } => {
|
||||
let inner = self.node(inner);
|
||||
let id = Scroll::new(inner, *axis).add(self.rsc);
|
||||
@@ -915,6 +936,10 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
||||
gap: Px::from_int(*gap),
|
||||
}
|
||||
.add(self.rsc);
|
||||
// A row takes the height it is given rather than its tallest
|
||||
// child, which is a rule beside the span rather than anything
|
||||
// it draws. Derived from `dir` rather than stored, so a plan
|
||||
// that says the direction says this too.
|
||||
if dir.axis == Axis::X {
|
||||
self.rsc
|
||||
.widgets_mut()
|
||||
|
||||
+10
-1
@@ -12,7 +12,16 @@ impl Widget for Image {
|
||||
}
|
||||
|
||||
fn size_hint(&self, axis: Axis) -> Option<LayoutLen> {
|
||||
Some(LayoutLen::px(self.handle.size().axis(axis)))
|
||||
Some(LayoutLen::px(self.handle.size()[axis]))
|
||||
}
|
||||
}
|
||||
|
||||
impl Image {
|
||||
/// One texture already uploaded, for a caller holding its handle: [`image`]
|
||||
/// uploads what it is given, and several widgets showing one picture want
|
||||
/// one upload and one slot between them.
|
||||
pub fn new(handle: TextureHandle) -> Self {
|
||||
Self { handle }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,8 @@ pub struct Offset {
|
||||
|
||||
impl Widget for Offset {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
// The whole of this widget's box, moved: the frame passes through, so
|
||||
// what the child declares or reports means the same as it would
|
||||
// without the offset.
|
||||
let moved = |len: Len, amt: Len| Place::Within(Part::From(UiSpan::new(amt, len + amt)));
|
||||
let place = [
|
||||
moved(painter.extent_len(Axis::X), self.amt.x),
|
||||
moved(painter.extent_len(Axis::Y), self.amt.y),
|
||||
];
|
||||
painter.widget_at(&self.inner, [None; 2], place).size()
|
||||
painter
|
||||
.widget_at(&self.inner, UiRegion::FULL.offset(self.amt))
|
||||
.size()
|
||||
}
|
||||
}
|
||||
@@ -14,31 +14,12 @@ impl Widget for Pad {
|
||||
// widget -- the slack is the inner's to sit in, and forcing the near
|
||||
// edge pinned it to a corner it had not asked for.
|
||||
//
|
||||
// Padding is an inset of both: it comes off the frame, so `rel(1)`
|
||||
// Padding is an inset of both: it comes off the rel base, so `rel(1)`
|
||||
// under it fills this widget rather than overflowing it by the
|
||||
// padding, and it comes off the box, so what is drawn sits inside.
|
||||
// The two stay distinct -- the box can be narrower still, where a row
|
||||
// asked this widget in the room left, and a text wraps at that.
|
||||
let inset = |lead: Px, trail: Px| {
|
||||
Place::Within(Part::Of(UiSpan::new(
|
||||
Len::from_parts(Rel::ZERO, lead),
|
||||
Len::from_parts(Rel::ONE, -trail),
|
||||
)))
|
||||
};
|
||||
let place = [
|
||||
inset(self.padding.left, self.padding.right),
|
||||
inset(self.padding.top, self.padding.bottom),
|
||||
];
|
||||
// Read from this widget's own frame rather than written as a
|
||||
// fraction of it: a frame is a length of the window like everything
|
||||
// else here, and taking the padding off is the whole of what this
|
||||
// widget does to it.
|
||||
let narrow = [
|
||||
(Axis::X, self.padding.left + self.padding.right),
|
||||
(Axis::Y, self.padding.top + self.padding.bottom),
|
||||
]
|
||||
.map(|(axis, pixels)| Some(painter.frame_len(axis) - Len::from_parts(Rel::ZERO, pixels)));
|
||||
let inner = painter.widget_at(&self.inner, narrow, place).size();
|
||||
let inner = painter.widget_at(&self.inner, self.padding.region()).size();
|
||||
Size {
|
||||
x: LayoutLen {
|
||||
px: inner.x.px + self.padding.left + self.padding.right,
|
||||
|
||||
@@ -14,58 +14,56 @@ impl Widget for Scroll {
|
||||
let container_len = painter.px_len(self.axis);
|
||||
// Asked in the whole viewport, then put at the scrolled offset.
|
||||
let answer_len = painter
|
||||
.widget_at(&self.inner, [None; 2], [Place::Fill(Part::All); 2])
|
||||
.widget_at(&self.inner, PlaceDesc::WHOLE.fills())
|
||||
.len(self.axis);
|
||||
let fixed = painter.to_px(Len::from_parts(answer_len.rel, answer_len.px), self.axis);
|
||||
let answer_px = painter.to_px(answer_len.without_leftover(), self.axis);
|
||||
self.container_len = container_len;
|
||||
self.content_len = fixed.max(container_len);
|
||||
self.content_len = answer_px.max(container_len);
|
||||
|
||||
if self.snap_end {
|
||||
self.amt = self.content_len - self.container_len;
|
||||
}
|
||||
self.update_amt();
|
||||
let align = painter.alignment().axis(self.axis);
|
||||
// Content of a fixed length that fits sits at the start of any box it
|
||||
// fits in -- but only anchored there. Anywhere else it is a part of
|
||||
// the room left over, so it moves with every length the box takes and
|
||||
// the drawing holds for that length alone. One scrolled part way sits
|
||||
// where it is until the box shrinks past what is left of it. Kept to
|
||||
// the end, it moves with every length.
|
||||
let fixed_len = answer_len.rel == Rel::ZERO && answer_len.leftover == Weight::ZERO;
|
||||
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG {
|
||||
painter.holds(self.axis, fixed..=Px::MAX);
|
||||
} else if fixed_len && !self.snap_end {
|
||||
// Reading the box in pixels above holds this drawing to that one
|
||||
// length, so these two say where it holds more widely.
|
||||
//
|
||||
// Content of a fixed length that fits is handed the whole box below,
|
||||
// and nothing here reads the box again, so every longer box gives the
|
||||
// same drawing: it holds from the length the content needs upwards,
|
||||
// and shrinking past that is what changes it. Where it sits in a box
|
||||
// longer than itself is not this widget's to say -- placing its
|
||||
// answer in the whole box is its own alignment, and that placement is
|
||||
// a fraction of the box, so it holds at every length too.
|
||||
//
|
||||
// One scrolled part way sits where it is until the box shrinks past
|
||||
// what is left of it. Kept to the end, it moves with every length.
|
||||
let answer_is_px = answer_len.is_px();
|
||||
if answer_is_px && self.content_len <= self.container_len {
|
||||
painter.holds(self.axis, answer_px..=Px::MAX);
|
||||
} else if answer_is_px && !self.snap_end {
|
||||
let left = self.content_len - self.amt;
|
||||
painter.holds(self.axis, Px::MIN..=left);
|
||||
}
|
||||
|
||||
// Content shorter than the viewport has room to sit in, and where it
|
||||
// sits is this widget's own alignment -- the same property that would
|
||||
// have placed the whole scroll in a box longer than it.
|
||||
let slack = (self.container_len - self.content_len).max(Px::ZERO);
|
||||
let anchor = slack.mul(align.rel());
|
||||
// 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;
|
||||
let content = match moved || self.content_len != self.container_len {
|
||||
// Content that fills the viewport is the viewport, and is handed back
|
||||
// as it came -- it has nothing to scroll through, so the clamp above
|
||||
// has already put `amt` at zero. Writing the same box as its own
|
||||
// length in pixels is the same box in another form, and the two do
|
||||
// not round alike: a part centred in `rel 1` lands a step from one
|
||||
// centred in `px 900`, since halving a difference is not halving each
|
||||
// part of it.
|
||||
let content = match self.content_len > self.container_len {
|
||||
true => {
|
||||
let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
|
||||
Part::From(UiSpan::new(start, start.offset(self.content_len)))
|
||||
let start = Len::from_parts(Rel::ZERO, -self.amt);
|
||||
UiSpan::new(start, start.offset(self.content_len)).shifted_desc()
|
||||
}
|
||||
false => Part::All,
|
||||
false => PlaceDescAxis::WHOLE,
|
||||
};
|
||||
// The viewport is the inner's frame, so a fraction it declares or
|
||||
// The viewport is the inner's rel base, so a fraction it declares or
|
||||
// reports is a fraction of what is on screen rather than of the
|
||||
// content box its own answer decided. Where it goes is the content
|
||||
// box, scrolled: its drawing moved there, not made again there.
|
||||
painter.place_at(
|
||||
&self.inner,
|
||||
self.axis.pair(Place::Fill(content), Place::Fill(Part::All)),
|
||||
);
|
||||
painter.place_at(&self.inner, content.on_axis(self.axis).fills());
|
||||
// What it occupies is its box, on both axes: it clips its content to
|
||||
// that box, so it can neither take less of one nor honestly ask for
|
||||
// more. The content's length is what it scrolls through, not what it
|
||||
|
||||
+62
-94
@@ -10,48 +10,32 @@ pub struct Span {
|
||||
impl Widget for Span {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let axis = self.dir.axis;
|
||||
// The row: this span's own box, as a length of the frame its children
|
||||
// are laid out against. Its start is nothing's business -- a slot is
|
||||
// a length from it -- so what this reads is the length alone.
|
||||
let far = painter.extent_len(axis);
|
||||
let along = |from: Len, to: Len| match self.dir.sign {
|
||||
Sign::Pos => UiSpan::new(from, to),
|
||||
Sign::Neg => UiSpan::new(far - to, far - from),
|
||||
};
|
||||
// Across itself the child sits where its own alignment says, in the
|
||||
// whole of the row: a span is what contains its children there, and
|
||||
// nothing divides that axis.
|
||||
let across = Place::Within(Part::All);
|
||||
// The row this span lays its children out along, as a length of the
|
||||
// rel base they are laid out against. Where it starts is nothing's
|
||||
// business -- a slot is a length from there -- so what this reads is
|
||||
// the length alone.
|
||||
let row = painter.region_len(axis);
|
||||
// A length for every child before their final slots are chosen: from
|
||||
// a hint where one says, and from drawing otherwise. The frame passes
|
||||
// a hint where one says, and from drawing otherwise. The rel base passes
|
||||
// through unchanged, so `rel(0.5)` is half the area this span was
|
||||
// given whatever else is in it and wherever this child sits among
|
||||
// them; what a drawn child is asked in is the room left from the
|
||||
// cursor, because a text has to wrap at the width actually there.
|
||||
// This is the one ask a drawn fixed child gets: its slot is its
|
||||
// answer, and the drawing is moved there once the shares are known.
|
||||
// A hinted child is asked once, in its slot.
|
||||
let mut cursor = Len::rel_min();
|
||||
let mut cursor = Len::ZERO;
|
||||
let mut lens = Vec::with_capacity(self.children.len());
|
||||
let mut measured = Vec::with_capacity(self.children.len());
|
||||
for child in &self.children {
|
||||
let size = match painter.size_hint(child, axis) {
|
||||
Some(len) => {
|
||||
measured.push(None);
|
||||
len
|
||||
}
|
||||
let len = match painter.size_hint(child, axis) {
|
||||
Some(len) => len,
|
||||
None => {
|
||||
let room = Place::Within(Part::From(along(cursor, far)));
|
||||
let size = painter
|
||||
.widget_at(child, [None; 2], axis.pair(room, across))
|
||||
.size();
|
||||
measured.push(Some(size));
|
||||
size.axis(axis)
|
||||
// Across itself the child sits where its own alignment
|
||||
// says, in the whole of the row: a span is what contains
|
||||
// its children there, and nothing divides that axis.
|
||||
let room = self.slot(row, cursor, row).shifted_desc().on_axis(axis);
|
||||
painter.widget_at(child, room).len(axis)
|
||||
}
|
||||
};
|
||||
let len = size;
|
||||
cursor.px += len.px + self.gap;
|
||||
cursor.rel += len.rel;
|
||||
cursor += len.without_leftover();
|
||||
cursor.px += self.gap;
|
||||
lens.push(len);
|
||||
}
|
||||
|
||||
@@ -67,26 +51,16 @@ impl Widget for Span {
|
||||
);
|
||||
|
||||
// What is left for the shares to divide: the row less everything
|
||||
// fixed, as a length of the frame rather than a number of pixels.
|
||||
let room = far - Len::from_parts(total.rel, total.px);
|
||||
// Whether anything is left over is a question in pixels: `rel(0.5)`
|
||||
// beside 300 px is full at 600 and overfull at 400. Asked of `room`
|
||||
// itself, and answered back through the same expression, so the
|
||||
// boundary is the drawing's own and not a second way of finding it:
|
||||
// the three cases a rounded division needed -- the fixed parts
|
||||
// fixed, as a length of the rel base rather than a number of pixels.
|
||||
let all_fixed = total.without_leftover();
|
||||
let room = row - all_fixed;
|
||||
// The three cases a rounded division needed -- the fixed parts
|
||||
// growing slower than the box, faster, or exactly with it -- are the
|
||||
// sign of `room.rel`, which `through` already reads. What the
|
||||
// generated oracle checks is the consequence, since which children
|
||||
// exist at all turns on this.
|
||||
let mut shares = false;
|
||||
if total.leftover > Weight::ZERO {
|
||||
shares = painter.to_px(room, axis) > Px::ZERO;
|
||||
let holds = match shares {
|
||||
true => Holds::from(Px::STEP..=Px::MAX),
|
||||
false => Holds::from(Px::MIN..=Px::ZERO),
|
||||
};
|
||||
painter.window_holds(axis, holds.through(room));
|
||||
}
|
||||
// sign of `room.rel`, which the range `longer_than` keeps already
|
||||
// reads. What the generated oracle checks is the consequence, since
|
||||
// which children exist at all turns on this.
|
||||
let any_leftover = total.leftover > Weight::ZERO;
|
||||
let has_room = any_leftover && painter.longer_than(row, all_fixed, axis);
|
||||
|
||||
// Across itself a span is as long as its longest child -- unless a
|
||||
// rule beside it gives that length outright, and then reading them
|
||||
@@ -96,65 +70,59 @@ impl Widget for Span {
|
||||
let shrinks = !painter.has_exact_size(!axis);
|
||||
// What the fixed parts and the gaps before here take, which is a sum
|
||||
// of lengths and exact, and how much of the leftover weight is
|
||||
// spoken for. A position is one from the other rather than a step
|
||||
// from the last child: the share of the room is rounded, and taking
|
||||
// each from the one before it would carry every rounding along the
|
||||
// row.
|
||||
let mut fixed = Len::rel_min();
|
||||
// spoken for. Both ends of a slot are read from those two rather
|
||||
// than stepped from the last child: the share of the room is
|
||||
// rounded, and taking each end from the one before it would carry
|
||||
// every rounding along the row.
|
||||
let mut fixed = Len::ZERO;
|
||||
let mut taken = Weight::ZERO;
|
||||
let mut start = Len::rel_min();
|
||||
let mut ortho = LayoutLen::ZERO;
|
||||
for ((child, len), measured) in self.children.iter().zip(&lens).zip(&measured) {
|
||||
let len = *len;
|
||||
// Nothing divides the room where no child asked for any of it, and a
|
||||
// ratio of a whole of nothing has no answer.
|
||||
let reached = |fixed: Len, taken: Weight| match any_leftover {
|
||||
false => fixed,
|
||||
true => fixed + room.scale(Rel::ratio(taken, total.leftover)),
|
||||
};
|
||||
for (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
|
||||
{
|
||||
if len.is_only_leftover() && !has_room {
|
||||
painter.undraw(child);
|
||||
fixed.px += self.gap;
|
||||
continue;
|
||||
}
|
||||
let from = start;
|
||||
if len.leftover > Weight::ZERO && shares {
|
||||
let from = reached(fixed, taken);
|
||||
if len.leftover > Weight::ZERO && has_room {
|
||||
taken += len.leftover;
|
||||
}
|
||||
fixed.px += len.px;
|
||||
fixed.rel += len.rel;
|
||||
start = shared(fixed, taken, total.leftover, room);
|
||||
fixed += len.without_leftover();
|
||||
let to = reached(fixed, taken);
|
||||
// Along the row the span says where the child goes, and that slot
|
||||
// is the child's box outright rather than something to place an
|
||||
// answer inside again. A share is decided here and nowhere
|
||||
// else: its slot narrows its frame, and the child is asked in
|
||||
// else: its slot narrows its rel base, and the child is asked in
|
||||
// it, since a text wraps at the width it is actually given. A
|
||||
// fixed child's slot is its own answer, so a drawing made in the
|
||||
// room is put there as it is, and one not made yet is made here.
|
||||
let slot = along(from, start);
|
||||
let place = axis.pair(Place::Fill(Part::From(slot)), across);
|
||||
let mut narrow = [None; 2];
|
||||
if len.leftover > Weight::ZERO && shares {
|
||||
narrow[axis as usize] = Some(slot.len());
|
||||
let slot = self.slot(row, from, to);
|
||||
let mut place = slot.shifted_desc().fills().on_axis(axis);
|
||||
if len.leftover > Weight::ZERO && has_room {
|
||||
place = place.rel_base(axis, slot.len());
|
||||
}
|
||||
let used = match (measured, narrow[axis as usize]) {
|
||||
(Some(size), None) => {
|
||||
painter.place_at(child, place);
|
||||
size.axis(!axis)
|
||||
}
|
||||
_ => painter.widget_at(child, narrow, place).len(!axis),
|
||||
};
|
||||
let used = painter.place_at(child, place).len(!axis);
|
||||
if shrinks {
|
||||
// Choosing between a fixed and a relative length from the
|
||||
// span's own eventual width admits multiple fixed points.
|
||||
// A scalable child therefore makes Children scalable too;
|
||||
// A scalable child therefore makes the span scalable too;
|
||||
// only fixed children are compared with one another.
|
||||
if used.rel != Rel::ZERO || used.leftover != Weight::ZERO {
|
||||
if !used.is_px() {
|
||||
ortho = LayoutLen::LEFTOVER;
|
||||
} else if ortho.leftover == Weight::ZERO {
|
||||
ortho.px = ortho.px.max(used.px);
|
||||
}
|
||||
}
|
||||
fixed.px += self.gap;
|
||||
start = shared(fixed, taken, total.leftover, room);
|
||||
}
|
||||
|
||||
// Carried whole rather than collapsed to one share: a span that sizes
|
||||
@@ -164,26 +132,26 @@ impl Widget for Span {
|
||||
// get a quarter each, which collapsing to `leftover(1)` per level does
|
||||
// not give. Resolution happens at the nearest ancestor with a length,
|
||||
// and the root always has one.
|
||||
let along = total;
|
||||
let ortho = match shrinks {
|
||||
true => ortho,
|
||||
false => LayoutLen::rel(1.0),
|
||||
};
|
||||
Size::from_axis(axis, along, ortho)
|
||||
Size::from_axis(axis, total, ortho)
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a row has reached: everything fixed before this point, which is a
|
||||
/// sum and exact, plus the share of the room the weights so far are worth,
|
||||
/// which is one rounding wherever it is asked for.
|
||||
fn shared(fixed: Len, taken: Weight, weight: Weight, room: Len) -> Len {
|
||||
if taken == Weight::ZERO {
|
||||
return fixed;
|
||||
}
|
||||
fixed + room.scale(Rel::ratio(taken, weight))
|
||||
}
|
||||
|
||||
impl Span {
|
||||
/// The stretch of the row between two distances from where this span
|
||||
/// starts laying children out, as a span of its own box. A negative
|
||||
/// direction lays out from the far end, so the same two distances mirror
|
||||
/// in a row `row` long.
|
||||
fn slot(&self, row: Len, from: Len, to: Len) -> UiSpan {
|
||||
match self.dir.sign {
|
||||
Sign::Pos => from.to(to),
|
||||
Sign::Neg => (row - to).to(row - from),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty(dir: Dir) -> Self {
|
||||
Self {
|
||||
children: Vec::new(),
|
||||
|
||||
@@ -22,9 +22,7 @@ impl Widget for Stack {
|
||||
// drawing belongs to the layer it was made on.
|
||||
Some((i, child)) => {
|
||||
painter.child_layer_at(i);
|
||||
painter
|
||||
.widget_at(child, [None; 2], [Place::Fill(Part::All); 2])
|
||||
.size()
|
||||
painter.widget_at(child, PlaceDesc::WHOLE.fills()).size()
|
||||
}
|
||||
None => Size::LEFTOVER,
|
||||
};
|
||||
@@ -33,11 +31,11 @@ impl Widget for Stack {
|
||||
// fraction under them is a fraction of it. A share leaves the axis
|
||||
// to whoever gave the stack its box. Where a child sits in a box
|
||||
// bigger than itself is its own business.
|
||||
let place = [Axis::X, Axis::Y].map(|axis| {
|
||||
let len = size.axis(axis);
|
||||
let place = PlaceDesc::from_axes(|axis| {
|
||||
let len = size[axis];
|
||||
match len.leftover == Weight::ZERO {
|
||||
true => Place::Fill(Part::Sized(Len::from_parts(len.rel, len.px))),
|
||||
false => Place::Within(Part::All),
|
||||
true => len.without_leftover().as_desc().fills(),
|
||||
false => PlaceDescAxis::WHOLE,
|
||||
}
|
||||
});
|
||||
for (i, child) in self.children.iter().enumerate() {
|
||||
@@ -45,7 +43,7 @@ impl Widget for Stack {
|
||||
continue;
|
||||
}
|
||||
painter.child_layer_at(i);
|
||||
painter.widget_at(child, [None; 2], place);
|
||||
painter.widget_at(child, place);
|
||||
}
|
||||
size
|
||||
}
|
||||
|
||||
@@ -321,12 +321,10 @@ impl<'a> TextEditCtx<'a> {
|
||||
let old = (self.text.view.buf.text().to_string(), self.text.selection);
|
||||
let mut undo = false;
|
||||
let res = self.apply_event_inner(event, modifiers, &mut undo);
|
||||
if undo {
|
||||
if let Some((old, selection)) = self.text.history.pop() {
|
||||
self.set(&old);
|
||||
self.text.selection = selection;
|
||||
self.clamp_selection_to_layout();
|
||||
}
|
||||
if undo && let Some((old, selection)) = self.text.history.pop() {
|
||||
self.set(&old);
|
||||
self.text.selection = selection;
|
||||
self.clamp_selection_to_layout();
|
||||
} else if self.text.view.buf.text() != old.0 {
|
||||
self.text.history.push(old);
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ widget_trait! {
|
||||
move |state| {
|
||||
let id = self.add(state);
|
||||
let widgets = &mut state.ui_mut().widgets;
|
||||
for (axis, align) in [(Axis::X, align.x), (Axis::Y, align.y)] {
|
||||
if let Some(align) = align {
|
||||
for axis in Axis::BOTH {
|
||||
if let Some(align) = align[axis] {
|
||||
widgets.set_alignment(id, axis, align);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-11
@@ -9,6 +9,7 @@ use std::marker::Unsize;
|
||||
///
|
||||
/// Its child is optional so it can also be the swappable slot a tab bar
|
||||
/// needs, which is what it was written for.
|
||||
#[derive(Default)]
|
||||
pub struct Wrapper {
|
||||
pub inner: Option<StrongWidget>,
|
||||
}
|
||||
@@ -26,11 +27,7 @@ impl Wrapper {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
inner: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) {
|
||||
self.inner = Some(to)
|
||||
}
|
||||
@@ -42,9 +39,3 @@ impl Wrapper {
|
||||
self.inner.replace(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Wrapper {
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
@@ -22,17 +22,17 @@ struct BranchesOnMeasurement {
|
||||
impl Widget for BranchesOnMeasurement {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
|
||||
let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut)));
|
||||
let top = UiSpan::new(Len::ZERO, cut).shifted_desc();
|
||||
let measured = painter
|
||||
.widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top])
|
||||
.widget_at(&self.probe, top.on_axis(Axis::Y))
|
||||
.len(Axis::X);
|
||||
let px = painter.to_px(measured.apply_leftover(), Axis::X);
|
||||
|
||||
let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y))));
|
||||
let place = [Place::Within(Part::All), below];
|
||||
let below = UiSpan::new(cut, painter.region_len(Axis::Y)).shifted_desc();
|
||||
let place = below.on_axis(Axis::Y);
|
||||
match px > Px::from_f32(self.threshold) {
|
||||
true => painter.widget_at(&self.wide, [None; 2], place),
|
||||
false => painter.widget_at(&self.narrow, [None; 2], place),
|
||||
true => painter.widget_at(&self.wide, place),
|
||||
false => painter.widget_at(&self.narrow, place),
|
||||
};
|
||||
Size::LEFTOVER
|
||||
}
|
||||
@@ -69,8 +69,8 @@ fn a_branch_taken_on_a_measurement_holds_across_repaints() {
|
||||
assert_ne!(first, (false, false), "threshold {threshold}: neither drew");
|
||||
|
||||
for frame in 0..4 {
|
||||
h.rsc.widgets_mut().get_dyn_mut(wide);
|
||||
h.rsc.widgets_mut().get_dyn_mut(narrow);
|
||||
h.rsc.widgets_mut().mark_for_redraw(wide);
|
||||
h.rsc.widgets_mut().mark_for_redraw(narrow);
|
||||
h.frame();
|
||||
assert_eq!(
|
||||
taken(&h, wide, narrow),
|
||||
@@ -88,7 +88,7 @@ fn a_branch_taken_on_a_measurement_is_the_one_a_cold_start_takes() {
|
||||
let (wide, narrow) = plant(&mut warm, threshold);
|
||||
warm.resize((640, 480));
|
||||
warm.frame();
|
||||
warm.rsc.widgets_mut().get_dyn_mut(wide);
|
||||
warm.rsc.widgets_mut().mark_for_redraw(wide);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 480));
|
||||
|
||||
@@ -18,7 +18,7 @@ fn a_wrapping_text_in_a_span_settles_on_one_width() {
|
||||
let r = h.region(&t.id()).unwrap();
|
||||
widths.push(r.bot_right.x - r.top_left.x);
|
||||
// Redrawing it changes nothing about the state, so nothing may move.
|
||||
h.rsc.widgets_mut().get_dyn_mut(t.id());
|
||||
h.rsc.widgets_mut().mark_for_redraw(t.id());
|
||||
h.frame();
|
||||
}
|
||||
println!("widths over six frames: {widths:?}");
|
||||
|
||||
+192
-9
@@ -1,5 +1,7 @@
|
||||
//! Where a frame puts things, with no window to put them in.
|
||||
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use iris::harness::{Harness, assert_corners};
|
||||
use iris::prelude::*;
|
||||
|
||||
@@ -108,7 +110,7 @@ const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the bo
|
||||
/// the icon's width, while a wrapping text beside it is asked in the room
|
||||
/// left, 900 - 24 - 32, and wraps there.
|
||||
#[test]
|
||||
fn padding_keeps_the_frame_distinct_from_the_room_left_in_a_row() {
|
||||
fn padding_keeps_the_rel_base_distinct_from_the_room_left_in_a_row() {
|
||||
let mut h = Harness::new((900, 200));
|
||||
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
|
||||
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
|
||||
@@ -124,8 +126,8 @@ fn padding_keeps_the_frame_distinct_from_the_room_left_in_a_row() {
|
||||
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
|
||||
let active = &h.render.active[&text.id()];
|
||||
let window = h.render.output_size().x;
|
||||
let asked = active.part.x.len().to_px(window);
|
||||
assert_eq!(active.frame.x.to_px(window), Px::from_int(868));
|
||||
let asked = active.region.x.len().to_px(window);
|
||||
assert_eq!(active.rel_base.x.to_px(window), Px::from_int(868));
|
||||
assert_eq!(asked, Px::from_int(844));
|
||||
}
|
||||
|
||||
@@ -155,7 +157,7 @@ fn a_share_inside_padding_fills_the_slot_it_was_given() {
|
||||
/// The same padding in a share instead: the slot is 450, so both the
|
||||
/// fraction and the wrap are the slot less the padding, and the two agree.
|
||||
#[test]
|
||||
fn padding_narrows_both_frame_and_box_inside_a_share() {
|
||||
fn padding_narrows_both_rel_base_and_box_inside_a_share() {
|
||||
let mut h = Harness::new((900, 200));
|
||||
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
|
||||
let padded = fill.pad(16).width(leftover(1)).add(&mut h.rsc);
|
||||
@@ -170,8 +172,8 @@ fn padding_narrows_both_frame_and_box_inside_a_share() {
|
||||
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
|
||||
let active = &h.render.active[&text.id()];
|
||||
let window = h.render.output_size().x;
|
||||
assert_eq!(active.frame.x.to_px(window), Px::from_int(418));
|
||||
assert_eq!(active.part.x.len().to_px(window), Px::from_int(418));
|
||||
assert_eq!(active.rel_base.x.to_px(window), Px::from_int(418));
|
||||
assert_eq!(active.region.x.len().to_px(window), Px::from_int(418));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -219,6 +221,134 @@ fn an_empty_widget_takes_a_share_of_a_span() {
|
||||
assert_corners!(h, right, (300, 0), (400, 200));
|
||||
}
|
||||
|
||||
/// A widget with a natural pixel size, like an image, which records the box
|
||||
/// it was asked in so a test can see which length decided it.
|
||||
struct NaturalSize {
|
||||
len: f32,
|
||||
asked: Rc<Cell<f32>>,
|
||||
}
|
||||
|
||||
impl Widget for NaturalSize {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.asked.set(painter.px_len(Axis::X).to_f32());
|
||||
Size::px(Vec2::new(self.len, self.len))
|
||||
}
|
||||
|
||||
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||
Some(LayoutLen::px(self.len))
|
||||
}
|
||||
}
|
||||
|
||||
/// A rule wins over what the widget says about itself, and a share is a rule:
|
||||
/// it is a length only to whoever divides one, and nobody here does, so the
|
||||
/// widget is asked in the whole box rather than in the size it asked for.
|
||||
#[test]
|
||||
fn a_share_rule_beats_the_widgets_own_pixel_size() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let asked = Rc::new(Cell::new(0.0));
|
||||
let natural = NaturalSize {
|
||||
len: 50.0,
|
||||
asked: asked.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(natural.wrapper());
|
||||
assert_eq!(asked.get(), 50.0, "its hint gives it its own size");
|
||||
|
||||
h.set_len(natural, Axis::X, LayoutLen::LEFTOVER);
|
||||
h.frame();
|
||||
|
||||
assert_eq!(asked.get(), 400.0, "the share is all of the box");
|
||||
}
|
||||
|
||||
/// Every box a widget is given comes of one ask, and the window is one of
|
||||
/// them: the root is asked in it exactly as a child is asked in its parent's
|
||||
/// box, so a rule of its own reads the same way at either place.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Asked {
|
||||
Root,
|
||||
Wrapped,
|
||||
InASpan,
|
||||
}
|
||||
|
||||
impl Asked {
|
||||
const ALL: [Self; 3] = [Self::Root, Self::Wrapped, Self::InASpan];
|
||||
|
||||
/// The width the probe is given under this parent, in a 400 px window.
|
||||
fn width(&self, rule: LayoutLen) -> Px {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_len(probe, Axis::X, rule);
|
||||
match self {
|
||||
Self::Root => h.set_root(probe),
|
||||
Self::Wrapped => h.set_root(probe.wrapper()),
|
||||
Self::InASpan => h.set_root((probe,).span(Dir::RIGHT)),
|
||||
}
|
||||
h.region(&probe).unwrap().size().x
|
||||
}
|
||||
}
|
||||
|
||||
/// A share with pixels or a fraction beside it is the longer of the two: it
|
||||
/// fills what they leave of the box and overflows the box where they are
|
||||
/// longer than it. A parent that divides nothing gives the same length as a
|
||||
/// span with one child, because in both there is nobody else to divide with --
|
||||
/// and so does the window, which divides nothing either.
|
||||
#[test]
|
||||
fn a_share_is_a_minimum_wherever_nothing_divides_it() {
|
||||
for (rule, want) in [
|
||||
(LayoutLen::LEFTOVER, 400),
|
||||
(LayoutLen::px(50) + LayoutLen::LEFTOVER, 400),
|
||||
(LayoutLen::px(500) + LayoutLen::LEFTOVER, 500),
|
||||
(LayoutLen::rel(0.5) + LayoutLen::LEFTOVER, 400),
|
||||
(LayoutLen::rel(2.0) + LayoutLen::LEFTOVER, 800),
|
||||
(LayoutLen::px(500), 500),
|
||||
] {
|
||||
let want = Px::from_int(want);
|
||||
for asked in Asked::ALL {
|
||||
assert_eq!(asked.width(rule), want, "{rule:?} asked {asked:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which of the two is longer is a question in pixels, so the box is decided
|
||||
/// again wherever the answer can change: a window that crosses the length the
|
||||
/// pixels ask for, and the rule itself crossing it while the window holds
|
||||
/// still. The first is a range the drawing holds for; the second cannot be
|
||||
/// seen in what the widget declares, since a share declares nothing either
|
||||
/// way, so it reaches the parent as a length only the parent can resolve.
|
||||
#[test]
|
||||
fn a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing() {
|
||||
// At the root as well as under a parent: the comparison is the same one,
|
||||
// and nothing above the root will make it again on its behalf, so the
|
||||
// range it holds for is the root's own.
|
||||
for wrapped in [false, true] {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_len(probe, Axis::X, LayoutLen::px(500) + LayoutLen::LEFTOVER);
|
||||
match wrapped {
|
||||
true => h.set_root(probe.wrapper()),
|
||||
false => h.set_root(probe),
|
||||
}
|
||||
let width = |h: &Harness| h.region(&probe).unwrap().size().x;
|
||||
assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
|
||||
|
||||
h.resize((900, 200));
|
||||
h.frame();
|
||||
assert_eq!(width(&h), Px::from_int(900), "wrapped: {wrapped}");
|
||||
|
||||
h.resize((400, 200));
|
||||
h.frame();
|
||||
assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
|
||||
|
||||
h.set_len(probe, Axis::X, LayoutLen::px(50) + LayoutLen::LEFTOVER);
|
||||
h.frame();
|
||||
assert_eq!(width(&h), Px::from_int(400), "wrapped: {wrapped}");
|
||||
|
||||
h.set_len(probe, Axis::X, LayoutLen::px(500) + LayoutLen::LEFTOVER);
|
||||
h.frame();
|
||||
assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_child_drawn_twice_moves_once() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
@@ -500,11 +630,11 @@ fn a_row_of_equal_shares_fills_it_exactly() {
|
||||
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
|
||||
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
|
||||
let active = &h.render.active[&id];
|
||||
let region = h.render.moves.resolve(active.move_idx, active.extent);
|
||||
let dim = h.size().axis(axis);
|
||||
let region = h.render.moves.resolve(active.move_idx, active.placement);
|
||||
let dim = h.size()[axis];
|
||||
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
|
||||
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
|
||||
let span = region.axis(axis);
|
||||
let span = region[axis];
|
||||
(edge(span.start), edge(span.end))
|
||||
}
|
||||
|
||||
@@ -821,3 +951,56 @@ fn a_root_with_a_fraction_rule_is_that_fraction_of_the_window() {
|
||||
h.set_root(root);
|
||||
assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_collapsed_share_keeps_the_gaps_before_the_next_slot() {
|
||||
for dir in [Dir::RIGHT, Dir::LEFT, Dir::DOWN, Dir::UP] {
|
||||
for collapsed in [1, 2] {
|
||||
let mut h = Harness::new((400, 400));
|
||||
let head = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_len(head, dir.axis, 200);
|
||||
let tail = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let tail_len = 200 - 10 * (collapsed + 1);
|
||||
h.set_len(tail, dir.axis, tail_len);
|
||||
let mut children: Vec<StrongWidget> = vec![head.add_strong(&mut h.rsc)];
|
||||
let mut shares = Vec::new();
|
||||
for _ in 0..collapsed {
|
||||
let share = rect(Color::GREEN).add(&mut h.rsc);
|
||||
shares.push(share);
|
||||
children.push(share.add_strong(&mut h.rsc));
|
||||
}
|
||||
children.push(tail.add_strong(&mut h.rsc));
|
||||
h.set_root(Span {
|
||||
children,
|
||||
dir,
|
||||
gap: Px::from_int(10),
|
||||
});
|
||||
for share in shares {
|
||||
assert!(h.region(&share).is_none());
|
||||
}
|
||||
let region = h.region(&tail).unwrap();
|
||||
let (from, to) = match dir.sign {
|
||||
Sign::Pos => (400 - tail_len, 400),
|
||||
Sign::Neg => (0, tail_len),
|
||||
};
|
||||
assert_eq!(region.top_left[dir.axis], Px::from_int(from));
|
||||
assert_eq!(region.bot_right[dir.axis], Px::from_int(to));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The root is asked the way any child is, so what it says about itself is
|
||||
/// read there too: a root that opted into a region node gets one, where the
|
||||
/// path it used to have ignored the flag.
|
||||
#[test]
|
||||
fn a_region_node_root_is_a_region_node() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||
let root = (probe,).span(Dir::RIGHT).region_node().add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(400));
|
||||
|
||||
h.resize((900, 200));
|
||||
h.frame();
|
||||
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900));
|
||||
}
|
||||
+44
-9
@@ -5,7 +5,9 @@
|
||||
//! 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 iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, grow, plan};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn some_edits(seed: u64, of: &Plan) -> Edits {
|
||||
@@ -25,11 +27,27 @@ fn some_edits(seed: u64, of: &Plan) -> Edits {
|
||||
Edits {
|
||||
sizes: pick(sized, &mut rng)
|
||||
.into_iter()
|
||||
.map(|i| (i, [Some(LayoutLen::LEFTOVER), None]))
|
||||
.map(|i| {
|
||||
(
|
||||
i,
|
||||
SizeRules {
|
||||
x: SizeRule::Exact(LayoutLen::LEFTOVER),
|
||||
y: SizeRule::Free,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
aligns: pick(aligned, &mut rng)
|
||||
.into_iter()
|
||||
.map(|i| (i, [Some(AxisAlign::POS), None]))
|
||||
.map(|i| {
|
||||
(
|
||||
i,
|
||||
Align {
|
||||
x: Some(AxisAlign::POS),
|
||||
y: None,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
nodes: pick(nodes, &mut rng)
|
||||
.into_iter()
|
||||
@@ -51,8 +69,6 @@ fn some_edits(seed: u64, of: &Plan) -> Edits {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -70,11 +86,13 @@ fn editing_a_plan_is_growing_one_with_those_edits() {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// No simplification is larger, which is the half of "the shrinker stops" a
|
||||
/// widget count can see. Most are not smaller either -- a dropped alignment
|
||||
/// and a simpler leaf both keep the count -- so what rules out circling is
|
||||
/// that those are one-way too: a `Some` becomes a `None`, and a kind steps
|
||||
/// down a ladder with no way back up.
|
||||
#[test]
|
||||
fn every_simplification_of_a_plan_is_smaller_than_it() {
|
||||
fn no_simplification_of_a_plan_is_larger_than_it() {
|
||||
for seed in 1..=60 {
|
||||
let tree = plan(seed, 4, &Edits::default());
|
||||
let mut queue = vec![tree];
|
||||
@@ -119,3 +137,20 @@ fn reducing_a_plan_all_the_way_ends() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every image in a tree is the same picture, and a handle is a reference to
|
||||
/// the texture rather than a copy of it, so one upload and one slot serve all
|
||||
/// of them however many a tree grows -- and the trees are grown in hundreds.
|
||||
#[test]
|
||||
fn a_tree_of_images_uploads_one_texture() {
|
||||
let mut images = 0;
|
||||
let mut tree = plan(1, 4, &Edits::default());
|
||||
tree.walk_mut(&mut |p| images += (p.kind == Kind::Image) as usize);
|
||||
assert!(images > 1, "a tree of {images} images tests nothing");
|
||||
|
||||
let mut h = Harness::new((900, 1200));
|
||||
let (root, _) = grow(&mut h.rsc, 1, 4, &Edits::default());
|
||||
h.state.root = Some(root);
|
||||
h.frame();
|
||||
assert_eq!(h.rsc.ui().textures.count(), 1);
|
||||
}
|
||||
+311
-90
@@ -45,7 +45,6 @@ fn counted(h: &mut Harness, size: Size, reads_box: bool) -> (WeakWidget<Counted>
|
||||
|
||||
struct Layered {
|
||||
children: [StrongWidget<Rect>; 2],
|
||||
_revision: usize,
|
||||
}
|
||||
|
||||
impl Widget for Layered {
|
||||
@@ -65,14 +64,10 @@ fn a_redrawn_layered_widget_keeps_the_layer_it_was_entered_on() {
|
||||
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);
|
||||
let root = Layered { children }.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
|
||||
h.rsc[root]._revision += 1;
|
||||
h.rsc.widgets_mut().mark_for_redraw(root.id());
|
||||
h.frame();
|
||||
|
||||
let label = h.rsc.widgets().label(root.id());
|
||||
@@ -125,7 +120,7 @@ fn moving_an_ordinary_subtree_remaps_its_mask() {
|
||||
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,)
|
||||
UiRegion::new(UiSpan::new(Len::px(150.0), Len::FULL), UiSpan::FULL,)
|
||||
);
|
||||
assert_corners!(h, inner, (150, 0), (400, 200));
|
||||
}
|
||||
@@ -181,9 +176,9 @@ fn a_repaint_that_keeps_its_size_does_not_relay_out() {
|
||||
h.set_root((first, second).span(Dir::RIGHT));
|
||||
let settled = draws.get();
|
||||
|
||||
// Taking mutable access is the ordinary content-change signal. This
|
||||
// widget returns the same size, so the parent has nothing to lay out.
|
||||
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id());
|
||||
// Marked with nothing about it changed, and it reports the same size
|
||||
// either way, so the parent has nothing to lay out.
|
||||
h.rsc.widgets_mut().mark_for_redraw(first.id());
|
||||
h.frame();
|
||||
|
||||
assert_eq!(draws.get(), settled + 1);
|
||||
@@ -198,7 +193,7 @@ fn a_span_child_survives_the_next_frame() {
|
||||
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
|
||||
h.set_root((top, bottom).span(Dir::DOWN));
|
||||
|
||||
h.rsc.widgets_mut().get_dyn_mut(top.id());
|
||||
h.rsc.widgets_mut().mark_for_redraw(top.id());
|
||||
h.frame();
|
||||
|
||||
assert_corners!(h, top, (0, 0), (400, 80));
|
||||
@@ -214,11 +209,7 @@ impl Widget for FromHint {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
|
||||
let top = UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, len.px));
|
||||
painter.widget_at(
|
||||
&self.inner,
|
||||
[None; 2],
|
||||
[Place::Within(Part::All), Place::Within(Part::From(top))],
|
||||
);
|
||||
painter.widget_at(&self.inner, top.shifted_desc().on_axis(Axis::Y));
|
||||
Size::LEFTOVER
|
||||
}
|
||||
}
|
||||
@@ -664,7 +655,7 @@ fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
|
||||
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.rsc.widgets_mut().mark_for_redraw(masked.id());
|
||||
h.frame();
|
||||
assert_corners!(h, inner, (100, 0), (400, 200));
|
||||
}
|
||||
@@ -778,6 +769,17 @@ fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Where a mask slot clips, in window pixels: the region it holds, carried
|
||||
/// through whatever move entry it hangs from. Taken by slot rather than by
|
||||
/// widget, so a test can name the slot it expects a redraw to keep.
|
||||
fn mask_bounds(h: &Harness, mask: MaskIdx) -> PixelRegion {
|
||||
let mask = &h.rsc.ui().masks[mask.idx()];
|
||||
h.render
|
||||
.moves
|
||||
.resolve(mask.move_idx, mask.region)
|
||||
.to_px(h.render.output_size())
|
||||
}
|
||||
|
||||
fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
|
||||
h.render.active[&id]
|
||||
.primitives
|
||||
@@ -797,7 +799,7 @@ fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_an_inherited_extent_keeps_the_original_measurement_offer() {
|
||||
fn changing_an_inherited_region_keeps_the_original_measurement_offer() {
|
||||
fn build(h: &mut Harness, width: i32, text: &str) -> (WeakWidget<Text>, WeakWidget<Rect>) {
|
||||
let first = rect(Color::RED).width(width).add(&mut h.rsc);
|
||||
let words = wtext(text).size(20).wrap(true).add(&mut h.rsc);
|
||||
@@ -877,11 +879,7 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
painter.widget_at(
|
||||
&self.child,
|
||||
[None; 2],
|
||||
[
|
||||
Place::Within(Part::From(self.region.x)),
|
||||
Place::Within(Part::From(self.region.y)),
|
||||
],
|
||||
PlaceDesc::new(self.region.x.shifted_desc(), self.region.y.shifted_desc()),
|
||||
);
|
||||
Size::LEFTOVER
|
||||
}
|
||||
@@ -927,15 +925,10 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
|
||||
primitive_bounds(&warm, leaf.id()),
|
||||
primitive_bounds(&cold, other.id())
|
||||
);
|
||||
let mask = |h: &Harness, id: WidgetId| {
|
||||
let active = &h.render.active[&id];
|
||||
let mask = &h.rsc.ui().masks[active.mask.idx()];
|
||||
h.render
|
||||
.moves
|
||||
.resolve(mask.move_idx, mask.region)
|
||||
.to_px(h.render.output_size())
|
||||
};
|
||||
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
|
||||
assert_eq!(
|
||||
mask_bounds(&warm, warm.render.active[&leaf.id()].mask),
|
||||
mask_bounds(&cold, cold.render.active[&other.id()].mask)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -959,18 +952,18 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
|
||||
}
|
||||
struct Frame {
|
||||
child: StrongWidget,
|
||||
frame: UiRegion,
|
||||
region: UiRegion,
|
||||
extent: UiRegion,
|
||||
}
|
||||
impl Widget for Frame {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
painter.widget_at(
|
||||
&self.child,
|
||||
[Some(self.region.x.len()), None],
|
||||
[
|
||||
Place::Fill(Part::From(self.extent.x)),
|
||||
Place::Fill(Part::From(self.extent.y)),
|
||||
],
|
||||
PlaceDesc::new(
|
||||
self.region.x.shifted_desc().fills(),
|
||||
self.region.y.shifted_desc().fills(),
|
||||
)
|
||||
.rel_base(Axis::X, self.frame.x.len()),
|
||||
);
|
||||
Size::LEFTOVER
|
||||
}
|
||||
@@ -986,15 +979,15 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
|
||||
h.rsc.widgets_mut().set_region_node(text, node);
|
||||
let root = Frame {
|
||||
child: text.add_strong(&mut h.rsc),
|
||||
frame: UiRegion::FULL,
|
||||
region: UiRegion::FULL,
|
||||
extent: UiRegion::FULL,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
for (start, end) in [(0.13, 0.83), (-0.17, 1.23), (0.31, 0.67)] {
|
||||
let before = draws.get();
|
||||
h.rsc[root].region.x = UiSpan::new(Len::px(13.125), Len::px(287.375));
|
||||
h.rsc[root].extent = UiRegion::new(
|
||||
h.rsc[root].frame.x = UiSpan::new(Len::px(13.125), Len::px(287.375));
|
||||
h.rsc[root].region = UiRegion::new(
|
||||
UiSpan::new(Len::rel(start), Len::rel(end)),
|
||||
UiSpan::new(Len::px(7.25), Len::rel(end)),
|
||||
);
|
||||
@@ -1002,7 +995,7 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
|
||||
assert_eq!(draws.get(), before);
|
||||
let retained = primitive_bounds(&h, text.id());
|
||||
assert!(!retained.is_empty());
|
||||
let _ = h.rsc.widgets_mut().get_dyn_mut(text.id());
|
||||
h.rsc.widgets_mut().mark_for_redraw(text.id());
|
||||
h.frame();
|
||||
assert!(draws.get() > before);
|
||||
assert_eq!(retained, primitive_bounds(&h, text.id()));
|
||||
@@ -1117,7 +1110,7 @@ fn widening_and_restoring_a_contract_does_not_invalidate_its_reader() {
|
||||
assert_eq!(leaf_draws.get(), settled + 1);
|
||||
}
|
||||
#[test]
|
||||
fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
|
||||
fn padding_and_stack_boxes_follow_the_region_without_drawing_again() {
|
||||
struct Observed<W> {
|
||||
widget: W,
|
||||
draws: Rc<Cell<usize>>,
|
||||
@@ -1130,23 +1123,22 @@ fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
|
||||
}
|
||||
struct Frame {
|
||||
child: StrongWidget,
|
||||
extent: UiRegion,
|
||||
region: UiRegion,
|
||||
}
|
||||
impl Widget for Frame {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
painter.widget_at(
|
||||
&self.child,
|
||||
[None; 2],
|
||||
[
|
||||
Place::Fill(Part::From(self.extent.x)),
|
||||
Place::Fill(Part::From(self.extent.y)),
|
||||
],
|
||||
PlaceDesc::new(
|
||||
self.region.x.shifted_desc().fills(),
|
||||
self.region.y.shifted_desc().fills(),
|
||||
),
|
||||
);
|
||||
Size::LEFTOVER
|
||||
}
|
||||
}
|
||||
for node in [false, true] {
|
||||
let plant = |h: &mut Harness, extent| {
|
||||
let plant = |h: &mut Harness, region| {
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let leaf = rect(Color::BLUE).masked().add(&mut h.rsc);
|
||||
h.rsc.widgets_mut().set_region_node(leaf, node);
|
||||
@@ -1167,7 +1159,7 @@ fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
|
||||
draws: draws.clone(),
|
||||
}
|
||||
.add_strong(&mut h.rsc);
|
||||
let root = Frame { child: pad, extent }.add(&mut h.rsc);
|
||||
let root = Frame { child: pad, region }.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
(root, leaf, fixed, draws)
|
||||
};
|
||||
@@ -1182,32 +1174,27 @@ fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
|
||||
let mut warm = Harness::new((403, 211));
|
||||
let (root, leaf, fixed, draws) = plant(&mut warm, at(0.13));
|
||||
for start in [0.13, -0.17, 0.31] {
|
||||
let extent = at(start);
|
||||
let region = at(start);
|
||||
let before = draws.get();
|
||||
warm.rsc[root].extent = extent;
|
||||
warm.rsc[root].region = region;
|
||||
warm.frame();
|
||||
assert_eq!(draws.get(), before);
|
||||
let mut cold = Harness::new((403, 211));
|
||||
let (_, other, other_fixed, _) = plant(&mut cold, extent);
|
||||
let (_, other, other_fixed, _) = plant(&mut cold, region);
|
||||
for (a, b) in [(leaf.id(), other.id()), (fixed.id(), other_fixed.id())] {
|
||||
assert_eq!(warm.region(&a), cold.region(&b));
|
||||
assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b));
|
||||
}
|
||||
let mask = |h: &Harness, id: WidgetId| {
|
||||
let active = &h.render.active[&id];
|
||||
let mask = &h.rsc.ui().masks[active.mask.idx()];
|
||||
h.render
|
||||
.moves
|
||||
.resolve(mask.move_idx, mask.region)
|
||||
.to_px(h.render.output_size())
|
||||
};
|
||||
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
|
||||
assert_eq!(
|
||||
mask_bounds(&warm, warm.render.active[&leaf.id()].mask),
|
||||
mask_bounds(&cold, cold.render.active[&other.id()].mask)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
|
||||
fn moving_a_childs_region_preserves_the_slot_chosen_from_its_measurement() {
|
||||
struct Measured;
|
||||
impl Widget for Measured {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
@@ -1224,14 +1211,12 @@ fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
painter.widget_at(
|
||||
&self.child,
|
||||
[None; 2],
|
||||
[
|
||||
Place::Fill(Part::From(UiSpan::new(
|
||||
Len::px(self.start),
|
||||
Len::px(self.start + 200.0),
|
||||
))),
|
||||
Place::Fill(Part::From(UiSpan::FULL)),
|
||||
],
|
||||
PlaceDesc::new(
|
||||
UiSpan::new(Len::px(self.start), Len::px(self.start + 200.0))
|
||||
.shifted_desc()
|
||||
.fills(),
|
||||
UiSpan::FULL.shifted_desc().fills(),
|
||||
),
|
||||
);
|
||||
Size::LEFTOVER
|
||||
}
|
||||
@@ -1256,7 +1241,7 @@ fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
|
||||
fn changing_regions_keep_fractional_reports_and_numeric_dependencies_valid() {
|
||||
struct Container {
|
||||
child: StrongWidget,
|
||||
region: UiRegion,
|
||||
@@ -1266,18 +1251,14 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
|
||||
painter
|
||||
.widget_at(
|
||||
&self.child,
|
||||
[None; 2],
|
||||
[
|
||||
Place::Within(Part::From(self.region.x)),
|
||||
Place::Within(Part::From(self.region.y)),
|
||||
],
|
||||
PlaceDesc::new(self.region.x.shifted_desc(), self.region.y.shifted_desc()),
|
||||
)
|
||||
.size()
|
||||
}
|
||||
}
|
||||
struct Frame {
|
||||
child: StrongWidget,
|
||||
extent: UiRegion,
|
||||
region: UiRegion,
|
||||
answer: Rc<Cell<Size>>,
|
||||
}
|
||||
impl Widget for Frame {
|
||||
@@ -1286,11 +1267,10 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
|
||||
painter
|
||||
.widget_at(
|
||||
&self.child,
|
||||
[None; 2],
|
||||
[
|
||||
Place::Fill(Part::From(self.extent.x)),
|
||||
Place::Fill(Part::From(self.extent.y)),
|
||||
],
|
||||
PlaceDesc::new(
|
||||
self.region.x.shifted_desc().fills(),
|
||||
self.region.y.shifted_desc().fills(),
|
||||
),
|
||||
)
|
||||
.size(),
|
||||
);
|
||||
@@ -1302,7 +1282,7 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
|
||||
UiRegion::FULL,
|
||||
UiRegion::new(UiSpan::new(Len::rel(0.13), Len::rel(0.79)), UiSpan::FULL),
|
||||
] {
|
||||
let plant = |h: &mut Harness, extent| {
|
||||
let plant = |h: &mut Harness, outer| {
|
||||
let size = if fractional {
|
||||
Size {
|
||||
x: rel(0.5),
|
||||
@@ -1320,7 +1300,7 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
|
||||
let answer = Rc::new(Cell::new(Size::ZERO));
|
||||
let root = Frame {
|
||||
child,
|
||||
extent,
|
||||
region: outer,
|
||||
answer: answer.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
@@ -1330,15 +1310,256 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
|
||||
let mut warm = Harness::new((403, 211));
|
||||
let (root, leaf, answer) = plant(&mut warm, UiRegion::FULL);
|
||||
for width in [191.125, 297.25, 83.75] {
|
||||
let extent =
|
||||
let region =
|
||||
UiRegion::new(UiSpan::new(Len::px(13.125), Len::px(width)), UiSpan::FULL);
|
||||
warm.rsc[root].extent = extent;
|
||||
warm.rsc[root].region = region;
|
||||
warm.frame();
|
||||
let mut cold = Harness::new((403, 211));
|
||||
let (_, other, other_answer) = plant(&mut cold, extent);
|
||||
let (_, other, other_answer) = plant(&mut cold, region);
|
||||
assert_eq!(answer.get(), other_answer.get());
|
||||
assert_eq!(warm.region(&leaf), cold.region(&other));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OptionalMask {
|
||||
inner: StrongWidget,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl Widget for OptionalMask {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
if self.enabled {
|
||||
painter.set_mask(UiRegion::FULL);
|
||||
}
|
||||
painter.widget(&self.inner);
|
||||
Size::LEFTOVER
|
||||
}
|
||||
}
|
||||
|
||||
fn primitive_masks(h: &Harness, id: WidgetId) -> Vec<MaskIdx> {
|
||||
h.render.active[&id]
|
||||
.primitives
|
||||
.iter()
|
||||
.map(|primitive| {
|
||||
let handle = &primitive.handle;
|
||||
h.render.layers[handle.layer].primitives()[handle.kind as usize]
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.instances()[handle.inst_idx]
|
||||
.mask_idx
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_redrawn_mask_keeps_reused_primitives_clipped_when_it_moves() {
|
||||
for node in [false, true] {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let first = rect(Color::RED).height(50).add(&mut h.rsc);
|
||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let child = Stretchy {
|
||||
inner: inner.add_strong(&mut h.rsc),
|
||||
draws: draws.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let masked = child.masked().add(&mut h.rsc);
|
||||
h.rsc.widgets_mut().set_region_node(masked, node);
|
||||
h.set_root((first, masked).span(Dir::DOWN));
|
||||
let mask = h.render.active[&masked.id()].mask;
|
||||
let settled = draws.get();
|
||||
h.rsc.widgets_mut().mark_for_redraw(masked.id());
|
||||
h.frame();
|
||||
assert_eq!(primitive_masks(&h, inner.id()), vec![mask]);
|
||||
assert_eq!(draws.get(), settled, "a mask repaint must reuse its child");
|
||||
assert_eq!(h.render.active[&masked.id()].mask, mask);
|
||||
h.set_len(first, Axis::Y, 10);
|
||||
h.frame();
|
||||
assert_eq!(mask_bounds(&h, mask), h.region(&masked).unwrap());
|
||||
assert_corners!(h, inner, (0, 10), (400, 200));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_and_removing_a_mask_updates_existing_primitives() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let masked = OptionalMask {
|
||||
inner: inner.add_strong(&mut h.rsc),
|
||||
enabled: false,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(masked);
|
||||
for enabled in [true, false, true, false] {
|
||||
h.rsc[masked].enabled = enabled;
|
||||
h.frame();
|
||||
let mask = h.render.active[&masked.id()].mask;
|
||||
assert_eq!(mask == MaskIdx::NONE, !enabled);
|
||||
assert_eq!(primitive_masks(&h, inner.id()), vec![mask]);
|
||||
}
|
||||
assert_eq!(h.rsc.ui().masks.len(), 1, "retired slots must be reusable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_masks_slot_is_released_when_the_mask_is_removed_or_undrawn() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (inner, _) = counted(&mut h, Size::LEFTOVER, false);
|
||||
let masked = OptionalMask {
|
||||
inner: inner.add_strong(&mut h.rsc),
|
||||
enabled: true,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let row = (masked,).span(Dir::DOWN).add(&mut h.rsc);
|
||||
h.set_root(row);
|
||||
for _ in 0..3 {
|
||||
h.rsc[masked].enabled = false;
|
||||
h.frame();
|
||||
h.rsc[masked].enabled = true;
|
||||
h.frame();
|
||||
let child = h.rsc[row].pop().unwrap();
|
||||
h.frame();
|
||||
h.rsc[row].push(child);
|
||||
h.frame();
|
||||
}
|
||||
assert_eq!(h.rsc.ui().masks.len(), 1);
|
||||
}
|
||||
|
||||
struct SharedChild(Rc<StrongWidget>);
|
||||
|
||||
impl Widget for SharedChild {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
painter.widget(self.0.as_ref()).size()
|
||||
}
|
||||
}
|
||||
|
||||
struct SwitchParent {
|
||||
choices: [StrongWidget; 2],
|
||||
choice: usize,
|
||||
}
|
||||
|
||||
impl Widget for SwitchParent {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
painter.widget(&self.choices[self.choice]).size()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_redrawn_subtree_is_not_undrawn_by_the_parent_it_left() {
|
||||
for node in [false, true] {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let leaf = rect(Color::RED).width(40).add(&mut h.rsc);
|
||||
let held: StrongWidget = leaf.add_strong(&mut h.rsc);
|
||||
let shared = Rc::new(held);
|
||||
let first = SharedChild(shared.clone()).add_strong(&mut h.rsc);
|
||||
let second = SharedChild(shared).add_strong(&mut h.rsc);
|
||||
h.rsc.widgets_mut().set_region_node(&second, node);
|
||||
let root = SwitchParent {
|
||||
choices: [first, second],
|
||||
choice: 0,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
let before = h.region(&leaf);
|
||||
h.rsc[root].choice = 1;
|
||||
h.frame();
|
||||
assert_eq!(h.region(&leaf), before);
|
||||
}
|
||||
}
|
||||
|
||||
/// A leaf that reports less than the box it is given and states which lengths
|
||||
/// of that box its drawing holds for, so a test can widen the contract
|
||||
/// without changing the answer. It counts its draws, since what a kept
|
||||
/// contract costs is whether the parent has to make it draw again.
|
||||
struct Contracted {
|
||||
holds: std::ops::RangeInclusive<Px>,
|
||||
size: Size,
|
||||
draws: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Widget for Contracted {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.draws.set(self.draws.get() + 1);
|
||||
painter.holds(Axis::X, self.holds.clone());
|
||||
self.size
|
||||
}
|
||||
}
|
||||
|
||||
struct CountedParent {
|
||||
inner: StrongWidget,
|
||||
draws: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Widget for CountedParent {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.draws.set(self.draws.get() + 1);
|
||||
painter.widget(&self.inner).size()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widening_what_a_drawing_holds_for_does_not_relay_out_the_parent() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let child = Contracted {
|
||||
holds: Px::from_int(300)..=Px::from_int(500),
|
||||
size: Size::from((100, 200)),
|
||||
draws: Rc::new(Cell::new(0)),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let root = CountedParent {
|
||||
inner: child.upgrade(&mut h.rsc),
|
||||
draws: draws.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
let settled = draws.get();
|
||||
|
||||
// The same answer, good for more boxes than before, so the guarantee the
|
||||
// parent kept still holds.
|
||||
h.rsc[child].holds = Px::from_int(200)..=Px::from_int(600);
|
||||
h.frame();
|
||||
|
||||
assert_eq!(
|
||||
draws.get(),
|
||||
settled,
|
||||
"a wider contract for the same answer is not a change to lay out"
|
||||
);
|
||||
}
|
||||
|
||||
/// The other half of the rule above: a kept contract is the narrower one, so
|
||||
/// it is only worth keeping where it still holds. A window the old range is
|
||||
/// outside is not one its parent can be handed back, and keeping it there
|
||||
/// throws away the drawing the widget just made.
|
||||
#[test]
|
||||
fn a_contract_this_window_is_outside_is_not_kept() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let leaf_draws = Rc::new(Cell::new(0));
|
||||
let child = Contracted {
|
||||
holds: Px::from_int(300)..=Px::from_int(500),
|
||||
size: Size::from((100, 200)),
|
||||
draws: leaf_draws.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let root = CountedParent {
|
||||
inner: child.upgrade(&mut h.rsc),
|
||||
draws: Rc::new(Cell::new(0)),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
|
||||
// Wide enough that the old contract leaves the new box out, and the leaf
|
||||
// is marked in the same frame -- so it settles itself first and its
|
||||
// parent draws afterwards, asking about what it settled.
|
||||
h.resize((600, 200));
|
||||
h.rsc[child].holds = Px::from_int(200)..=Px::from_int(700);
|
||||
let settled = leaf_draws.get();
|
||||
h.frame();
|
||||
|
||||
assert_eq!(
|
||||
leaf_draws.get(),
|
||||
settled + 1,
|
||||
"the leaf settled once and its parent kept what it settled"
|
||||
);
|
||||
}
|
||||
@@ -113,7 +113,7 @@ fn wrapping_content_beside_a_fixed_length_is_stable_warm_and_cold() {
|
||||
|
||||
let mut warm = Harness::new((900, 300));
|
||||
let (text, content) = plant(&mut warm);
|
||||
warm.rsc.widgets_mut().get_dyn_mut(text);
|
||||
warm.rsc.widgets_mut().mark_for_redraw(text);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 300));
|
||||
@@ -142,8 +142,8 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
|
||||
let mut h = Harness::new((100, 100));
|
||||
let tall = rect(Color::RED).height(400).add_strong(&mut h.rsc);
|
||||
let clipper = Clipper(tall).add(&mut h.rsc);
|
||||
// `set_root` lays the tree out, so this is where it is caught.
|
||||
h.set_root(clipper);
|
||||
h.frame();
|
||||
}
|
||||
|
||||
/// Content that fits sits in the viewport, not in a box of the window's
|
||||
|
||||
+88
-102
@@ -9,17 +9,45 @@
|
||||
//! reached through a region node's own entry rather than through the offer
|
||||
//! that node was given. The last is a wrapping text handed back the width
|
||||
//! it measured, rounded to a step below the line it measured there.
|
||||
//!
|
||||
//! Each says which seed it was shrunk from, of the generator as it stood when
|
||||
//! it was found. Those numbers no longer grow those trees -- a seed names one
|
||||
//! only while the generator draws the same things in the same order, and the
|
||||
//! leaves have grown an image since -- so what is written out below is the
|
||||
//! record of the case, and the seed is where it came from.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use iris::random::Branch;
|
||||
|
||||
/// Every widget in the same place warm as cold, reported all at once: which
|
||||
/// of a dozen boxes moved is the whole of what a shrunk case has to say.
|
||||
///
|
||||
/// A list that names one widget twice is an error rather than a redundant
|
||||
/// check. `width`, `sized` and `align` give back the widget they were handed,
|
||||
/// so a fixture built through them can name one text three times, and then a
|
||||
/// case comparing six boxes compares four and says nothing about it. One
|
||||
/// fixture builds both lists, so checking the warm one checks both.
|
||||
#[track_caller]
|
||||
fn assert_same_regions(
|
||||
warm: &Harness,
|
||||
warm_ids: &[WidgetId],
|
||||
cold: &Harness,
|
||||
cold_ids: &[WidgetId],
|
||||
) {
|
||||
assert_eq!(
|
||||
warm_ids.len(),
|
||||
cold_ids.len(),
|
||||
"the warm and cold fixtures list different widgets"
|
||||
);
|
||||
let named: HashSet<&WidgetId> = warm_ids.iter().collect();
|
||||
assert_eq!(
|
||||
named.len(),
|
||||
warm_ids.len(),
|
||||
"a widget is listed twice: {warm_ids:?}"
|
||||
);
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in warm_ids.iter().zip(cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
@@ -92,7 +120,7 @@ fn repainting_a_stack_uses_the_box_its_sizing_child_decided() {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let ids = plant_stack_in_its_sizing_childs_box(&mut warm);
|
||||
for &id in &ids {
|
||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||
warm.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
warm.frame();
|
||||
|
||||
@@ -188,36 +216,31 @@ fn reordering_nested_spans_keeps_the_answer_from_the_decided_box() {
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
||||
/// Four widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
||||
/// the tree changes -- every widget is marked for redraw and the frame is
|
||||
/// taken again -- so no box may move, and a warm frame has to land where a
|
||||
/// cold one does.
|
||||
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
||||
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
||||
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
||||
let sized = wrapped.width(76).add(&mut h.rsc);
|
||||
let aligned = sized;
|
||||
let wrapped = wtext("Wrapping shapes")
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.width(76)
|
||||
.add(&mut h.rsc);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(sized, Axis::X, AxisAlign::POS);
|
||||
.set_alignment(wrapped, Axis::X, AxisAlign::POS);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(sized, Axis::Y, AxisAlign::POS);
|
||||
.set_alignment(wrapped, Axis::Y, AxisAlign::POS);
|
||||
let stack = Stack {
|
||||
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
||||
children: vec![plain.add_strong(&mut h.rsc), wrapped.add_strong(&mut h.rsc)],
|
||||
size: StackSize::Child(0),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
vec![
|
||||
plain.id(),
|
||||
wrapped.id(),
|
||||
sized.id(),
|
||||
aligned.id(),
|
||||
stack.id(),
|
||||
root.id(),
|
||||
]
|
||||
vec![plain.id(), wrapped.id(), stack.id(), root.id()]
|
||||
}
|
||||
|
||||
/// The first frame does not reach the layout a second one does, so "cold" is
|
||||
@@ -230,7 +253,7 @@ fn one_frame_is_enough() {
|
||||
let first = h.region(&ids[1]).unwrap();
|
||||
for _ in 0..3 {
|
||||
for &id in &ids {
|
||||
h.rsc.widgets_mut().get_dyn_mut(id);
|
||||
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
h.frame();
|
||||
}
|
||||
@@ -252,46 +275,30 @@ fn repainting_everything_moves_nothing() {
|
||||
let mut warm = Harness::new((640, 900));
|
||||
let ids = plant(&mut warm);
|
||||
for &id in &ids {
|
||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||
warm.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let cold_ids = plant(&mut cold);
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box
|
||||
/// Four widgets, shrunk from 905. Everything inside the declared 189x176 box
|
||||
/// is the same size whatever the output is, so a resize may not change any of
|
||||
/// it -- but the text comes out 3.92px narrower warm than cold.
|
||||
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
||||
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
||||
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||
let aligned = text;
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(text, Axis::X, AxisAlign::NEG);
|
||||
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let sized = inner.sized((189, 176)).add(&mut h.rsc);
|
||||
let inner = (text,).span(Dir::RIGHT).sized((189, 176)).add(&mut h.rsc);
|
||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let root = (filler, inner).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||
vec![
|
||||
text.id(),
|
||||
aligned.id(),
|
||||
inner.id(),
|
||||
sized.id(),
|
||||
filler.id(),
|
||||
root.id(),
|
||||
]
|
||||
vec![text.id(), inner.id(), filler.id(), root.id()]
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -306,17 +313,10 @@ fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
|
||||
let cold_ids = plant_fixed(&mut cold);
|
||||
cold.frame();
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by
|
||||
/// Three widgets, shrunk from 486. A span's two children are swapped: warm by
|
||||
/// moving them, cold by growing them that way. Same widgets, same sizes, one
|
||||
/// ends up 29.9px from where the other does.
|
||||
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
|
||||
@@ -340,16 +340,11 @@ fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span
|
||||
gap: Px::ZERO,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let span_handle = span;
|
||||
let aligned = span;
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(span, Axis::X, AxisAlign::CENTER);
|
||||
h.state.root = Some(aligned.add_strong(&mut h.rsc));
|
||||
(
|
||||
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
|
||||
span_handle,
|
||||
)
|
||||
h.state.root = Some(span.add_strong(&mut h.rsc));
|
||||
(vec![wrapped.id(), plain.id(), span.id()], span)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -364,17 +359,10 @@ fn swapping_two_children_lands_where_growing_them_that_way_does() {
|
||||
let (cold_ids, _) = plant_pair(&mut cold, true);
|
||||
cold.frame();
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its
|
||||
/// Seven widgets, shrunk from 80. The scroll decides how wide to make its
|
||||
/// content from what the content says, and hands that box down through a
|
||||
/// pass-through; the span under it was given that box once, so nothing at its
|
||||
/// own edge says the box was its own answer.
|
||||
@@ -393,8 +381,7 @@ fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
|
||||
gap: Px::ZERO,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let block = rect(Color::RED).add(&mut h.rsc);
|
||||
let fixed = block.width(87).add(&mut h.rsc);
|
||||
let fixed = rect(Color::RED).width(87).add(&mut h.rsc);
|
||||
let mut outer_children: Vec<StrongWidget> =
|
||||
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
|
||||
if swapped {
|
||||
@@ -416,7 +403,6 @@ fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
|
||||
text.id(),
|
||||
filler.id(),
|
||||
inner.id(),
|
||||
block.id(),
|
||||
fixed.id(),
|
||||
outer.id(),
|
||||
through.id(),
|
||||
@@ -440,14 +426,7 @@ fn a_span_given_the_box_its_answer_decided_matches_a_cold_layout() {
|
||||
let (cold_ids, _) = plant_scrolled(&mut cold, true);
|
||||
cold.frame();
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Reports a width derived from the box it is asked in. Reading through the
|
||||
@@ -471,11 +450,10 @@ impl Widget for Wider {
|
||||
fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) {
|
||||
let content = Wider { extra }.add(&mut h.rsc);
|
||||
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||
let root = scroll;
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(scroll, Axis::X, AxisAlign::NEG);
|
||||
h.set_root(root);
|
||||
h.set_root(scroll);
|
||||
(content, scroll.id())
|
||||
}
|
||||
|
||||
@@ -567,14 +545,7 @@ fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() {
|
||||
let (cold_ids, _) = plant_boundary(&mut cold, true);
|
||||
cold.frame();
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed
|
||||
@@ -616,20 +587,13 @@ fn plant_nested_scrolls(h: &mut Harness) -> Vec<WidgetId> {
|
||||
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.rsc.widgets_mut().mark_for_redraw(ids[0]);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let cold_ids = plant_nested_scrolls(&mut cold);
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220
|
||||
@@ -720,14 +684,7 @@ fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
|
||||
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"));
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \
|
||||
@@ -1067,3 +1024,32 @@ fn resizing_under_a_short_scroll_snaps_its_window_tall_content_again() {
|
||||
|
||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||
}
|
||||
|
||||
/// A scroll clamps its position against the box it is drawn in, so drawing it
|
||||
/// once at one viewport and again at another writes state the second draw then
|
||||
/// reads. That the answer is still the one a cold layout gives is a property
|
||||
/// of the clamp, not something the layout enforces.
|
||||
#[test]
|
||||
fn a_scrolled_view_resized_lands_where_a_cold_layout_puts_it() {
|
||||
for amt in [10.0, 40.0, 90.0, 140.0] {
|
||||
let mut warm = Harness::new((100, 100));
|
||||
let (_, warm_scroll) = plant_wider(&mut warm, 100.0);
|
||||
warm.move_to((50.0, 50.0));
|
||||
warm.scroll((-amt, 0.0));
|
||||
warm.frame();
|
||||
warm.resize((160, 100));
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((160, 100));
|
||||
let (_, cold_scroll) = plant_wider(&mut cold, 100.0);
|
||||
cold.move_to((50.0, 50.0));
|
||||
cold.scroll((-amt, 0.0));
|
||||
cold.frame();
|
||||
|
||||
assert_eq!(
|
||||
warm.region(&warm_scroll),
|
||||
cold.region(&cold_scroll),
|
||||
"scrolled by {amt} then widened"
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-29
@@ -11,8 +11,6 @@
|
||||
//! The instances are two pixels wide so that vertex work dominates; a chain
|
||||
//! walk that does not show up against small quads will not show up against
|
||||
//! anything.
|
||||
//!
|
||||
//! The instance is leaked deliberately, for the reason `draw_cost.rs` gives.
|
||||
|
||||
use iris::prelude::*;
|
||||
use iris_core::{
|
||||
@@ -21,6 +19,9 @@ use iris_core::{
|
||||
};
|
||||
use wgpu::{Color as GpuColor, *};
|
||||
|
||||
#[path = "gpu/mod.rs"]
|
||||
mod gpu;
|
||||
|
||||
const SIZE: u32 = 1024;
|
||||
const INSTANCES: usize = 200_000;
|
||||
const FRAMES: u32 = 20;
|
||||
@@ -29,18 +30,7 @@ const FRAMES: u32 = 20;
|
||||
const BATCHES: u32 = 8;
|
||||
|
||||
fn gpu() -> Option<(Device, Queue, f32)> {
|
||||
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
|
||||
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
||||
{
|
||||
Ok(_) => all,
|
||||
Err(_) => Instance::new(InstanceDescriptor {
|
||||
backends: Backends::GL,
|
||||
..InstanceDescriptor::new_without_display_handle()
|
||||
}),
|
||||
};
|
||||
let instance: &'static Instance = Box::leak(Box::new(instance));
|
||||
let adapter =
|
||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
|
||||
let adapter = gpu::adapter()?;
|
||||
if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
|
||||
println!("no timestamp queries on {:?}", adapter.get_info().name);
|
||||
return None;
|
||||
@@ -55,20 +45,6 @@ fn gpu() -> Option<(Device, Queue, f32)> {
|
||||
Some((device, queue, period))
|
||||
}
|
||||
|
||||
fn config(format: TextureFormat) -> SurfaceConfiguration {
|
||||
SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format,
|
||||
color_space: SurfaceColorSpace::Auto,
|
||||
width: SIZE,
|
||||
height: SIZE,
|
||||
present_mode: PresentMode::Fifo,
|
||||
desired_maximum_frame_latency: 2,
|
||||
alpha_mode: CompositeAlphaMode::Auto,
|
||||
view_formats: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// A chain `depth` slots long, and instances that all resolve through its end.
|
||||
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
||||
let kind = ui.primitives.kind::<RectPrimitive>();
|
||||
@@ -103,7 +79,7 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
||||
/// Nanoseconds the pass took on the GPU, best of `BATCHES`.
|
||||
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
|
||||
let format = TextureFormat::Bgra8Unorm;
|
||||
let mut node = UiRenderNode::new(device, &config(format));
|
||||
let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
|
||||
let mut ui = UiData::default();
|
||||
let mut render = UiRenderState::new();
|
||||
fill(&mut ui, &mut render, depth);
|
||||
|
||||
+5
-34
@@ -13,10 +13,6 @@
|
||||
//! That is how `PrimitiveRender` was measured against a match in the renderer:
|
||||
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording
|
||||
//! one.
|
||||
//!
|
||||
//! The instance is leaked deliberately. A Vulkan loader may unload the driver
|
||||
//! when the last one drops, which can fault as a thread that used it exits --
|
||||
//! and every test runs on a spawned thread.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -27,6 +23,9 @@ use iris_core::{
|
||||
};
|
||||
use wgpu::{Color as GpuColor, *};
|
||||
|
||||
#[path = "gpu/mod.rs"]
|
||||
mod gpu;
|
||||
|
||||
const SIZE: u32 = 1024;
|
||||
const FRAMES: u32 = 200;
|
||||
/// Reported as the best of this many batches, since the mean moves by more
|
||||
@@ -34,39 +33,11 @@ const FRAMES: u32 = 200;
|
||||
const BATCHES: u32 = 8;
|
||||
|
||||
fn gpu() -> Option<(Device, Queue)> {
|
||||
// Probed rather than assumed: there may be no Vulkan adapter, and GL is
|
||||
// what is left when there is not.
|
||||
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
|
||||
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
||||
{
|
||||
Ok(_) => all,
|
||||
Err(_) => Instance::new(InstanceDescriptor {
|
||||
backends: Backends::GL,
|
||||
..InstanceDescriptor::new_without_display_handle()
|
||||
}),
|
||||
};
|
||||
// Leaked rather than dropped: see the note at the top of the file.
|
||||
let instance: &'static Instance = Box::leak(Box::new(instance));
|
||||
let adapter =
|
||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
|
||||
let adapter = gpu::adapter()?;
|
||||
println!("adapter: {:?}", adapter.get_info());
|
||||
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
|
||||
}
|
||||
|
||||
fn config(format: TextureFormat) -> SurfaceConfiguration {
|
||||
SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format,
|
||||
color_space: SurfaceColorSpace::Auto,
|
||||
width: SIZE,
|
||||
height: SIZE,
|
||||
present_mode: PresentMode::Fifo,
|
||||
desired_maximum_frame_latency: 2,
|
||||
alpha_mode: CompositeAlphaMode::Auto,
|
||||
view_formats: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Every layer draws all three primitives, so the renderer takes a different
|
||||
/// path for each list it walks -- which is the case a single-primitive layer
|
||||
/// would never exercise. Images are bound per instance, so there are few.
|
||||
@@ -136,7 +107,7 @@ fn fill(
|
||||
|
||||
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 {
|
||||
let format = TextureFormat::Bgra8Unorm;
|
||||
let mut node = UiRenderNode::new(device, &config(format));
|
||||
let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
|
||||
let mut ui = UiData::default();
|
||||
let mut render = UiRenderState::new();
|
||||
let _handles = fill(&mut ui, &mut render, layers, per_layer);
|
||||
|
||||
+57
-56
@@ -14,7 +14,7 @@
|
||||
#[path = "scenario/mod.rs"]
|
||||
mod scenario;
|
||||
|
||||
use iris::random::{Edits, plan};
|
||||
use iris::random::{Edits, Plan, plan};
|
||||
use scenario::{ALL, Case, diverges, env, over_seeds};
|
||||
|
||||
/// How deep the generator branches. The generator widens two to four ways per
|
||||
@@ -25,15 +25,22 @@ fn depth() -> usize {
|
||||
env("IRIS_GENERATED_DEPTH", 4)
|
||||
}
|
||||
|
||||
/// The seeds the ordinary tests take. Seven that have never failed; 86,
|
||||
/// which a `Scroll` fixed point once settled differently on; and 20, which
|
||||
/// caught a locally redrawn widget being placed twice in the box its parent
|
||||
/// had already placed it in.
|
||||
/// The seeds the ordinary tests take: a corpus rather than a set of
|
||||
/// regression cases, since a seed names a tree only for as long as the
|
||||
/// generator draws the same things in the same order. Adding images to the
|
||||
/// leaves moved every one of them, so 20 and 86 -- which once caught a widget
|
||||
/// placed twice in a box its parent had already placed it in, and a `Scroll`
|
||||
/// fixed point settling differently -- no longer grow those trees. Both
|
||||
/// defects are pinned by the shrunk fixtures in `cases/unsettled.rs`, which
|
||||
/// are trees rather than numbers.
|
||||
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
|
||||
|
||||
fn check(seed: u64, depth: usize, case: Case) {
|
||||
let grown = plan(seed, depth, &Edits::default());
|
||||
if let Some(how) = diverges(&grown, case, seed) {
|
||||
check_plan(&plan(seed, depth, &Edits::default()), seed, depth, case);
|
||||
}
|
||||
|
||||
fn check_plan(grown: &Plan, seed: u64, depth: usize, case: Case) {
|
||||
if let Some(how) = diverges(grown, case, seed) {
|
||||
panic!(
|
||||
"seed {seed} at depth {depth} differs after {}: {how}\n\
|
||||
reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \
|
||||
@@ -44,57 +51,50 @@ fn check(seed: u64, depth: usize, case: Case) {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! case {
|
||||
($name:ident, $case:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
for seed in SEEDS {
|
||||
check(seed, depth(), $case);
|
||||
/// A test per case, and the list of which cases have one, from the same
|
||||
/// place. A case the ordinary suite leaves out runs only in the long scan,
|
||||
/// which nobody runs by hand.
|
||||
macro_rules! cases {
|
||||
($($name:ident = $case:expr,)*) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() {
|
||||
for seed in SEEDS {
|
||||
check(seed, depth(), $case);
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
const NAMED: [Case; [$($case,)*].len()] = [$($case,)*];
|
||||
};
|
||||
}
|
||||
|
||||
case!(
|
||||
many_widgets_redrawing_at_once_leaves_every_box_where_it_was,
|
||||
Case::RepaintSome
|
||||
);
|
||||
case!(
|
||||
everything_redrawing_at_once_leaves_every_box_where_it_was,
|
||||
Case::Repaint
|
||||
);
|
||||
case!(
|
||||
a_resize_lands_where_starting_at_that_size_would,
|
||||
Case::Resize
|
||||
);
|
||||
case!(
|
||||
a_resize_and_a_repaint_land_where_starting_that_way_would,
|
||||
Case::ResizeRepaint
|
||||
);
|
||||
case!(
|
||||
a_size_change_after_a_resize_lands_the_same_way,
|
||||
Case::ResizeSize
|
||||
);
|
||||
case!(
|
||||
a_size_change_lands_where_growing_it_that_way_would,
|
||||
Case::Size
|
||||
);
|
||||
case!(
|
||||
every_size_changing_at_once_lands_where_growing_it_that_way_would,
|
||||
Case::EverySize
|
||||
);
|
||||
case!(
|
||||
an_alignment_change_lands_where_growing_it_that_way_would,
|
||||
Case::Align
|
||||
);
|
||||
case!(
|
||||
giving_and_taking_a_movable_region_rebuilds_what_resolves_it,
|
||||
Case::RegionNode
|
||||
);
|
||||
case!(
|
||||
reordering_a_span_lands_where_growing_it_that_way_would,
|
||||
Case::Reorder
|
||||
);
|
||||
cases! {
|
||||
many_widgets_redrawing_at_once_leaves_every_box_where_it_was = Case::RepaintSome,
|
||||
everything_redrawing_at_once_leaves_every_box_where_it_was = Case::Repaint,
|
||||
a_resize_lands_where_starting_at_that_size_would = Case::Resize,
|
||||
a_resize_and_a_repaint_land_where_starting_that_way_would = Case::ResizeRepaint,
|
||||
a_size_change_after_a_resize_lands_the_same_way = Case::ResizeSize,
|
||||
a_resize_after_a_size_change_lands_the_same_way = Case::SizeResize,
|
||||
a_size_change_lands_where_growing_it_that_way_would = Case::Size,
|
||||
every_size_changing_at_once_lands_where_growing_it_that_way_would = Case::EverySize,
|
||||
an_alignment_change_lands_where_growing_it_that_way_would = Case::Align,
|
||||
giving_and_taking_a_movable_region_rebuilds_what_resolves_it = Case::RegionNode,
|
||||
reordering_a_span_lands_where_growing_it_that_way_would = Case::Reorder,
|
||||
}
|
||||
|
||||
/// The shuffles are one test between them, so they are the only cases `ALL`
|
||||
/// may hold without a test of their own.
|
||||
#[test]
|
||||
fn every_case_runs_without_the_long_scan() {
|
||||
for case in ALL {
|
||||
assert!(
|
||||
NAMED.contains(&case) || matches!(case, Case::Shuffle(_)),
|
||||
"{} runs only in the long seed scan; give it a case here",
|
||||
case.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
|
||||
@@ -108,7 +108,7 @@ fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "as many seeds as it is asked for, rather than the nine the others check"]
|
||||
#[ignore = "as many seeds as it is asked for, rather than the ten the others check"]
|
||||
fn a_long_run_of_seeds_agrees() {
|
||||
let depth = depth();
|
||||
let seeds: Vec<u64> = match std::env::var("IRIS_GENERATED_SEED")
|
||||
@@ -119,8 +119,9 @@ fn a_long_run_of_seeds_agrees() {
|
||||
None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(),
|
||||
};
|
||||
over_seeds(seeds, |seed| {
|
||||
let grown = plan(seed, depth, &Edits::default());
|
||||
for case in ALL {
|
||||
check(seed, depth, case);
|
||||
check_plan(&grown, seed, depth, case);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! The adapter and the surface configuration the GPU measurement rigs share,
|
||||
//! so the two cannot probe for a device in two different ways.
|
||||
|
||||
use wgpu::*;
|
||||
|
||||
/// An adapter on whatever this machine has, or `None` where there is none.
|
||||
///
|
||||
/// Probed rather than assumed: there may be no Vulkan adapter, and GL is what
|
||||
/// is left when there is not.
|
||||
///
|
||||
/// The instance is leaked deliberately. A Vulkan loader may unload the driver
|
||||
/// when the last one drops, which can fault as a thread that used it exits --
|
||||
/// and every test runs on a spawned thread.
|
||||
pub fn adapter() -> Option<Adapter> {
|
||||
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
|
||||
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
||||
{
|
||||
Ok(_) => all,
|
||||
Err(_) => Instance::new(InstanceDescriptor {
|
||||
backends: Backends::GL,
|
||||
..InstanceDescriptor::new_without_display_handle()
|
||||
}),
|
||||
};
|
||||
let instance: &'static Instance = Box::leak(Box::new(instance));
|
||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()
|
||||
}
|
||||
|
||||
pub fn config(format: TextureFormat, size: u32) -> SurfaceConfiguration {
|
||||
SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format,
|
||||
color_space: SurfaceColorSpace::Auto,
|
||||
width: size,
|
||||
height: size,
|
||||
present_mode: PresentMode::Fifo,
|
||||
desired_maximum_frame_latency: 2,
|
||||
alpha_mode: CompositeAlphaMode::Auto,
|
||||
view_formats: vec![],
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,30 @@ use std::time::Instant;
|
||||
|
||||
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
|
||||
|
||||
/// A scroll whose content fits is the same drawing in every box it still
|
||||
/// fits in, so a longer or shorter one relays out nothing. Where the content
|
||||
/// sits in that box is decided by placing its answer in the whole of it,
|
||||
/// which is a fraction of the box and holds at every length -- so the
|
||||
/// contract must not turn on the alignment. It did, and at the default
|
||||
/// alignment, which is the middle, every box change redrew the scroll.
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
#[test]
|
||||
fn a_fitting_scroll_holds_for_every_box_its_content_fits_in() {
|
||||
use iris::core::layout_diagnostics as diag;
|
||||
|
||||
for align in [Align::TOP_LEFT, Align::CENTER, Align::BOT_RIGHT] {
|
||||
let mut harness = Harness::new((400, 200));
|
||||
let inner = rect(Color::RED).height(50).add(&mut harness.rsc);
|
||||
harness.set_root(inner.scrollable().align(align));
|
||||
harness.frame();
|
||||
let _ = diag::take();
|
||||
// Still far longer than the 50 the content needs.
|
||||
harness.resize((400, 180));
|
||||
harness.frame();
|
||||
assert_eq!(diag::take().distinct_widgets(), 0, "{align:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
#[test]
|
||||
fn a_selected_widget_retains_its_layout_events() {
|
||||
@@ -37,8 +61,8 @@ fn a_selected_widget_retains_its_layout_events() {
|
||||
diagnostics::trace_widget(leaf.id());
|
||||
let _ = diagnostics::take();
|
||||
|
||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(root.id());
|
||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf.id());
|
||||
harness.rsc.widgets_mut().mark_for_redraw(root.id());
|
||||
harness.rsc.widgets_mut().mark_for_redraw(leaf.id());
|
||||
harness.frame();
|
||||
|
||||
let report = diagnostics::take();
|
||||
@@ -195,7 +219,6 @@ fn layout_cost() {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _ = iris::core::layout_diagnostics::take();
|
||||
run("cold", 1, &mut harness, |_, _| {});
|
||||
drop(tree);
|
||||
}
|
||||
|
||||
if selected("repaint") {
|
||||
@@ -203,7 +226,7 @@ fn layout_cost() {
|
||||
trace_selected(&tree);
|
||||
let leaf = tree.ids[0];
|
||||
run("repaint", frames, &mut harness, move |harness, _| {
|
||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf);
|
||||
harness.rsc.widgets_mut().mark_for_redraw(leaf);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -218,7 +241,7 @@ fn layout_cost() {
|
||||
println!("marking {} of {} widgets", dirty.len(), tree.ids.len());
|
||||
run("many", frames, &mut harness, move |harness, _| {
|
||||
for &id in &dirty {
|
||||
harness.rsc.widgets_mut().get_dyn_mut(id);
|
||||
harness.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -251,6 +274,5 @@ fn layout_cost() {
|
||||
run("resize", frames, &mut harness, |harness, frame| {
|
||||
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
|
||||
});
|
||||
drop(tree);
|
||||
}
|
||||
}
|
||||
@@ -194,7 +194,9 @@ fn text_memory() {
|
||||
h.frame();
|
||||
}
|
||||
report("after 40 resizes");
|
||||
// Settled: the output holds still and one leaf repaints per frame.
|
||||
// Settled: the output holds still and one leaf repaints per frame. Marked
|
||||
// by taking it mutably because the revision at the top of this file has no
|
||||
// `mark_for_redraw`, and the same source has to build against both.
|
||||
for _ in 0..10 {
|
||||
let _ = h.rsc.widgets_mut().get_dyn_mut(paragraphs[0]);
|
||||
h.frame();
|
||||
|
||||
+24
-19
@@ -13,7 +13,7 @@
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use iris::random::{Aligns, Edits, Kind, Lens, Plan, Rng, SpanEdit, Tree, build};
|
||||
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, Tree, build};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A seed per thread but one, since a seed grows, lays out and drops its tree
|
||||
@@ -190,7 +190,7 @@ impl Case {
|
||||
|
||||
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);
|
||||
warm.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,27 +198,32 @@ 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)];
|
||||
fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> SizeRules {
|
||||
let lens = SizeRules {
|
||||
x: a_len(rng).into(),
|
||||
y: a_len(rng).into(),
|
||||
};
|
||||
warm.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rules(tree.sized[idx], lens[0], lens[1]);
|
||||
.set_size_rules(tree.sized[idx], lens.x, lens.y);
|
||||
lens
|
||||
}
|
||||
|
||||
fn realign_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns {
|
||||
fn realign_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Align {
|
||||
let side = |rng: &mut Rng| match rng.below(4) {
|
||||
0 => None,
|
||||
1 => Some(AxisAlign::NEG),
|
||||
2 => Some(AxisAlign::CENTER),
|
||||
_ => Some(AxisAlign::POS),
|
||||
};
|
||||
let align = [side(rng), side(rng)];
|
||||
let align = Align {
|
||||
x: side(rng),
|
||||
y: 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());
|
||||
let taken = RegionAlign::from(align);
|
||||
for axis in Axis::BOTH {
|
||||
warm.rsc.widgets_mut().set_alignment(id, axis, taken[axis]);
|
||||
}
|
||||
align
|
||||
}
|
||||
@@ -406,8 +411,8 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
|
||||
fn record(id: WidgetId, h: &Harness) -> String {
|
||||
let active = &h.render.active[&id];
|
||||
format!(
|
||||
"frame {} ask {} box {} size {}",
|
||||
active.frame, active.part, active.extent, active.size,
|
||||
"rel_base {} region {} placement {} size {}",
|
||||
active.rel_base, active.region, active.placement, active.size,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -444,12 +449,6 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
|
||||
cold.state.root = Some(root);
|
||||
cold.frame();
|
||||
|
||||
let places: HashMap<WidgetId, usize> = tree
|
||||
.ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &id)| (id, i))
|
||||
.collect();
|
||||
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));
|
||||
@@ -457,6 +456,12 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
|
||||
if got == want {
|
||||
continue;
|
||||
}
|
||||
let places: HashMap<WidgetId, usize> = tree
|
||||
.ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &id)| (id, i))
|
||||
.collect();
|
||||
// Where two trees disagree is rarely where the cause is, so the
|
||||
// ancestry comes with it, marking the widgets that own a region.
|
||||
let mut chain = Vec::new();
|
||||
|
||||
+17
-31
@@ -1,5 +1,5 @@
|
||||
//! Traces the six-widget tree in `unsettled.rs`, to see what box its text is
|
||||
//! actually drawn in on a first frame against a settled one.
|
||||
//! Traces the four-widget trees in `unsettled.rs`, to see what box their text
|
||||
//! is actually drawn in on a first frame against a settled one.
|
||||
|
||||
#![cfg(feature = "layout-diagnostics")]
|
||||
|
||||
@@ -9,30 +9,25 @@ use iris::prelude::*;
|
||||
|
||||
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
||||
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
||||
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
||||
let sized = wrapped.width(76).add(&mut h.rsc);
|
||||
let aligned = sized;
|
||||
let wrapped = wtext("Wrapping shapes")
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.width(76)
|
||||
.add(&mut h.rsc);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(sized, Axis::X, AxisAlign::POS);
|
||||
.set_alignment(wrapped, Axis::X, AxisAlign::POS);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(sized, Axis::Y, AxisAlign::POS);
|
||||
.set_alignment(wrapped, Axis::Y, AxisAlign::POS);
|
||||
let stack = Stack {
|
||||
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
||||
children: vec![plain.add_strong(&mut h.rsc), wrapped.add_strong(&mut h.rsc)],
|
||||
size: StackSize::Child(0),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||
vec![
|
||||
plain.id(),
|
||||
wrapped.id(),
|
||||
sized.id(),
|
||||
aligned.id(),
|
||||
stack.id(),
|
||||
root.id(),
|
||||
]
|
||||
vec![plain.id(), wrapped.id(), stack.id(), root.id()]
|
||||
}
|
||||
|
||||
fn dump(label: &str, report: &diag::Report, text: WidgetId) {
|
||||
@@ -42,12 +37,12 @@ fn dump(label: &str, report: &diag::Report, text: WidgetId) {
|
||||
TraceEvent::DrawRequest {
|
||||
id,
|
||||
region,
|
||||
pixel_size,
|
||||
region_px,
|
||||
..
|
||||
} if *id == text => {
|
||||
println!(
|
||||
" draw in {:.2}x{:.2} region {region:?}",
|
||||
pixel_size.x, pixel_size.y
|
||||
region_px.x, region_px.y
|
||||
)
|
||||
}
|
||||
TraceEvent::SizeReported { id, size } if *id == text => {
|
||||
@@ -81,7 +76,7 @@ fn what_box_the_text_is_drawn_in() {
|
||||
|
||||
for _ in 0..2 {
|
||||
for &id in &ids {
|
||||
h.rsc.widgets_mut().get_dyn_mut(id);
|
||||
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
let _ = diag::take();
|
||||
h.frame();
|
||||
@@ -93,23 +88,14 @@ fn what_box_the_text_is_drawn_in() {
|
||||
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
||||
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
||||
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||
let aligned = text;
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(text, Axis::X, AxisAlign::NEG);
|
||||
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let sized = inner.sized((189, 176)).add(&mut h.rsc);
|
||||
let inner = (text,).span(Dir::RIGHT).sized((189, 176)).add(&mut h.rsc);
|
||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let root = (filler, inner).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||
vec![
|
||||
text.id(),
|
||||
aligned.id(),
|
||||
inner.id(),
|
||||
sized.id(),
|
||||
filler.id(),
|
||||
root.id(),
|
||||
]
|
||||
vec![text.id(), inner.id(), filler.id(), root.id()]
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user