Compare commits
57
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1096c3167a | ||
|
|
445287c95c | ||
|
|
750217631d | ||
|
|
9b4cc329ce | ||
|
|
02048eab77 | ||
|
|
d8d51221ee | ||
|
|
781199a7c9 | ||
|
|
69ba91588a | ||
|
|
8088a1fa59 | ||
|
|
713e3e747b | ||
|
|
aea0387567 | ||
|
|
1ebd4d3a05 | ||
|
|
7e2b4cd9db | ||
|
|
3da1c71870 | ||
|
|
6c84b6f2cb | ||
|
|
8d2b7a512b | ||
|
|
40b89c1f79 | ||
|
|
4d42f1c8ca | ||
|
|
2807a925af | ||
|
|
55df32a33c | ||
|
|
58ce74dd7d | ||
|
|
c55be21761 | ||
|
|
beb138632a | ||
|
|
aeb60e50f5 | ||
|
|
a904cf4f36 | ||
|
|
5642f2010a | ||
|
|
84dad211f5 | ||
|
|
add6774980 | ||
|
|
cadfba05dd | ||
|
|
38b3a81053 | ||
|
|
f6242aa33c | ||
|
|
a888717ee9 | ||
|
|
e8a5792dcb | ||
|
|
a30971e4c5 | ||
|
|
adbedaf264 | ||
|
|
23523eea29 | ||
|
|
1512d8418b | ||
|
|
0ef87ebfcf | ||
|
|
3091fb86df | ||
|
|
4328eac756 | ||
|
|
b842e4f474 | ||
|
|
49cec82c1b | ||
|
|
0954770ceb | ||
|
|
1956be3f3d | ||
|
|
34cafb6edc | ||
|
|
3bf22935ce | ||
|
|
e6ba570d07 | ||
|
|
0e107f0e89 | ||
|
|
f860f716e6 | ||
|
|
c44bd198ee | ||
|
|
a7307d95fd | ||
|
|
7601aa2a5d | ||
|
|
c330ecec2b | ||
|
|
39f7b08c6c | ||
|
|
2ed5503717 | ||
|
|
efb416bbc3 | ||
|
|
5fcace1bfa |
No files matched your search
@@ -14,3 +14,27 @@ WidgetRef<W> or smth instead of Id
|
|||||||
vecs for each widget type?
|
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..??
|
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)
|
||||||
+100
-90
@@ -15,7 +15,7 @@
|
|||||||
//! reuse, size, placement, and text events for one suspicious widget. The
|
//! reuse, size, placement, and text events for one suspicious widget. The
|
||||||
//! selection is a set and survives [`take`] until cleared.
|
//! selection is a set and survives [`take`] until cleared.
|
||||||
|
|
||||||
use crate::{Axis, LayoutLen, PxVec2, Size, UiRegion, WidgetId};
|
use crate::{Axis, LayoutHolds, LayoutLen, PxVec2, Size, UiRegion, UiVec2, WidgetId};
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
@@ -23,98 +23,71 @@ use std::{
|
|||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// 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)]
|
#[derive(Clone, Copy)]
|
||||||
|
$vis enum $Name { $($variant,)* }
|
||||||
|
|
||||||
|
impl $Name {
|
||||||
|
const COUNT: usize = [$($label,)*].len();
|
||||||
|
const NAMES: [&'static str; Self::COUNT] = [$($label,)*];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
labelled! {
|
||||||
pub(crate) enum Counter {
|
pub(crate) enum Counter {
|
||||||
Updates,
|
Updates = "updates",
|
||||||
DrawRequests,
|
DrawRequests = "draw requests",
|
||||||
WidgetDraws,
|
WidgetDraws = "widget draws",
|
||||||
RegionNodeDraws,
|
RegionNodeDraws = "region-node draws",
|
||||||
SizeReads,
|
SizeReads = "draw-result size reads",
|
||||||
HintHits,
|
HintHits = "hint hits",
|
||||||
HintMisses,
|
HintMisses = "hint misses",
|
||||||
RetainedSizeHits,
|
ReuseAttempts = "reuse attempts",
|
||||||
ReuseAttempts,
|
ReuseExact = "reuse exact",
|
||||||
ReuseExact,
|
ReuseMoved = "reuse moved",
|
||||||
ReuseMoved,
|
ReuseDirty = "reuse: dirty",
|
||||||
ReuseDirty,
|
ReuseUndrawn = "reuse: nothing drawn to keep",
|
||||||
ReuseWrongParent,
|
ReuseWrongParent = "reuse: wrong parent",
|
||||||
ReuseRemapped,
|
ReuseRemapped = "reuse remapped",
|
||||||
ReuseOutside,
|
ReuseOutside = "reuse: outside what it holds for",
|
||||||
ReuseWrongLayer,
|
ReuseWrongLayer = "reuse: another layer",
|
||||||
ReuseWrongNode,
|
ReuseWrongNode = "reuse: region-node choice changed",
|
||||||
PlaceRedraws,
|
ReuseWrongMask = "reuse: a different inherited mask",
|
||||||
QueuePops,
|
QueuePops = "redraw queue pops",
|
||||||
DepthReads,
|
DepthReads = "depth reads",
|
||||||
LocalRedraws,
|
LocalRedraws = "local redraws",
|
||||||
SizeChanges,
|
SizeChanges = "size changes",
|
||||||
ReaderEdges,
|
ReaderEdges = "reader edges",
|
||||||
PrimitiveWrites,
|
PrimitiveWrites = "primitive writes",
|
||||||
TextRenders,
|
TextRenders = "text renders",
|
||||||
TextShapeHits,
|
TextShapeHits = "text shape hits",
|
||||||
TextShapes,
|
TextShapes = "text shapes",
|
||||||
TextBreaks,
|
TextBreaks = "text line breaks",
|
||||||
GlyphPlacements,
|
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",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Counter {
|
labelled! {
|
||||||
const COUNT: usize = Self::GlyphPlacements as usize + 1;
|
|
||||||
|
|
||||||
const NAMES: [&'static str; Self::COUNT] = [
|
|
||||||
"updates",
|
|
||||||
"draw requests",
|
|
||||||
"widget draws",
|
|
||||||
"region-node draws",
|
|
||||||
"draw-result size reads",
|
|
||||||
"hint hits",
|
|
||||||
"hint misses",
|
|
||||||
"retained size hits",
|
|
||||||
"reuse attempts",
|
|
||||||
"reuse exact",
|
|
||||||
"reuse moved",
|
|
||||||
"reuse: dirty",
|
|
||||||
"reuse: wrong parent",
|
|
||||||
"reuse remapped",
|
|
||||||
"reuse: outside what it holds for",
|
|
||||||
"reuse: another layer",
|
|
||||||
"reuse: region-node choice changed",
|
|
||||||
"placed by redrawing",
|
|
||||||
"redraw queue pops",
|
|
||||||
"depth reads",
|
|
||||||
"local redraws",
|
|
||||||
"size changes",
|
|
||||||
"reader edges",
|
|
||||||
"primitive writes",
|
|
||||||
"text renders",
|
|
||||||
"text shape hits",
|
|
||||||
"text shapes",
|
|
||||||
"text line breaks",
|
|
||||||
"glyph placements",
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub(crate) enum TimerKind {
|
pub(crate) enum TimerKind {
|
||||||
Update,
|
Update = "update total",
|
||||||
FullLayout,
|
FullLayout = "full layout",
|
||||||
IncrementalLayout,
|
IncrementalLayout = "incremental layout",
|
||||||
TextRender,
|
TextRender = "text render",
|
||||||
TextShape,
|
TextShape = "text shape",
|
||||||
TextBreak,
|
TextBreak = "text line break",
|
||||||
GlyphPlacement,
|
GlyphPlacement = "glyph placement",
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TimerKind {
|
|
||||||
const COUNT: usize = Self::GlyphPlacement as usize + 1;
|
|
||||||
|
|
||||||
const NAMES: [&'static str; Self::COUNT] = [
|
|
||||||
"update total",
|
|
||||||
"full layout",
|
|
||||||
"incremental layout",
|
|
||||||
"text render",
|
|
||||||
"text shape",
|
|
||||||
"text line break",
|
|
||||||
"glyph placement",
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -249,6 +222,8 @@ pub enum ReuseOutcome {
|
|||||||
Dirty,
|
Dirty,
|
||||||
WrongParent,
|
WrongParent,
|
||||||
WrongLayer,
|
WrongLayer,
|
||||||
|
WrongMask,
|
||||||
|
WrongNode,
|
||||||
Remapped,
|
Remapped,
|
||||||
Outside,
|
Outside,
|
||||||
Undrawn,
|
Undrawn,
|
||||||
@@ -262,7 +237,7 @@ pub enum TraceEvent {
|
|||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
parent: Option<WidgetId>,
|
parent: Option<WidgetId>,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
pixel_size: PxVec2,
|
region_px: PxVec2,
|
||||||
region_node: bool,
|
region_node: bool,
|
||||||
},
|
},
|
||||||
Reuse {
|
Reuse {
|
||||||
@@ -358,7 +333,7 @@ pub(crate) fn draw_request(
|
|||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
parent: Option<WidgetId>,
|
parent: Option<WidgetId>,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
pixel_size: PxVec2,
|
region_px: PxVec2,
|
||||||
region_node: bool,
|
region_node: bool,
|
||||||
) {
|
) {
|
||||||
trace(
|
trace(
|
||||||
@@ -367,7 +342,7 @@ pub(crate) fn draw_request(
|
|||||||
id,
|
id,
|
||||||
parent,
|
parent,
|
||||||
region,
|
region,
|
||||||
pixel_size,
|
region_px,
|
||||||
region_node,
|
region_node,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -377,6 +352,41 @@ pub(crate) fn reuse(id: WidgetId, outcome: ReuseOutcome) {
|
|||||||
trace(id, TraceEvent::Reuse { id, outcome });
|
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) {
|
pub(crate) fn size_reported(id: WidgetId, size: Size) {
|
||||||
trace(id, TraceEvent::SizeReported { id, size });
|
trace(id, TraceEvent::SizeReported { id, size });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#![feature(unsize)]
|
#![feature(unsize)]
|
||||||
#![feature(coerce_unsized)]
|
#![feature(coerce_unsized)]
|
||||||
#![feature(option_into_flat_iter)]
|
#![feature(option_into_flat_iter)]
|
||||||
|
#![feature(const_index)]
|
||||||
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
pub mod layout_diagnostics;
|
pub mod layout_diagnostics;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::util::impl_axis_index;
|
||||||
use crate::{Px, Rel};
|
use crate::{Px, Rel};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -83,28 +84,6 @@ pub struct RegionAlign {
|
|||||||
pub y: AxisAlign,
|
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 {
|
impl RegionAlign {
|
||||||
pub const TOP_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::NEG);
|
pub const TOP_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::NEG);
|
||||||
pub const TOP_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::NEG);
|
pub const TOP_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::NEG);
|
||||||
@@ -231,3 +210,5 @@ impl RegionAlign {
|
|||||||
UiVec2::from(self)
|
UiVec2::from(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl_axis_index!(RegionAlign => AxisAlign);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::util::impl_axis_index;
|
||||||
use crate::{Fixed, FixedVec2};
|
use crate::{Fixed, FixedVec2};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
@@ -7,6 +8,11 @@ pub enum Axis {
|
|||||||
Y,
|
Y,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Axis {
|
||||||
|
/// Both of them, for the layout code that asks the same question of each.
|
||||||
|
pub const BOTH: [Self; 2] = [Self::X, Self::Y];
|
||||||
|
}
|
||||||
|
|
||||||
impl std::ops::Not for Axis {
|
impl std::ops::Not for Axis {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
@@ -42,20 +48,6 @@ pub enum Sign {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
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 {
|
pub const fn from_axis(axis: Axis, aligned: Fixed<SHIFT>, ortho: Fixed<SHIFT>) -> Self {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => Self::new(aligned, ortho),
|
Axis::X => Self::new(aligned, ortho),
|
||||||
@@ -65,20 +57,6 @@ impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Vec2 {
|
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 {
|
pub const fn from_axis(axis: Axis, aligned: f32, ortho: f32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: match axis {
|
x: match axis {
|
||||||
@@ -137,3 +115,6 @@ impl<T> BothAxis<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl_axis_index!({const SHIFT: u32} FixedVec2<SHIFT> => Fixed<SHIFT>);
|
||||||
|
impl_axis_index!(Vec2 => f32);
|
||||||
+32
-15
@@ -1,4 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::util::impl_axis_index;
|
||||||
use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op};
|
use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||||
@@ -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 {
|
impl LayoutLen {
|
||||||
@@ -158,11 +145,39 @@ impl LayoutLen {
|
|||||||
Len::from_parts(self.rel.add(share), self.px)
|
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
|
/// 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
|
/// 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.
|
/// on whoever divides the room, not a fraction of anything.
|
||||||
pub const fn within_len(self, len: Len) -> Self {
|
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 {
|
Self {
|
||||||
px: part.px,
|
px: part.px,
|
||||||
rel: part.rel,
|
rel: part.rel,
|
||||||
@@ -236,3 +251,5 @@ impl std::fmt::Display for LayoutLen {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl_axis_index!(Size => LayoutLen);
|
||||||
+13
-40
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::util::impl_axis_index;
|
||||||
use std::{fmt::Display, marker::Destruct};
|
use std::{fmt::Display, marker::Destruct};
|
||||||
|
|
||||||
use super::*;
|
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
|
/// Resolved against a box of `size`, which is where a fraction stops
|
||||||
/// being one and becomes a place.
|
/// being one and becomes a place.
|
||||||
pub fn to_px(&self, size: PxVec2) -> PxVec2 {
|
pub fn to_px(&self, size: PxVec2) -> PxVec2 {
|
||||||
@@ -176,14 +163,6 @@ impl Len {
|
|||||||
Self::from_parts(Rel::ZERO, Px::from_f32(px))
|
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 {
|
pub const fn max(&self, other: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rel: self.rel.max(other.rel),
|
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) {
|
pub const fn flip(&mut self) {
|
||||||
self.rel = Rel::ONE.sub(self.rel);
|
self.rel = Rel::ONE.sub(self.rel);
|
||||||
self.px = self.px.neg();
|
self.px = self.px.neg();
|
||||||
@@ -294,6 +269,15 @@ impl UiSpan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A box `len` long inside this one, on the side `align` says. Both must
|
||||||
|
/// be lengths of the same rel base: it subtracts one from the other
|
||||||
|
/// rather than composing it in, which is what keeps a fraction the same
|
||||||
|
/// fraction however long this box turns out to be.
|
||||||
|
pub const fn place(self, len: Len, align: AxisAlign) -> Self {
|
||||||
|
let start = self.start + (self.len() - len).scale(align.rel());
|
||||||
|
Self::new(start, start + len)
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn len(&self) -> Len {
|
pub const fn len(&self) -> Len {
|
||||||
self.end - self.start
|
self.end - self.start
|
||||||
}
|
}
|
||||||
@@ -348,20 +332,6 @@ impl UiRegion {
|
|||||||
y: self.y.within(&parent.y),
|
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) {
|
pub const fn flip(&mut self, axis: Axis) {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => self.x.flip(),
|
Axis::X => self.x.flip(),
|
||||||
@@ -462,3 +432,6 @@ impl Display for PixelRegion {
|
|||||||
write!(f, "{} -> {}", self.top_left, self.bot_right)
|
write!(f, "{} -> {}", self.top_left, self.bot_right)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl_axis_index!(UiVec2 => Len);
|
||||||
|
impl_axis_index!(UiRegion => UiSpan);
|
||||||
@@ -176,6 +176,23 @@ impl TextBuffer {
|
|||||||
self.layout_key.as_ref()?.max_width
|
self.layout_key.as_ref()?.max_width
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Widths covered by the current line breaks, including a wider shaping
|
||||||
|
/// retained when a later draw requested a narrower box.
|
||||||
|
pub fn width_holds(&self) -> crate::Holds {
|
||||||
|
let Some(width) = self.wrap_width() else {
|
||||||
|
return crate::Holds::ANY;
|
||||||
|
};
|
||||||
|
let width = Px::from_f32(width);
|
||||||
|
let soft_wrapped = self.layout.lines().any(|line| {
|
||||||
|
matches!(
|
||||||
|
line.break_reason(),
|
||||||
|
parley::layout::BreakReason::Regular | parley::layout::BreakReason::Emergency
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let upper = if soft_wrapped { width } else { Px::MAX };
|
||||||
|
crate::Holds::from(Px::ceil_from_f32(self.layout.width()).min(width)..=upper)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn size(&self) -> Vec2 {
|
pub fn size(&self) -> Vec2 {
|
||||||
Vec2::new(self.layout.width(), self.layout.height())
|
Vec2::new(self.layout.width(), self.layout.height())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ impl UiRenderNode {
|
|||||||
self.active.push(i);
|
self.active.push(i);
|
||||||
for change in draws.apply_free() {
|
for change in draws.apply_free() {
|
||||||
if let Some(inst) = ui_render.active.get_mut(&change.id) {
|
if let Some(inst) = ui_render.active.get_mut(&change.id) {
|
||||||
for h in &mut inst.primitives {
|
for primitive in &mut inst.primitives {
|
||||||
|
let h = &mut primitive.handle;
|
||||||
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
|
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
|
||||||
h.inst_idx = change.new;
|
h.inst_idx = change.new;
|
||||||
break;
|
break;
|
||||||
|
|||||||
+55
-41
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle,
|
Declared, LayerId, LayoutHolds, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, RetainedPrimitive,
|
||||||
UiRegion, UiVec2, WidgetId,
|
Size, TextureHandle, UiRegion, UiVec2, WidgetId,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// What is kept of a widget its parent has asked about. `drawn` says whether
|
/// What is kept of a widget its parent has asked about. `drawn` says whether
|
||||||
@@ -9,29 +9,35 @@ use crate::{
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ActiveData {
|
pub struct ActiveData {
|
||||||
pub id: WidgetId,
|
pub id: WidgetId,
|
||||||
/// The box its drawing is in, in `parent_move`'s coordinates.
|
/// Where its drawing goes, in its region node's coordinates.
|
||||||
|
pub placement: UiRegion,
|
||||||
|
/// What a fraction declared or reported under this widget is a fraction
|
||||||
|
/// of, as a length of the window.
|
||||||
|
pub rel_base: UiVec2,
|
||||||
|
/// Where its drawing was put, and where it was asked. The two differ
|
||||||
|
/// where a container asks in one place and puts the answer in another --
|
||||||
|
/// a row measures from its cursor and puts the child in its slot. Each
|
||||||
|
/// carries the rel base that ask stated, so asking again from either is
|
||||||
|
/// the same question it was.
|
||||||
|
pub placed: PlaceDesc,
|
||||||
|
pub asked: PlaceDesc,
|
||||||
|
/// The box it was asked in, in the parent's region-node coordinates: the
|
||||||
|
/// box its drawing was made in and the one its contract is about. Its
|
||||||
|
/// drawing is placed elsewhere by re-expression, never by asking again.
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
/// The box its parent gave it, in the same coordinates: what it was
|
/// The measured answer and its dependencies. A hint-only dependency or
|
||||||
/// asked about, before its own answer placed its drawing inside it.
|
/// a widget first encountered during placement has no measurement yet.
|
||||||
/// `region` is that placement, and a local redraw asks here.
|
pub answer: Option<Answer>,
|
||||||
pub given: UiRegion,
|
/// Asked more than once in its parent's last draw -- measured in one box
|
||||||
/// The same box as lengths of its parent's box, which is the one route
|
/// and then asked in the one the parent decided. The parent's layout
|
||||||
/// to a box in pixels: a draw threads these down a level at a time, and
|
/// rests on the first answer and its drawing on the last, so only the
|
||||||
/// [`crate::UiRenderState::redraw`] takes the same steps back up.
|
/// parent can ask either again.
|
||||||
pub given_len: UiVec2,
|
pub re_asked: bool,
|
||||||
/// The lengths of the box its parent first asked about it in, as
|
/// What the widget reported, in window-unit lengths.
|
||||||
/// lengths of the box the parent was itself offered. Any later box it
|
|
||||||
/// was given was decided knowing its answer, so this is the question
|
|
||||||
/// asked again -- and a chain of fractions has no frame in it, which is
|
|
||||||
/// why a region node between two widgets cannot break it.
|
|
||||||
pub offer_len: UiVec2,
|
|
||||||
/// What it answered there: the size and what that held for.
|
|
||||||
pub answer: (Size, [Holds; 2]),
|
|
||||||
/// What the widget said it used of its box, the last time it drew.
|
|
||||||
pub size: Size,
|
pub size: Size,
|
||||||
/// The pixel lengths of `region`, per axis, that its drawing and `size`
|
/// The window and region reads that this drawing holds for, and the
|
||||||
/// hold for.
|
/// rel base and region it pinned.
|
||||||
pub holds: [Holds; 2],
|
pub holds: LayoutHolds,
|
||||||
pub drawn: bool,
|
pub drawn: bool,
|
||||||
pub parent: Option<WidgetId>,
|
pub parent: Option<WidgetId>,
|
||||||
/// How far down the tree it was drawn, the root being 1. Carried down a
|
/// How far down the tree it was drawn, the root being 1. Carried down a
|
||||||
@@ -39,24 +45,24 @@ pub struct ActiveData {
|
|||||||
/// widget a frame visits and cannot drift while one is being drawn.
|
/// widget a frame visits and cannot drift while one is being drawn.
|
||||||
pub depth: usize,
|
pub depth: usize,
|
||||||
pub textures: Vec<TextureHandle>,
|
pub textures: Vec<TextureHandle>,
|
||||||
pub primitives: Vec<PrimitiveHandle>,
|
/// Its primitives, each keeping the box it was written in -- in this
|
||||||
|
/// widget's placement coordinates, which is what a move recomposes from.
|
||||||
|
pub primitives: Vec<RetainedPrimitive>,
|
||||||
|
/// An owned mask holds one reference independently of its primitives.
|
||||||
|
pub mask_region: Option<UiRegion>,
|
||||||
pub children: Vec<WidgetId>,
|
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
|
/// The movable region its primitives are positioned through: its own when
|
||||||
/// opted in, otherwise the nearest ancestor's.
|
/// opted in, otherwise the nearest ancestor's.
|
||||||
pub move_idx: MoveIdx,
|
pub move_idx: MoveIdx,
|
||||||
/// The declared lengths whoever drew this widget resolved into its box.
|
/// The declared lengths whoever drew this widget resolved into its rel base.
|
||||||
/// A change to one moves a box this widget cannot fix by drawing again,
|
/// A change to one moves a box this widget cannot fix by drawing again,
|
||||||
/// and comparing them is what says so.
|
/// and comparing them is what says so.
|
||||||
pub declared: [Option<LayoutLen>; 2],
|
pub declared: Declared,
|
||||||
/// The axes along which its parent chose its box from its own answer,
|
|
||||||
/// so a local redraw asks the question its parent asked.
|
|
||||||
pub decided: [bool; 2],
|
|
||||||
/// Its alignment when it was last drawn, which a change to the property
|
/// Its alignment when it was last drawn, which a change to the property
|
||||||
/// is found against.
|
/// is found against.
|
||||||
pub own_align: RegionAlign,
|
pub own_align: RegionAlign,
|
||||||
/// The movable region whose coordinates `region` uses.
|
/// The movable region whose coordinates its placement is in when this
|
||||||
|
/// widget does not own a region node.
|
||||||
pub parent_move: MoveIdx,
|
pub parent_move: MoveIdx,
|
||||||
/// The mask its drawing is clipped to: one it set itself, or the one it
|
/// The mask its drawing is clipped to: one it set itself, or the one it
|
||||||
/// inherited from whoever drew it.
|
/// inherited from whoever drew it.
|
||||||
@@ -70,16 +76,24 @@ pub struct ActiveData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveData {
|
impl ActiveData {
|
||||||
/// Whether its drawing and size hold for a box of these pixel lengths.
|
/// What it answered when its parent asked, where it has been asked at
|
||||||
pub fn holds_at(&self, px: crate::PxVec2) -> bool {
|
/// all. Not `size`, which is what its last drawing reported: a drawing
|
||||||
self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
|
/// re-expressed in the box that answer chose is not a second answer.
|
||||||
|
pub fn measured(&self) -> Option<Size> {
|
||||||
|
self.answer.map(|answer| answer.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether what it answered still stands for a box of these pixel
|
/// Whether it owns a region node rather than sharing the one it was drawn
|
||||||
/// lengths -- the box it was asked in, where `holds` is about the box its
|
/// under, which is what its two move indices being different says.
|
||||||
/// answer then chose.
|
pub fn is_region_node(&self) -> bool {
|
||||||
pub fn answers_at(&self, px: crate::PxVec2) -> bool {
|
self.move_idx != self.parent_move
|
||||||
let (_, holds) = self.answer;
|
|
||||||
holds[0].contains(px.x) && holds[1].contains(px.y)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
+21
-2
@@ -33,7 +33,13 @@ impl Holds {
|
|||||||
len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
|
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 {
|
Self {
|
||||||
lo: self.lo.max(other.lo),
|
lo: self.lo.max(other.lo),
|
||||||
hi: self.hi.min(other.hi),
|
hi: self.hi.min(other.hi),
|
||||||
@@ -51,7 +57,10 @@ impl Holds {
|
|||||||
/// boxes therefore give one length. That is a floor rather than an
|
/// boxes therefore give one length. That is a floor rather than an
|
||||||
/// allowance: inverting it is two divisions and nothing else, and the
|
/// allowance: inverting it is two divisions and nothing else, and the
|
||||||
/// whole of a box maps back to itself.
|
/// 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;
|
||||||
|
}
|
||||||
let rel = len.rel.raw() as i64;
|
let rel = len.rel.raw() as i64;
|
||||||
if rel == 0 {
|
if rel == 0 {
|
||||||
return Self::ANY;
|
return Self::ANY;
|
||||||
@@ -92,6 +101,16 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::Rel;
|
use crate::Rel;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_unrestricted_range_stays_unrestricted_through_any_length() {
|
||||||
|
for rel in [-2.0, -0.5, 0.0, 0.5, 1.0, 2.0] {
|
||||||
|
for px in [-8, 0, 8] {
|
||||||
|
let len = Len::from_parts(Rel::from_f32(rel), Px::from_int(px));
|
||||||
|
assert_eq!(Holds::ANY.through(len), Holds::ANY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn through_reverses_a_range_for_a_negative_fraction() {
|
fn through_reverses_a_range_for_a_negative_fraction() {
|
||||||
// `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
|
// `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
use crate::util::impl_axis_index;
|
||||||
|
use crate::{Axis, Holds, Len, Px, PxVec2, UiRegion, UiVec2};
|
||||||
|
|
||||||
|
/// What one evaluation of a widget depends on along one axis: the window
|
||||||
|
/// lengths its reads hold for, the pixel lengths of its own box, and the
|
||||||
|
/// symbolic lengths of that box and of its rel base where either one is what
|
||||||
|
/// it was expressed in.
|
||||||
|
///
|
||||||
|
/// The symbolic lengths are pins rather than ranges: a container places its
|
||||||
|
/// children as lengths of its rel base measured from where its own box starts,
|
||||||
|
/// so what it draws turns on that box's length and on nothing about where it
|
||||||
|
/// is. A box pin reaches the parent only where the box it pinned is the
|
||||||
|
/// parent's own; anywhere else the parent chose that length itself, and a
|
||||||
|
/// widget pinned this way is checked when it is re-placed.
|
||||||
|
///
|
||||||
|
/// A rel base pin says the answer or the drawing is a fraction of the rel base,
|
||||||
|
/// which is a different length wherever the rel base is a different one -- at
|
||||||
|
/// the same window size, so no range of window pixels can say it. A length
|
||||||
|
/// of the rel base that is only pixels is not one: it is that many pixels
|
||||||
|
/// whatever the rel base turns out to be.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct AxisHolds {
|
||||||
|
pub window: Holds,
|
||||||
|
pub rel_base: Option<Len>,
|
||||||
|
pub region: Holds,
|
||||||
|
pub region_len: Option<Len>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AxisHolds {
|
||||||
|
pub const ANY: Self = Self {
|
||||||
|
window: Holds::ANY,
|
||||||
|
rel_base: None,
|
||||||
|
region: Holds::ANY,
|
||||||
|
region_len: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn and(&self, other: Self) -> Self {
|
||||||
|
// Two pins of the same length disagreeing would mean one drawing was
|
||||||
|
// a fraction of two different lengths at once.
|
||||||
|
debug_assert!(
|
||||||
|
self.region_len.is_none()
|
||||||
|
|| other.region_len.is_none()
|
||||||
|
|| self.region_len == other.region_len
|
||||||
|
);
|
||||||
|
debug_assert!(
|
||||||
|
self.rel_base.is_none() || other.rel_base.is_none() || self.rel_base == other.rel_base
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
window: self.window.and(other.window),
|
||||||
|
rel_base: self.rel_base.or(other.rel_base),
|
||||||
|
region: self.region.and(other.region),
|
||||||
|
region_len: self.region_len.or(other.region_len),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn covers(&self, other: Self) -> bool {
|
||||||
|
self.window.covers(other.window)
|
||||||
|
&& self.region.covers(other.region)
|
||||||
|
&& self
|
||||||
|
.region_len
|
||||||
|
.is_none_or(|len| other.region_len == Some(len))
|
||||||
|
&& self.rel_base.is_none_or(|len| other.rel_base == Some(len))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a widget in a box `len` long, with that rel base, in that
|
||||||
|
/// window, is one this drawing holds for.
|
||||||
|
pub fn contains(&self, window: Px, rel_base: Len, len: Len) -> bool {
|
||||||
|
self.window.contains(window)
|
||||||
|
&& self.rel_base.is_none_or(|pinned| pinned == rel_base)
|
||||||
|
&& self.region.contains(len.to_px(window))
|
||||||
|
&& self.region_len.is_none_or(|pinned| pinned == len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`AxisHolds`] on both axes. Every question asked of it is asked of one
|
||||||
|
/// axis at a time, since a widget that read one length holds for any length
|
||||||
|
/// of the other.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct LayoutHolds {
|
||||||
|
pub x: AxisHolds,
|
||||||
|
pub y: AxisHolds,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutHolds {
|
||||||
|
pub const ANY: Self = Self {
|
||||||
|
x: AxisHolds::ANY,
|
||||||
|
y: AxisHolds::ANY,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn and(&self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.x.and(other.x),
|
||||||
|
y: self.y.and(other.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn covers(&self, other: Self) -> bool {
|
||||||
|
self.x.covers(other.x) && self.y.covers(other.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&self, window: PxVec2, rel_base: UiVec2, region: UiRegion) -> bool {
|
||||||
|
Axis::BOTH
|
||||||
|
.into_iter()
|
||||||
|
.all(|axis| self[axis].contains(window[axis], rel_base[axis], region[axis].len()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_axis_index!(LayoutHolds => AxisHolds);
|
||||||
@@ -11,12 +11,16 @@ pub const CHAIN_LIMIT: u32 = 64;
|
|||||||
|
|
||||||
mod active;
|
mod active;
|
||||||
mod holds;
|
mod holds;
|
||||||
|
mod layout_holds;
|
||||||
mod painter;
|
mod painter;
|
||||||
|
mod place;
|
||||||
mod render_state;
|
mod render_state;
|
||||||
|
|
||||||
pub use active::*;
|
pub use active::*;
|
||||||
pub use holds::*;
|
pub use holds::*;
|
||||||
|
pub use layout_holds::*;
|
||||||
pub use painter::{Painter, PrimitiveLike};
|
pub use painter::{Painter, PrimitiveLike};
|
||||||
|
pub use place::{PlaceDesc, PlaceDescAxis, RetainedPrimitive};
|
||||||
pub use render_state::*;
|
pub use render_state::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|||||||
+435
-321
@@ -1,50 +1,55 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
use crate::layout_diagnostics::{self as diag, Counter};
|
use crate::layout_diagnostics::{self as diag, Counter};
|
||||||
use crate::{
|
use crate::{
|
||||||
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Size, StrongWidget,
|
Axis, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Rel,
|
||||||
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight,
|
RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextureHandle,
|
||||||
WidgetId, Widgets,
|
UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
|
||||||
render::{
|
render::{
|
||||||
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
|
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
|
||||||
PrimitiveKind, TexturePrimitive,
|
TexturePrimitive,
|
||||||
|
},
|
||||||
|
ui::{
|
||||||
|
place::{PlaceSpan, RelBase},
|
||||||
|
render_state::{DrawInfo, Placing},
|
||||||
},
|
},
|
||||||
ui::render_state::DrawInfo,
|
|
||||||
};
|
};
|
||||||
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
|
||||||
|
|
||||||
/// makes your surfaces look pretty
|
/// makes your surfaces look pretty
|
||||||
pub struct Painter<'a> {
|
pub struct Painter<'a> {
|
||||||
pub(super) state: &'a mut UiRenderState,
|
pub(super) state: &'a mut UiRenderState,
|
||||||
pub(super) rsc: &'a mut dyn UiRsc,
|
pub(super) rsc: &'a mut dyn UiRsc,
|
||||||
|
|
||||||
/// This widget's box, in the coordinates of `move_idx`.
|
/// 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
|
||||||
|
/// 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,
|
pub(super) region: UiRegion,
|
||||||
/// That box in pixels, which its children's are a length of: threaded
|
/// The window in pixels. Frames and boxes become pixels against this one
|
||||||
/// down from the box this widget was given rather than composed back up
|
/// unit, regardless of region-node boundaries.
|
||||||
/// the chain, so every length in layout is one multiply from its
|
pub(super) window: PxVec2,
|
||||||
/// parent's and [`Holds::through`] inverts exactly that.
|
|
||||||
pub(super) px: PxVec2,
|
|
||||||
pub(super) mask: MaskIdx,
|
pub(super) mask: MaskIdx,
|
||||||
pub(super) textures: Vec<TextureHandle>,
|
pub(super) textures: Vec<TextureHandle>,
|
||||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
pub(super) primitives: Vec<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>,
|
pub(super) children: Vec<WidgetId>,
|
||||||
/// The children asked about so far, so the first box each was asked in
|
|
||||||
/// is the one recorded as its offer.
|
|
||||||
pub(super) offered: Vec<WidgetId>,
|
|
||||||
/// The lengths of the box this widget was first asked about in, in
|
|
||||||
/// pixels. Its children's offers are a fraction of it.
|
|
||||||
pub(super) offered_px: PxVec2,
|
|
||||||
/// Whether this draw is in a box of those lengths, which makes the
|
|
||||||
/// questions it asks the ones a cold layout asks and their answers the
|
|
||||||
/// ones to keep.
|
|
||||||
pub(super) at_offer: bool,
|
|
||||||
/// The children whose size this widget read while drawing.
|
/// The children whose size this widget read while drawing.
|
||||||
pub(super) size_deps: Vec<WidgetId>,
|
pub(super) size_deps: Vec<WidgetId>,
|
||||||
/// What this draw itself read of its box in pixels, per axis: every
|
/// What this draw itself reads, as against what its children's drawings
|
||||||
/// length until it reads one, then that one, unless it says otherwise.
|
/// hold for: every window and every length of its own region until it
|
||||||
pub(super) own: [Holds; 2],
|
/// reads one, then that one unless it says otherwise, and the rel base or
|
||||||
/// What the children it asked about and drew keep it to.
|
/// region length it read symbolically, each of which makes the drawing
|
||||||
pub(super) under: [Holds; 2],
|
/// 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)>,
|
||||||
/// The movable region this widget's primitives are positioned through:
|
/// The movable region this widget's primitives are positioned through:
|
||||||
/// its own when opted in, otherwise the nearest ancestor's.
|
/// its own when opted in, otherwise the nearest ancestor's.
|
||||||
pub(super) move_idx: MoveIdx,
|
pub(super) move_idx: MoveIdx,
|
||||||
@@ -64,6 +69,22 @@ impl<'a> Painter<'a> {
|
|||||||
|
|
||||||
/// Takes the kind, for a caller writing many of one primitive.
|
/// Takes the kind, for a caller writing many of one primitive.
|
||||||
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
|
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
|
||||||
|
self.write_resolved(kind, primitive, region, self.resolve(region));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A box in this widget's region, composed into its region node's
|
||||||
|
/// coordinates.
|
||||||
|
fn resolve(&self, region: UiRegion) -> UiRegion {
|
||||||
|
region.within(&self.region)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_resolved<P: Primitive>(
|
||||||
|
&mut self,
|
||||||
|
kind: PrimitiveKind<P>,
|
||||||
|
primitive: P,
|
||||||
|
region: UiRegion,
|
||||||
|
resolved: UiRegion,
|
||||||
|
) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::PrimitiveWrites);
|
diag::bump(Counter::PrimitiveWrites);
|
||||||
let h = self.state.layers.write(
|
let h = self.state.layers.write(
|
||||||
@@ -72,142 +93,118 @@ impl<'a> Painter<'a> {
|
|||||||
kind,
|
kind,
|
||||||
id: self.id,
|
id: self.id,
|
||||||
primitive,
|
primitive,
|
||||||
region,
|
region: resolved,
|
||||||
mask_idx: self.mask,
|
mask_idx: self.mask,
|
||||||
move_idx: self.move_idx,
|
move_idx: self.move_idx,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.push_primitive(h);
|
self.push_primitive(RetainedPrimitive { handle: h, region });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_primitive(&mut self, h: PrimitiveHandle) {
|
fn push_primitive(&mut self, h: RetainedPrimitive) {
|
||||||
if self.mask != MaskIdx::NONE {
|
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.rsc.ui_mut().masks.push_ref(self.mask);
|
||||||
}
|
}
|
||||||
self.primitives.push(h);
|
self.primitives.push(h);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes a primitive to be rendered
|
/// Writes a primitive over the whole of this widget's own box.
|
||||||
pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
|
pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
|
||||||
let primitive = primitive.into_primitive(self);
|
let primitive = primitive.into_primitive(self);
|
||||||
self.primitive_at(primitive, self.region)
|
self.primitive_at(primitive, UiRegion::FULL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes a primitive in a part of this widget's own box, in that box's
|
||||||
|
/// coordinates.
|
||||||
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
|
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
|
||||||
let primitive = primitive.into_primitive(self);
|
let primitive = primitive.into_primitive(self);
|
||||||
self.primitive_at(primitive, region.within(&self.region));
|
self.primitive_at(primitive, region);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets a mask, in this widget's own box's coordinates.
|
||||||
pub fn set_mask(&mut self, region: UiRegion) {
|
pub fn set_mask(&mut self, region: UiRegion) {
|
||||||
|
self.mask_region = Some(region);
|
||||||
assert!(self.mask == MaskIdx::NONE);
|
assert!(self.mask == MaskIdx::NONE);
|
||||||
self.mask = self.rsc.ui_mut().masks.push(Mask {
|
let resolved = self.resolve(region);
|
||||||
region,
|
let move_idx = self.move_idx;
|
||||||
move_idx: self.move_idx,
|
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 within this widget's region.
|
/// 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> {
|
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
|
||||||
self.widget_within(id, UiRegion::FULL)
|
self.widget_at(id, UiRegion::FULL)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a widget's rules declare its lengths to be, which whoever draws
|
/// Resolves what the place says about the child's rel base into a length,
|
||||||
/// it resolves into its box. Reading them depends on nothing -- the box
|
/// where that is this widget's own narrowed the way the region is. An
|
||||||
/// that comes of them is kept on the child, and `redraw` compares it
|
/// axis the region leaves whole is not read at all, so a wrapper that
|
||||||
/// there.
|
/// only moves its child does not pin its drawing to a rel base.
|
||||||
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] {
|
fn resolve_rel_base(&mut self, mut place: PlaceDesc) -> PlaceDesc {
|
||||||
declared_lens(self.rsc.widgets(), id.id())
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes back a child that was drawn only to find out how long it is.
|
/// Asks a child, saying what its fractions are of and where it is asked.
|
||||||
/// Its drawing is dropped and it is not one of this widget's children
|
|
||||||
/// this frame; what it answered is still something this widget asked.
|
|
||||||
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
|
|
||||||
self.children.retain(|child| *child != id.id());
|
|
||||||
self.state.undraw_rec(id.id(), self.rsc);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draws a widget somewhere within this one. `region` is in this widget's
|
|
||||||
/// own coordinates, and the child's declared lengths are still to be
|
|
||||||
/// taken from it. Where the child's drawing sits inside what it is given
|
|
||||||
/// is the child's alignment, applied where the child is drawn, so a
|
|
||||||
/// container positions a child either by handing it a box of exactly its
|
|
||||||
/// length or by leaving it room and letting its alignment decide.
|
|
||||||
pub fn widget_within<'s, W: ?Sized>(
|
|
||||||
&'s mut self,
|
|
||||||
id: &'s StrongWidget<W>,
|
|
||||||
region: UiRegion,
|
|
||||||
) -> DrawResult<'s, 'a, W> {
|
|
||||||
self.widget_at(id, region, region.size(), [false; 2])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draws a widget in `region`, saying what the answer means.
|
|
||||||
///
|
///
|
||||||
/// `reports_of` is what a fraction the child reports is a fraction of, as
|
/// `place` says where the child goes and what its fractions are of:
|
||||||
/// lengths of this widget's own box. It is the box the child was given
|
/// see [`PlaceDesc`]. A `UiRegion` converts into the common case, which
|
||||||
/// wherever that box is the child's whole area -- a pad's inset, a stack
|
/// is a box of this widget's own with the answer placed inside it.
|
||||||
/// child, a scroll's content -- and a span passes its own extent along
|
|
||||||
/// the row instead: it offers each child the room left from its cursor,
|
|
||||||
/// because a text has to wrap at the width actually there, while
|
|
||||||
/// `rel(0.5)` still means half the span wherever the child sits in it.
|
|
||||||
///
|
///
|
||||||
/// A `decided` axis is one where this box was chosen from the widget's
|
/// The child draws once, in the region that comes of it, and its answer
|
||||||
/// own answer. On those the answer is not placed inside the box again: it
|
/// is placed inside that region by re-expressing the drawing. Nothing is
|
||||||
/// already is the box, and a fraction taken of it a second time would
|
/// drawn again in a box an answer chose; a container that puts the
|
||||||
/// shrink it twice. A container uses that where it hands back exactly
|
/// answer somewhere else says so with [`Self::place_at`].
|
||||||
/// what a child asked for -- a span placing a child at the length it
|
|
||||||
/// reported, a scroll giving its content the content's own length.
|
|
||||||
pub fn widget_at<'s, W: ?Sized>(
|
pub fn widget_at<'s, W: ?Sized>(
|
||||||
&'s mut self,
|
&'s mut self,
|
||||||
id: &'s StrongWidget<W>,
|
id: &'s StrongWidget<W>,
|
||||||
region: UiRegion,
|
place: impl Into<PlaceDesc>,
|
||||||
reports_of: UiVec2,
|
|
||||||
decided: [bool; 2],
|
|
||||||
) -> DrawResult<'s, 'a, W> {
|
) -> DrawResult<'s, 'a, W> {
|
||||||
|
let place = self.resolve_rel_base(place.into());
|
||||||
let region_node = self.rsc.widgets().is_region_node(id.id());
|
let region_node = self.rsc.widgets().is_region_node(id.id());
|
||||||
let declared = self.declared_lens(id);
|
let declared = self.declared_lens(id);
|
||||||
let align = self.rsc.widgets().alignment(id.id());
|
let align = self.rsc.widgets().alignment(id.id());
|
||||||
// Composing `FULL` through a box is not quite the identity in f32,
|
let (rel_base, region) =
|
||||||
// so a child with nothing declared keeps the box it would have had.
|
place.rel_base_and_region(self.region, self.rel_base, declared, align);
|
||||||
let local = match declared.iter().any(Option::is_some) {
|
|
||||||
true => declared_box(region, declared, align),
|
|
||||||
false => region,
|
|
||||||
};
|
|
||||||
let within = match local == UiRegion::FULL {
|
|
||||||
true => self.region,
|
|
||||||
false => local.within(&self.region),
|
|
||||||
};
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
if region_node {
|
if region_node {
|
||||||
diag::bump(Counter::RegionNodeDraws);
|
diag::bump(Counter::RegionNodeDraws);
|
||||||
diag::region_node(id.id(), self.id, within);
|
diag::region_node(id.id(), self.id, region);
|
||||||
}
|
}
|
||||||
// A child listed twice would be moved twice.
|
// A child listed twice would be moved twice.
|
||||||
if !self.children.contains(&id.id()) {
|
let re_asked = self.children.contains(&id.id());
|
||||||
|
if !re_asked {
|
||||||
self.children.push(id.id());
|
self.children.push(id.id());
|
||||||
}
|
}
|
||||||
let first_ask = self.offer(id.id());
|
let drawn = self.state.draw_inner(
|
||||||
let given_len = local.size();
|
|
||||||
let offer_len = match first_ask {
|
|
||||||
true => given_len,
|
|
||||||
false => self
|
|
||||||
.state
|
|
||||||
.active
|
|
||||||
.get(&id.id())
|
|
||||||
.map_or(given_len, |a| a.offer_len),
|
|
||||||
};
|
|
||||||
let px = given_len.to_px(self.px);
|
|
||||||
let offered_px = offer_len.to_px(self.offered_px);
|
|
||||||
// Whether this ask is the child's offer question, which is a question
|
|
||||||
// about lengths: the same lengths somewhere else is the same question.
|
|
||||||
let answers_offer = self.at_offer && px == offered_px;
|
|
||||||
// The answer and what it holds for, both about the box asked in. The
|
|
||||||
// child's record may say something else once its drawing has been
|
|
||||||
// placed: a drawing made again in its placed box holds for that box.
|
|
||||||
let (size, holds) = self.state.draw_inner(
|
|
||||||
id.id(),
|
id.id(),
|
||||||
within,
|
|
||||||
DrawInfo {
|
DrawInfo {
|
||||||
layer: self.layer,
|
layer: self.layer,
|
||||||
parent: Some(self.id),
|
parent: Some(self.id),
|
||||||
@@ -215,105 +212,127 @@ impl<'a> Painter<'a> {
|
|||||||
parent_move: self.move_idx,
|
parent_move: self.move_idx,
|
||||||
region_node,
|
region_node,
|
||||||
mask: self.mask,
|
mask: self.mask,
|
||||||
given_len,
|
rel_base,
|
||||||
offer_len,
|
region,
|
||||||
px,
|
placed: place,
|
||||||
offered_px,
|
asked: place,
|
||||||
decided,
|
re_asked,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
self.rsc,
|
self.rsc,
|
||||||
);
|
);
|
||||||
if answers_offer {
|
let holds = self.in_parent(drawn.drawing_holds, region, place, declared);
|
||||||
self.state.active.get_mut(&id.id()).unwrap().answer = (size, holds);
|
let answer_holds = self.in_parent(drawn.answer.holds, region, place, declared);
|
||||||
}
|
match self.under.iter_mut().find(|(child, _)| *child == id.id()) {
|
||||||
// Whatever the child's answer holds for keeps this one to the boxes
|
Some((_, kept)) => *kept = holds,
|
||||||
// that give the child a length inside it.
|
None => self.under.push((id.id(), holds)),
|
||||||
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
|
|
||||||
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
|
|
||||||
}
|
}
|
||||||
DrawResult {
|
DrawResult {
|
||||||
child: id,
|
child: id,
|
||||||
painter: self,
|
painter: self,
|
||||||
size: in_parent_frame(size, reports_of, declared),
|
size: drawn.answer.size,
|
||||||
|
answer_holds,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a child says its length is without being drawn, if it can say.
|
/// Takes back a child that was drawn only to find out how long it is.
|
||||||
/// Asking counts as reading its size.
|
/// Its drawing is dropped and it is not one of this widget's children
|
||||||
|
/// this frame; what it answered is still something this widget asked.
|
||||||
|
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
|
||||||
|
self.children.retain(|child| *child != id.id());
|
||||||
|
self.under.retain(|(child, _)| *child != id.id());
|
||||||
|
self.state.undraw_rec(id.id(), self.rsc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts a child 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,
|
||||||
|
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 rel base. 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>) -> Declared {
|
||||||
|
self.rsc.widgets().declared_lens(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 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> {
|
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
|
||||||
let widgets = self.rsc.widgets();
|
let widgets = self.rsc.widgets();
|
||||||
// A rule is the answer where there is one: it wins over whatever the
|
// A rule is the answer where there is one: it wins over whatever the
|
||||||
// widget would draw, so it has to win over what the widget says too.
|
// widget would draw, so it has to win over what the widget says too.
|
||||||
let hint = widgets.size_rules(id.id()).axis(axis).exact().or_else(|| {
|
let hint = widgets.size_rules(id.id())[axis].exact().or_else(|| {
|
||||||
widgets
|
widgets
|
||||||
.get_dyn(id.id())
|
.get_dyn(id.id())
|
||||||
.and_then(|widget| widget.size_hint(axis))
|
.and_then(|widget| widget.size_hint(axis))
|
||||||
});
|
});
|
||||||
|
let rel_base = self.rel_base[axis];
|
||||||
|
let resolved = hint.map(|hint| hint.within_len(rel_base));
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::hint_read(id.id(), self.id, axis, hint);
|
{
|
||||||
match hint {
|
diag::hint_read(id.id(), self.id, axis, resolved);
|
||||||
Some(hint) => {
|
diag::bump(match resolved {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
Some(_) => Counter::HintHits,
|
||||||
diag::bump(Counter::HintHits);
|
None => Counter::HintMisses,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(hint) = hint {
|
||||||
self.depend_on(id);
|
self.depend_on(id);
|
||||||
Some(hint)
|
// Resolving a fraction against this rel base makes this draw a
|
||||||
}
|
// function of the rel base's length. The fraction to ask about is
|
||||||
None => {
|
// the child's own: resolved against a rel base of pixels, none is
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
// left to see it by.
|
||||||
diag::bump(Counter::HintMisses);
|
if hint.rel != Rel::ZERO {
|
||||||
None
|
self.own[axis].rel_base = Some(rel_base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
resolved
|
||||||
|
|
||||||
/// A child's length in the box it is about to be offered, if it can be
|
|
||||||
/// had without drawing it: from its hint, or from a drawing it already
|
|
||||||
/// has that holds for that box. `reports_of` is what a fraction in the
|
|
||||||
/// answer is a fraction of, as it is for [`Self::widget_at`].
|
|
||||||
pub fn known_len<W: ?Sized>(
|
|
||||||
&mut self,
|
|
||||||
child: &StrongWidget<W>,
|
|
||||||
axis: Axis,
|
|
||||||
region: UiRegion,
|
|
||||||
reports_of: UiVec2,
|
|
||||||
) -> Option<LayoutLen> {
|
|
||||||
let declared = self.declared_lens(child);
|
|
||||||
let align = self.rsc.widgets().alignment(child.id());
|
|
||||||
let local = declared_box(region, declared, align);
|
|
||||||
let first_ask = self.offer(child.id());
|
|
||||||
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
|
|
||||||
active.offer_len = local.size();
|
|
||||||
}
|
|
||||||
if let Some(hint) = self.size_hint(child, axis) {
|
|
||||||
return Some(hint);
|
|
||||||
}
|
|
||||||
let px = local.size().to_px(self.px);
|
|
||||||
let (size, holds) =
|
|
||||||
self.state
|
|
||||||
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?;
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::RetainedSizeHits);
|
|
||||||
self.depend_on(child);
|
|
||||||
if first_ask {
|
|
||||||
let active = self.state.active.get_mut(&child.id()).unwrap();
|
|
||||||
active.answer = (size, holds);
|
|
||||||
}
|
|
||||||
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
|
|
||||||
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
|
|
||||||
}
|
|
||||||
Some(in_parent_frame(size, reports_of, declared).axis(axis))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether this is the first box a child is asked about in during a draw
|
|
||||||
/// that is itself in the box it was asked in -- the question a cold
|
|
||||||
/// layout asks, whose answer is the one to keep.
|
|
||||||
fn offer(&mut self, child: WidgetId) -> bool {
|
|
||||||
if !self.at_offer || self.offered.contains(&child) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
self.offered.push(child);
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
||||||
@@ -334,11 +353,15 @@ impl<'a> Painter<'a> {
|
|||||||
ui.text.render(buffer, attrs, width)
|
ui.text.render(buffer, attrs, width)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes glyphs in the selected rel base or region coordinates.
|
||||||
// TODO: merge the text methods into the primitive ones.
|
// TODO: merge the text methods into the primitive ones.
|
||||||
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
|
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 region.
|
||||||
|
let resolved = self.resolve(origin);
|
||||||
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
|
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
|
||||||
for glyph in text.glyphs.iter() {
|
for glyph in text.glyphs.iter() {
|
||||||
let mut region = origin;
|
let place = |mut region: UiRegion| {
|
||||||
region.x.end = region.x.start;
|
region.x.end = region.x.start;
|
||||||
region.y.end = region.y.start;
|
region.y.end = region.y.start;
|
||||||
let mut region = region.offset(UiVec2::from_px(glyph.offset));
|
let mut region = region.offset(UiVec2::from_px(glyph.offset));
|
||||||
@@ -348,7 +371,9 @@ impl<'a> Painter<'a> {
|
|||||||
);
|
);
|
||||||
region.x.end = region.x.start.offset(size.x);
|
region.x.end = region.x.start.offset(size.x);
|
||||||
region.y.end = region.y.start.offset(size.y);
|
region.y.end = region.y.start.offset(size.y);
|
||||||
self.write(
|
region
|
||||||
|
};
|
||||||
|
self.write_resolved(
|
||||||
kind,
|
kind,
|
||||||
GlyphPrimitive {
|
GlyphPrimitive {
|
||||||
uv_min: glyph.entry.uv_min,
|
uv_min: glyph.entry.uv_min,
|
||||||
@@ -357,15 +382,33 @@ impl<'a> Painter<'a> {
|
|||||||
color: text.color,
|
color: text.color,
|
||||||
flags: glyph.entry.flags(),
|
flags: glyph.entry.flags(),
|
||||||
},
|
},
|
||||||
region,
|
place(origin),
|
||||||
|
place(resolved),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This widget's box, in the coordinates its own primitives are written
|
/// The symbolic length of this widget's own box along one axis, in the
|
||||||
/// in -- so a region composed `within` it may be drawn directly.
|
/// lengths of its rel base that it places its children in. Reading it pins
|
||||||
pub fn region(&self) -> UiRegion {
|
/// the drawing to that length -- and to nothing about where the box
|
||||||
self.region
|
/// 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 region_len(&mut self, axis: Axis) -> Len {
|
||||||
|
let len = self.region[axis].len();
|
||||||
|
self.own[axis].region_len = Some(len);
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where this widget sits in a box longer than the length it takes. A
|
/// Where this widget sits in a box longer than the length it takes. A
|
||||||
@@ -385,61 +428,73 @@ impl<'a> Painter<'a> {
|
|||||||
/// worth anything, since reading one is also what makes its own size
|
/// worth anything, since reading one is also what makes its own size
|
||||||
/// depend on it.
|
/// depend on it.
|
||||||
pub fn has_exact_size(&self, axis: Axis) -> bool {
|
pub fn has_exact_size(&self, axis: Axis) -> bool {
|
||||||
self.rsc
|
self.rsc.widgets().size_rules(self.id)[axis]
|
||||||
.widgets()
|
|
||||||
.size_rules(self.id)
|
|
||||||
.axis(axis)
|
|
||||||
.exact()
|
.exact()
|
||||||
.is_some()
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The part of this widget's box that something of `size` takes, at the
|
/// This widget's own box in pixels. Reading it makes the drawing one
|
||||||
/// near edge. A container that reports one child's size gives every child
|
/// that holds for this box only, until `holds` says how far it goes.
|
||||||
/// this, so what it draws is inside what it says it occupies.
|
|
||||||
pub fn box_of(&self, size: Size) -> UiRegion {
|
|
||||||
let lens = placed_lens(size, [None; 2], [false; 2]);
|
|
||||||
placed_box(UiRegion::FULL, lens, RegionAlign::NEAR)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This widget's box in pixels. Reading it makes the drawing one that
|
|
||||||
/// holds for this box only, until `holds` says how far it goes.
|
|
||||||
pub fn px_size(&mut self) -> PxVec2 {
|
pub fn px_size(&mut self) -> PxVec2 {
|
||||||
for (own, len) in self.own.iter_mut().zip([self.px.x, self.px.y]) {
|
PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y))
|
||||||
if *own == Holds::ANY {
|
|
||||||
*own = Holds::at(len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.px
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One axis of this widget's box in pixels. Prefer this to
|
/// One axis of this widget's own box in pixels. Prefer this to
|
||||||
/// [`Self::px_size`] when the other axis cannot affect the drawing.
|
/// [`Self::px_size`] when the other axis cannot affect the drawing.
|
||||||
pub fn px_len(&mut self, axis: Axis) -> Px {
|
pub fn px_len(&mut self, axis: Axis) -> Px {
|
||||||
let len = self.px.axis(axis);
|
let len = self.region[axis].len();
|
||||||
let own = &mut self.own[axis as usize];
|
let px = len.to_px(self.window[axis]);
|
||||||
|
let own = &mut self.own[axis].region;
|
||||||
if *own == Holds::ANY {
|
if *own == Holds::ANY {
|
||||||
*own = Holds::at(len);
|
*own = Holds::at(px);
|
||||||
}
|
}
|
||||||
len
|
px
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The lengths of this widget's box on `axis` that what it is drawing
|
/// The lengths of this widget's own box on `axis` that what it is drawing
|
||||||
/// holds for -- the same primitives, in the same fractions and offsets
|
/// holds for -- the same primitives, in the same fractions and offsets
|
||||||
/// of the box, and the same reported size. A widget that read its
|
/// of the box, and the same reported size. A widget that read its length
|
||||||
/// length in pixels holds for that one alone until it says otherwise.
|
/// in pixels holds for that one alone until it says otherwise.
|
||||||
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
|
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
|
||||||
|
let len = self.region[axis].len();
|
||||||
let holds = holds.into();
|
let holds = holds.into();
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
holds.contains(self.px.axis(axis)),
|
holds.contains(len.to_px(self.window[axis])),
|
||||||
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
|
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
|
||||||
self.label(),
|
self.label(),
|
||||||
self.id
|
self.id
|
||||||
);
|
);
|
||||||
self.own[axis as usize] = holds;
|
self.own[axis].region = holds;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_data(&mut self) -> &mut TextData {
|
/// A window length in pixels, which is what every length in layout is
|
||||||
&mut self.rsc.ui_mut().text
|
/// measured in. Reading one pins the drawing to this window wherever the
|
||||||
|
/// 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];
|
||||||
|
if len.rel != Rel::ZERO {
|
||||||
|
let own = &mut self.own[axis].window;
|
||||||
|
if *own == Holds::ANY {
|
||||||
|
*own = Holds::at(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
len.to_px(window)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// length pins, and replaces it.
|
||||||
|
pub fn window_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
|
||||||
|
let holds = holds.into();
|
||||||
|
debug_assert!(
|
||||||
|
holds.contains(self.window[axis]),
|
||||||
|
"'{}' ({:?}) says its drawing holds for windows that leave out this one",
|
||||||
|
self.label(),
|
||||||
|
self.id
|
||||||
|
);
|
||||||
|
self.own[axis].window = holds;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn child_layer(&mut self) {
|
pub fn child_layer(&mut self) {
|
||||||
@@ -478,6 +533,7 @@ pub struct DrawResult<'p, 'a, W: ?Sized> {
|
|||||||
painter: &'p mut Painter<'a>,
|
painter: &'p mut Painter<'a>,
|
||||||
child: &'p StrongWidget<W>,
|
child: &'p StrongWidget<W>,
|
||||||
size: Size,
|
size: Size,
|
||||||
|
answer_holds: LayoutHolds,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: ?Sized> DrawResult<'_, '_, W> {
|
impl<W: ?Sized> DrawResult<'_, '_, W> {
|
||||||
@@ -488,11 +544,12 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
|
|||||||
diag::size_read(self.child.id(), self.painter.id, self.size);
|
diag::size_read(self.child.id(), self.painter.id, self.size);
|
||||||
}
|
}
|
||||||
self.painter.depend_on(self.child);
|
self.painter.depend_on(self.child);
|
||||||
|
self.painter.answer_under = self.painter.answer_under.and(self.answer_holds);
|
||||||
self.size
|
self.size
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn len(self, axis: Axis) -> LayoutLen {
|
pub fn len(self, axis: Axis) -> LayoutLen {
|
||||||
self.size().axis(axis)
|
self.size()[axis]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,112 +578,169 @@ impl PrimitiveLike for &TextureHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A child's answer as lengths of the parent's own box. A widget reports a
|
impl Painter<'_> {
|
||||||
/// fraction, and `reports_of` is the length that fraction is of: the box the
|
/// Moves what a child depends on into this widget's own terms, taking
|
||||||
/// child was given wherever that is the child's whole area, and the parent's
|
/// only what the child was asked with.
|
||||||
/// own extent wherever the box is a positional remainder, as a span's is
|
///
|
||||||
/// after an earlier child. Pixels come through untouched either way, being
|
/// Window ranges are already about the one unit and combine directly.
|
||||||
/// that many pixels wherever they end up. A declared axis is already the
|
/// A rel base pin becomes this widget's own rel base wherever a length of it
|
||||||
/// parent's: it resolved the rule in its own box, and the rule is what the
|
/// is what reached the child; where only pixels did, no length of this
|
||||||
/// report says.
|
/// rel base can change the child's and the pin stops here.
|
||||||
fn in_parent_frame(size: Size, reports_of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size {
|
///
|
||||||
let mut size = size;
|
/// A child's validity maps back through the part of this widget's box,
|
||||||
for (axis, declared) in AXES.into_iter().zip(declared) {
|
/// where the box the child was asked in is that part; a declared length
|
||||||
if declared.is_none() {
|
/// places the box inside the part instead, and then only that length
|
||||||
*size.axis_mut(axis) = size.axis(axis).within_len(reports_of.axis(axis));
|
/// 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,
|
||||||
|
region: UiRegion,
|
||||||
|
place: PlaceDesc,
|
||||||
|
declared: Declared,
|
||||||
|
) -> LayoutHolds {
|
||||||
|
let mut result = LayoutHolds::ANY;
|
||||||
|
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 = 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
|
||||||
|
// is this widget's length less the part's pixels where the
|
||||||
|
// 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.
|
||||||
|
(PlaceSpan::Within(span), None) => {
|
||||||
|
let part_len = span.len();
|
||||||
|
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.region[axis].len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Its box is a length this widget decided, from its own
|
||||||
|
// 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 = result.window.and(holds.region.through(region[axis].len()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
size
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a widget declares a length of its box to be. `leftover` is not one: a
|
impl Widgets {
|
||||||
/// share of what is left over is only a length to the widget dividing one,
|
/// What a widget's box is where a rule or its own hint says so outright.
|
||||||
/// so it passes up in the size instead.
|
pub(super) fn declared_lens(&self, id: WidgetId) -> Declared {
|
||||||
pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLen>; 2] {
|
let rules = self.size_rules(id);
|
||||||
let rules = widgets.size_rules(id);
|
let widget = self.get_dyn(id);
|
||||||
let widget = widgets.get_dyn(id);
|
Declared::from_axes(|axis| {
|
||||||
AXES.map(|axis| {
|
rules[axis].declared().or_else(|| {
|
||||||
rules.axis(axis).declared().or_else(|| {
|
// A hint still narrows the box where no rule does, which is
|
||||||
// A hint still narrows the box where no rule does, which is how a
|
// how a widget with a natural pixel size -- an image, a gap
|
||||||
// widget with a natural pixel size -- an image, a gap -- gets that
|
// -- gets that size rather than the whole offer. That is the
|
||||||
// size rather than the whole offer. That is the offer's business
|
// offer's business rather than a declaration's, and this
|
||||||
// rather than a declaration's, and this falls away once a widget
|
// falls away once a widget occupies its reported size inside
|
||||||
// occupies its reported size inside the box it was offered.
|
// the box it was offered.
|
||||||
widget
|
widget
|
||||||
.and_then(|widget| widget.size_hint(axis))
|
.and_then(|widget| widget.size_hint(axis))
|
||||||
.filter(|len| len.leftover == Weight::ZERO)
|
.and_then(|len| len.declared())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether what a widget reported along an axis is the whole of the box it
|
|
||||||
/// is in rather than a part to be placed inside it. A share fills, because a
|
|
||||||
/// share is a length only to whoever divides one, and whoever did is the one
|
|
||||||
/// that handed down this box. A declared axis does too: `declared_box`
|
|
||||||
/// already placed it, in the parent's box, and the rule's length is what the
|
|
||||||
/// widget reports there. And an axis the parent decided from the answer is
|
|
||||||
/// the answer already.
|
|
||||||
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
|
|
||||||
reported.leftover != Weight::ZERO || declared.is_some() || decided
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What of the box it was given a widget's drawing occupies, as lengths of
|
impl LayoutLen {
|
||||||
/// that box: the size it reported wherever that is a part to be placed, and
|
/// Whether what a widget reported along an axis is the whole of the box
|
||||||
/// the whole of the box wherever the answer fills it.
|
/// 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.
|
||||||
///
|
///
|
||||||
/// A reported fraction is a fraction of the box the widget drew in, where a
|
/// The length it reported is a length of its rel base, and the part is one
|
||||||
/// declared one is a fraction of the box its parent handed down -- a span
|
/// too, so this takes one from the other rather than composing it into the
|
||||||
/// reporting `rel(1.0)` means all of what it was given, whatever that was a
|
/// part. That is what makes a fraction the same fraction wherever the part
|
||||||
/// fraction of. So this is a length of the box rather than a length composed
|
/// it is placed in sits and however long it is -- the fraction is resolved
|
||||||
/// into it, and a box in pixels is this step from the given box's pixels.
|
/// once, here, against the rel base it was reported of.
|
||||||
pub(crate) fn placed_lens(
|
pub(super) fn placement(
|
||||||
|
self,
|
||||||
|
region: UiRegion,
|
||||||
size: Size,
|
size: Size,
|
||||||
declared: [Option<LayoutLen>; 2],
|
declared: Declared,
|
||||||
decided: [bool; 2],
|
align: RegionAlign,
|
||||||
) -> UiVec2 {
|
) -> UiRegion {
|
||||||
let mut lens = UiVec2::FULL_SIZE;
|
|
||||||
for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) {
|
|
||||||
let reported = size.axis(axis);
|
|
||||||
if !fills(reported, declared, decided) {
|
|
||||||
*lens.axis_mut(axis) = Len::from_parts(reported.rel, reported.px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lens
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Where that drawing sits: those lengths taken of the box the widget was
|
|
||||||
/// asked in, on the side of it that the widget's alignment says.
|
|
||||||
pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) -> UiRegion {
|
|
||||||
let mut placed = region;
|
let mut placed = region;
|
||||||
for axis in AXES {
|
for axis in Axis::BOTH {
|
||||||
// The whole of the box is already where it sits, and the arithmetic
|
let reported = size[axis];
|
||||||
// below is the identity for it.
|
if reported.fills(declared[axis], self[axis].fills) {
|
||||||
if lens.axis(axis) == Len::FULL {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let span = placed.axis_mut(axis);
|
placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]);
|
||||||
let len = lens.axis(axis).within_len(span.len());
|
|
||||||
span.start += (span.len() - len).scale(align.axis(axis).rel());
|
|
||||||
span.end = span.start + len;
|
|
||||||
}
|
}
|
||||||
placed
|
placed
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes a widget's declared lengths in the box `region` is given in, since a
|
/// The rel base length and the box a child is asked in, in the coordinates the
|
||||||
/// fraction of a length means a fraction of that one, and puts what is left
|
/// widget asking draws in.
|
||||||
/// over on the side its alignment says. A caller that already reserved the
|
///
|
||||||
/// space hands back the same length, so this is the identity for it.
|
/// `own` is that widget's own box, and `place` what of it the child is
|
||||||
pub(crate) fn declared_box(
|
/// given, including any rel base it states -- a row's slot, or padding's rel
|
||||||
mut region: UiRegion,
|
/// base less its pixels. That is a window length, like every other length
|
||||||
declared: [Option<LayoutLen>; 2],
|
/// 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,
|
align: RegionAlign,
|
||||||
) -> UiRegion {
|
) -> (UiVec2, UiRegion) {
|
||||||
for (axis, len) in AXES.into_iter().zip(declared) {
|
let given = self.of(own, align);
|
||||||
let Some(len) = len else { continue };
|
let mut rel_base = parent_rel_base;
|
||||||
let span = region.axis_mut(axis);
|
let mut region = given;
|
||||||
let len = Len::from_parts(len.rel, len.px);
|
for axis in Axis::BOTH {
|
||||||
span.start += (span.len() - len).scale(align.axis(axis).rel());
|
let base = match self[axis].rel_base {
|
||||||
span.end = span.start + len;
|
RelBase::Len(len) => len,
|
||||||
|
RelBase::Inherit | RelBase::WithRegion => parent_rel_base[axis],
|
||||||
|
};
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
region
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
use crate::util::impl_axis_index;
|
||||||
|
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan};
|
||||||
|
|
||||||
|
/// How a child's region along one axis comes from the region of the widget
|
||||||
|
/// asking, and what its fractions are of.
|
||||||
|
///
|
||||||
|
/// The three ways of saying a region are the three the geometry already has:
|
||||||
|
/// a span composed into the caller's box, a span shifted to where that box
|
||||||
|
/// starts, and a length placed in it by alignment. Which one is meant cannot
|
||||||
|
/// be read off the numbers, since two of them take the same span and apply
|
||||||
|
/// it differently, so it is said here.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct PlaceDescAxis {
|
||||||
|
pub span: PlaceSpan,
|
||||||
|
pub fills: bool,
|
||||||
|
pub rel_base: RelBase,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum PlaceSpan {
|
||||||
|
Within(UiSpan),
|
||||||
|
Shifted(UiSpan),
|
||||||
|
Sized(Len),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a child's fractions are of. [`PlaceSpan::Sized`] is a length the
|
||||||
|
/// caller named, which is always its own base, so nothing here constructs one
|
||||||
|
/// beside anything but [`Self::Len`].
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum RelBase {
|
||||||
|
/// The caller's own, unchanged.
|
||||||
|
Inherit,
|
||||||
|
/// The caller's own, narrowed the way the region is.
|
||||||
|
WithRegion,
|
||||||
|
/// This length of the window.
|
||||||
|
Len(Len),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaceDescAxis {
|
||||||
|
/// The whole of the caller's box.
|
||||||
|
pub const WHOLE: Self = UiSpan::FULL.within_desc();
|
||||||
|
|
||||||
|
/// This region is the child's placement: its answer is not placed inside
|
||||||
|
/// it again. A container uses it where it hands back exactly what the
|
||||||
|
/// child asked for -- a row placing a child at the length it reported.
|
||||||
|
pub const fn fills(mut self) -> Self {
|
||||||
|
self.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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A primitive as it was written: its box in the widget's own box's
|
||||||
|
/// coordinates, which is what a move of that box re-composes from.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RetainedPrimitive {
|
||||||
|
pub handle: PrimitiveHandle,
|
||||||
|
pub region: UiRegion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_axis_index!(PlaceDesc => PlaceDescAxis);
|
||||||
+532
-512
File diff suppressed because it is too large.
Load diff
@@ -93,3 +93,31 @@ macro_rules! impl_op {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) use 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,4 @@
|
|||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct SlotId {
|
pub struct SlotId {
|
||||||
idx: u32,
|
idx: u32,
|
||||||
genr: u32,
|
genr: u32,
|
||||||
|
|||||||
@@ -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
|
/// 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.
|
/// it draws it rather than an answer the widget gives about itself.
|
||||||
@@ -19,14 +20,9 @@ pub enum SizeRule {
|
|||||||
|
|
||||||
impl SizeRule {
|
impl SizeRule {
|
||||||
/// The length this rule gives without the widget being drawn, if it can
|
/// 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
|
/// give one.
|
||||||
/// whoever divides one, so it passes up in the reported size instead and
|
pub fn declared(&self) -> Option<Len> {
|
||||||
/// is resolved there.
|
self.exact().and_then(|len| len.declared())
|
||||||
pub fn declared(&self) -> Option<LayoutLen> {
|
|
||||||
match self {
|
|
||||||
Self::Exact(len) if len.leftover == Weight::ZERO => Some(*len),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The length this rule gives outright, whatever the widget reports --
|
/// The length this rule gives outright, whatever the widget reports --
|
||||||
@@ -40,14 +36,6 @@ impl SizeRule {
|
|||||||
Self::Exact(len) => Some(*len),
|
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 {
|
impl From<LayoutLen> for SizeRule {
|
||||||
@@ -70,18 +58,29 @@ pub struct SizeRules {
|
|||||||
pub y: SizeRule,
|
pub y: SizeRule,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SizeRules {
|
impl_axis_index!(SizeRules => SizeRule);
|
||||||
pub fn axis(&self, axis: Axis) -> SizeRule {
|
|
||||||
match axis {
|
/// What a widget's box is on each axis where something says so outright,
|
||||||
Axis::X => self.x,
|
/// before it is drawn: a rule beside it, or a hint it gives about itself.
|
||||||
Axis::Y => self.y,
|
/// 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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut SizeRule {
|
impl_axis_index!(Declared => Option<Len>);
|
||||||
match axis {
|
|
||||||
Axis::X => &mut self.x,
|
|
||||||
Axis::Y => &mut self.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -130,10 +130,10 @@ impl Widgets {
|
|||||||
pub fn set_size_rule(&mut self, id: impl IdLike, axis: Axis, rule: SizeRule) {
|
pub fn set_size_rule(&mut self, id: impl IdLike, axis: Axis, rule: SizeRule) {
|
||||||
let id = id.id();
|
let id = id.id();
|
||||||
let data = self.data_mut(id).unwrap();
|
let data = self.data_mut(id).unwrap();
|
||||||
if *data.size.axis_mut(axis) == rule {
|
if data.size[axis] == rule {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
*data.size.axis_mut(axis) = rule;
|
data.size[axis] = rule;
|
||||||
self.needs_redraw.insert(id);
|
self.needs_redraw.insert(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,10 +147,10 @@ impl Widgets {
|
|||||||
pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) {
|
pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) {
|
||||||
let id = id.id();
|
let id = id.id();
|
||||||
let data = self.data_mut(id).unwrap();
|
let data = self.data_mut(id).unwrap();
|
||||||
if *data.align.axis_mut(axis) == align {
|
if data.align[axis] == align {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
*data.align.axis_mut(axis) = align;
|
data.align[axis] = align;
|
||||||
self.needs_redraw.insert(id);
|
self.needs_redraw.insert(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ impl UiRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(&mut self) {
|
pub fn draw(&mut self) {
|
||||||
let output = match self.surface.get_current_texture() {
|
let (output, suboptimal) = match self.surface.get_current_texture() {
|
||||||
CurrentSurfaceTexture::Success(texture) => texture,
|
CurrentSurfaceTexture::Success(texture) => (texture, false),
|
||||||
CurrentSurfaceTexture::Suboptimal(texture) => {
|
// Used for this frame, and the swapchain rebuilt after it has
|
||||||
self.surface.configure(&self.device, &self.config);
|
// been presented: configuring the surface while a texture it
|
||||||
texture
|
// handed out is still alive panics.
|
||||||
}
|
CurrentSurfaceTexture::Suboptimal(texture) => (texture, true),
|
||||||
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
|
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
|
||||||
self.surface.configure(&self.device, &self.config);
|
self.surface.configure(&self.device, &self.config);
|
||||||
return;
|
return;
|
||||||
@@ -60,6 +60,9 @@ impl UiRenderer {
|
|||||||
self.queue.submit(std::iter::once(encoder.finish()));
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
self.window.pre_present_notify();
|
self.window.pre_present_notify();
|
||||||
self.queue.present(output);
|
self.queue.present(output);
|
||||||
|
if suboptimal {
|
||||||
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
|
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
|
||||||
|
|||||||
+25
-9
@@ -117,19 +117,35 @@ pub struct Branch {
|
|||||||
|
|
||||||
impl Widget for Branch {
|
impl Widget for Branch {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let mut top = UiRegion::FULL;
|
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
|
||||||
top.y.end = top.y.start.offset(Px::from_int(40));
|
let top = UiSpan::new(Len::ZERO, cut).shifted_desc();
|
||||||
let measured = painter.widget_within(&self.probe, top).len(Axis::X);
|
let measured = painter
|
||||||
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
|
.widget_at(&self.probe, top.on_axis(Axis::Y))
|
||||||
|
.len(Axis::X);
|
||||||
|
let len = measured.apply_leftover();
|
||||||
|
let px = painter.to_px(len, Axis::X);
|
||||||
|
// The range it actually branched on, said the way a container says
|
||||||
|
// one: pinning the window instead would redraw this widget on every
|
||||||
|
// resize, which is a fixture that never exercises reuse.
|
||||||
|
let threshold = Px::from_f32(self.threshold);
|
||||||
|
let holds = match px > threshold {
|
||||||
|
true => Holds::from(threshold + Px::STEP..=Px::MAX),
|
||||||
|
false => Holds::from(Px::MIN..=threshold),
|
||||||
|
};
|
||||||
|
painter.window_holds(Axis::X, holds.through(len));
|
||||||
|
|
||||||
let mut below = UiRegion::FULL;
|
let below = UiSpan::new(cut, painter.region_len(Axis::Y)).shifted_desc();
|
||||||
below.y.start = below.y.start.offset(Px::from_int(40));
|
let place = below.on_axis(Axis::Y);
|
||||||
match px > Px::from_f32(self.threshold) {
|
match px > threshold {
|
||||||
true => painter.widget_within(&self.wide, below),
|
true => painter.widget_at(&self.wide, place),
|
||||||
false => painter.widget_within(&self.narrow, below),
|
false => painter.widget_at(&self.narrow, place),
|
||||||
};
|
};
|
||||||
Size::LEFTOVER
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||||
|
Some(LayoutLen::LEFTOVER)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Spanned {
|
pub struct Spanned {
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ impl Widget for Image {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn size_hint(&self, axis: Axis) -> Option<LayoutLen> {
|
fn size_hint(&self, axis: Axis) -> Option<LayoutLen> {
|
||||||
Some(LayoutLen::px(self.handle.size().axis(axis)))
|
Some(LayoutLen::px(self.handle.size()[axis]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -6,7 +6,7 @@ pub struct Masked {
|
|||||||
|
|
||||||
impl Widget for Masked {
|
impl Widget for Masked {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
painter.set_mask(painter.region());
|
painter.set_mask(UiRegion::FULL);
|
||||||
painter.widget(&self.inner);
|
painter.widget(&self.inner);
|
||||||
// What it occupies is its box, on both axes, for the reason `Scroll`
|
// What it occupies is its box, on both axes, for the reason `Scroll`
|
||||||
// reports the same: it clips what is inside to that box, so it can
|
// reports the same: it clips what is inside to that box, so it can
|
||||||
@@ -15,4 +15,8 @@ impl Widget for Masked {
|
|||||||
// draw, and the framework would place the drawing it clipped away.
|
// draw, and the framework would place the drawing it clipped away.
|
||||||
Size::LEFTOVER
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||||
|
Some(LayoutLen::LEFTOVER)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,8 @@ pub struct Offset {
|
|||||||
|
|
||||||
impl Widget for Offset {
|
impl Widget for Offset {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let region = UiRegion::FULL.offset(self.amt);
|
painter
|
||||||
painter.widget_within(&self.inner, region).size()
|
.widget_at(&self.inner, UiRegion::FULL.offset(self.amt))
|
||||||
|
.size()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,9 +13,13 @@ impl Widget for Pad {
|
|||||||
// it; where the box is bigger -- a share of a row, a rule over this
|
// it; where the box is bigger -- a share of a row, a rule over this
|
||||||
// widget -- the slack is the inner's to sit in, and forcing the near
|
// widget -- the slack is the inner's to sit in, and forcing the near
|
||||||
// edge pinned it to a corner it had not asked for.
|
// edge pinned it to a corner it had not asked for.
|
||||||
let inner = painter
|
//
|
||||||
.widget_within(&self.inner, self.padding.region())
|
// Padding is an inset of both: it comes off the rel base, so `rel(1)`
|
||||||
.size();
|
// under it fills this widget rather than overflowing it by the
|
||||||
|
// padding, and it comes off the box, so what is drawn sits inside.
|
||||||
|
// The two stay distinct -- the box can be narrower still, where a row
|
||||||
|
// asked this widget in the room left, and a text wraps at that.
|
||||||
|
let inner = painter.widget_at(&self.inner, self.padding.region()).size();
|
||||||
Size {
|
Size {
|
||||||
x: LayoutLen {
|
x: LayoutLen {
|
||||||
px: inner.x.px + self.padding.left + self.padding.right,
|
px: inner.x.px + self.padding.left + self.padding.right,
|
||||||
@@ -53,14 +57,18 @@ impl Padding {
|
|||||||
bottom: amt,
|
bottom: amt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn region(&self) -> UiRegion {
|
/// `region` less this padding on each side.
|
||||||
let mut region = UiRegion::FULL;
|
pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
|
||||||
region.x.start.px += self.left;
|
region.x.start.px += self.left;
|
||||||
region.y.start.px += self.top;
|
region.y.start.px += self.top;
|
||||||
region.x.end.px -= self.right;
|
region.x.end.px -= self.right;
|
||||||
region.y.end.px -= self.bottom;
|
region.y.end.px -= self.bottom;
|
||||||
region
|
region
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn region(&self) -> UiRegion {
|
||||||
|
self.region_of(UiRegion::FULL)
|
||||||
|
}
|
||||||
pub fn x(amt: impl UiNum) -> Self {
|
pub fn x(amt: impl UiNum) -> Self {
|
||||||
let amt = Px::from_num(amt);
|
let amt = Px::from_num(amt);
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -12,32 +12,29 @@ pub struct Scroll {
|
|||||||
impl Widget for Scroll {
|
impl Widget for Scroll {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let container_len = painter.px_len(self.axis);
|
let container_len = painter.px_len(self.axis);
|
||||||
// Draw in the whole container only when its scrolling-axis length is
|
// Asked in the whole viewport, then put at the scrolled offset.
|
||||||
// not already known, then draw it at the scrolled offset.
|
let answer_len = painter
|
||||||
let whole = UiRegion::FULL;
|
.widget_at(&self.inner, PlaceDesc::WHOLE.fills())
|
||||||
let answer_len = match painter.known_len(&self.inner, self.axis, whole, whole.size()) {
|
.len(self.axis);
|
||||||
Some(len) => len,
|
let answer_px = painter.to_px(answer_len.without_leftover(), self.axis);
|
||||||
None => painter.widget(&self.inner).size().axis(self.axis),
|
|
||||||
};
|
|
||||||
let content = answer_len.apply_leftover();
|
|
||||||
self.container_len = container_len;
|
self.container_len = container_len;
|
||||||
self.content_len = content.to_px(container_len);
|
self.content_len = answer_px.max(container_len);
|
||||||
|
|
||||||
if self.snap_end {
|
if self.snap_end {
|
||||||
self.amt = self.content_len - self.container_len;
|
self.amt = self.content_len - self.container_len;
|
||||||
}
|
}
|
||||||
self.update_amt();
|
self.update_amt();
|
||||||
let align = painter.alignment().axis(self.axis);
|
let align = painter.alignment()[self.axis];
|
||||||
// Content of a fixed length that fits sits at the start of any box it
|
// 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
|
// 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 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
|
// the drawing holds for that length alone. One scrolled part way sits
|
||||||
// where it is until the box shrinks past what is left of it. Kept to
|
// where it is until the box shrinks past what is left of it. Kept to
|
||||||
// the end, it moves with every length.
|
// the end, it moves with every length.
|
||||||
let fixed_len = content.rel == Rel::ZERO;
|
let answer_is_px = answer_len.is_px();
|
||||||
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG {
|
if answer_is_px && self.content_len <= self.container_len && align == AxisAlign::NEG {
|
||||||
painter.holds(self.axis, self.content_len..=Px::MAX);
|
painter.holds(self.axis, answer_px..=Px::MAX);
|
||||||
} else if fixed_len && !self.snap_end {
|
} else if answer_is_px && !self.snap_end {
|
||||||
let left = self.content_len - self.amt;
|
let left = self.content_len - self.amt;
|
||||||
painter.holds(self.axis, Px::MIN..=left);
|
painter.holds(self.axis, Px::MIN..=left);
|
||||||
}
|
}
|
||||||
@@ -47,7 +44,6 @@ impl Widget for Scroll {
|
|||||||
// have placed the whole scroll in a box longer than it.
|
// have placed the whole scroll in a box longer than it.
|
||||||
let slack = (self.container_len - self.content_len).max(Px::ZERO);
|
let slack = (self.container_len - self.content_len).max(Px::ZERO);
|
||||||
let anchor = slack.mul(align.rel());
|
let anchor = slack.mul(align.rel());
|
||||||
let mut region = UiRegion::FULL;
|
|
||||||
// Content that fills the viewport and has not been scrolled is the
|
// Content that fills the viewport and has not been scrolled is the
|
||||||
// viewport, and is handed back as it came. Writing the same box as
|
// viewport, and is handed back as it came. Writing the same box as
|
||||||
// its own length in pixels is the same box in another form, and the
|
// its own length in pixels is the same box in another form, and the
|
||||||
@@ -55,22 +51,28 @@ impl Widget for Scroll {
|
|||||||
// one centred in `px 900`, since halving a difference is not halving
|
// one centred in `px 900`, since halving a difference is not halving
|
||||||
// each part of it.
|
// each part of it.
|
||||||
let moved = anchor != Px::ZERO || self.amt != Px::ZERO;
|
let moved = anchor != Px::ZERO || self.amt != Px::ZERO;
|
||||||
if moved || self.content_len != self.container_len {
|
let content = match moved || self.content_len != self.container_len {
|
||||||
let offset = UiVec2::from_axis(
|
true => {
|
||||||
self.axis,
|
let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
|
||||||
Len::from_parts(Rel::ZERO, anchor - self.amt),
|
UiSpan::new(start, start.offset(self.content_len)).shifted_desc()
|
||||||
Len::ZERO,
|
|
||||||
);
|
|
||||||
region = region.offset(offset);
|
|
||||||
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
|
||||||
}
|
}
|
||||||
painter.widget_at(&self.inner, region, region.size(), [true; 2]);
|
false => PlaceDescAxis::WHOLE,
|
||||||
|
};
|
||||||
|
// The viewport is the inner's rel base, so a fraction it declares or
|
||||||
|
// reports is a fraction of what is on screen rather than of the
|
||||||
|
// content box its own answer decided. Where it goes is the content
|
||||||
|
// box, scrolled: its drawing moved there, not made again there.
|
||||||
|
painter.place_at(&self.inner, content.on_axis(self.axis).fills());
|
||||||
// What it occupies is its box, on both axes: it clips its content to
|
// What it occupies is its box, on both axes: it clips its content to
|
||||||
// that box, so it can neither take less of one nor honestly ask for
|
// that box, so it can neither take less of one nor honestly ask for
|
||||||
// more. The content's length is what it scrolls through, not what it
|
// more. The content's length is what it scrolls through, not what it
|
||||||
// is.
|
// is.
|
||||||
Size::LEFTOVER
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||||
|
Some(LayoutLen::LEFTOVER)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scroll {
|
impl Scroll {
|
||||||
|
|||||||
+73
-70
@@ -10,28 +10,32 @@ pub struct Span {
|
|||||||
impl Widget for Span {
|
impl Widget for Span {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let axis = self.dir.axis;
|
let axis = self.dir.axis;
|
||||||
// A length for every child before their final boxes are chosen: from
|
// The row this span lays its children out along, as a length of the
|
||||||
// a hint where one exists, and from drawing otherwise.
|
// rel base they are laid out against. Where it starts is nothing's
|
||||||
let mut cursor = Len::rel_min();
|
// 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 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.
|
||||||
|
let mut cursor = Len::ZERO;
|
||||||
let mut lens = Vec::with_capacity(self.children.len());
|
let mut lens = Vec::with_capacity(self.children.len());
|
||||||
for child in &self.children {
|
for child in &self.children {
|
||||||
let mut span = UiSpan::new(cursor, Len::rel_max());
|
let len = match painter.size_hint(child, axis) {
|
||||||
if self.dir.sign == Sign::Neg {
|
|
||||||
span.flip();
|
|
||||||
}
|
|
||||||
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
|
||||||
// Offered the room left from the cursor, because a text has to
|
|
||||||
// wrap at the width actually there, but reporting a fraction of
|
|
||||||
// the whole row: `rel(0.5)` is half the span whatever else is in
|
|
||||||
// it and wherever this child sits among them.
|
|
||||||
let len = match painter.known_len(child, axis, region, UiVec2::FULL_SIZE) {
|
|
||||||
Some(len) => len,
|
Some(len) => len,
|
||||||
None => painter
|
None => {
|
||||||
.widget_at(child, region, UiVec2::FULL_SIZE, [false; 2])
|
// Across itself the child sits where its own alignment
|
||||||
.len(axis),
|
// 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)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
cursor.px += len.px + self.gap;
|
cursor += len.without_leftover();
|
||||||
cursor.rel += len.rel;
|
cursor.px += self.gap;
|
||||||
lens.push(len);
|
lens.push(len);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,9 +50,9 @@ impl Widget for Span {
|
|||||||
|sum, len| sum + *len,
|
|sum, len| sum + *len,
|
||||||
);
|
);
|
||||||
|
|
||||||
// What is left for the shares to divide: the box less everything
|
// What is left for the shares to divide: the row less everything
|
||||||
// fixed, as a length of the box rather than a number of pixels.
|
// fixed, as a length of the rel base rather than a number of pixels.
|
||||||
let room = Len::rel_max() - Len::from_parts(total.rel, total.px);
|
let room = row - total.without_leftover();
|
||||||
// Whether anything is left over is a question in pixels: `rel(0.5)`
|
// Whether anything is left over is a question in pixels: `rel(0.5)`
|
||||||
// beside 300 px is full at 600 and overfull at 400. Asked of `room`
|
// beside 300 px is full at 600 and overfull at 400. Asked of `room`
|
||||||
// itself, and answered back through the same expression, so the
|
// itself, and answered back through the same expression, so the
|
||||||
@@ -58,14 +62,14 @@ impl Widget for Span {
|
|||||||
// sign of `room.rel`, which `through` already reads. What the
|
// sign of `room.rel`, which `through` already reads. What the
|
||||||
// generated oracle checks is the consequence, since which children
|
// generated oracle checks is the consequence, since which children
|
||||||
// exist at all turns on this.
|
// exist at all turns on this.
|
||||||
let mut shares = false;
|
let any_leftover = total.leftover > Weight::ZERO;
|
||||||
if total.leftover > Weight::ZERO {
|
let has_room = any_leftover && painter.to_px(room, axis) > Px::ZERO;
|
||||||
shares = room.to_px(painter.px_len(axis)) > Px::ZERO;
|
if any_leftover {
|
||||||
let holds = match shares {
|
let holds = match has_room {
|
||||||
true => Holds::from(Px::STEP..=Px::MAX),
|
true => Holds::from(Px::STEP..=Px::MAX),
|
||||||
false => Holds::from(Px::MIN..=Px::ZERO),
|
false => Holds::from(Px::MIN..=Px::ZERO),
|
||||||
};
|
};
|
||||||
painter.holds(axis, holds.through(room));
|
painter.window_holds(axis, holds.through(room));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Across itself a span is as long as its longest child -- unless a
|
// Across itself a span is as long as its longest child -- unless a
|
||||||
@@ -76,60 +80,59 @@ impl Widget for Span {
|
|||||||
let shrinks = !painter.has_exact_size(!axis);
|
let shrinks = !painter.has_exact_size(!axis);
|
||||||
// What the fixed parts and the gaps before here take, which is a sum
|
// 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
|
// 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
|
// spoken for. Both ends of a slot are read from those two rather
|
||||||
// from the last child: the share of the room is rounded, and taking
|
// than stepped from the last child: the share of the room is
|
||||||
// each from the one before it would carry every rounding along the
|
// rounded, and taking each end from the one before it would carry
|
||||||
// row.
|
// every rounding along the row.
|
||||||
let mut fixed = Len::rel_min();
|
let mut fixed = Len::ZERO;
|
||||||
let mut taken = Weight::ZERO;
|
let mut taken = Weight::ZERO;
|
||||||
let mut start = Len::rel_min();
|
|
||||||
let mut ortho = LayoutLen::ZERO;
|
let mut ortho = LayoutLen::ZERO;
|
||||||
for (child, len) in self.children.iter().zip(&lens) {
|
// 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,
|
// A child asking for nothing but a part of what is left over,
|
||||||
// when nothing is, is not drawn at all. One that also asked for
|
// when nothing is, is not drawn at all. One that also asked for
|
||||||
// pixels or a fraction keeps those and overflows.
|
// pixels or a fraction keeps those and overflows.
|
||||||
if len.leftover > Weight::ZERO && len.px == Px::ZERO && len.rel == Rel::ZERO && !shares
|
if len.is_only_leftover() && !has_room {
|
||||||
{
|
|
||||||
painter.undraw(child);
|
painter.undraw(child);
|
||||||
fixed.px += self.gap;
|
fixed.px += self.gap;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut span = UiSpan::FULL;
|
let from = reached(fixed, taken);
|
||||||
span.start = start;
|
if len.leftover > Weight::ZERO && has_room {
|
||||||
if len.leftover > Weight::ZERO && shares {
|
|
||||||
taken += len.leftover;
|
taken += len.leftover;
|
||||||
}
|
}
|
||||||
fixed.px += len.px;
|
fixed += len.without_leftover();
|
||||||
fixed.rel += len.rel;
|
let to = reached(fixed, taken);
|
||||||
start = shared(fixed, taken, total.leftover, room);
|
// Along the row the span says where the child goes, and that slot
|
||||||
span.end = start;
|
// is the child's box outright rather than something to place an
|
||||||
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
// answer inside again. A share is decided here and nowhere
|
||||||
if self.dir.sign == Sign::Neg {
|
// else: its slot narrows its rel base, and the child is asked in
|
||||||
region.flip(axis);
|
// it, since a text wraps at the width it is actually given. A
|
||||||
|
// fixed child's slot is its own answer, so a drawing made in the
|
||||||
|
// room is put there as it is, and one not made yet is made here.
|
||||||
|
let slot = self.slot(row, from, to);
|
||||||
|
let mut place = slot.shifted_desc().fills().on_axis(axis);
|
||||||
|
if len.leftover > Weight::ZERO && has_room {
|
||||||
|
place = place.rel_base(axis, slot.len());
|
||||||
}
|
}
|
||||||
// Along the row this box is the child's own answer, so the answer
|
let used = painter.place_at(child, place).len(!axis);
|
||||||
// is not placed in it again; across it the child sits where its
|
|
||||||
// alignment says.
|
|
||||||
let placed = painter.widget_at(
|
|
||||||
child,
|
|
||||||
region,
|
|
||||||
UiVec2::FULL_SIZE,
|
|
||||||
[axis == Axis::X, axis == Axis::Y],
|
|
||||||
);
|
|
||||||
if shrinks {
|
if shrinks {
|
||||||
let used = placed.len(!axis);
|
|
||||||
// Choosing between a fixed and a relative length from the
|
// Choosing between a fixed and a relative length from the
|
||||||
// span's own eventual width admits multiple fixed points.
|
// span's own eventual width admits multiple fixed points.
|
||||||
// A scalable child therefore makes Children scalable too;
|
// A scalable child therefore makes the span scalable too;
|
||||||
// only fixed children are compared with one another.
|
// only fixed children are compared with one another.
|
||||||
if used.rel != Rel::ZERO || used.leftover != Weight::ZERO {
|
if !used.is_px() {
|
||||||
ortho = LayoutLen::LEFTOVER;
|
ortho = LayoutLen::LEFTOVER;
|
||||||
} else if ortho.leftover == Weight::ZERO {
|
} else if ortho.leftover == Weight::ZERO {
|
||||||
ortho.px = ortho.px.max(used.px);
|
ortho.px = ortho.px.max(used.px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fixed.px += self.gap;
|
fixed.px += self.gap;
|
||||||
start = shared(fixed, taken, total.leftover, room);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carried whole rather than collapsed to one share: a span that sizes
|
// Carried whole rather than collapsed to one share: a span that sizes
|
||||||
@@ -139,26 +142,26 @@ impl Widget for Span {
|
|||||||
// get a quarter each, which collapsing to `leftover(1)` per level does
|
// get a quarter each, which collapsing to `leftover(1)` per level does
|
||||||
// not give. Resolution happens at the nearest ancestor with a length,
|
// not give. Resolution happens at the nearest ancestor with a length,
|
||||||
// and the root always has one.
|
// and the root always has one.
|
||||||
let along = total;
|
|
||||||
let ortho = match shrinks {
|
let ortho = match shrinks {
|
||||||
true => ortho,
|
true => ortho,
|
||||||
false => LayoutLen::rel(1.0),
|
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 {
|
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 {
|
pub fn empty(dir: Dir) -> Self {
|
||||||
Self {
|
Self {
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
|
|||||||
@@ -13,34 +13,49 @@ impl Widget for Stack {
|
|||||||
StackSize::Default => None,
|
StackSize::Default => None,
|
||||||
StackSize::Child(i) => Some(i),
|
StackSize::Child(i) => Some(i),
|
||||||
};
|
};
|
||||||
// Whichever child sizes the stack decides the box every child gets.
|
// Whichever child sizes the stack is given the stack's whole box --
|
||||||
// The stack reports that size, so a child given a longer box would
|
// the stack is the length that child asked for, so placing that
|
||||||
// draw outside what the stack says it occupies.
|
// answer inside the box it decided would apply it twice.
|
||||||
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) {
|
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) {
|
||||||
// On the layer that child ends up on, so the ask below is a reuse
|
// On the layer that child ends up on, so the ask below is a reuse
|
||||||
// rather than a second drawing of it somewhere else: a retained
|
// rather than a second drawing of it somewhere else: a retained
|
||||||
// drawing belongs to the layer it was made on.
|
// drawing belongs to the layer it was made on.
|
||||||
Some((i, child)) => {
|
Some((i, child)) => {
|
||||||
painter.child_layer_at(i);
|
painter.child_layer_at(i);
|
||||||
painter.widget(child).size()
|
painter.widget_at(child, PlaceDesc::WHOLE.fills()).size()
|
||||||
}
|
}
|
||||||
None => Size::LEFTOVER,
|
None => Size::LEFTOVER,
|
||||||
};
|
};
|
||||||
let region = painter.box_of(size);
|
// Every other child gets the box the sizing child decided: the
|
||||||
|
// stack is that length, so that is the box they are asked in, and a
|
||||||
|
// fraction under them is a fraction of it. A share leaves the axis
|
||||||
|
// to whoever gave the stack its box. Where a child sits in a box
|
||||||
|
// bigger than itself is its own business.
|
||||||
|
let place = PlaceDesc::from_axes(|axis| {
|
||||||
|
let len = size[axis];
|
||||||
|
match len.leftover == Weight::ZERO {
|
||||||
|
true => len.without_leftover().as_desc().fills(),
|
||||||
|
false => PlaceDescAxis::WHOLE,
|
||||||
|
}
|
||||||
|
});
|
||||||
for (i, child) in self.children.iter().enumerate() {
|
for (i, child) in self.children.iter().enumerate() {
|
||||||
|
if sizing == Some(i) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
painter.child_layer_at(i);
|
painter.child_layer_at(i);
|
||||||
// The sizing child placed its own content in the box its answer
|
painter.widget_at(child, place);
|
||||||
// decided, and this box was derived from that answer, so applying
|
|
||||||
// its alignment again here would place it twice. Every other
|
|
||||||
// child is handed a box that owes nothing to its own answer, and
|
|
||||||
// where it sits in one bigger than itself is its own business.
|
|
||||||
match sizing == Some(i) {
|
|
||||||
true => painter.widget_at(child, region, region.size(), [true; 2]),
|
|
||||||
false => painter.widget_within(child, region),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Without a sizing child a stack is whatever box it is given, which it
|
||||||
|
/// can say without drawing anything.
|
||||||
|
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||||
|
match self.size {
|
||||||
|
StackSize::Default => Some(LayoutLen::LEFTOVER),
|
||||||
|
StackSize::Child(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
|
|||||||
+5
-15
@@ -50,20 +50,11 @@ impl TextView {
|
|||||||
let width = self.attrs.wrap.then(|| painter.px_len(Axis::X));
|
let width = self.attrs.wrap.then(|| painter.px_len(Axis::X));
|
||||||
// The shaper measures in floats, which is where a glyph advance comes
|
// The shaper measures in floats, which is where a glyph advance comes
|
||||||
// from; what it answers goes back on the grid.
|
// from; what it answers goes back on the grid.
|
||||||
let text = painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32));
|
painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32));
|
||||||
// A greedy break is the same break at every width from its longest
|
if width.is_some() {
|
||||||
// line up to the one it was made at: each line still fits, and none
|
painter.holds(Axis::X, self.buf.width_holds());
|
||||||
// could take a word that did not fit in the wider box. A line too
|
|
||||||
// long to fit at all says nothing about narrower boxes.
|
|
||||||
//
|
|
||||||
// The step at or above that longest line rather than the nearest
|
|
||||||
// one, since the shaper measures in floats: the nearest step is
|
|
||||||
// under the line half the time, and a range starting there admits a
|
|
||||||
// box the line does not fit in, where the break is not this one.
|
|
||||||
if let Some(width) = width {
|
|
||||||
painter.holds(Axis::X, Px::ceil_from_f32(text.size.x).min(width)..=width);
|
|
||||||
}
|
}
|
||||||
text
|
self.buf.rendered().expect("render_text placed the glyphs")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tex(&self) -> Option<&RenderedText> {
|
pub fn tex(&self) -> Option<&RenderedText> {
|
||||||
@@ -89,8 +80,7 @@ impl TextView {
|
|||||||
// hair under that line, and the break made in it is not the break a
|
// hair under that line, and the break made in it is not the break a
|
||||||
// cold layout makes there.
|
// cold layout makes there.
|
||||||
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
|
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
|
||||||
let within = region.within(&painter.region());
|
painter.glyphs(tex, region);
|
||||||
painter.glyphs(tex, within);
|
|
||||||
(region, size)
|
(region, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,16 +21,18 @@ struct BranchesOnMeasurement {
|
|||||||
|
|
||||||
impl Widget for BranchesOnMeasurement {
|
impl Widget for BranchesOnMeasurement {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let mut top = UiRegion::FULL;
|
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
|
||||||
top.y.end = top.y.start.offset(Px::from_int(40));
|
let top = UiSpan::new(Len::ZERO, cut).shifted_desc();
|
||||||
let measured = painter.widget_within(&self.probe, top).len(Axis::X);
|
let measured = painter
|
||||||
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
|
.widget_at(&self.probe, top.on_axis(Axis::Y))
|
||||||
|
.len(Axis::X);
|
||||||
|
let px = painter.to_px(measured.apply_leftover(), Axis::X);
|
||||||
|
|
||||||
let mut below = UiRegion::FULL;
|
let below = UiSpan::new(cut, painter.region_len(Axis::Y)).shifted_desc();
|
||||||
below.y.start = below.y.start.offset(Px::from_int(40));
|
let place = below.on_axis(Axis::Y);
|
||||||
match px > Px::from_f32(self.threshold) {
|
match px > Px::from_f32(self.threshold) {
|
||||||
true => painter.widget_within(&self.wide, below),
|
true => painter.widget_at(&self.wide, place),
|
||||||
false => painter.widget_within(&self.narrow, below),
|
false => painter.widget_at(&self.narrow, place),
|
||||||
};
|
};
|
||||||
Size::LEFTOVER
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
|
|||||||
+175
-15
@@ -20,11 +20,12 @@ fn a_span_gives_each_child_the_width_it_asked_for() {
|
|||||||
assert_corners!(h, right, (100, 0), (400, 200));
|
assert_corners!(h, right, (100, 0), (400, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A span offers each child the room left after the one before, because a
|
/// A span places each child in the room left after the one before, because a
|
||||||
/// text has to wrap at the width actually there, but reads what the child
|
/// text has to wrap at the width actually there, but the child's region is
|
||||||
/// reports as a fraction of the whole row. So two children asking for half
|
/// the whole row. So two children asking for half each take the whole row
|
||||||
/// each take the whole row between them, however much of it was left when
|
/// between them, however much of it was left when each was asked, and a third
|
||||||
/// each was asked, and a third overflows.
|
/// overflows -- and a span passes its own region on unchanged, so a child of
|
||||||
|
/// a nested span asking for half asks for half of the same row.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
|
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
|
||||||
let mut h = Harness::new((400, 100));
|
let mut h = Harness::new((400, 100));
|
||||||
@@ -34,10 +35,10 @@ fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
|
|||||||
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
|
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
|
||||||
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
|
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
|
||||||
// The nested span is placed at the length it reported and drawn there
|
// The nested span is placed at the length it reported, and its own child
|
||||||
// once more; half of that final box is what its own child takes.
|
// asks for half of the row rather than half of that placement.
|
||||||
assert_corners!(h, nested, (200, 0), (400, 100));
|
assert_corners!(h, nested, (200, 0), (400, 100));
|
||||||
assert_corners!(h, inner, (200, 0), (300, 100));
|
assert_corners!(h, inner, (200, 0), (400, 100));
|
||||||
assert_corners!(h, tail, (400, 0), (500, 100));
|
assert_corners!(h, tail, (400, 0), (500, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,11 +83,10 @@ fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
|
|||||||
assert!(crowded > whole_row, "{crowded} against {whole_row}");
|
assert!(crowded > whole_row, "{crowded} against {whole_row}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The same reading through a pad: its inset is the whole box less the
|
/// Padding is an inset: it narrows the frame a fraction resolves against and
|
||||||
/// padding, so half of the inset plus the padding is half the box plus one
|
/// adds itself back to the padded widget's reported length.
|
||||||
/// padding, not two.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
|
fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() {
|
||||||
let mut h = Harness::new((400, 100));
|
let mut h = Harness::new((400, 100));
|
||||||
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
||||||
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
|
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
|
||||||
@@ -95,10 +95,85 @@ fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
|
|||||||
// placed inside it by its own alignment, which is not what is under test.
|
// placed inside it by its own alignment, which is not what is under test.
|
||||||
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
|
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
|
||||||
|
assert_corners!(h, inner, (10, 10), (200, 90));
|
||||||
assert_corners!(h, padded, (0, 0), (210, 100));
|
assert_corners!(h, padded, (0, 0), (210, 100));
|
||||||
assert_corners!(h, tail, (210, 0), (310, 100));
|
assert_corners!(h, tail, (210, 0), (310, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the box \
|
||||||
|
leaves room for, so a paragraph's height is an answer and not a setting.";
|
||||||
|
|
||||||
|
/// The worked example of what padding insets: in a 900 px row after a 24 px
|
||||||
|
/// icon, a `rel(1.0)` inside `pad(16)` is 900 - 32 and overflows the row by
|
||||||
|
/// the icon's width, while a wrapping text beside it is asked in the room
|
||||||
|
/// left, 900 - 24 - 32, and wraps there.
|
||||||
|
#[test]
|
||||||
|
fn padding_keeps_the_rel_base_distinct_from_the_room_left_in_a_row() {
|
||||||
|
let mut h = Harness::new((900, 200));
|
||||||
|
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
|
||||||
|
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
|
||||||
|
let padded = fill.pad(16).add(&mut h.rsc);
|
||||||
|
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
let fill_width = h.region(&fill).unwrap().size().x;
|
||||||
|
assert_eq!(fill_width, Px::from_int(868));
|
||||||
|
|
||||||
|
let mut h = Harness::new((900, 200));
|
||||||
|
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
|
||||||
|
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let padded = text.pad(16).add(&mut h.rsc);
|
||||||
|
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
let active = &h.render.active[&text.id()];
|
||||||
|
let window = h.render.output_size().x;
|
||||||
|
let asked = active.region.x.len().to_px(window);
|
||||||
|
assert_eq!(active.rel_base.x.to_px(window), Px::from_int(868));
|
||||||
|
assert_eq!(asked, Px::from_int(844));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The other way round: a share inside padding. A slot is a length of the
|
||||||
|
/// row, which is already the padded width, so what the span decided reaches
|
||||||
|
/// the child as it stands -- taking the padding off a second time would make
|
||||||
|
/// `rel(1.0)` in the slot shorter than the slot.
|
||||||
|
#[test]
|
||||||
|
fn a_share_inside_padding_fills_the_slot_it_was_given() {
|
||||||
|
let mut h = Harness::new((900, 200));
|
||||||
|
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
|
||||||
|
let first = Span {
|
||||||
|
children: vec![fill.add_strong(&mut h.rsc)],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.width(leftover(1))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let second = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
|
||||||
|
let row = (first, second).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.set_root(row.pad(16));
|
||||||
|
|
||||||
|
assert_eq!(h.region(&first).unwrap().size().x, Px::from_int(434));
|
||||||
|
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(434));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same padding in a share instead: the slot is 450, so both the
|
||||||
|
/// fraction and the wrap are the slot less the padding, and the two agree.
|
||||||
|
#[test]
|
||||||
|
fn padding_narrows_both_rel_base_and_box_inside_a_share() {
|
||||||
|
let mut h = Harness::new((900, 200));
|
||||||
|
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
|
||||||
|
let padded = fill.pad(16).width(leftover(1)).add(&mut h.rsc);
|
||||||
|
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
|
||||||
|
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(418));
|
||||||
|
|
||||||
|
let mut h = Harness::new((900, 200));
|
||||||
|
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let padded = text.pad(16).width(leftover(1)).add(&mut h.rsc);
|
||||||
|
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
|
||||||
|
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
let active = &h.render.active[&text.id()];
|
||||||
|
let window = h.render.output_size().x;
|
||||||
|
assert_eq!(active.rel_base.x.to_px(window), Px::from_int(418));
|
||||||
|
assert_eq!(active.region.x.len().to_px(window), Px::from_int(418));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
|
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
@@ -425,11 +500,11 @@ fn a_row_of_equal_shares_fills_it_exactly() {
|
|||||||
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
|
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
|
||||||
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
|
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
|
||||||
let active = &h.render.active[&id];
|
let active = &h.render.active[&id];
|
||||||
let region = h.render.moves.resolve(active.parent_move, active.region);
|
let region = h.render.moves.resolve(active.move_idx, active.placement);
|
||||||
let dim = h.size().axis(axis);
|
let dim = h.size()[axis];
|
||||||
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
|
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
|
||||||
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
|
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
|
||||||
let span = region.axis(axis);
|
let span = region[axis];
|
||||||
(edge(span.start), edge(span.end))
|
(edge(span.start), edge(span.end))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,3 +773,88 @@ fn equal_shares_differ_by_at_most_two_steps_and_fill_the_row() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_stack_sized_by_a_child_does_not_take_that_childs_fraction_twice() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
|
||||||
|
let behind = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let stack = Stack {
|
||||||
|
children: vec![behind.add_strong(&mut h.rsc), half.add_strong(&mut h.rsc)],
|
||||||
|
size: StackSize::Child(1),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root((stack,).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
|
||||||
|
assert_corners!(h, stack, (0, 0), (200, 200));
|
||||||
|
assert_corners!(h, half, (0, 0), (200, 200));
|
||||||
|
assert_corners!(h, behind, (0, 0), (200, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fixed_child_is_centered_in_its_wrappers_share() {
|
||||||
|
let mut h = Harness::new((600, 300));
|
||||||
|
let leaf = rect(Color::RED).sized((100, 100)).center().add(&mut h.rsc);
|
||||||
|
let wrapper = leaf
|
||||||
|
.wrapper()
|
||||||
|
.width(leftover(2))
|
||||||
|
.height(rel(1.0))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let other = rect(Color::BLUE).width(200).add(&mut h.rsc);
|
||||||
|
h.set_root((other, wrapper).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
assert_corners!(h, wrapper, (200, 0), (600, 300));
|
||||||
|
assert_corners!(h, leaf, (350, 100), (450, 200));
|
||||||
|
|
||||||
|
h.resize((900, 400));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, wrapper, (200, 0), (900, 400));
|
||||||
|
assert_corners!(h, leaf, (500, 150), (600, 250));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The root's frame is the window and its rule is a fraction of that, which
|
||||||
|
/// is one resolution and not two: nothing above it narrowed anything.
|
||||||
|
#[test]
|
||||||
|
fn a_root_with_a_fraction_rule_is_that_fraction_of_the_window() {
|
||||||
|
let mut h = Harness::new((900, 200));
|
||||||
|
let root = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_collapsed_share_keeps_the_gaps_before_the_next_slot() {
|
||||||
|
for dir in [Dir::RIGHT, Dir::LEFT, Dir::DOWN, Dir::UP] {
|
||||||
|
for collapsed in [1, 2] {
|
||||||
|
let mut h = Harness::new((400, 400));
|
||||||
|
let head = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
h.set_len(head, dir.axis, 200);
|
||||||
|
let tail = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let tail_len = 200 - 10 * (collapsed + 1);
|
||||||
|
h.set_len(tail, dir.axis, tail_len);
|
||||||
|
let mut children: Vec<StrongWidget> = vec![head.add_strong(&mut h.rsc)];
|
||||||
|
let mut shares = Vec::new();
|
||||||
|
for _ in 0..collapsed {
|
||||||
|
let share = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
|
shares.push(share);
|
||||||
|
children.push(share.add_strong(&mut h.rsc));
|
||||||
|
}
|
||||||
|
children.push(tail.add_strong(&mut h.rsc));
|
||||||
|
h.set_root(Span {
|
||||||
|
children,
|
||||||
|
dir,
|
||||||
|
gap: Px::from_int(10),
|
||||||
|
});
|
||||||
|
for share in shares {
|
||||||
|
assert!(h.region(&share).is_none());
|
||||||
|
}
|
||||||
|
let region = h.region(&tail).unwrap();
|
||||||
|
let (from, to) = match dir.sign {
|
||||||
|
Sign::Pos => (400 - tail_len, 400),
|
||||||
|
Sign::Neg => (0, tail_len),
|
||||||
|
};
|
||||||
|
assert_eq!(region.top_left[dir.axis], Px::from_int(from));
|
||||||
|
assert_eq!(region.bot_right[dir.axis], Px::from_int(to));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+857
-21
@@ -125,7 +125,7 @@ fn moving_an_ordinary_subtree_remaps_its_mask() {
|
|||||||
let active = &h.render.active[&masked.id()];
|
let active = &h.render.active[&masked.id()];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
h.rsc.ui().masks[active.mask.idx()].region,
|
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));
|
assert_corners!(h, inner, (150, 0), (400, 200));
|
||||||
}
|
}
|
||||||
@@ -156,16 +156,9 @@ fn a_span_child_that_declares_its_length_is_drawn_once() {
|
|||||||
h.set_root((hinted, asked).span(Dir::RIGHT));
|
h.set_root((hinted, asked).span(Dir::RIGHT));
|
||||||
|
|
||||||
assert_eq!(told_draws.get(), 1);
|
assert_eq!(told_draws.get(), 1);
|
||||||
// Reading its box makes its drawing hold for the measuring box alone,
|
// Asked once, from the cursor; its slot is its answer and the drawing is
|
||||||
// and it reports less than that box: so it is drawn again in the box its
|
// moved there.
|
||||||
// answer places it in, and once more in the final box the span chooses.
|
assert_eq!(asked_draws.get(), 1);
|
||||||
// A widget that says what it holds for, as text does, skips the middle
|
|
||||||
// one.
|
|
||||||
assert_eq!(
|
|
||||||
asked_draws.get(),
|
|
||||||
3,
|
|
||||||
"drawn to be measured, in its placed box, then in its final box"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -220,9 +213,8 @@ struct FromHint {
|
|||||||
impl Widget for FromHint {
|
impl Widget for FromHint {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
|
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
|
||||||
let mut region = UiRegion::FULL;
|
let top = UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, len.px));
|
||||||
region.y.end = region.y.start.offset(len.px);
|
painter.widget_at(&self.inner, top.shifted_desc().on_axis(Axis::Y));
|
||||||
painter.widget_within(&self.inner, region);
|
|
||||||
Size::LEFTOVER
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,10 +251,9 @@ impl Widget for ReadsBox {
|
|||||||
/// Reads its box across one axis only, so its drawing holds for a taller
|
/// Reads its box across one axis only, so its drawing holds for a taller
|
||||||
/// box on its own and only a wider one is worth a draw.
|
/// box on its own and only a wider one is worth a draw.
|
||||||
///
|
///
|
||||||
/// Both of these report a quarter of what they read, without saying that the
|
/// Both of these report a quarter of what they read. The quarter-sized box
|
||||||
/// drawing holds there too, so each length they are asked at costs two draws:
|
/// the answer places them in is not a question: the drawing is moved there,
|
||||||
/// one to answer, and one in the quarter-sized box that answer places them
|
/// so each length they are asked at costs one draw.
|
||||||
/// in. The counts below are in those pairs.
|
|
||||||
struct ReadsWidth {
|
struct ReadsWidth {
|
||||||
draws: Rc<Cell<usize>>,
|
draws: Rc<Cell<usize>>,
|
||||||
}
|
}
|
||||||
@@ -312,6 +303,51 @@ fn a_span_ruled_across_itself_moves_its_child_without_redrawing_it() {
|
|||||||
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0));
|
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A row places its children as lengths from where its own box starts, so a
|
||||||
|
/// child that grew moves the ones after it and nothing else: each of them is
|
||||||
|
/// the same box in a new place, which the retained drawing follows without
|
||||||
|
/// being made again. Both kinds of length: one the row resolves from a rule,
|
||||||
|
/// and one it takes from what the child reported.
|
||||||
|
#[test]
|
||||||
|
fn a_row_moves_what_follows_a_child_that_grew_rather_than_drawing_it() {
|
||||||
|
for declared in [false, true] {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let first = rect(Color::RED).width(50).add(&mut h.rsc);
|
||||||
|
let ruled = Rc::new(Cell::new(0));
|
||||||
|
let second = Counted {
|
||||||
|
draws: ruled.clone(),
|
||||||
|
size: Size::LEFTOVER,
|
||||||
|
reads_box: false,
|
||||||
|
};
|
||||||
|
let second = match declared {
|
||||||
|
true => second.width(rel(0.25)).add(&mut h.rsc),
|
||||||
|
false => second.width(60).add(&mut h.rsc),
|
||||||
|
};
|
||||||
|
let (third, reported) = counted(&mut h, Size::from((70, 20)), false);
|
||||||
|
h.set_root((first, second, third).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
let (was_ruled, was_reported) = (ruled.get(), reported.get());
|
||||||
|
// A quarter of the row is a quarter of the row, wherever it sits in
|
||||||
|
// it and whatever the first child takes.
|
||||||
|
let width = match declared {
|
||||||
|
true => 100,
|
||||||
|
false => 60,
|
||||||
|
};
|
||||||
|
assert_corners!(h, second, (50, 0), (50 + width, 200));
|
||||||
|
|
||||||
|
h.set_len(first, Axis::X, 80);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_eq!(ruled.get(), was_ruled, "the ruled child was drawn again");
|
||||||
|
assert_eq!(
|
||||||
|
reported.get(),
|
||||||
|
was_reported,
|
||||||
|
"the reported child was drawn again"
|
||||||
|
);
|
||||||
|
assert_corners!(h, second, (80, 0), (80 + width, 200));
|
||||||
|
assert_corners!(h, third, (80 + width, 90), (150 + width, 110));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The output is the root of the box chain, so a resize is a box that changed
|
/// The output is the root of the box chain, so a resize is a box that changed
|
||||||
/// length like any other -- there is not a second rule for the window. A
|
/// length like any other -- there is not a second rule for the window. A
|
||||||
/// drawing that holds for one length is drawn again whichever box moved.
|
/// drawing that holds for one length is drawn again whichever box moved.
|
||||||
@@ -343,7 +379,7 @@ fn a_resize_redraws_what_read_its_box() {
|
|||||||
h.resize((800, 100));
|
h.resize((800, 100));
|
||||||
h.frame();
|
h.frame();
|
||||||
|
|
||||||
assert_eq!(draws.get(), settled + 2);
|
assert_eq!(draws.get(), settled + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -363,7 +399,7 @@ fn a_resize_only_redraws_read_axes() {
|
|||||||
|
|
||||||
h.resize((800, 300));
|
h.resize((800, 300));
|
||||||
h.frame();
|
h.frame();
|
||||||
assert_eq!(draws.get(), settled + 2, "width changes its answer");
|
assert_eq!(draws.get(), settled + 1, "width changes its answer");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A window is measured onto the grid like everything else, so a resize too
|
/// A window is measured onto the grid like everything else, so a resize too
|
||||||
@@ -390,7 +426,7 @@ fn a_resize_within_one_step_is_not_a_resize() {
|
|||||||
|
|
||||||
h.resize((400.0 + step, 200.0));
|
h.resize((400.0 + step, 200.0));
|
||||||
h.frame();
|
h.frame();
|
||||||
assert_eq!(draws.get(), settled + 2);
|
assert_eq!(draws.get(), settled + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The same for a box that changes because a sibling did: what is compared
|
/// The same for a box that changes because a sibling did: what is compared
|
||||||
@@ -737,3 +773,803 @@ fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
|
|||||||
"the span it moved to is the one the change has to reach"
|
"the span it moved to is the one the change has to reach"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
|
||||||
|
h.render.active[&id]
|
||||||
|
.primitives
|
||||||
|
.iter()
|
||||||
|
.map(|primitive| {
|
||||||
|
let handle = &primitive.handle;
|
||||||
|
let instance = &h.render.layers[handle.layer].primitives()[handle.kind as usize]
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.instances()[handle.inst_idx];
|
||||||
|
h.render
|
||||||
|
.moves
|
||||||
|
.resolve(instance.move_idx, instance.region)
|
||||||
|
.to_px(h.render.output_size())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn 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);
|
||||||
|
let through = Stretchy {
|
||||||
|
inner: words.add_strong(&mut h.rsc),
|
||||||
|
draws: Rc::new(Cell::new(0)),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root((first, through).span(Dir::RIGHT));
|
||||||
|
(words, first)
|
||||||
|
}
|
||||||
|
let short = "one two";
|
||||||
|
let long = "one two three four five six seven eight nine ten eleven twelve";
|
||||||
|
let mut warm = Harness::new((400, 200));
|
||||||
|
let (words, first) = build(&mut warm, 50, short);
|
||||||
|
warm.set_len(first, Axis::X, 200);
|
||||||
|
warm.frame();
|
||||||
|
*warm.rsc[words].content = long.to_string();
|
||||||
|
warm.frame();
|
||||||
|
let mut cold = Harness::new((400, 200));
|
||||||
|
let (other, _) = build(&mut cold, 200, long);
|
||||||
|
assert_eq!(warm.region(&words), cold.region(&other));
|
||||||
|
assert_eq!(
|
||||||
|
primitive_bounds(&warm, words.id()),
|
||||||
|
primitive_bounds(&cold, other.id())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn widening_text_without_soft_breaks_reuses_its_drawing() {
|
||||||
|
struct CountedText {
|
||||||
|
text: Text,
|
||||||
|
draws: Rc<Cell<usize>>,
|
||||||
|
}
|
||||||
|
impl Widget for CountedText {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
self.draws.set(self.draws.get() + 1);
|
||||||
|
self.text.draw(painter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for content in ["Short text", "Two hard\nline breaks\nhere", ""] {
|
||||||
|
let plant = |h: &mut Harness| {
|
||||||
|
let mut text = Text::new(content);
|
||||||
|
text.wrap = true;
|
||||||
|
let draws = Rc::new(Cell::new(0));
|
||||||
|
let root = CountedText {
|
||||||
|
text,
|
||||||
|
draws: draws.clone(),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
(root, draws)
|
||||||
|
};
|
||||||
|
let mut warm = Harness::new((300, 200));
|
||||||
|
let (root, draws) = plant(&mut warm);
|
||||||
|
let before = draws.get();
|
||||||
|
warm.resize((500, 200));
|
||||||
|
warm.frame();
|
||||||
|
assert_eq!(draws.get(), before, "{content:?}");
|
||||||
|
let mut cold = Harness::new((500, 200));
|
||||||
|
let (other, _) = plant(&mut cold);
|
||||||
|
assert_eq!(warm.region(&root), cold.region(&other));
|
||||||
|
assert_eq!(
|
||||||
|
primitive_bounds(&warm, root.id()),
|
||||||
|
primitive_bounds(&cold, other.id())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
|
||||||
|
struct Frame {
|
||||||
|
child: StrongWidget,
|
||||||
|
region: UiRegion,
|
||||||
|
}
|
||||||
|
impl Widget for Frame {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
painter.widget_at(
|
||||||
|
&self.child,
|
||||||
|
PlaceDesc::new(self.region.x.shifted_desc(), self.region.y.shifted_desc()),
|
||||||
|
);
|
||||||
|
Size::LEFTOVER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct Painted(Rc<Cell<usize>>);
|
||||||
|
impl Widget for Painted {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
self.0.set(self.0.get() + 1);
|
||||||
|
painter.set_mask(UiRegion::FULL);
|
||||||
|
painter.primitive(RectPrimitive::color(Color::BLUE));
|
||||||
|
Size::LEFTOVER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fixed = |start, end| UiRegion::new(UiSpan::new(Len::px(start), Len::px(end)), UiSpan::FULL);
|
||||||
|
for node in [false, true] {
|
||||||
|
let plant = |h: &mut Harness, region| {
|
||||||
|
let draws = Rc::new(Cell::new(0));
|
||||||
|
let leaf = Painted(draws.clone()).add(&mut h.rsc);
|
||||||
|
h.rsc.widgets_mut().set_region_node(leaf, node);
|
||||||
|
let inner = Frame {
|
||||||
|
child: leaf.add_strong(&mut h.rsc),
|
||||||
|
region: UiRegion::new(UiSpan::new(Len::rel(0.23), Len::rel(0.83)), UiSpan::FULL),
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
let root = Frame {
|
||||||
|
child: inner,
|
||||||
|
region,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
(root, leaf, draws)
|
||||||
|
};
|
||||||
|
let mut warm = Harness::new((400, 200));
|
||||||
|
let (root, leaf, draws) = plant(&mut warm, fixed(7.0, 104.0));
|
||||||
|
let before = draws.get();
|
||||||
|
warm.rsc[root].region = fixed(19.0, 180.0);
|
||||||
|
warm.frame();
|
||||||
|
assert_eq!(draws.get(), before);
|
||||||
|
let mut cold = Harness::new((400, 200));
|
||||||
|
let (_, other, _) = plant(&mut cold, fixed(19.0, 180.0));
|
||||||
|
assert_eq!(warm.region(&leaf), cold.region(&other));
|
||||||
|
assert_eq!(
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
|
||||||
|
struct Glyphs {
|
||||||
|
buffer: TextBuffer,
|
||||||
|
draws: Rc<Cell<usize>>,
|
||||||
|
}
|
||||||
|
impl Widget for Glyphs {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
self.draws.set(self.draws.get() + 1);
|
||||||
|
let text = painter.render_text(&mut self.buffer, &TextAttrs::default(), None);
|
||||||
|
let origin = UiRegion::new(
|
||||||
|
UiSpan::new(Len::rel(0.23) + Len::px(-7.125), Len::FULL),
|
||||||
|
UiSpan::new(Len::rel(0.37) + Len::px(3.25), Len::FULL),
|
||||||
|
);
|
||||||
|
painter.glyphs(text, origin);
|
||||||
|
Size::LEFTOVER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct Frame {
|
||||||
|
child: StrongWidget,
|
||||||
|
frame: UiRegion,
|
||||||
|
region: UiRegion,
|
||||||
|
}
|
||||||
|
impl Widget for Frame {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
painter.widget_at(
|
||||||
|
&self.child,
|
||||||
|
PlaceDesc::new(
|
||||||
|
self.region.x.shifted_desc().fills(),
|
||||||
|
self.region.y.shifted_desc().fills(),
|
||||||
|
)
|
||||||
|
.rel_base(Axis::X, self.frame.x.len()),
|
||||||
|
);
|
||||||
|
Size::LEFTOVER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for node in [false, true] {
|
||||||
|
let mut h = Harness::new((403, 211));
|
||||||
|
let draws = Rc::new(Cell::new(0));
|
||||||
|
let text = Glyphs {
|
||||||
|
buffer: TextBuffer::new("Glyphs: gj AV\nsecond line"),
|
||||||
|
draws: draws.clone(),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.rsc.widgets_mut().set_region_node(text, node);
|
||||||
|
let root = Frame {
|
||||||
|
child: text.add_strong(&mut h.rsc),
|
||||||
|
frame: UiRegion::FULL,
|
||||||
|
region: 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].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)),
|
||||||
|
);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(draws.get(), before);
|
||||||
|
let retained = primitive_bounds(&h, text.id());
|
||||||
|
assert!(!retained.is_empty());
|
||||||
|
let _ = h.rsc.widgets_mut().get_dyn_mut(text.id());
|
||||||
|
h.frame();
|
||||||
|
assert!(draws.get() > before);
|
||||||
|
assert_eq!(retained, primitive_bounds(&h, text.id()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resizing_does_not_remeasure_a_fixed_stack_for_its_unmeasured_overlay() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (sizing, _) = counted(&mut h, Size::from((100, 80)), false);
|
||||||
|
let (overlay, draws) = counted(&mut h, Size::LEFTOVER, true);
|
||||||
|
h.set_root((sizing, overlay).stack().size(StackSize::Child(0)));
|
||||||
|
let settled = draws.get();
|
||||||
|
|
||||||
|
h.resize((800, 300));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_eq!(draws.get(), settled);
|
||||||
|
assert_corners!(h, overlay, (350, 110), (450, 190));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Unmeasured {
|
||||||
|
child: StrongWidget,
|
||||||
|
draws: Rc<Cell<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Unmeasured {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
self.draws.set(self.draws.get() + 1);
|
||||||
|
painter.widget(&self.child);
|
||||||
|
Size::LEFTOVER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_declared_size_change_stops_at_an_independent_parent() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let leaf = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
let parent = Unmeasured {
|
||||||
|
child: leaf.add_strong(&mut h.rsc),
|
||||||
|
draws: Rc::new(Cell::new(0)),
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
let draws = Rc::new(Cell::new(0));
|
||||||
|
h.set_root(Unmeasured {
|
||||||
|
child: parent,
|
||||||
|
draws: draws.clone(),
|
||||||
|
});
|
||||||
|
let settled = draws.get();
|
||||||
|
|
||||||
|
h.set_len(leaf, Axis::X, 150);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_corners!(h, leaf, (125, 0), (275, 200));
|
||||||
|
assert_eq!(draws.get(), settled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let draws = Rc::new(Cell::new(0));
|
||||||
|
let leaf = ReadsWidth {
|
||||||
|
draws: draws.clone(),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root((leaf,).stack());
|
||||||
|
let settled = draws.get();
|
||||||
|
|
||||||
|
h.resize((800, 200));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert!(draws.get() > settled);
|
||||||
|
assert_corners!(h, leaf, (300, 90), (500, 110));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changed_drawing_dependencies_reach_ancestors_without_a_size_change() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
|
||||||
|
h.set_root(((leaf,).stack(),).stack());
|
||||||
|
|
||||||
|
h.rsc[leaf].reads_box = true;
|
||||||
|
h.frame();
|
||||||
|
let settled = draws.get();
|
||||||
|
h.resize((800, 200));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_eq!(draws.get(), settled + 1);
|
||||||
|
assert_corners!(h, leaf, (0, 0), (800, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn widening_and_restoring_a_contract_does_not_invalidate_its_reader() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (leaf, leaf_draws) = counted(&mut h, Size::LEFTOVER, true);
|
||||||
|
let draws = Rc::new(Cell::new(0));
|
||||||
|
let child = leaf.add_strong(&mut h.rsc);
|
||||||
|
h.set_root(Unmeasured {
|
||||||
|
child,
|
||||||
|
draws: draws.clone(),
|
||||||
|
});
|
||||||
|
let settled = draws.get();
|
||||||
|
for reads_box in [false, true, false, true] {
|
||||||
|
h.rsc[leaf].reads_box = reads_box;
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(draws.get(), settled);
|
||||||
|
}
|
||||||
|
let settled = leaf_draws.get();
|
||||||
|
h.resize((800, 200));
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(leaf_draws.get(), settled + 1);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn padding_and_stack_boxes_follow_the_region_without_drawing_again() {
|
||||||
|
struct Observed<W> {
|
||||||
|
widget: W,
|
||||||
|
draws: Rc<Cell<usize>>,
|
||||||
|
}
|
||||||
|
impl<W: Widget> Widget for Observed<W> {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
self.draws.set(self.draws.get() + 1);
|
||||||
|
self.widget.draw(painter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct Frame {
|
||||||
|
child: StrongWidget,
|
||||||
|
region: UiRegion,
|
||||||
|
}
|
||||||
|
impl Widget for Frame {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
painter.widget_at(
|
||||||
|
&self.child,
|
||||||
|
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, 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);
|
||||||
|
let fixed = rect(Color::RED).width(31).height(19).add(&mut h.rsc);
|
||||||
|
let stack = Observed {
|
||||||
|
widget: Stack {
|
||||||
|
children: vec![leaf.add_strong(&mut h.rsc), fixed.add_strong(&mut h.rsc)],
|
||||||
|
size: StackSize::Default,
|
||||||
|
},
|
||||||
|
draws: draws.clone(),
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
let pad = Observed {
|
||||||
|
widget: Pad {
|
||||||
|
inner: stack,
|
||||||
|
padding: Padding::uniform(7).with_left(13),
|
||||||
|
},
|
||||||
|
draws: draws.clone(),
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
let root = Frame { child: pad, region }.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
(root, leaf, fixed, draws)
|
||||||
|
};
|
||||||
|
// The same box in three places. A pad places its child as lengths of
|
||||||
|
// its own box measured from where that box starts, so moving it is
|
||||||
|
// nothing to the pad -- where changing its length is a different
|
||||||
|
// question, and does draw it again.
|
||||||
|
let at = |start: f32| {
|
||||||
|
let span = |start: Len| UiSpan::new(start, start + Len::rel(0.4));
|
||||||
|
UiRegion::new(span(Len::rel(start) + Len::px(3.125)), span(Len::px(11.25)))
|
||||||
|
};
|
||||||
|
let mut warm = Harness::new((403, 211));
|
||||||
|
let (root, leaf, fixed, draws) = plant(&mut warm, at(0.13));
|
||||||
|
for start in [0.13, -0.17, 0.31] {
|
||||||
|
let region = at(start);
|
||||||
|
let before = draws.get();
|
||||||
|
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, 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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 {
|
||||||
|
let width = painter.px_len(Axis::X);
|
||||||
|
painter.primitive(RectPrimitive::color(Color::BLUE));
|
||||||
|
Size::from((80, if width > Px::from_int(100) { 40 } else { 60 }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct Frame {
|
||||||
|
child: StrongWidget,
|
||||||
|
start: f32,
|
||||||
|
}
|
||||||
|
impl Widget for Frame {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
painter.widget_at(
|
||||||
|
&self.child,
|
||||||
|
PlaceDesc::new(
|
||||||
|
UiSpan::new(Len::px(self.start), Len::px(self.start + 200.0))
|
||||||
|
.shifted_desc()
|
||||||
|
.fills(),
|
||||||
|
UiSpan::FULL.shifted_desc().fills(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Size::LEFTOVER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let leaf = Measured.add(&mut h.rsc);
|
||||||
|
let stack = (leaf,).stack().add_strong(&mut h.rsc);
|
||||||
|
let root = Frame {
|
||||||
|
child: stack,
|
||||||
|
start: 0.0,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
assert_corners!(h, leaf, (60, 80), (140, 120));
|
||||||
|
h.rsc[root].start = 30.0;
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, leaf, (90, 80), (170, 120));
|
||||||
|
assert_eq!(
|
||||||
|
primitive_bounds(&h, leaf.id()),
|
||||||
|
vec![h.region(&leaf).unwrap()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changing_regions_keep_fractional_reports_and_numeric_dependencies_valid() {
|
||||||
|
struct Container {
|
||||||
|
child: StrongWidget,
|
||||||
|
region: UiRegion,
|
||||||
|
}
|
||||||
|
impl Widget for Container {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
painter
|
||||||
|
.widget_at(
|
||||||
|
&self.child,
|
||||||
|
PlaceDesc::new(self.region.x.shifted_desc(), self.region.y.shifted_desc()),
|
||||||
|
)
|
||||||
|
.size()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct Frame {
|
||||||
|
child: StrongWidget,
|
||||||
|
region: UiRegion,
|
||||||
|
answer: Rc<Cell<Size>>,
|
||||||
|
}
|
||||||
|
impl Widget for Frame {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
self.answer.set(
|
||||||
|
painter
|
||||||
|
.widget_at(
|
||||||
|
&self.child,
|
||||||
|
PlaceDesc::new(
|
||||||
|
self.region.x.shifted_desc().fills(),
|
||||||
|
self.region.y.shifted_desc().fills(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.size(),
|
||||||
|
);
|
||||||
|
Size::LEFTOVER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for fractional in [false, true] {
|
||||||
|
for region in [
|
||||||
|
UiRegion::FULL,
|
||||||
|
UiRegion::new(UiSpan::new(Len::rel(0.13), Len::rel(0.79)), UiSpan::FULL),
|
||||||
|
] {
|
||||||
|
let plant = |h: &mut Harness, outer| {
|
||||||
|
let size = if fractional {
|
||||||
|
Size {
|
||||||
|
x: rel(0.5),
|
||||||
|
y: LayoutLen::px(27),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Size::from((80, 27))
|
||||||
|
};
|
||||||
|
let (leaf, _) = counted(h, size, !fractional);
|
||||||
|
let child = Container {
|
||||||
|
child: leaf.add_strong(&mut h.rsc),
|
||||||
|
region,
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
let answer = Rc::new(Cell::new(Size::ZERO));
|
||||||
|
let root = Frame {
|
||||||
|
child,
|
||||||
|
region: outer,
|
||||||
|
answer: answer.clone(),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
(root, leaf, answer)
|
||||||
|
};
|
||||||
|
let mut warm = Harness::new((403, 211));
|
||||||
|
let (root, leaf, answer) = plant(&mut warm, UiRegion::FULL);
|
||||||
|
for width in [191.125, 297.25, 83.75] {
|
||||||
|
let region =
|
||||||
|
UiRegion::new(UiSpan::new(Len::px(13.125), Len::px(width)), UiSpan::FULL);
|
||||||
|
warm.rsc[root].region = region;
|
||||||
|
warm.frame();
|
||||||
|
let mut cold = Harness::new((403, 211));
|
||||||
|
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().get_dyn_mut(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();
|
||||||
|
let clip = h.rsc.ui().masks[mask.idx()];
|
||||||
|
let clip = h
|
||||||
|
.render
|
||||||
|
.moves
|
||||||
|
.resolve(clip.move_idx, clip.region)
|
||||||
|
.to_px(h.render.output_size());
|
||||||
|
assert_eq!(clip, 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
+80
-1
@@ -60,6 +60,69 @@ fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
|
|||||||
assert_corners!(h, top, (0, 0), (400, 200));
|
assert_corners!(h, top, (0, 0), (400, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_content_and_a_share_fill_one_viewport() {
|
||||||
|
let mut h = Harness::new((900, 100));
|
||||||
|
let content = rect(Color::RED)
|
||||||
|
.width(LayoutLen {
|
||||||
|
px: Px::from_int(600),
|
||||||
|
rel: Rel::ZERO,
|
||||||
|
leftover: Weight::ONE,
|
||||||
|
})
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
||||||
|
h.set_root(scroll);
|
||||||
|
|
||||||
|
assert_corners!(h, content, (0, 0), (900, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_content_wider_than_the_viewport_still_scrolls() {
|
||||||
|
let mut h = Harness::new((900, 100));
|
||||||
|
let content = rect(Color::RED).width(1200).add(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
||||||
|
h.set_root(scroll);
|
||||||
|
|
||||||
|
assert_corners!(h, content, (-300, 0), (900, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_lone_share_fills_without_scrolling() {
|
||||||
|
let mut h = Harness::new((900, 100));
|
||||||
|
let content = rect(Color::RED).width(LayoutLen::LEFTOVER).add(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
||||||
|
h.set_root(scroll);
|
||||||
|
|
||||||
|
assert_corners!(h, content, (0, 0), (900, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrapping_content_beside_a_fixed_length_is_stable_warm_and_cold() {
|
||||||
|
fn plant(h: &mut Harness) -> (WidgetId, WidgetId) {
|
||||||
|
let fixed = rect(Color::RED).width(600).add(&mut h.rsc);
|
||||||
|
let text = wtext("Wrapping shapes one source into as many lines as the box leaves room for, so a paragraph's height is an answer and not a setting.")
|
||||||
|
.size(16)
|
||||||
|
.wrap(true)
|
||||||
|
.width(LayoutLen::LEFTOVER)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let content = (fixed, text).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
||||||
|
h.set_root(scroll);
|
||||||
|
(text.id(), content.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut warm = Harness::new((900, 300));
|
||||||
|
let (text, content) = plant(&mut warm);
|
||||||
|
warm.rsc.widgets_mut().get_dyn_mut(text);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 300));
|
||||||
|
let (cold_text, cold_content) = plant(&mut cold);
|
||||||
|
|
||||||
|
assert_eq!(warm.region(&text), cold.region(&cold_text));
|
||||||
|
assert_eq!(warm.region(&content), cold.region(&cold_content));
|
||||||
|
}
|
||||||
|
|
||||||
/// A widget that clips to its box may not report more than the box: its
|
/// A widget that clips to its box may not report more than the box: its
|
||||||
/// parent would place the part it cut off, and the framework would put a
|
/// parent would place the part it cut off, and the framework would put a
|
||||||
/// drawing longer than its box somewhere. `Masked` is the second of these
|
/// drawing longer than its box somewhere. `Masked` is the second of these
|
||||||
@@ -71,7 +134,7 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
|
|||||||
|
|
||||||
impl Widget for Clipper {
|
impl Widget for Clipper {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
painter.set_mask(painter.region());
|
painter.set_mask(UiRegion::FULL);
|
||||||
painter.widget(&self.0).size()
|
painter.widget(&self.0).size()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,3 +145,19 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
|
|||||||
h.set_root(clipper);
|
h.set_root(clipper);
|
||||||
h.frame();
|
h.frame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Content that fits sits in the viewport, not in a box of the window's
|
||||||
|
/// length anchored at the viewport's start. `Part::From` takes window
|
||||||
|
/// lengths, so a `rel(1.0)` span in one is the window, and only a scroll
|
||||||
|
/// filling the window would land right.
|
||||||
|
#[test]
|
||||||
|
fn content_that_fits_is_placed_in_the_viewport_and_not_in_the_window() {
|
||||||
|
let mut h = Harness::new((400, 400));
|
||||||
|
let head = rect(Color::RED).height(100).add(&mut h.rsc);
|
||||||
|
let inner = rect(Color::BLUE).height(50).add(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(inner.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
||||||
|
h.set_root((head, scroll).span(Dir::DOWN));
|
||||||
|
|
||||||
|
assert_corners!(h, scroll, (0, 100), (400, 400));
|
||||||
|
assert_corners!(h, inner, (0, 225), (400, 275));
|
||||||
|
}
|
||||||
@@ -14,6 +14,180 @@ use iris::harness::Harness;
|
|||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use iris::random::Branch;
|
use iris::random::Branch;
|
||||||
|
|
||||||
|
fn assert_same_regions(
|
||||||
|
warm: &Harness,
|
||||||
|
warm_ids: &[WidgetId],
|
||||||
|
cold: &Harness,
|
||||||
|
cold_ids: &[WidgetId],
|
||||||
|
) {
|
||||||
|
let mut wrong = Vec::new();
|
||||||
|
for (i, (&w, &c)) in warm_ids.iter().zip(cold_ids).enumerate() {
|
||||||
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
|
if got != want {
|
||||||
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ten widgets, shrunk from seed 2 at depth 5. The stack is as tall as its
|
||||||
|
/// first child, so its other children belong in that one-line box. A cold
|
||||||
|
/// layout used to keep the span's answer from the larger measuring box while
|
||||||
|
/// a repaint asked it in the stack's final box.
|
||||||
|
fn plant_stack_in_its_sizing_childs_box(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
|
let sizing = wtext("one line, overflowing whatever it is given")
|
||||||
|
.size(16)
|
||||||
|
.wrap(false)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let filler = rect(Color::CYAN.alpha(252)).add(&mut h.rsc);
|
||||||
|
let plain = wtext("one line, overflowing whatever it is given")
|
||||||
|
.size(16)
|
||||||
|
.wrap(false)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let span = (filler, plain).span(Dir::DOWN).add(&mut h.rsc);
|
||||||
|
let pad = Pad {
|
||||||
|
padding: Padding::ZERO,
|
||||||
|
inner: span.add_strong(&mut h.rsc),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let wide = rect(Color::YELLOW.alpha(252)).add(&mut h.rsc);
|
||||||
|
let narrow = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let branch = Branch {
|
||||||
|
probe: probe.add_strong(&mut h.rsc),
|
||||||
|
wide: wide.add_strong(&mut h.rsc),
|
||||||
|
narrow: narrow.add_strong(&mut h.rsc),
|
||||||
|
threshold: 55.0,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let stack = Stack {
|
||||||
|
children: vec![
|
||||||
|
sizing.add_strong(&mut h.rsc),
|
||||||
|
pad.add_strong(&mut h.rsc),
|
||||||
|
branch.add_strong(&mut h.rsc),
|
||||||
|
],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rules(stack.id(), Some(LayoutLen::LEFTOVER), None);
|
||||||
|
h.set_root(stack);
|
||||||
|
vec![
|
||||||
|
sizing.id(),
|
||||||
|
filler.id(),
|
||||||
|
plain.id(),
|
||||||
|
span.id(),
|
||||||
|
pad.id(),
|
||||||
|
probe.id(),
|
||||||
|
wide.id(),
|
||||||
|
narrow.id(),
|
||||||
|
branch.id(),
|
||||||
|
stack.id(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repainting_a_stack_uses_the_box_its_sizing_child_decided() {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let ids = plant_stack_in_its_sizing_childs_box(&mut warm);
|
||||||
|
for &id in &ids {
|
||||||
|
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||||
|
}
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let cold_ids = plant_stack_in_its_sizing_childs_box(&mut cold);
|
||||||
|
|
||||||
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ten widgets, shrunk from seed 108 at depth 5. The nested reverse spans
|
||||||
|
/// evaluate the branch in successively narrower boxes. The answer from the
|
||||||
|
/// final, decided box must be the one retained after every span is reordered.
|
||||||
|
fn plant_branch_in_nested_reverse_spans(
|
||||||
|
h: &mut Harness,
|
||||||
|
reordered: bool,
|
||||||
|
) -> (Vec<WidgetId>, [WeakWidget<Span>; 3]) {
|
||||||
|
let pair = |first: StrongWidget, second: StrongWidget| match reordered {
|
||||||
|
true => vec![second, first],
|
||||||
|
false => vec![first, second],
|
||||||
|
};
|
||||||
|
let probe = rect(Color::RED.alpha(63)).add(&mut h.rsc);
|
||||||
|
let wide = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let narrow = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let branch = Branch {
|
||||||
|
probe: probe.add_strong(&mut h.rsc),
|
||||||
|
wide: wide.add_strong(&mut h.rsc),
|
||||||
|
narrow: narrow.add_strong(&mut h.rsc),
|
||||||
|
threshold: 483.0,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let wrapped = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let down = Span {
|
||||||
|
children: pair(
|
||||||
|
branch.add_strong(&mut h.rsc),
|
||||||
|
wrapped.add_strong(&mut h.rsc),
|
||||||
|
),
|
||||||
|
dir: Dir::DOWN,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let inner_filler = rect(Color::CYAN.alpha(63)).add(&mut h.rsc);
|
||||||
|
let inner = Span {
|
||||||
|
children: pair(
|
||||||
|
down.add_strong(&mut h.rsc),
|
||||||
|
inner_filler.add_strong(&mut h.rsc),
|
||||||
|
),
|
||||||
|
dir: Dir::LEFT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.height(LayoutLen::rel(1.0))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let outer_filler = rect(Color::GREEN.alpha(63)).add(&mut h.rsc);
|
||||||
|
let outer = Span {
|
||||||
|
children: pair(
|
||||||
|
inner.add_strong(&mut h.rsc),
|
||||||
|
outer_filler.add_strong(&mut h.rsc),
|
||||||
|
),
|
||||||
|
dir: Dir::LEFT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.height(LayoutLen::rel(1.0))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(outer);
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
probe.id(),
|
||||||
|
wide.id(),
|
||||||
|
narrow.id(),
|
||||||
|
branch.id(),
|
||||||
|
wrapped.id(),
|
||||||
|
down.id(),
|
||||||
|
inner_filler.id(),
|
||||||
|
inner.id(),
|
||||||
|
outer_filler.id(),
|
||||||
|
outer.id(),
|
||||||
|
],
|
||||||
|
[down, inner, outer],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reordering_nested_spans_keeps_the_answer_from_the_decided_box() {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let (ids, spans) = plant_branch_in_nested_reverse_spans(&mut warm, false);
|
||||||
|
for span in spans {
|
||||||
|
warm.rsc[span].children.rotate_left(1);
|
||||||
|
}
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let (cold_ids, _) = plant_branch_in_nested_reverse_spans(&mut cold, true);
|
||||||
|
|
||||||
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
|
}
|
||||||
|
|
||||||
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
||||||
/// the tree changes -- every widget is marked for redraw and the frame is
|
/// the tree changes -- every widget is marked for redraw and the frame is
|
||||||
/// taken again -- so no box may move, and a warm frame has to land where a
|
/// taken again -- so no box may move, and a warm frame has to land where a
|
||||||
@@ -559,6 +733,51 @@ fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
|
|||||||
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \
|
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \
|
||||||
box leaves room for, so a paragraph's height is an answer and not a setting.";
|
box leaves room for, so a paragraph's height is an answer and not a setting.";
|
||||||
|
|
||||||
|
fn plant_stack_resized_from_free(h: &mut Harness, fixed: bool) -> (Vec<WidgetId>, WidgetId) {
|
||||||
|
let sizing = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
|
||||||
|
h.rsc.widgets_mut().set_size_rules(sizing.id(), None, None);
|
||||||
|
if fixed {
|
||||||
|
h.rsc.widgets_mut().set_size_rules(
|
||||||
|
sizing.id(),
|
||||||
|
Some(LayoutLen::px(112)),
|
||||||
|
Some(LayoutLen::px(101)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let pad = Pad {
|
||||||
|
padding: Padding::ZERO,
|
||||||
|
inner: text.add_strong(&mut h.rsc),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let stack = Stack {
|
||||||
|
children: vec![sizing.add_strong(&mut h.rsc), pad.add_strong(&mut h.rsc)],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(stack);
|
||||||
|
(
|
||||||
|
vec![sizing.id(), text.id(), pad.id(), stack.id()],
|
||||||
|
sizing.id(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixing_a_stacks_sizing_child_repositions_its_overlay() {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let (ids, sizing) = plant_stack_resized_from_free(&mut warm, false);
|
||||||
|
warm.frame();
|
||||||
|
warm.rsc.widgets_mut().set_size_rules(
|
||||||
|
sizing,
|
||||||
|
Some(LayoutLen::px(112)),
|
||||||
|
Some(LayoutLen::px(101)),
|
||||||
|
);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let (cold_ids, _) = plant_stack_resized_from_free(&mut cold, true);
|
||||||
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
|
}
|
||||||
|
|
||||||
/// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4,
|
/// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4,
|
||||||
/// `shuffle-swap-for-three`). The stack takes its size from the span above,
|
/// `shuffle-swap-for-three`). The stack takes its size from the span above,
|
||||||
/// the span takes its width from the longest line of the texts in it, and
|
/// the span takes its width from the longest line of the texts in it, and
|
||||||
@@ -614,3 +833,266 @@ fn a_text_is_given_back_a_box_the_line_it_measured_fits_in() {
|
|||||||
|
|
||||||
assert_eq!(warm.region(&text), cold.region(&cold_text));
|
assert_eq!(warm.region(&text), cold.region(&cold_text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adding_text_to_a_reverse_row_keeps_its_shared_height() {
|
||||||
|
fn build(
|
||||||
|
h: &mut Harness,
|
||||||
|
changed: bool,
|
||||||
|
) -> (WeakWidget<Span>, WeakWidget<Text>, Vec<StrongWidget>) {
|
||||||
|
let wrap = wtext("Wrapping shapes one source into as many lines as the box leaves room for, so a paragraph's height is an answer and not a setting.").size(16).wrap(true).add_strong(&mut h.rsc);
|
||||||
|
let one = || {
|
||||||
|
wtext("one line, overflowing whatever it is given")
|
||||||
|
.size(16)
|
||||||
|
.wrap(false)
|
||||||
|
};
|
||||||
|
let plain = one().add_strong(&mut h.rsc);
|
||||||
|
let shared = one()
|
||||||
|
.width(LayoutLen::LEFTOVER)
|
||||||
|
.height(LayoutLen::LEFTOVER)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let mut extra: Vec<StrongWidget> = vec![
|
||||||
|
rect(Color::RED).add_strong(&mut h.rsc),
|
||||||
|
one().add_strong(&mut h.rsc),
|
||||||
|
one().add_strong(&mut h.rsc),
|
||||||
|
];
|
||||||
|
let children: Vec<StrongWidget> = if changed {
|
||||||
|
let mut children: Vec<StrongWidget> = vec![plain, shared.add_strong(&mut h.rsc)];
|
||||||
|
children.append(&mut extra);
|
||||||
|
children
|
||||||
|
} else {
|
||||||
|
vec![wrap, plain, shared.add_strong(&mut h.rsc)]
|
||||||
|
};
|
||||||
|
let row = Span {
|
||||||
|
children,
|
||||||
|
dir: Dir::LEFT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.height(LayoutLen::rel(1.0))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let fill: StrongWidget = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let children: Vec<StrongWidget> = vec![fill, row.add_strong(&mut h.rsc)];
|
||||||
|
let root = Span {
|
||||||
|
children,
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::from_int(4),
|
||||||
|
}
|
||||||
|
.height(LayoutLen::rel(1.0))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
(row, shared, extra)
|
||||||
|
}
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let (row, shared, extra) = build(&mut warm, false);
|
||||||
|
warm.rsc[row].children.remove(0);
|
||||||
|
warm.rsc[row].children.extend(extra);
|
||||||
|
warm.frame();
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let (_, other, _) = build(&mut cold, true);
|
||||||
|
assert_eq!(warm.region(&shared), cold.region(&other));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nine widgets, shrunk from seed 946 at depth 6. The column is a share of
|
||||||
|
/// the row while its rect has room to draw and a fixed width once it has
|
||||||
|
/// not, so the row asks it twice: in the room, where it answers a share,
|
||||||
|
/// and in its slot, where it answers its text's width. Emptying the column
|
||||||
|
/// changes only the first answer. A local redraw that asked only the second
|
||||||
|
/// question kept the row as it was; the column has to defer to the row.
|
||||||
|
fn plant_column_that_is_a_share_only_while_its_rect_fits(
|
||||||
|
h: &mut Harness,
|
||||||
|
emptied: bool,
|
||||||
|
) -> (Vec<WidgetId>, WeakWidget<Span>, Vec<StrongWidget>) {
|
||||||
|
let first = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let filler = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
|
||||||
|
let second = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let mut spare: Vec<StrongWidget> =
|
||||||
|
vec![filler.add_strong(&mut h.rsc), second.add_strong(&mut h.rsc)];
|
||||||
|
let mut children: Vec<StrongWidget> = vec![first.add_strong(&mut h.rsc)];
|
||||||
|
if !emptied {
|
||||||
|
children.append(&mut spare);
|
||||||
|
}
|
||||||
|
let column = Span {
|
||||||
|
children,
|
||||||
|
dir: Dir::DOWN,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.height(159)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let left = rect(Color::MAGENTA.alpha(189)).add(&mut h.rsc);
|
||||||
|
let right = rect(Color::BLUE.alpha(0)).add(&mut h.rsc);
|
||||||
|
let row = Span {
|
||||||
|
children: vec![
|
||||||
|
left.add_strong(&mut h.rsc),
|
||||||
|
column.add_strong(&mut h.rsc),
|
||||||
|
right.add_strong(&mut h.rsc),
|
||||||
|
],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let end = rect(Color::MAGENTA.alpha(189)).add(&mut h.rsc);
|
||||||
|
let root = Span {
|
||||||
|
children: vec![end.add_strong(&mut h.rsc), row.add_strong(&mut h.rsc)],
|
||||||
|
dir: Dir::LEFT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
first.id(),
|
||||||
|
filler.id(),
|
||||||
|
second.id(),
|
||||||
|
column.id(),
|
||||||
|
left.id(),
|
||||||
|
right.id(),
|
||||||
|
row.id(),
|
||||||
|
end.id(),
|
||||||
|
root.id(),
|
||||||
|
],
|
||||||
|
column,
|
||||||
|
spare,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emptying_a_column_the_row_asked_twice_asks_the_row_again() {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let (ids, column, _spare) =
|
||||||
|
plant_column_that_is_a_share_only_while_its_rect_fits(&mut warm, false);
|
||||||
|
warm.frame();
|
||||||
|
// Kept alive: dropping the last share of a widget frees its id.
|
||||||
|
let _removed: Vec<StrongWidget> = warm.rsc[column].children.drain(1..).collect();
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let (cold_ids, _, _spare) =
|
||||||
|
plant_column_that_is_a_share_only_while_its_rect_fits(&mut cold, true);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Six widgets, shrunk from seed 59 at depth 5 (`resize-size`). The column
|
||||||
|
/// divides the box it is given between two shares, so its drawing holds for
|
||||||
|
/// that box's length alone, and the pads above it pass that dependency up:
|
||||||
|
/// each one's box is a part of the box it was asked in. Padding narrowing
|
||||||
|
/// the frame it hands down does not change that, and while it was taken to,
|
||||||
|
/// changing the rule over the pads relocated the column's drawing into the
|
||||||
|
/// new box instead of dividing it again.
|
||||||
|
fn plant_two_shares_under_two_pads(h: &mut Harness, height: f32) -> Vec<WidgetId> {
|
||||||
|
let top = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
|
||||||
|
let bottom = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let column = (top, bottom).span(Dir::DOWN).add(&mut h.rsc);
|
||||||
|
let inner = Pad {
|
||||||
|
padding: Padding::ZERO,
|
||||||
|
inner: column.add_strong(&mut h.rsc),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let outer = Pad {
|
||||||
|
padding: Padding::ZERO,
|
||||||
|
inner: inner.add_strong(&mut h.rsc),
|
||||||
|
}
|
||||||
|
.height(height)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let beside = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((outer, beside).span(Dir::RIGHT));
|
||||||
|
vec![
|
||||||
|
top.id(),
|
||||||
|
bottom.id(),
|
||||||
|
column.id(),
|
||||||
|
inner.id(),
|
||||||
|
outer.id(),
|
||||||
|
beside.id(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changing_a_rule_over_two_pads_divides_the_column_again() {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let ids = plant_two_shares_under_two_pads(&mut warm, 88.0);
|
||||||
|
warm.frame();
|
||||||
|
warm.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rules(ids[4], None, Some(LayoutLen::px(105)));
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let cold_ids = plant_two_shares_under_two_pads(&mut cold, 105.0);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Six widgets, shrunk from seed 942 at depth 6 (`resize`). A `Branch` asks
|
||||||
|
/// its probe in the top 40 px of its box and forwards the frame, so the
|
||||||
|
/// scroll's own box is 40 px tall whatever the window is -- but its content
|
||||||
|
/// is as tall as the frame, which is the window, and a scroll kept to its
|
||||||
|
/// end has to be told when that changes. Resolving a length against the
|
||||||
|
/// window is what reads it, so that is where the dependency is taken.
|
||||||
|
fn plant_a_window_tall_column_in_a_short_scroll(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
|
let leaf = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let column = Span {
|
||||||
|
children: vec![leaf.add_strong(&mut h.rsc)],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.height(rel(1.0))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(column.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
||||||
|
let wide = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let narrow = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
|
let root = Branch {
|
||||||
|
probe: scroll.add_strong(&mut h.rsc),
|
||||||
|
wide: wide.add_strong(&mut h.rsc),
|
||||||
|
narrow: narrow.add_strong(&mut h.rsc),
|
||||||
|
threshold: 55.0,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
vec![leaf.id(), column.id(), scroll.id(), root.id()]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resizing_under_a_short_scroll_snaps_its_window_tall_content_again() {
|
||||||
|
let mut warm = Harness::new((1920, 1200));
|
||||||
|
let ids = plant_a_window_tall_column_in_a_short_scroll(&mut warm);
|
||||||
|
warm.frame();
|
||||||
|
warm.resize((640, 900));
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let cold_ids = plant_a_window_tall_column_in_a_short_scroll(&mut cold);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scroll clamps its position against the box it is drawn in, so drawing it
|
||||||
|
/// once at one viewport and again at another writes state the second draw then
|
||||||
|
/// reads. That the answer is still the one a cold layout gives is a property
|
||||||
|
/// of the clamp, not something the layout enforces.
|
||||||
|
#[test]
|
||||||
|
fn a_scrolled_view_resized_lands_where_a_cold_layout_puts_it() {
|
||||||
|
for amt in [10.0, 40.0, 90.0, 140.0] {
|
||||||
|
let mut warm = Harness::new((100, 100));
|
||||||
|
let (_, warm_scroll) = plant_wider(&mut warm, 100.0);
|
||||||
|
warm.move_to((50.0, 50.0));
|
||||||
|
warm.scroll((-amt, 0.0));
|
||||||
|
warm.frame();
|
||||||
|
warm.resize((160, 100));
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((160, 100));
|
||||||
|
let (_, cold_scroll) = plant_wider(&mut cold, 100.0);
|
||||||
|
cold.move_to((50.0, 50.0));
|
||||||
|
cold.scroll((-amt, 0.0));
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
warm.region(&warm_scroll),
|
||||||
|
cold.region(&cold_scroll),
|
||||||
|
"scrolled by {amt} then widened"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
-45
@@ -14,7 +14,7 @@
|
|||||||
#[path = "scenario/mod.rs"]
|
#[path = "scenario/mod.rs"]
|
||||||
mod scenario;
|
mod scenario;
|
||||||
|
|
||||||
use iris::random::{Edits, plan};
|
use iris::random::{Edits, Plan, plan};
|
||||||
use scenario::{ALL, Case, diverges, env, over_seeds};
|
use scenario::{ALL, Case, diverges, env, over_seeds};
|
||||||
|
|
||||||
/// How deep the generator branches. The generator widens two to four ways per
|
/// How deep the generator branches. The generator widens two to four ways per
|
||||||
@@ -32,8 +32,11 @@ fn depth() -> usize {
|
|||||||
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
|
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
|
||||||
|
|
||||||
fn check(seed: u64, depth: usize, case: Case) {
|
fn check(seed: u64, depth: usize, case: Case) {
|
||||||
let grown = plan(seed, depth, &Edits::default());
|
check_plan(&plan(seed, depth, &Edits::default()), seed, depth, case);
|
||||||
if let Some(how) = diverges(&grown, case, seed) {
|
}
|
||||||
|
|
||||||
|
fn check_plan(grown: &Plan, seed: u64, depth: usize, case: Case) {
|
||||||
|
if let Some(how) = diverges(grown, case, seed) {
|
||||||
panic!(
|
panic!(
|
||||||
"seed {seed} at depth {depth} differs after {}: {how}\n\
|
"seed {seed} at depth {depth} differs after {}: {how}\n\
|
||||||
reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \
|
reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \
|
||||||
@@ -44,57 +47,50 @@ fn check(seed: u64, depth: usize, case: Case) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! case {
|
/// A test per case, and the list of which cases have one, from the same
|
||||||
($name:ident, $case:expr) => {
|
/// 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]
|
#[test]
|
||||||
fn $name() {
|
fn $name() {
|
||||||
for seed in SEEDS {
|
for seed in SEEDS {
|
||||||
check(seed, depth(), $case);
|
check(seed, depth(), $case);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)*
|
||||||
|
|
||||||
|
const NAMED: [Case; [$($case,)*].len()] = [$($case,)*];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case!(
|
cases! {
|
||||||
many_widgets_redrawing_at_once_leaves_every_box_where_it_was,
|
many_widgets_redrawing_at_once_leaves_every_box_where_it_was = Case::RepaintSome,
|
||||||
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,
|
||||||
case!(
|
a_resize_and_a_repaint_land_where_starting_that_way_would = Case::ResizeRepaint,
|
||||||
everything_redrawing_at_once_leaves_every_box_where_it_was,
|
a_size_change_after_a_resize_lands_the_same_way = Case::ResizeSize,
|
||||||
Case::Repaint
|
a_resize_after_a_size_change_lands_the_same_way = Case::SizeResize,
|
||||||
);
|
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,
|
||||||
a_resize_lands_where_starting_at_that_size_would,
|
an_alignment_change_lands_where_growing_it_that_way_would = Case::Align,
|
||||||
Case::Resize
|
giving_and_taking_a_movable_region_rebuilds_what_resolves_it = Case::RegionNode,
|
||||||
);
|
reordering_a_span_lands_where_growing_it_that_way_would = Case::Reorder,
|
||||||
case!(
|
}
|
||||||
a_resize_and_a_repaint_land_where_starting_that_way_would,
|
|
||||||
Case::ResizeRepaint
|
/// The shuffles are one test between them, so they are the only cases `ALL`
|
||||||
);
|
/// may hold without a test of their own.
|
||||||
case!(
|
#[test]
|
||||||
a_size_change_after_a_resize_lands_the_same_way,
|
fn every_case_runs_without_the_long_scan() {
|
||||||
Case::ResizeSize
|
for case in ALL {
|
||||||
);
|
assert!(
|
||||||
case!(
|
NAMED.contains(&case) || matches!(case, Case::Shuffle(_)),
|
||||||
a_size_change_lands_where_growing_it_that_way_would,
|
"{} runs only in the long seed scan; give it a case here",
|
||||||
Case::Size
|
case.name()
|
||||||
);
|
|
||||||
case!(
|
|
||||||
every_size_changing_at_once_lands_where_growing_it_that_way_would,
|
|
||||||
Case::EverySize
|
|
||||||
);
|
|
||||||
case!(
|
|
||||||
an_alignment_change_lands_where_growing_it_that_way_would,
|
|
||||||
Case::Align
|
|
||||||
);
|
|
||||||
case!(
|
|
||||||
giving_and_taking_a_movable_region_rebuilds_what_resolves_it,
|
|
||||||
Case::RegionNode
|
|
||||||
);
|
|
||||||
case!(
|
|
||||||
reordering_a_span_lands_where_growing_it_that_way_would,
|
|
||||||
Case::Reorder
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
|
fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
|
||||||
@@ -119,8 +115,9 @@ fn a_long_run_of_seeds_agrees() {
|
|||||||
None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(),
|
None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(),
|
||||||
};
|
};
|
||||||
over_seeds(seeds, |seed| {
|
over_seeds(seeds, |seed| {
|
||||||
|
let grown = plan(seed, depth, &Edits::default());
|
||||||
for case in ALL {
|
for case in ALL {
|
||||||
check(seed, depth, case);
|
check_plan(&grown, seed, depth, case);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -5,11 +5,11 @@
|
|||||||
//! cargo test --release --features layout-diagnostics \
|
//! cargo test --release --features layout-diagnostics \
|
||||||
//! --test layout_diagnostics -- --ignored --nocapture
|
//! --test layout_diagnostics -- --ignored --nocapture
|
||||||
//!
|
//!
|
||||||
//! Uninstrumented hardware totals for one phase:
|
//! Build the uninstrumented test with `cargo test --release --test
|
||||||
|
//! layout_diagnostics --no-run`, then run the emitted executable directly:
|
||||||
//!
|
//!
|
||||||
//! IRIS_PHASE=resize IRIS_FRAMES=1000 perf stat \
|
//! IRIS_PHASE=resize IRIS_FRAMES=10000 perf stat -r 7 \
|
||||||
//! -e cycles:u,instructions:u cargo test --release \
|
//! -e cycles:u,instructions:u /path/to/layout_diagnostics --ignored --nocapture
|
||||||
//! --test layout_diagnostics -- --ignored --nocapture
|
|
||||||
//!
|
//!
|
||||||
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
|
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
|
||||||
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
|
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
|
||||||
@@ -134,6 +134,9 @@ fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
|
|||||||
{
|
{
|
||||||
let diagnostics = iris::core::layout_diagnostics::take();
|
let diagnostics = iris::core::layout_diagnostics::take();
|
||||||
print!("{}", diagnostics.per_frame(frames));
|
print!("{}", diagnostics.per_frame(frames));
|
||||||
|
for event in diagnostics.traces() {
|
||||||
|
println!(" {event:?}");
|
||||||
|
}
|
||||||
for callsite in diagnostics.hot_text().iter().take(3) {
|
for callsite in diagnostics.hot_text().iter().take(3) {
|
||||||
let mut ancestry = Vec::new();
|
let mut ancestry = Vec::new();
|
||||||
let mut id = Some(callsite.id);
|
let mut id = Some(callsite.id);
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
//! Prints where a cold layout puts every widget of many grown trees, so two
|
||||||
|
//! commits can be compared on cold layout alone. The warm/cold oracle cannot
|
||||||
|
//! see a change that moves cold layout, since both of its sides move; this
|
||||||
|
//! can, by diffing its output across the change:
|
||||||
|
//!
|
||||||
|
//! IRIS_DUMP_SEEDS=400 IRIS_DUMP_DEPTH=5 cargo test --release \
|
||||||
|
//! --test layout_dump -- --ignored --nocapture > /tmp/before.txt
|
||||||
|
//!
|
||||||
|
//! then the same after, and `diff` the two. A line is one widget: the seed,
|
||||||
|
//! its index in creation order, and its box in window pixels, or `-` where
|
||||||
|
//! it is not drawn.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::random::{Edits, grow};
|
||||||
|
|
||||||
|
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||||
|
std::env::var(name)
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.parse().ok())
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "a dump to diff across commits, not a check"]
|
||||||
|
fn every_cold_layout_is_printed() {
|
||||||
|
let seeds = env("IRIS_DUMP_SEEDS", 400_u64);
|
||||||
|
let depth = env("IRIS_DUMP_DEPTH", 5_usize);
|
||||||
|
let mut out = String::new();
|
||||||
|
for seed in 1..=seeds {
|
||||||
|
let mut harness = Harness::new((1920.0, 1200.0));
|
||||||
|
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
||||||
|
harness.state.root = Some(root);
|
||||||
|
harness.frame();
|
||||||
|
for (index, id) in tree.ids.iter().enumerate() {
|
||||||
|
match harness.region(id) {
|
||||||
|
Some(region) => out.push_str(&format!("{seed} {index} {region:?}\n")),
|
||||||
|
None => out.push_str(&format!("{seed} {index} -\n")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print!("{out}");
|
||||||
|
}
|
||||||
+59
-32
@@ -42,21 +42,6 @@ const OUTER: (f32, f32) = (1920.0, 1200.0);
|
|||||||
const INNER: (f32, f32) = (640.0, 900.0);
|
const INNER: (f32, f32) = (640.0, 900.0);
|
||||||
const STILL: (f32, f32) = (900.0, 1200.0);
|
const STILL: (f32, f32) = (900.0, 1200.0);
|
||||||
|
|
||||||
/// The same box, to two steps of the grid between the two ways of reaching
|
|
||||||
/// it. A move, a repaint, a row of shares and every length in pixels land on
|
|
||||||
/// the same number. What needs the slack is a position: a box centred in a
|
|
||||||
/// fraction of its parent against the same box centred in its own pixels,
|
|
||||||
/// and a box re-expressed as a fraction of a parent that changed length.
|
|
||||||
/// A step is a thousandth of a pixel, where this was a twentieth of one
|
|
||||||
/// before any of it was on a grid.
|
|
||||||
///
|
|
||||||
/// **One step is not enough**, tried 2026-09-17 once a length in pixels
|
|
||||||
/// stopped being composed: it passes the 100-seed oracle and fails the
|
|
||||||
/// 400-seed shrinker on `resize-size`, seeds 384 and 162, by 0.002 px. So
|
|
||||||
/// what is left here is the resize path's own rounding rather than a length
|
|
||||||
/// reached two ways.
|
|
||||||
const AGREE_STEPS: i32 = 2;
|
|
||||||
|
|
||||||
/// A way of changing what a span holds. Each is a shape worth its own case:
|
/// A way of changing what a span holds. Each is a shape worth its own case:
|
||||||
/// taking a child out of the middle is not the same as emptying a span, and
|
/// taking a child out of the middle is not the same as emptying a span, and
|
||||||
/// adding one is not the same as adding three.
|
/// adding one is not the same as adding three.
|
||||||
@@ -118,6 +103,11 @@ pub enum Case {
|
|||||||
/// A resize and then a size change, so a retained answer is asked to
|
/// A resize and then a size change, so a retained answer is asked to
|
||||||
/// survive two different kinds of invalidation in a row.
|
/// survive two different kinds of invalidation in a row.
|
||||||
ResizeSize,
|
ResizeSize,
|
||||||
|
/// A size change and then a resize, which is the other order and not the
|
||||||
|
/// same test: a length answered as a fraction of one box and kept as a
|
||||||
|
/// fraction of another agrees at the size it was changed at and parts
|
||||||
|
/// from it at every other one.
|
||||||
|
SizeResize,
|
||||||
/// A few declared sizes.
|
/// A few declared sizes.
|
||||||
Size,
|
Size,
|
||||||
/// Every declared size at once, so every reader of a size has a changed
|
/// Every declared size at once, so every reader of a size has a changed
|
||||||
@@ -134,12 +124,13 @@ pub enum Case {
|
|||||||
Shuffle(Shuffle),
|
Shuffle(Shuffle),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ALL: [Case; 15] = [
|
pub const ALL: [Case; 16] = [
|
||||||
Case::Repaint,
|
Case::Repaint,
|
||||||
Case::RepaintSome,
|
Case::RepaintSome,
|
||||||
Case::Resize,
|
Case::Resize,
|
||||||
Case::ResizeRepaint,
|
Case::ResizeRepaint,
|
||||||
Case::ResizeSize,
|
Case::ResizeSize,
|
||||||
|
Case::SizeResize,
|
||||||
Case::Size,
|
Case::Size,
|
||||||
Case::EverySize,
|
Case::EverySize,
|
||||||
Case::Align,
|
Case::Align,
|
||||||
@@ -161,6 +152,7 @@ impl Case {
|
|||||||
Self::Resize => "resize",
|
Self::Resize => "resize",
|
||||||
Self::ResizeRepaint => "resize-repaint",
|
Self::ResizeRepaint => "resize-repaint",
|
||||||
Self::ResizeSize => "resize-size",
|
Self::ResizeSize => "resize-size",
|
||||||
|
Self::SizeResize => "size-resize",
|
||||||
Self::Size => "size",
|
Self::Size => "size",
|
||||||
Self::EverySize => "every-size",
|
Self::EverySize => "every-size",
|
||||||
Self::Align => "align",
|
Self::Align => "align",
|
||||||
@@ -185,6 +177,15 @@ impl Case {
|
|||||||
_ => (STILL, STILL),
|
_ => (STILL, STILL),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The window the warm tree is taken to after the change, where the case
|
||||||
|
/// is about what the change left behind rather than about the change.
|
||||||
|
fn then_resize(self) -> Option<(f32, f32)> {
|
||||||
|
match self {
|
||||||
|
Self::SizeResize => Some(INNER),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
|
fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
|
||||||
@@ -299,7 +300,7 @@ fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mu
|
|||||||
warm.frame();
|
warm.frame();
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
Case::Size | Case::ResizeSize => Edits {
|
Case::Size | Case::ResizeSize | Case::SizeResize => Edits {
|
||||||
sizes: some_sizes(warm, tree, rng),
|
sizes: some_sizes(warm, tree, rng),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -399,18 +400,15 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
|
|||||||
label
|
label
|
||||||
}
|
}
|
||||||
|
|
||||||
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
|
/// One widget's layout as it stands: the frame its fractions resolved
|
||||||
match (got, want) {
|
/// against, the box it was asked in, the box its drawing went in, and what
|
||||||
(Some(got), Some(want)) => {
|
/// it reported. In window units, which is what both trees are in.
|
||||||
let same = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS);
|
fn record(id: WidgetId, h: &Harness) -> String {
|
||||||
same(got.top_left.x, want.top_left.x)
|
let active = &h.render.active[&id];
|
||||||
&& same(got.top_left.y, want.top_left.y)
|
format!(
|
||||||
&& same(got.bot_right.x, want.bot_right.x)
|
"rel_base {} region {} placement {} size {}",
|
||||||
&& same(got.bot_right.y, want.bot_right.y)
|
active.rel_base, active.region, active.placement, active.size,
|
||||||
}
|
)
|
||||||
(None, None) => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs `case` on the tree `plan` describes, warm and cold, and says where
|
/// Runs `case` on the tree `plan` describes, warm and cold, and says where
|
||||||
@@ -429,6 +427,17 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
|
|||||||
warm.frame();
|
warm.frame();
|
||||||
}
|
}
|
||||||
let cold_plan = change(case, &mut warm, &mut tree, plan, &mut Rng::new(seed));
|
let cold_plan = change(case, &mut warm, &mut tree, plan, &mut Rng::new(seed));
|
||||||
|
// Whatever the change left, seen at another window: an answer kept as a
|
||||||
|
// fraction of the wrong length is the same number of pixels where it was
|
||||||
|
// made and a different one everywhere else.
|
||||||
|
let end = match case.then_resize() {
|
||||||
|
Some(after) => {
|
||||||
|
warm.resize(after);
|
||||||
|
warm.frame();
|
||||||
|
after
|
||||||
|
}
|
||||||
|
None => end,
|
||||||
|
};
|
||||||
|
|
||||||
let mut cold = Harness::new(end);
|
let mut cold = Harness::new(end);
|
||||||
let (root, cold_tree) = build(&mut cold.rsc, &cold_plan);
|
let (root, cold_tree) = build(&mut cold.rsc, &cold_plan);
|
||||||
@@ -439,12 +448,19 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
|
|||||||
for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() {
|
for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() {
|
||||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
drawn += got.is_some() as usize;
|
drawn += got.is_some() as usize;
|
||||||
if same_region(got, want) {
|
if got == want {
|
||||||
continue;
|
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
|
// Where two trees disagree is rarely where the cause is, so the
|
||||||
// ancestry comes with it, marking the widgets that own a region.
|
// ancestry comes with it, marking the widgets that own a region.
|
||||||
let mut chain = Vec::new();
|
let mut chain = Vec::new();
|
||||||
|
let mut records = Vec::new();
|
||||||
let mut at = Some(w);
|
let mut at = Some(w);
|
||||||
while let Some(id) = at {
|
while let Some(id) = at {
|
||||||
let active = &warm.render.active[&id];
|
let active = &warm.render.active[&id];
|
||||||
@@ -453,11 +469,22 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
|
|||||||
false => "*",
|
false => "*",
|
||||||
};
|
};
|
||||||
chain.push(format!("{}{node}", describe(id, &warm)));
|
chain.push(format!("{}{node}", describe(id, &warm)));
|
||||||
|
// What each level was asked in on both sides, since the level
|
||||||
|
// where the two stop agreeing is the one to look at rather than
|
||||||
|
// the leaf that reported the difference.
|
||||||
|
let cold_id = places.get(&id).and_then(|&i| cold_tree.ids.get(i));
|
||||||
|
records.push(format!(
|
||||||
|
" {}\n warm {}\n cold {}",
|
||||||
|
describe(id, &warm),
|
||||||
|
record(id, &warm),
|
||||||
|
cold_id.map_or("-".into(), |&id| record(id, &cold)),
|
||||||
|
));
|
||||||
at = active.parent;
|
at = active.parent;
|
||||||
}
|
}
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"widget {i}\n warm {got:?}\n cold {want:?}\n {}",
|
"widget {i}\n warm {got:?}\n cold {want:?}\n {}\n{}",
|
||||||
chain.join(" < ")
|
chain.join(" < "),
|
||||||
|
records.join("\n"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
match drawn {
|
match drawn {
|
||||||
|
|||||||
+6
-2
@@ -70,10 +70,14 @@ fn no_grown_tree_lays_out_differently_warm_than_cold() {
|
|||||||
over_seeds(seeds, |seed| {
|
over_seeds(seeds, |seed| {
|
||||||
let grown = plan(seed, depth, &Edits::default());
|
let grown = plan(seed, depth, &Edits::default());
|
||||||
for &case in &cases {
|
for &case in &cases {
|
||||||
let Some(how) = diverges(&grown, case, seed) else {
|
if diverges(&grown, case, seed).is_none() {
|
||||||
continue;
|
continue;
|
||||||
};
|
}
|
||||||
let small = shrink(grown.clone(), case, seed);
|
let small = shrink(grown.clone(), case, seed);
|
||||||
|
// Described from the shrunk tree: the grown tree's chain names
|
||||||
|
// widgets that are no longer there, and the ancestry of the
|
||||||
|
// failure is what a test is written from.
|
||||||
|
let how = diverges(&small, case, seed).unwrap_or_default();
|
||||||
println!(
|
println!(
|
||||||
"seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
|
"seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
|
||||||
case.name(),
|
case.name(),
|
||||||
|
|||||||
@@ -42,12 +42,12 @@ fn dump(label: &str, report: &diag::Report, text: WidgetId) {
|
|||||||
TraceEvent::DrawRequest {
|
TraceEvent::DrawRequest {
|
||||||
id,
|
id,
|
||||||
region,
|
region,
|
||||||
pixel_size,
|
region_px,
|
||||||
..
|
..
|
||||||
} if *id == text => {
|
} if *id == text => {
|
||||||
println!(
|
println!(
|
||||||
" draw in {:.2}x{:.2} region {region:?}",
|
" 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 => {
|
TraceEvent::SizeReported { id, size } if *id == text => {
|
||||||
|
|||||||
Reference in new issue
Block a user