Compare commits
169
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cb6f6882a | ||
|
|
0e838e9dd1 | ||
|
|
8780b40bb7 | ||
|
|
de1eb7e406 | ||
|
|
76aaf06c0b | ||
|
|
0d0326769c | ||
|
|
2dba90bd0f | ||
|
|
b295c8b97a | ||
|
|
c2b8bf83de | ||
|
|
77ed7a24c0 | ||
|
|
f8aa0c5cdf | ||
|
|
b7b8d09e40 | ||
|
|
1096c3167a | ||
|
|
445287c95c | ||
|
|
750217631d | ||
|
|
9b4cc329ce | ||
|
|
02048eab77 | ||
|
|
d8d51221ee | ||
|
|
781199a7c9 | ||
|
|
69ba91588a | ||
|
|
8088a1fa59 | ||
|
|
713e3e747b | ||
|
|
aea0387567 | ||
|
|
1ebd4d3a05 | ||
|
|
7e2b4cd9db | ||
|
|
3da1c71870 | ||
|
|
6c84b6f2cb | ||
|
|
8d2b7a512b | ||
|
|
40b89c1f79 | ||
|
|
4d42f1c8ca | ||
|
|
2807a925af | ||
|
|
55df32a33c | ||
|
|
58ce74dd7d | ||
|
|
c55be21761 | ||
|
|
beb138632a | ||
|
|
aeb60e50f5 | ||
|
|
a904cf4f36 | ||
|
|
5642f2010a | ||
|
|
84dad211f5 | ||
|
|
add6774980 | ||
|
|
cadfba05dd | ||
|
|
38b3a81053 | ||
|
|
f6242aa33c | ||
|
|
a888717ee9 | ||
|
|
e8a5792dcb | ||
|
|
a30971e4c5 | ||
|
|
adbedaf264 | ||
|
|
23523eea29 | ||
|
|
1512d8418b | ||
|
|
0ef87ebfcf | ||
|
|
3091fb86df | ||
|
|
4328eac756 | ||
|
|
b842e4f474 | ||
|
|
49cec82c1b | ||
|
|
0954770ceb | ||
|
|
1956be3f3d | ||
|
|
34cafb6edc | ||
|
|
3bf22935ce | ||
|
|
e6ba570d07 | ||
|
|
0e107f0e89 | ||
|
|
f860f716e6 | ||
|
|
c44bd198ee | ||
|
|
a7307d95fd | ||
|
|
7601aa2a5d | ||
|
|
c330ecec2b | ||
|
|
39f7b08c6c | ||
|
|
2ed5503717 | ||
|
|
efb416bbc3 | ||
|
|
5fcace1bfa | ||
|
|
e44dea34b4 | ||
|
|
a0693acc56 | ||
|
|
25e456e0b5 | ||
|
|
53b00c68e9 | ||
|
|
a92c6acdbf | ||
|
|
c8beca5753 | ||
|
|
4bd8607968 | ||
|
|
ffd79f32d3 | ||
|
|
0e0d4af326 | ||
|
|
ea6dbae0dc | ||
|
|
32542d0c0b | ||
|
|
5b7800264d | ||
|
|
5f16617511 | ||
|
|
45a717695b | ||
|
|
d21a21524f | ||
|
|
e166e005dc | ||
|
|
38eba543f6 | ||
|
|
2bc6bdfc77 | ||
|
|
d8ae9c3bdd | ||
|
|
08c9d5aa32 | ||
|
|
60367d806e | ||
|
|
aea878d141 | ||
|
|
98d4e98a29 | ||
|
|
4febabfd2e | ||
|
|
394d5149a5 | ||
|
|
4cbb242a5d | ||
|
|
d75a1e2129 | ||
|
|
1940e85c70 | ||
|
|
cb1bba4682 | ||
|
|
490918b789 | ||
|
|
a8898aaa54 | ||
|
|
4f5e27cba9 | ||
|
|
11c55bcef9 | ||
|
|
f11f5f4825 | ||
|
|
97cc8b32ed | ||
|
|
95fb4f962c | ||
|
|
bdab55824f | ||
|
|
9d8415d65f | ||
|
|
cb955f1023 | ||
|
|
39e4ca20e6 | ||
|
|
bd6de71a55 | ||
|
|
4e28f1047e | ||
|
|
7548139861 | ||
|
|
5ed9e874a3 | ||
|
|
d3b0ebf90c | ||
|
|
8220a78d4a | ||
|
|
0283c9d6c7 | ||
|
|
71c9c39523 | ||
|
|
f437495309 | ||
|
|
29c7881c8a | ||
|
|
691e3eb23c | ||
|
|
9644971daf | ||
|
|
de9ddc0ad4 | ||
|
|
169db7f16f | ||
|
|
4063635f39 | ||
|
|
f61e8936f1 | ||
|
|
02ff8c7454 | ||
|
|
65f68bbb8a | ||
|
|
99131940ab | ||
|
|
e5a3e640d4 | ||
|
|
c596bf12c6 | ||
|
|
f0c7df06ac | ||
|
|
b7caab3b9e | ||
|
|
386a0d1b8f | ||
|
|
1b1378b05a | ||
|
|
60175c3821 | ||
|
|
b165164e59 | ||
|
|
ef815dadfd | ||
|
|
9f4311774b | ||
|
|
7c50a3e51b | ||
|
|
3f7cd8251b | ||
|
|
bf9438087a | ||
|
|
77bb75e5de | ||
|
|
2525637e26 | ||
|
|
e5f8b6b244 | ||
|
|
f1a47e9b7b | ||
|
|
b1b3eca1c0 | ||
|
|
82fa6c1123 | ||
|
|
480f0bc99f | ||
|
|
84f589e364 | ||
|
|
a640c6cce2 | ||
|
|
cdec29351a | ||
|
|
4178dfbff9 | ||
|
|
2272634dc5 | ||
|
|
b0f9f046da | ||
|
|
86a7e8dfc3 | ||
|
|
d98969158f | ||
|
|
1f9dc48b80 | ||
|
|
db1751fdfd | ||
|
|
78a53b6bf6 | ||
|
|
d8c497fcd7 | ||
|
|
c8ec0866d4 | ||
|
|
8223a55cfb | ||
|
|
f9ef7514e7 | ||
|
|
ca2b4b2173 | ||
|
|
f9423855e1 | ||
|
|
43ce8c7d02 | ||
|
|
c8ac669f95 | ||
|
|
32b10383d8 | ||
|
|
00d2230b84 |
No files matched your search
Generated
+228
-245
File diff suppressed because it is too large.
Load diff
+20
-3
@@ -3,6 +3,9 @@ name = "iris"
|
|||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
layout-diagnostics = ["iris-core/layout-diagnostics"]
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -20,7 +23,19 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
|
|||||||
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
|
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["core", "macro"]
|
members = ["core", "macro", "rig-input"]
|
||||||
|
|
||||||
|
# Full debug info was the bulk of what the linker wrote here and almost none of
|
||||||
|
# what anything read. `dev` keeps line tables and scopes, which is what stepping
|
||||||
|
# through an example wants; the tests keep the line tables alone, which is what
|
||||||
|
# a backtrace reads. Measured when the tests became one target: relinking them
|
||||||
|
# went from 9.8 s to 7.7 s with these, and target/ from 45 GB to 13 GB with the
|
||||||
|
# two changes together.
|
||||||
|
[profile.dev]
|
||||||
|
debug = 1
|
||||||
|
|
||||||
|
[profile.test]
|
||||||
|
debug = "line-tables-only"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -29,9 +44,9 @@ edition = "2024"
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
pollster = "0.4.0"
|
pollster = "0.4.0"
|
||||||
winit = "0.30.12"
|
winit = "0.30.12"
|
||||||
wgpu = "28.0.0"
|
wgpu = "30.0.1"
|
||||||
bytemuck = "1.23.1"
|
bytemuck = "1.23.1"
|
||||||
image = "0.25.6"
|
image = "0.25.10"
|
||||||
parley = "0.11.1"
|
parley = "0.11.1"
|
||||||
swash = "0.2.10"
|
swash = "0.2.10"
|
||||||
fxhash = "0.2.1"
|
fxhash = "0.2.1"
|
||||||
@@ -40,3 +55,5 @@ arboard = "3.6.1"
|
|||||||
iris-core = { path = "core" }
|
iris-core = { path = "core" }
|
||||||
iris-macro = { path = "macro" }
|
iris-macro = { path = "macro" }
|
||||||
tokio = "1.49.0"
|
tokio = "1.49.0"
|
||||||
|
wayland-client = "0.31.15"
|
||||||
|
wayland-protocols-wlr = { version = "0.3.12", features = ["client"] }
|
||||||
@@ -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)
|
||||||
@@ -3,6 +3,9 @@ name = "iris-core"
|
|||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
layout-diagnostics = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wgpu = { workspace = true }
|
wgpu = { workspace = true }
|
||||||
bytemuck ={ workspace = true }
|
bytemuck ={ workspace = true }
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ type EventData<Rsc, E> = (E, Rc<dyn for<'a> EventFn<Rsc, <E as Event>::Data<'a>>
|
|||||||
pub struct TypeEventManager<Rsc: HasEvents, E: Event> {
|
pub struct TypeEventManager<Rsc: HasEvents, E: Event> {
|
||||||
// TODO: reduce visiblity!!
|
// TODO: reduce visiblity!!
|
||||||
pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>,
|
pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>,
|
||||||
|
pub global: E::Global,
|
||||||
map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>,
|
map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +108,7 @@ impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
active: Default::default(),
|
active: Default::default(),
|
||||||
|
global: Default::default(),
|
||||||
map: Default::default(),
|
map: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,11 +140,13 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
|
|||||||
pub fn run_fn<'a>(
|
pub fn run_fn<'a>(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: impl IdLike,
|
id: impl IdLike,
|
||||||
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) + 'a {
|
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) -> bool + 'a {
|
||||||
let fs = self.map.get(&id.id()).cloned().unwrap_or_default();
|
let fs = self.map.get(&id.id()).cloned().unwrap_or_default();
|
||||||
move |ctx, rsc| {
|
move |ctx, rsc| {
|
||||||
|
let mut consumed = false;
|
||||||
for (e, f) in fs {
|
for (e, f) in fs {
|
||||||
if let Some(data) = e.should_run(&ctx.data) {
|
if let Some(data) = e.should_run(&ctx.data) {
|
||||||
|
consumed |= e.consumes(&data);
|
||||||
f(
|
f(
|
||||||
EventCtx {
|
EventCtx {
|
||||||
state: ctx.state,
|
state: ctx.state,
|
||||||
@@ -152,6 +156,7 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
consumed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,10 +9,19 @@ pub use rsc::*;
|
|||||||
pub trait Event: Sized + 'static + Clone {
|
pub trait Event: Sized + 'static + Clone {
|
||||||
type Data<'a>: Clone = ();
|
type Data<'a>: Clone = ();
|
||||||
type State: Default = ();
|
type State: Default = ();
|
||||||
|
/// State the whole event type keeps, rather than one copy per widget.
|
||||||
|
type Global: Default = ();
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
||||||
Some(data.clone())
|
Some(data.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether having run on this data uses up whatever triggered it, so
|
||||||
|
/// nothing further should see it.
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn consumes(&self, data: &Self::Data<'_>) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait EventLike {
|
pub trait EventLike {
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ pub trait HasEvents: Sized + UiRsc + HasState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait RunEvents: HasEvents {
|
pub trait RunEvents: HasEvents {
|
||||||
|
/// Whether anything that ran used up what triggered it.
|
||||||
fn run_event<E: EventLike>(
|
fn run_event<E: EventLike>(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: impl IdLike,
|
id: impl IdLike,
|
||||||
data: <E::Event as Event>::Data<'_>,
|
data: <E::Event as Event>::Data<'_>,
|
||||||
state: &mut Self::State,
|
state: &mut Self::State,
|
||||||
) {
|
) -> bool {
|
||||||
let f = self.events_mut().get_type::<E>().run_fn(id);
|
let f = self.events_mut().get_type::<E>().run_fn(id);
|
||||||
f(EventCtx { state, data }, self)
|
f(EventCtx { state, data }, self)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,547 @@
|
|||||||
|
use crate::{UiNum, util::Vec2};
|
||||||
|
use std::{
|
||||||
|
fmt::{Debug, Display, Formatter},
|
||||||
|
ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A number held as a whole count of `1 / 2^SHIFT`.
|
||||||
|
///
|
||||||
|
/// Layout reaches one place by more than one route -- a box composed down the
|
||||||
|
/// chain, and the same box summed from what its children asked for -- and has
|
||||||
|
/// to decide whether the two are the same place. In floats they land a few
|
||||||
|
/// bits apart, which is a defect wherever the answer changes what is drawn
|
||||||
|
/// rather than where. Here adding and subtracting are exact, a multiply
|
||||||
|
/// drops to the step below, and a conversion between grids takes the nearest
|
||||||
|
/// one, so two routes to one place land on one number and everything
|
||||||
|
/// downstream compares for equality instead of for nearness.
|
||||||
|
///
|
||||||
|
/// `SHIFT` is the number of fractional bits, which is what makes the steps
|
||||||
|
/// divide a whole number: a power of two also converts to `f32` without
|
||||||
|
/// rounding while the value fits in its mantissa.
|
||||||
|
///
|
||||||
|
/// Arithmetic wraps at the ends of the range, the way the `i32` underneath
|
||||||
|
/// does. Saturating instead was measured at a twelfth of layout's
|
||||||
|
/// instructions -- five per add against one -- to keep the ordering of
|
||||||
|
/// coordinates two million pixels out, where nothing draws anyway. A value
|
||||||
|
/// off the end is a defect either way; wrapping makes it an obvious one.
|
||||||
|
/// Only [`Self::from_f32`] clamps, since a float has further to come from.
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, bytemuck::Pod, bytemuck::Zeroable,
|
||||||
|
)]
|
||||||
|
pub struct Fixed<const SHIFT: u32>(i32);
|
||||||
|
|
||||||
|
/// A length or a coordinate in pixels, in steps of `1/1024`. Finer than
|
||||||
|
/// anything a display can show, and exact in `f32` up to 16,384 px, which is
|
||||||
|
/// what lets the same number reach the GPU.
|
||||||
|
pub type Px = Fixed<PX_SHIFT>;
|
||||||
|
|
||||||
|
/// How many bits of a pixel a [`Px`] keeps. One place, because [`PxVec2`]
|
||||||
|
/// and the shader's own decoding are the same grid or nothing lines up.
|
||||||
|
pub const PX_SHIFT: u32 = 10;
|
||||||
|
|
||||||
|
/// A share of what a box has left over, which is a weight beside its
|
||||||
|
/// siblings rather than a fraction of anything: a list divides its room by
|
||||||
|
/// the total of these, so the range has to hold a whole list's worth and the
|
||||||
|
/// precision only has to tell two weights apart.
|
||||||
|
pub type Weight = Fixed<16>;
|
||||||
|
|
||||||
|
/// A fraction of a box. Twenty-four bits of it, which matches `f32` around a
|
||||||
|
/// half and beats it above one -- where anchors actually sit -- and leaves
|
||||||
|
/// +/-128 of range, enough to sum a hundred children each asking for a whole
|
||||||
|
/// box. A `leftover` weight is not one of these: it is a share of what is
|
||||||
|
/// left rather than a fraction of anything, and it sums over a whole list.
|
||||||
|
pub type Rel = Fixed<REL_SHIFT>;
|
||||||
|
|
||||||
|
/// How many bits of a box a [`Rel`] keeps, beside [`PX_SHIFT`] and for the
|
||||||
|
/// same reason.
|
||||||
|
pub const REL_SHIFT: u32 = 24;
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||||
|
pub const ZERO: Self = Self(0);
|
||||||
|
pub const ONE: Self = Self::one();
|
||||||
|
/// The gap between neighbouring values, which is also how far apart two
|
||||||
|
/// numbers can be and still mean the same place.
|
||||||
|
pub const STEP: Self = Self(1);
|
||||||
|
/// Also what stands in for an unbounded end: compared against, never
|
||||||
|
/// added to, since arithmetic wraps past it.
|
||||||
|
pub const MIN: Self = Self(i32::MIN);
|
||||||
|
pub const MAX: Self = Self(i32::MAX);
|
||||||
|
|
||||||
|
const fn one() -> Self {
|
||||||
|
assert!(SHIFT < 31, "a Fixed needs a bit for the whole part");
|
||||||
|
Self(1 << SHIFT)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_raw(raw: i32) -> Self {
|
||||||
|
Self(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The count of steps, for a caller that needs the representation rather
|
||||||
|
/// than the number.
|
||||||
|
pub const fn raw(self) -> i32 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_int(v: i32) -> Self {
|
||||||
|
Self(v.wrapping_mul(Self::one().0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rounds to the nearest step, and clamps to the ends of the grid rather
|
||||||
|
/// than wrapping: this is where a number from outside arrives, and a float
|
||||||
|
/// has the range to be anywhere. A NaN has no nearest step and becomes
|
||||||
|
/// zero, which is a caller's mistake rather than a value worth carrying.
|
||||||
|
///
|
||||||
|
/// Half-away is written out rather than called through `f32::round`,
|
||||||
|
/// which is not `const`: a layout constant has to stay a constant.
|
||||||
|
pub const fn from_f32(v: f32) -> Self {
|
||||||
|
debug_assert!(!v.is_nan(), "a NaN has no place on the grid");
|
||||||
|
let scaled = v * Self::one().0 as f32;
|
||||||
|
// Above 2^23 an `f32` has no fractional part left to round, and
|
||||||
|
// adding a half there rounds the number itself up instead. The cast
|
||||||
|
// saturates at both ends and sends NaN to zero, which is the
|
||||||
|
// behaviour wanted at both.
|
||||||
|
const WHOLE: f32 = (1 << 23) as f32;
|
||||||
|
Self(match (scaled >= WHOLE, scaled <= -WHOLE, scaled < 0.0) {
|
||||||
|
(true, _, _) | (_, true, _) => scaled as i32,
|
||||||
|
(_, _, true) => (scaled - 0.5) as i32,
|
||||||
|
_ => (scaled + 0.5) as i32,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first step at or above `v`, where [`Self::from_f32`] takes the
|
||||||
|
/// nearest one and is below it half the time. For a bound that has to
|
||||||
|
/// admit the value it came from: a measurement rounded down is a bound
|
||||||
|
/// that leaves out the thing it was measured from.
|
||||||
|
pub const fn ceil_from_f32(v: f32) -> Self {
|
||||||
|
let nearest = Self::from_f32(v);
|
||||||
|
match nearest.to_f32() < v {
|
||||||
|
true => nearest.next_up(),
|
||||||
|
false => nearest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From a number as it is written in source -- `16`, `1.5` -- which is
|
||||||
|
/// the other place a value enters the grid.
|
||||||
|
pub fn from_num(v: impl UiNum) -> Self {
|
||||||
|
Self::from_f32(v.to_f32())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn to_f32(self) -> f32 {
|
||||||
|
self.0 as f32 / Self::one().0 as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn add(self, rhs: Self) -> Self {
|
||||||
|
Self(self.0.wrapping_add(rhs.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn sub(self, rhs: Self) -> Self {
|
||||||
|
Self(self.0.wrapping_sub(rhs.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn neg(self) -> Self {
|
||||||
|
Self(self.0.wrapping_neg())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scaled by a number on any grid, which is how a length takes a fraction
|
||||||
|
/// of itself and keeps being a length: the product is measured in the
|
||||||
|
/// receiver's steps.
|
||||||
|
///
|
||||||
|
/// Dropped to the step below rather than taken to the nearest one
|
||||||
|
/// (Bryan, 2026-09-16), which costs a share a thousandth of a pixel of
|
||||||
|
/// its row -- less than an even number of pixels draws. Toward negative
|
||||||
|
/// infinity on both sides of zero, since that is a shift and nothing
|
||||||
|
/// else: a value and its negation therefore land different distances
|
||||||
|
/// from where they came, so a flipped span can sit a step from its
|
||||||
|
/// mirror image.
|
||||||
|
pub const fn mul<const BY: u32>(self, by: Fixed<BY>) -> Self {
|
||||||
|
Self(((self.0 as i64 * by.0 as i64) >> BY) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repeated a whole number of times, which no grid rounds.
|
||||||
|
pub const fn mul_int(self, by: i32) -> Self {
|
||||||
|
Self(self.0.wrapping_mul(by))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Divided into a whole number of parts, rounded to the nearest step.
|
||||||
|
pub const fn div_int(self, by: i32) -> Self {
|
||||||
|
debug_assert!(by != 0, "no part of nothing");
|
||||||
|
if by == 0 {
|
||||||
|
return Self::ZERO;
|
||||||
|
}
|
||||||
|
Self(div_round(self.0 as i64, by as i64) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Divided by a number on any grid. A zero divisor is a caller bug -- a
|
||||||
|
/// box of no length has no fraction of itself -- and answers with the end
|
||||||
|
/// of the range so that a release build lays out something absurd rather
|
||||||
|
/// than dying.
|
||||||
|
pub const fn div<const BY: u32>(self, by: Fixed<BY>) -> Self {
|
||||||
|
debug_assert!(by.0 != 0, "dividing by a length of zero");
|
||||||
|
if by.0 == 0 {
|
||||||
|
return match self.0 < 0 {
|
||||||
|
true => Self::MIN,
|
||||||
|
false => Self::MAX,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Self(div_round((self.0 as i64) << BY, by.0 as i64) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `num / den` on *this* grid rather than on theirs, for weights coarser
|
||||||
|
/// than the share they divide.
|
||||||
|
pub const fn ratio<const OF: u32>(num: Fixed<OF>, den: Fixed<OF>) -> Self {
|
||||||
|
debug_assert!(den.0 != 0, "no part of a whole of nothing");
|
||||||
|
if den.0 == 0 {
|
||||||
|
return Self::ZERO;
|
||||||
|
}
|
||||||
|
Self(div_round((num.0 as i64) << SHIFT, den.0 as i64) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `from` and `to` a fraction of the way apart, the fraction being the
|
||||||
|
/// receiver -- the argument order [`crate::util::LerpUtil`] already uses.
|
||||||
|
pub const fn lerp<const OF: u32>(self, from: Fixed<OF>, to: Fixed<OF>) -> Fixed<OF> {
|
||||||
|
from.add(to.sub(from).mul(self))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn min(self, other: Self) -> Self {
|
||||||
|
match self.0 < other.0 {
|
||||||
|
true => self,
|
||||||
|
false => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn max(self, other: Self) -> Self {
|
||||||
|
match self.0 > other.0 {
|
||||||
|
true => self,
|
||||||
|
false => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn abs(self) -> Self {
|
||||||
|
Self(self.0.wrapping_abs())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn clamp(self, lo: Self, hi: Self) -> Self {
|
||||||
|
debug_assert!(lo.0 <= hi.0, "an empty clamp has no answer");
|
||||||
|
self.max(lo).min(hi)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The next value along, for an interval that must not admit its own
|
||||||
|
/// boundary. The step is the whole gap, so there is nothing to exclude
|
||||||
|
/// between this and the boundary itself.
|
||||||
|
pub const fn next_up(self) -> Self {
|
||||||
|
Self(self.0.wrapping_add(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn next_down(self) -> Self {
|
||||||
|
Self(self.0.wrapping_sub(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn div_round(num: i64, den: i64) -> i64 {
|
||||||
|
let (q, rem) = (num / den, num % den);
|
||||||
|
match rem.unsigned_abs() * 2 >= den.unsigned_abs() {
|
||||||
|
true => match (num < 0) == (den < 0) {
|
||||||
|
true => q + 1,
|
||||||
|
false => q - 1,
|
||||||
|
},
|
||||||
|
false => q,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toward positive infinity when `up`, toward negative infinity otherwise.
|
||||||
|
pub(crate) const fn div_toward(num: i64, den: i64, up: bool) -> i64 {
|
||||||
|
let (q, rem) = (num / den, num % den);
|
||||||
|
if rem == 0 {
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
match (rem < 0) == (den < 0) {
|
||||||
|
true => q + up as i64,
|
||||||
|
false => q - !up as i64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clamped to the ends, unlike a [`Fixed`]'s own arithmetic: a range of box
|
||||||
|
/// lengths that runs past `i32` really is unbounded.
|
||||||
|
pub(crate) const fn narrow(v: i64) -> i32 {
|
||||||
|
if v > i32::MAX as i64 {
|
||||||
|
return i32::MAX;
|
||||||
|
}
|
||||||
|
if v < i32::MIN as i64 {
|
||||||
|
return i32::MIN;
|
||||||
|
}
|
||||||
|
v as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> Add for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, rhs: Self) -> Self {
|
||||||
|
Fixed::add(self, rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> Sub for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Self) -> Self {
|
||||||
|
Fixed::sub(self, rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> Neg for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn neg(self) -> Self {
|
||||||
|
Fixed::neg(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> AddAssign for Fixed<SHIFT> {
|
||||||
|
fn add_assign(&mut self, rhs: Self) {
|
||||||
|
*self = Fixed::add(*self, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> SubAssign for Fixed<SHIFT> {
|
||||||
|
fn sub_assign(&mut self, rhs: Self) {
|
||||||
|
*self = Fixed::sub(*self, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32, const BY: u32> Mul<Fixed<BY>> for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn mul(self, rhs: Fixed<BY>) -> Self {
|
||||||
|
Fixed::mul(self, rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32, const BY: u32> Div<Fixed<BY>> for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn div(self, rhs: Fixed<BY>) -> Self {
|
||||||
|
Fixed::div(self, rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> Display for Fixed<SHIFT> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self.to_f32(), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints the number rather than the count of steps: a failing layout test
|
||||||
|
/// reports boxes, and `1126` is not a height anybody can read.
|
||||||
|
impl<const SHIFT: u32> Debug for Fixed<SHIFT> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self.to_f32(), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Two of them, for the places a size or a position needs both axes: a
|
||||||
|
/// window, a box in pixels, a pointer. Held apart from [`crate::util::Vec2`]
|
||||||
|
/// because that one is what the GPU and the platform speak.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub struct FixedVec2<const SHIFT: u32> {
|
||||||
|
pub x: Fixed<SHIFT>,
|
||||||
|
pub y: Fixed<SHIFT>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type PxVec2 = FixedVec2<PX_SHIFT>;
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
||||||
|
pub const ZERO: Self = Self::splat(Fixed::ZERO);
|
||||||
|
|
||||||
|
pub const fn new(x: Fixed<SHIFT>, y: Fixed<SHIFT>) -> Self {
|
||||||
|
Self { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn splat(v: Fixed<SHIFT>) -> Self {
|
||||||
|
Self { x: v, y: v }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_f32(v: Vec2) -> Self {
|
||||||
|
Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first step at or above each part, for a measurement reported as a
|
||||||
|
/// box: what it occupies is not less than what was measured.
|
||||||
|
pub fn ceil_from_f32(v: Vec2) -> Self {
|
||||||
|
Self::new(Fixed::ceil_from_f32(v.x), Fixed::ceil_from_f32(v.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_f32(self) -> Vec2 {
|
||||||
|
Vec2::new(self.x.to_f32(), self.y.to_f32())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn div_int(self, by: i32) -> Self {
|
||||||
|
Self::new(self.x.div_int(by), self.y.div_int(by))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn min(self, other: Self) -> Self {
|
||||||
|
Self::new(self.x.min(other.x), self.y.min(other.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn max(self, other: Self) -> Self {
|
||||||
|
Self::new(self.x.max(other.x), self.y.max(other.y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `impl_op!` names one concrete type, and this one is generic.
|
||||||
|
const impl<const SHIFT: u32> Add for FixedVec2<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, rhs: Self) -> Self {
|
||||||
|
Self::new(self.x.add(rhs.x), self.y.add(rhs.y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> Sub for FixedVec2<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Self) -> Self {
|
||||||
|
Self::new(self.x.sub(rhs.x), self.y.sub(rhs.y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> AddAssign for FixedVec2<SHIFT> {
|
||||||
|
fn add_assign(&mut self, rhs: Self) {
|
||||||
|
*self = Add::add(*self, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> SubAssign for FixedVec2<SHIFT> {
|
||||||
|
fn sub_assign(&mut self, rhs: Self) {
|
||||||
|
*self = Sub::sub(*self, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> Debug for FixedVec2<SHIFT> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "({}, {})", self.x, self.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> Display for FixedVec2<SHIFT> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "({}, {})", self.x, self.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_sum_of_steps_does_not_drift() {
|
||||||
|
let mut at = Px::ZERO;
|
||||||
|
for _ in 0..20_000 {
|
||||||
|
at += Px::from_raw(3);
|
||||||
|
}
|
||||||
|
assert_eq!(at, Px::from_raw(60_000));
|
||||||
|
for _ in 0..20_000 {
|
||||||
|
at -= Px::from_raw(3);
|
||||||
|
}
|
||||||
|
assert_eq!(at, Px::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_pixel_survives_the_trip_through_f32() {
|
||||||
|
for raw in [0, 1, -1, 64, -1000, 16_777_215, -16_777_215] {
|
||||||
|
let px = Px::from_raw(raw);
|
||||||
|
assert_eq!(Px::from_f32(px.to_f32()), px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fraction_of_a_length_is_a_length() {
|
||||||
|
let half = Px::from_int(100) * Rel::from_f32(0.5);
|
||||||
|
assert_eq!(half, Px::from_int(50));
|
||||||
|
assert_eq!(Px::from_int(100) * Rel::ONE, Px::from_int(100));
|
||||||
|
assert_eq!(Px::from_int(100) * Rel::ZERO, Px::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toward negative infinity on both sides of zero, which is what makes
|
||||||
|
/// it a shift rather than a shift and a sign branch -- and what makes a
|
||||||
|
/// value and its negation land different distances from where they came,
|
||||||
|
/// so a flipped span can sit a step from its mirror image.
|
||||||
|
#[test]
|
||||||
|
fn a_multiply_drops_to_the_step_below_on_both_sides_of_zero() {
|
||||||
|
// A step and a half of one, which has no step of its own.
|
||||||
|
let step_and_a_half = Rel::from_f32(1.5).div_int(Px::ONE.raw());
|
||||||
|
assert_eq!(Px::ONE * step_and_a_half, Px::from_raw(1));
|
||||||
|
assert_eq!(Px::ONE.neg() * step_and_a_half, Px::from_raw(-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A division rounds to the nearest step, so it cannot put back the
|
||||||
|
/// steps a truncating multiply dropped: a round trip comes back short,
|
||||||
|
/// never long, and by the few steps the two operations gave up.
|
||||||
|
#[test]
|
||||||
|
fn dividing_by_a_fraction_cannot_undo_a_truncating_multiply() {
|
||||||
|
let third = Rel::ONE / Rel::from_int(3);
|
||||||
|
let len = Px::from_int(300);
|
||||||
|
let back = len * third / third;
|
||||||
|
assert!(back <= len, "{back:?} is longer than {len:?}");
|
||||||
|
assert!(len - back <= Px::from_raw(3), "{back:?} against {len:?}");
|
||||||
|
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bound a greedy line break needs: the width it was measured at is
|
||||||
|
/// not on the grid, and the narrowest box the break still holds for is
|
||||||
|
/// the step at or above it, never the one below.
|
||||||
|
#[test]
|
||||||
|
fn a_ceiling_never_lands_below_the_number_it_came_from() {
|
||||||
|
let step = 1.0 / (1 << PX_SHIFT) as f32;
|
||||||
|
for n in 0..64 {
|
||||||
|
let v = 189.0 + n as f32 * step / 3.0;
|
||||||
|
let up = Px::ceil_from_f32(v);
|
||||||
|
assert!(up.to_f32() >= v, "{up:?} is below {v}");
|
||||||
|
assert!(
|
||||||
|
up.to_f32() - v < step,
|
||||||
|
"{up:?} is more than a step above {v}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// An exact step is its own ceiling.
|
||||||
|
assert_eq!(Px::ceil_from_f32(189.5), Px::from_f32(189.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_number_from_outside_is_clamped_to_the_grid() {
|
||||||
|
assert_eq!(Px::from_f32(1e12), Px::MAX);
|
||||||
|
assert_eq!(Px::from_f32(-1e12), Px::MIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lerp_takes_the_fraction_as_the_receiver() {
|
||||||
|
let (from, to) = (Px::from_int(10), Px::from_int(20));
|
||||||
|
assert_eq!(Rel::ZERO.lerp(from, to), from);
|
||||||
|
assert_eq!(Rel::ONE.lerp(from, to), to);
|
||||||
|
assert_eq!(Rel::from_f32(0.5).lerp(from, to), Px::from_int(15));
|
||||||
|
assert_eq!(Rel::from_f32(0.5).lerp(to, from), Px::from_int(15));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_ratio_is_finer_than_the_weights_it_divides() {
|
||||||
|
let (one, three) = (Weight::ONE, Weight::from_int(3));
|
||||||
|
// A third, which the weights' own grid could only hold to 1/65536.
|
||||||
|
assert_eq!(Rel::ratio(one, three), Rel::from_raw(5592405));
|
||||||
|
assert_eq!(Rel::ratio(three, three), Rel::ONE);
|
||||||
|
assert_eq!(Rel::ratio(Weight::ZERO, three), Rel::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nothing_sits_between_a_value_and_the_next_one() {
|
||||||
|
let at = Px::from_int(3);
|
||||||
|
assert_eq!(at.next_up().next_down(), at);
|
||||||
|
assert_eq!(at.next_up().raw() - at.raw(), 1);
|
||||||
|
assert!(at.next_down() < at && at < at.next_up());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_prints_the_number_rather_than_the_steps() {
|
||||||
|
assert_eq!(format!("{:?}", Px::from_f32(17.59375)), "17.59375");
|
||||||
|
assert_eq!(format!("{}", Px::from_int(-2)), "-2");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,493 @@
|
|||||||
|
//! Opt-in counters and coarse timers for explaining CPU layout cost.
|
||||||
|
//!
|
||||||
|
//! Enable the `layout-diagnostics` feature. With it disabled, none of the
|
||||||
|
//! instrumentation is compiled into Iris. The retained rig in
|
||||||
|
//! `tests/layout_diagnostics.rs` is the ordinary entry point.
|
||||||
|
//!
|
||||||
|
//! Timers are inclusive: `update total` contains `full layout` or
|
||||||
|
//! `incremental layout`, and `text render` contains shaping and glyph
|
||||||
|
//! placement. They locate cost within one instrumented run and must not be
|
||||||
|
//! added together. Use an uninstrumented build under `perf` for final CPU
|
||||||
|
//! totals; counting every primitive and distinct widget deliberately perturbs
|
||||||
|
//! the instrumented run.
|
||||||
|
//!
|
||||||
|
//! Call [`trace_widget`] before a frame to retain the ordered constraint,
|
||||||
|
//! reuse, size, placement, and text events for one suspicious widget. The
|
||||||
|
//! selection is a set and survives [`take`] until cleared.
|
||||||
|
|
||||||
|
use crate::{Axis, LayoutHolds, LayoutLen, PxVec2, Size, UiRegion, UiVec2, WidgetId};
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
fmt::Write,
|
||||||
|
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)]
|
||||||
|
$vis enum $Name { $($variant,)* }
|
||||||
|
|
||||||
|
impl $Name {
|
||||||
|
const COUNT: usize = [$($label,)*].len();
|
||||||
|
const NAMES: [&'static str; Self::COUNT] = [$($label,)*];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
labelled! {
|
||||||
|
pub(crate) enum Counter {
|
||||||
|
Updates = "updates",
|
||||||
|
DrawRequests = "draw requests",
|
||||||
|
WidgetDraws = "widget draws",
|
||||||
|
RegionNodeDraws = "region-node draws",
|
||||||
|
SizeReads = "draw-result size reads",
|
||||||
|
HintHits = "hint hits",
|
||||||
|
HintMisses = "hint misses",
|
||||||
|
ReuseAttempts = "reuse attempts",
|
||||||
|
ReuseExact = "reuse exact",
|
||||||
|
ReuseMoved = "reuse moved",
|
||||||
|
ReuseDirty = "reuse: dirty",
|
||||||
|
ReuseUndrawn = "reuse: nothing drawn to keep",
|
||||||
|
ReuseWrongParent = "reuse: wrong parent",
|
||||||
|
ReuseRemapped = "reuse remapped",
|
||||||
|
ReuseOutside = "reuse: outside what it holds for",
|
||||||
|
ReuseWrongLayer = "reuse: another layer",
|
||||||
|
ReuseWrongNode = "reuse: region-node choice changed",
|
||||||
|
ReuseWrongMask = "reuse: a different inherited mask",
|
||||||
|
QueuePops = "redraw queue pops",
|
||||||
|
DepthReads = "depth reads",
|
||||||
|
LocalRedraws = "local redraws",
|
||||||
|
SizeChanges = "size changes",
|
||||||
|
ReaderEdges = "reader edges",
|
||||||
|
PrimitiveWrites = "primitive writes",
|
||||||
|
TextRenders = "text renders",
|
||||||
|
TextShapeHits = "text shape hits",
|
||||||
|
TextShapes = "text shapes",
|
||||||
|
TextBreaks = "text line breaks",
|
||||||
|
GlyphPlacements = "glyph placements",
|
||||||
|
OutsidePinnedLen = "reuse outside: the length it was pinned to",
|
||||||
|
OutsideWindow = "reuse outside: this window",
|
||||||
|
OutsideRelBase = "reuse outside: a rel base",
|
||||||
|
OutsideRegion = "reuse outside: a region length",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
labelled! {
|
||||||
|
pub(crate) enum TimerKind {
|
||||||
|
Update = "update total",
|
||||||
|
FullLayout = "full layout",
|
||||||
|
IncrementalLayout = "incremental layout",
|
||||||
|
TextRender = "text render",
|
||||||
|
TextShape = "text shape",
|
||||||
|
TextBreak = "text line break",
|
||||||
|
GlyphPlacement = "glyph placement",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Report {
|
||||||
|
counters: [u64; Counter::COUNT],
|
||||||
|
nanos: [u64; TimerKind::COUNT],
|
||||||
|
distinct_widgets: usize,
|
||||||
|
distinct_text_widgets: usize,
|
||||||
|
hot_widgets: Vec<Callsite>,
|
||||||
|
hot_text: Vec<Callsite>,
|
||||||
|
traces: Vec<TraceEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Report {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
counters: [0; Counter::COUNT],
|
||||||
|
nanos: [0; TimerKind::COUNT],
|
||||||
|
distinct_widgets: 0,
|
||||||
|
distinct_text_widgets: 0,
|
||||||
|
hot_widgets: Vec::new(),
|
||||||
|
hot_text: Vec::new(),
|
||||||
|
traces: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Report {
|
||||||
|
pub fn counters(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
|
||||||
|
Counter::NAMES.into_iter().zip(self.counters)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inclusive elapsed time accumulated for each targeted operation.
|
||||||
|
pub fn timings_ns(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
|
||||||
|
TimerKind::NAMES.into_iter().zip(self.nanos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn distinct_widgets(&self) -> usize {
|
||||||
|
self.distinct_widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn distinct_text_widgets(&self) -> usize {
|
||||||
|
self.distinct_text_widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hot_widgets(&self) -> &[Callsite] {
|
||||||
|
&self.hot_widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hot_text(&self) -> &[Callsite] {
|
||||||
|
&self.hot_text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ordered layout events for widgets selected with [`trace_widget`].
|
||||||
|
pub fn traces(&self) -> &[TraceEvent] {
|
||||||
|
&self.traces
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats nonzero totals divided by `frames`.
|
||||||
|
pub fn per_frame(&self, frames: usize) -> String {
|
||||||
|
let divisor = frames.max(1) as f64;
|
||||||
|
let mut out = String::new();
|
||||||
|
for (name, value) in self.counters() {
|
||||||
|
if value != 0 {
|
||||||
|
let _ = writeln!(out, " {name:<27} {:>12.2}", value as f64 / divisor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.distinct_widgets != 0 {
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
" {:<27} {:>12}",
|
||||||
|
"distinct widgets", self.distinct_widgets
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self.distinct_text_widgets != 0 {
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
" {:<27} {:>12}",
|
||||||
|
"distinct text widgets", self.distinct_text_widgets
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (name, nanos) in self.timings_ns() {
|
||||||
|
if nanos != 0 {
|
||||||
|
let ms = nanos as f64 / divisor / 1_000_000.0;
|
||||||
|
let _ = writeln!(out, " {name:<27} {ms:>12.3} ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !self.hot_widgets.is_empty() {
|
||||||
|
let _ = writeln!(out, " hottest widget draws:");
|
||||||
|
for callsite in &self.hot_widgets {
|
||||||
|
let calls = callsite.calls as f64 / divisor;
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
" {calls:>9.2} {:?} {}",
|
||||||
|
callsite.id, callsite.label
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !self.hot_text.is_empty() {
|
||||||
|
let _ = writeln!(out, " hottest text renders:");
|
||||||
|
for callsite in &self.hot_text {
|
||||||
|
let calls = callsite.calls as f64 / divisor;
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
" {calls:>9.2} {:>3} widths {:?} {}",
|
||||||
|
callsite.distinct_widths, callsite.id, callsite.label
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !self.traces.is_empty() {
|
||||||
|
let _ = writeln!(out, " targeted layout trace:");
|
||||||
|
for event in &self.traces {
|
||||||
|
let _ = writeln!(out, " {event:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Callsite {
|
||||||
|
pub id: WidgetId,
|
||||||
|
pub label: String,
|
||||||
|
pub calls: u64,
|
||||||
|
pub distinct_widths: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum ReuseOutcome {
|
||||||
|
Exact,
|
||||||
|
Moved,
|
||||||
|
Dirty,
|
||||||
|
WrongParent,
|
||||||
|
WrongLayer,
|
||||||
|
WrongMask,
|
||||||
|
WrongNode,
|
||||||
|
Remapped,
|
||||||
|
Outside,
|
||||||
|
Undrawn,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One targeted layout event. Events are retained in execution order, making
|
||||||
|
/// repeated constraint paths visible without logging every widget globally.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum TraceEvent {
|
||||||
|
DrawRequest {
|
||||||
|
id: WidgetId,
|
||||||
|
parent: Option<WidgetId>,
|
||||||
|
region: UiRegion,
|
||||||
|
region_px: PxVec2,
|
||||||
|
region_node: bool,
|
||||||
|
},
|
||||||
|
Reuse {
|
||||||
|
id: WidgetId,
|
||||||
|
outcome: ReuseOutcome,
|
||||||
|
},
|
||||||
|
SizeReported {
|
||||||
|
id: WidgetId,
|
||||||
|
size: Size,
|
||||||
|
},
|
||||||
|
RegionNode {
|
||||||
|
id: WidgetId,
|
||||||
|
parent: WidgetId,
|
||||||
|
region: UiRegion,
|
||||||
|
},
|
||||||
|
SizeRead {
|
||||||
|
id: WidgetId,
|
||||||
|
reader: WidgetId,
|
||||||
|
size: Size,
|
||||||
|
},
|
||||||
|
HintRead {
|
||||||
|
id: WidgetId,
|
||||||
|
reader: WidgetId,
|
||||||
|
axis: Axis,
|
||||||
|
hint: Option<LayoutLen>,
|
||||||
|
},
|
||||||
|
TextRendered {
|
||||||
|
id: WidgetId,
|
||||||
|
width: Option<f32>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Calls {
|
||||||
|
label: String,
|
||||||
|
count: u64,
|
||||||
|
widths: HashSet<Option<u32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Current {
|
||||||
|
report: Report,
|
||||||
|
widgets: HashMap<WidgetId, Calls>,
|
||||||
|
text_widgets: HashMap<WidgetId, Calls>,
|
||||||
|
traced: HashSet<WidgetId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static CURRENT: RefCell<Current> = RefCell::new(Current::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn bump(counter: Counter) {
|
||||||
|
CURRENT.with_borrow_mut(|current| current.report.counters[counter as usize] += 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn draw_widget(id: WidgetId, label: &str) {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
let calls = current.widgets.entry(id).or_default();
|
||||||
|
if calls.label.is_empty() {
|
||||||
|
calls.label = label.to_owned();
|
||||||
|
}
|
||||||
|
calls.count += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a widget to the targeted trace set. Selection survives [`take`]
|
||||||
|
/// until explicitly removed or cleared.
|
||||||
|
pub fn trace_widget(id: impl Into<WidgetId>) {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
current.traced.insert(id.into());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn untrace_widget(id: impl Into<WidgetId>) {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
current.traced.remove(&id.into());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_traced_widgets() {
|
||||||
|
CURRENT.with_borrow_mut(|current| current.traced.clear());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trace(id: WidgetId, event: TraceEvent) {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
if current.traced.contains(&id) {
|
||||||
|
current.report.traces.push(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn draw_request(
|
||||||
|
id: WidgetId,
|
||||||
|
parent: Option<WidgetId>,
|
||||||
|
region: UiRegion,
|
||||||
|
region_px: PxVec2,
|
||||||
|
region_node: bool,
|
||||||
|
) {
|
||||||
|
trace(
|
||||||
|
id,
|
||||||
|
TraceEvent::DrawRequest {
|
||||||
|
id,
|
||||||
|
parent,
|
||||||
|
region,
|
||||||
|
region_px,
|
||||||
|
region_node,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reuse(id: WidgetId, outcome: ReuseOutcome) {
|
||||||
|
trace(id, TraceEvent::Reuse { id, outcome });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A drawing that cannot be reused because the box on offer is outside what
|
||||||
|
/// it holds for, and which of the four contracts said so. They overlap: a
|
||||||
|
/// drawing can be outside two of them at once, and counting each is what
|
||||||
|
/// says where a rel base redrawing more than it should is coming from.
|
||||||
|
pub(crate) fn outside(
|
||||||
|
id: WidgetId,
|
||||||
|
holds: LayoutHolds,
|
||||||
|
region: UiRegion,
|
||||||
|
rel_base: UiVec2,
|
||||||
|
window: PxVec2,
|
||||||
|
) {
|
||||||
|
for axis in Axis::BOTH {
|
||||||
|
let holds = holds[axis];
|
||||||
|
let len = region[axis].len();
|
||||||
|
let window = window[axis];
|
||||||
|
if holds.region_len.is_some_and(|pinned| pinned != len) {
|
||||||
|
bump(Counter::OutsidePinnedLen);
|
||||||
|
}
|
||||||
|
if !holds.window.contains(window) {
|
||||||
|
bump(Counter::OutsideWindow);
|
||||||
|
}
|
||||||
|
if holds
|
||||||
|
.rel_base
|
||||||
|
.is_some_and(|pinned| pinned != rel_base[axis])
|
||||||
|
{
|
||||||
|
bump(Counter::OutsideRelBase);
|
||||||
|
}
|
||||||
|
if !holds.region.contains(len.to_px(window)) {
|
||||||
|
bump(Counter::OutsideRegion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bump(Counter::ReuseOutside);
|
||||||
|
reuse(id, ReuseOutcome::Outside);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn size_reported(id: WidgetId, size: Size) {
|
||||||
|
trace(id, TraceEvent::SizeReported { id, size });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn region_node(id: WidgetId, parent: WidgetId, region: UiRegion) {
|
||||||
|
trace(id, TraceEvent::RegionNode { id, parent, region });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn size_read(id: WidgetId, reader: WidgetId, size: Size) {
|
||||||
|
trace(id, TraceEvent::SizeRead { id, reader, size });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn hint_read(id: WidgetId, reader: WidgetId, axis: Axis, hint: Option<LayoutLen>) {
|
||||||
|
trace(
|
||||||
|
id,
|
||||||
|
TraceEvent::HintRead {
|
||||||
|
id,
|
||||||
|
reader,
|
||||||
|
axis,
|
||||||
|
hint,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn render_text(id: WidgetId, label: &str, width: Option<f32>) {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
let calls = current.text_widgets.entry(id).or_default();
|
||||||
|
if calls.label.is_empty() {
|
||||||
|
calls.label = label.to_owned();
|
||||||
|
}
|
||||||
|
calls.count += 1;
|
||||||
|
calls.widths.insert(width.map(f32::to_bits));
|
||||||
|
if current.traced.contains(&id) {
|
||||||
|
current
|
||||||
|
.report
|
||||||
|
.traces
|
||||||
|
.push(TraceEvent::TextRendered { id, width });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Timer {
|
||||||
|
kind: TimerKind,
|
||||||
|
start: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn timer(kind: TimerKind) -> Timer {
|
||||||
|
Timer {
|
||||||
|
kind,
|
||||||
|
start: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Timer {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let nanos = self.start.elapsed().as_nanos().min(u64::MAX as u128) as u64;
|
||||||
|
CURRENT.with_borrow_mut(|current| current.report.nanos[self.kind as usize] += nanos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes all diagnostics accumulated on this thread and resets them.
|
||||||
|
pub fn take() -> Report {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
current.report.distinct_widgets = current.widgets.len();
|
||||||
|
current.report.distinct_text_widgets = current.text_widgets.len();
|
||||||
|
current.report.hot_widgets = hottest(¤t.widgets);
|
||||||
|
current.report.hot_text = hottest(¤t.text_widgets);
|
||||||
|
let report = std::mem::take(&mut current.report);
|
||||||
|
current.widgets.clear();
|
||||||
|
current.text_widgets.clear();
|
||||||
|
report
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hottest(calls: &HashMap<WidgetId, Calls>) -> Vec<Callsite> {
|
||||||
|
let mut calls: Vec<_> = calls
|
||||||
|
.iter()
|
||||||
|
.map(|(&id, calls)| Callsite {
|
||||||
|
id,
|
||||||
|
label: calls.label.clone(),
|
||||||
|
calls: calls.count,
|
||||||
|
distinct_widths: calls.widths.len(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
calls.sort_by(|a, b| b.calls.cmp(&a.calls).then_with(|| a.label.cmp(&b.label)));
|
||||||
|
calls.truncate(8);
|
||||||
|
calls
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn taking_a_report_resets_its_counters() {
|
||||||
|
let _ = take();
|
||||||
|
bump(Counter::Updates);
|
||||||
|
bump(Counter::Updates);
|
||||||
|
|
||||||
|
let report = take();
|
||||||
|
assert_eq!(report.counters().next(), Some(("updates", 2)));
|
||||||
|
assert!(take().counters().all(|(_, count)| count == 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,9 +9,14 @@
|
|||||||
#![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")]
|
||||||
|
pub mod layout_diagnostics;
|
||||||
|
|
||||||
mod attr;
|
mod attr;
|
||||||
mod event;
|
mod event;
|
||||||
|
mod fixed;
|
||||||
mod num;
|
mod num;
|
||||||
mod orientation;
|
mod orientation;
|
||||||
mod primitive;
|
mod primitive;
|
||||||
@@ -23,6 +28,7 @@ pub mod util;
|
|||||||
|
|
||||||
pub use attr::*;
|
pub use attr::*;
|
||||||
pub use event::*;
|
pub use event::*;
|
||||||
|
pub use fixed::*;
|
||||||
pub use num::*;
|
pub use num::*;
|
||||||
pub use orientation::*;
|
pub use orientation::*;
|
||||||
pub use primitive::*;
|
pub use primitive::*;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::vec2;
|
use crate::util::impl_axis_index;
|
||||||
|
use crate::{Px, Rel};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Align {
|
pub struct Align {
|
||||||
pub x: Option<AxisAlign>,
|
pub x: Option<AxisAlign>,
|
||||||
pub y: Option<AxisAlign>,
|
pub y: Option<AxisAlign>,
|
||||||
@@ -30,20 +31,32 @@ impl Align {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
/// Where a widget sits in a box longer than it is. The default is the middle,
|
||||||
pub enum AxisAlign {
|
/// because the two edges are the ones that assume a direction: which of them
|
||||||
Neg,
|
/// is the near one depends on the writing system and on which way a container
|
||||||
Center,
|
/// runs, and the middle is the same either way.
|
||||||
Pos,
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
}
|
pub struct AxisAlign(Rel);
|
||||||
|
|
||||||
impl AxisAlign {
|
impl AxisAlign {
|
||||||
pub const fn rel(&self) -> f32 {
|
pub const NEG: Self = Self::new(0.0);
|
||||||
match self {
|
pub const CENTER: Self = Self::new(0.5);
|
||||||
Self::Neg => 0.0,
|
pub const POS: Self = Self::new(1.0);
|
||||||
Self::Center => 0.5,
|
|
||||||
Self::Pos => 1.0,
|
pub const fn new(rel: f32) -> Self {
|
||||||
|
Self(Rel::from_f32(rel))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A fraction of the room left over, which is what the layout reads: the
|
||||||
|
/// three constants are the familiar places along it, not the only ones.
|
||||||
|
pub const fn rel(&self) -> Rel {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AxisAlign {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::CENTER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,41 +66,38 @@ pub struct CardinalAlign {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CardinalAlign {
|
impl CardinalAlign {
|
||||||
pub const LEFT: Self = Self::new(Axis::X, AxisAlign::Neg);
|
pub const LEFT: Self = Self::new(Axis::X, AxisAlign::NEG);
|
||||||
pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::Center);
|
pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::CENTER);
|
||||||
pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::Pos);
|
pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::POS);
|
||||||
pub const TOP: Self = Self::new(Axis::Y, AxisAlign::Neg);
|
pub const TOP: Self = Self::new(Axis::Y, AxisAlign::NEG);
|
||||||
pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::Center);
|
pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::CENTER);
|
||||||
pub const BOT: Self = Self::new(Axis::Y, AxisAlign::Pos);
|
pub const BOT: Self = Self::new(Axis::Y, AxisAlign::POS);
|
||||||
|
|
||||||
pub const fn new(axis: Axis, align: AxisAlign) -> Self {
|
pub const fn new(axis: Axis, align: AxisAlign) -> Self {
|
||||||
Self { axis, align }
|
Self { axis, align }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
pub struct RegionAlign {
|
pub struct RegionAlign {
|
||||||
pub x: AxisAlign,
|
pub x: AxisAlign,
|
||||||
pub y: AxisAlign,
|
pub y: AxisAlign,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegionAlign {
|
impl RegionAlign {
|
||||||
pub const TOP_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Neg);
|
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);
|
||||||
pub const TOP_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Neg);
|
pub const TOP_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::NEG);
|
||||||
pub const CENTER_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Center);
|
pub const CENTER_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::CENTER);
|
||||||
pub const CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Center);
|
pub const CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::CENTER);
|
||||||
pub const CENTER_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Center);
|
pub const CENTER_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::CENTER);
|
||||||
pub const BOT_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Pos);
|
pub const BOT_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::POS);
|
||||||
pub const BOT_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Pos);
|
pub const BOT_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::POS);
|
||||||
pub const BOT_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Pos);
|
pub const BOT_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::POS);
|
||||||
|
|
||||||
pub const fn new(x: AxisAlign, y: AxisAlign) -> Self {
|
pub const fn new(x: AxisAlign, y: AxisAlign) -> Self {
|
||||||
Self { x, y }
|
Self { x, y }
|
||||||
}
|
}
|
||||||
pub const fn rel(&self) -> Vec2 {
|
|
||||||
vec2(self.x.rel(), self.y.rel())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiVec2 {
|
impl UiVec2 {
|
||||||
@@ -140,16 +150,17 @@ impl Vec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiScalar {
|
impl Len {
|
||||||
|
/// This length placed in the box it is measured in: the alignment names a
|
||||||
|
/// point along that box, and the two ends are that point less the part of
|
||||||
|
/// the length falling before it and plus the part falling after.
|
||||||
pub const fn align(&self, align: AxisAlign) -> UiSpan {
|
pub const fn align(&self, align: AxisAlign) -> UiSpan {
|
||||||
let rel = align.rel();
|
let rel = align.rel();
|
||||||
let mut start = UiScalar::rel(rel);
|
let at = Len::from_parts(rel, Px::ZERO);
|
||||||
start.abs -= self.abs * rel;
|
UiSpan {
|
||||||
start.rel -= self.rel * rel;
|
start: at - self.scale(rel),
|
||||||
let mut end = UiScalar::rel(rel);
|
end: at + self.scale(Rel::ONE.sub(rel)),
|
||||||
end.abs += self.abs * (1.0 - rel);
|
}
|
||||||
end.rel += self.rel * (1.0 - rel);
|
|
||||||
UiSpan { start, end }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +176,8 @@ impl From<RegionAlign> for Align {
|
|||||||
impl From<Align> for RegionAlign {
|
impl From<Align> for RegionAlign {
|
||||||
fn from(align: Align) -> Self {
|
fn from(align: Align) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: align.x.unwrap_or(AxisAlign::Center),
|
x: align.x.unwrap_or(AxisAlign::CENTER),
|
||||||
y: align.y.unwrap_or(AxisAlign::Center),
|
y: align.y.unwrap_or(AxisAlign::CENTER),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,7 +200,10 @@ impl From<CardinalAlign> for Align {
|
|||||||
|
|
||||||
const impl From<RegionAlign> for UiVec2 {
|
const impl From<RegionAlign> for UiVec2 {
|
||||||
fn from(align: RegionAlign) -> Self {
|
fn from(align: RegionAlign) -> Self {
|
||||||
Self::rel(align.rel())
|
Self::new(
|
||||||
|
Len::from_parts(align.x.rel(), Px::ZERO),
|
||||||
|
Len::from_parts(align.y.rel(), Px::ZERO),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,3 +212,6 @@ impl RegionAlign {
|
|||||||
UiVec2::from(self)
|
UiVec2::from(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl_axis_index!(RegionAlign => AxisAlign);
|
||||||
|
impl_axis_index!(Align => Option<AxisAlign>);
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::util::impl_axis_index;
|
||||||
|
use crate::{Fixed, FixedVec2};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum Axis {
|
pub enum Axis {
|
||||||
X,
|
X,
|
||||||
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;
|
||||||
|
|
||||||
@@ -40,21 +47,16 @@ pub enum Sign {
|
|||||||
Pos,
|
Pos,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
||||||
|
pub const fn from_axis(axis: Axis, aligned: Fixed<SHIFT>, ortho: Fixed<SHIFT>) -> Self {
|
||||||
|
match axis {
|
||||||
|
Axis::X => Self::new(aligned, ortho),
|
||||||
|
Axis::Y => Self::new(ortho, aligned),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Vec2 {
|
impl Vec2 {
|
||||||
pub fn axis(&self, axis: Axis) -> f32 {
|
|
||||||
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 {
|
||||||
@@ -69,47 +71,5 @@ impl Vec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const trait AxisT {
|
impl_axis_index!({const SHIFT: u32} FixedVec2<SHIFT> => Fixed<SHIFT>);
|
||||||
fn get() -> Axis;
|
impl_axis_index!(Vec2 => f32);
|
||||||
}
|
|
||||||
|
|
||||||
pub struct XAxis;
|
|
||||||
const impl AxisT for XAxis {
|
|
||||||
fn get() -> Axis {
|
|
||||||
Axis::X
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct YAxis;
|
|
||||||
const impl AxisT for YAxis {
|
|
||||||
fn get() -> Axis {
|
|
||||||
Axis::Y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
|
||||||
pub struct BothAxis<T> {
|
|
||||||
pub x: T,
|
|
||||||
pub y: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> BothAxis<T> {
|
|
||||||
pub const fn axis<A: const AxisT>(&mut self) -> &mut T {
|
|
||||||
match A::get() {
|
|
||||||
Axis::X => &mut self.x,
|
|
||||||
Axis::Y => &mut self.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn take_axis<A: const AxisT>(self) -> T {
|
|
||||||
match A::get() {
|
|
||||||
Axis::X => self.x,
|
|
||||||
Axis::Y => self.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn axis_dyn(&mut self, axis: Axis) -> &mut T {
|
|
||||||
match axis {
|
|
||||||
Axis::X => &mut self.x,
|
|
||||||
Axis::Y => &mut self.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+153
-88
@@ -1,22 +1,39 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{UiNum, util::impl_op};
|
use crate::util::impl_axis_index;
|
||||||
|
use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||||
pub struct Size {
|
pub struct Size {
|
||||||
pub x: Len,
|
pub x: LayoutLen,
|
||||||
pub y: Len,
|
pub y: LayoutLen,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
/// What a widget asks for along one axis: a [`Len`] -- pixels and a fraction
|
||||||
pub struct Len {
|
/// of the box it is given -- plus a share of whatever is left over once
|
||||||
pub abs: f32,
|
/// everything fixed has been taken. The parts add up rather than choosing
|
||||||
pub rel: f32,
|
/// between one another.
|
||||||
pub rest: f32,
|
///
|
||||||
|
/// Only a container dividing its room can answer a share, so a length nobody
|
||||||
|
/// divides is a `Len`: a position, a padding, a cap, anything already
|
||||||
|
/// resolved.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct LayoutLen {
|
||||||
|
pub px: Px,
|
||||||
|
pub rel: Rel,
|
||||||
|
pub leftover: Weight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A bare number is pixels, which is the one length that needs no box to be
|
||||||
|
/// read in.
|
||||||
impl<N: UiNum> From<N> for Len {
|
impl<N: UiNum> From<N> for Len {
|
||||||
fn from(value: N) -> Self {
|
fn from(value: N) -> Self {
|
||||||
Len::abs(value.to_f32())
|
Len::px(value.to_f32())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N: UiNum> From<N> for LayoutLen {
|
||||||
|
fn from(value: N) -> Self {
|
||||||
|
LayoutLen::px(value.to_f32())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,52 +46,76 @@ impl<Nx: UiNum, Ny: UiNum> From<(Nx, Ny)> for Size {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Len> for Size {
|
/// A length with no share in it is a length a container does not have to
|
||||||
fn from(value: Len) -> Self {
|
/// divide, which is one it can always give.
|
||||||
|
impl From<Len> for LayoutLen {
|
||||||
|
fn from(len: Len) -> Self {
|
||||||
|
Self {
|
||||||
|
px: len.px,
|
||||||
|
rel: len.rel,
|
||||||
|
leftover: Weight::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LayoutLen> for Size {
|
||||||
|
fn from(value: LayoutLen) -> Self {
|
||||||
Self { x: value, y: value }
|
Self { x: value, y: value }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Size {
|
impl Size {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
x: Len::ZERO,
|
x: LayoutLen::ZERO,
|
||||||
y: Len::ZERO,
|
y: LayoutLen::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const REST: Self = Self {
|
pub const LEFTOVER: Self = Self {
|
||||||
x: Len::REST,
|
x: LayoutLen::LEFTOVER,
|
||||||
y: Len::REST,
|
y: LayoutLen::LEFTOVER,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn abs(v: Vec2) -> Self {
|
/// From something measured outside layout -- a texture, a shaped line --
|
||||||
|
/// which is where a size in floats comes from.
|
||||||
|
pub fn px(v: Vec2) -> Self {
|
||||||
|
Self::from_px(PxVec2::from_f32(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_px(v: PxVec2) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: Len::abs(v.x),
|
x: LayoutLen {
|
||||||
y: Len::abs(v.y),
|
px: v.x,
|
||||||
|
..LayoutLen::ZERO
|
||||||
|
},
|
||||||
|
y: LayoutLen {
|
||||||
|
px: v.y,
|
||||||
|
..LayoutLen::ZERO
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rel(v: Vec2) -> Self {
|
pub fn rel(v: Vec2) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: Len::rel(v.x),
|
x: LayoutLen::rel(v.x),
|
||||||
y: Len::rel(v.y),
|
y: LayoutLen::rel(v.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rest(v: Vec2) -> Self {
|
pub fn leftover(v: Vec2) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: Len::rest(v.x),
|
x: LayoutLen::leftover(v.x),
|
||||||
y: Len::rest(v.y),
|
y: LayoutLen::leftover(v.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_uivec2(self) -> UiVec2 {
|
pub fn to_uivec2(self) -> UiVec2 {
|
||||||
UiVec2 {
|
UiVec2 {
|
||||||
x: self.x.apply_rest(),
|
x: self.x.apply_leftover(),
|
||||||
y: self.y.apply_rest(),
|
y: self.y.apply_leftover(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_axis(axis: Axis, aligned: Len, ortho: Len) -> Self {
|
pub fn from_axis(axis: Axis, aligned: LayoutLen, ortho: LayoutLen) -> Self {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => Self {
|
Axis::X => Self {
|
||||||
x: aligned,
|
x: aligned,
|
||||||
@@ -86,54 +127,88 @@ impl Size {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn axis(&self, axis: Axis) -> Len {
|
|
||||||
match axis {
|
|
||||||
Axis::X => self.x,
|
|
||||||
Axis::Y => self.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Len {
|
impl LayoutLen {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
abs: 0.0,
|
px: Px::ZERO,
|
||||||
rel: 0.0,
|
rel: Rel::ZERO,
|
||||||
rest: 0.0,
|
leftover: Weight::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const REST: Self = Self {
|
pub const LEFTOVER: Self = Self {
|
||||||
abs: 0.0,
|
px: Px::ZERO,
|
||||||
rel: 0.0,
|
rel: Rel::ZERO,
|
||||||
rest: 1.0,
|
leftover: Weight::ONE,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn apply_rest(&self) -> UiScalar {
|
/// The whole of what is left over counts as the whole box, which is what
|
||||||
UiScalar {
|
/// a length means to something that is not dividing a box between
|
||||||
rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 },
|
/// siblings -- a scroll asking how long its content is.
|
||||||
abs: self.abs,
|
pub fn apply_leftover(&self) -> Len {
|
||||||
}
|
let share = match self.leftover > Weight::ZERO {
|
||||||
|
true => Rel::ONE,
|
||||||
|
false => Rel::ZERO,
|
||||||
|
};
|
||||||
|
Len::from_parts(self.rel.add(share), self.px)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn abs(abs: impl UiNum) -> Self {
|
/// Only pixels: the same number of them whatever box it lands in, and
|
||||||
|
/// whatever anyone else in the row asks for. A length that is any part
|
||||||
|
/// of a box or of what is left over is not one.
|
||||||
|
pub fn is_px(&self) -> bool {
|
||||||
|
self.rel == Rel::ZERO && self.leftover == Weight::ZERO
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nothing but a claim on what is left over, so there is no length here
|
||||||
|
/// at all where nothing is.
|
||||||
|
pub fn is_only_leftover(&self) -> bool {
|
||||||
|
self.leftover > Weight::ZERO && self.without_leftover() == Len::ZERO
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This as a length of a box, where it is one. `leftover` is not: a
|
||||||
|
/// share of what is left over is a length only to whoever divides one,
|
||||||
|
/// so it passes up in the reported size instead and is resolved there.
|
||||||
|
pub fn declared(&self) -> Option<Len> {
|
||||||
|
(self.leftover == Weight::ZERO).then(|| self.without_leftover())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What this takes whatever is left over: the reading of a length for
|
||||||
|
/// anyone not dividing a box between siblings, where a share is a claim
|
||||||
|
/// on someone else's room rather than a length of its own.
|
||||||
|
/// [`Self::apply_leftover`] is the opposite reading of the same value.
|
||||||
|
pub const fn without_leftover(&self) -> Len {
|
||||||
|
Len::from_parts(self.rel, self.px)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This length, given as a part of a box `len` long, as a part of the
|
||||||
|
/// box `len` is itself a part of. The share is untouched: it is a claim
|
||||||
|
/// on whoever divides the room, not a fraction of anything.
|
||||||
|
pub const fn within_len(self, len: Len) -> Self {
|
||||||
|
let part = self.without_leftover().within_len(len);
|
||||||
Self {
|
Self {
|
||||||
abs: abs.to_f32(),
|
px: part.px,
|
||||||
rel: 0.0,
|
rel: part.rel,
|
||||||
rest: 0.0,
|
leftover: self.leftover,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn px(px: impl UiNum) -> Self {
|
||||||
|
Self {
|
||||||
|
px: Px::from_num(px),
|
||||||
|
..Self::ZERO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rel(rel: impl UiNum) -> Self {
|
pub fn rel(rel: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
abs: 0.0,
|
rel: Rel::from_num(rel),
|
||||||
rel: rel.to_f32(),
|
..Self::ZERO
|
||||||
rest: 0.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rest(ratio: impl UiNum) -> Self {
|
pub fn leftover(ratio: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
abs: 0.0,
|
leftover: Weight::from_num(ratio),
|
||||||
rel: 0.0,
|
..Self::ZERO
|
||||||
rest: ratio.to_f32(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,38 +216,26 @@ impl Len {
|
|||||||
pub mod len_fns {
|
pub mod len_fns {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn abs(abs: impl UiNum) -> Len {
|
pub fn px(px: impl UiNum) -> LayoutLen {
|
||||||
Len {
|
LayoutLen::px(px)
|
||||||
abs: abs.to_f32(),
|
|
||||||
rel: 0.0,
|
|
||||||
rest: 0.0,
|
|
||||||
}
|
}
|
||||||
|
pub fn rel(rel: impl UiNum) -> LayoutLen {
|
||||||
|
LayoutLen::rel(rel)
|
||||||
}
|
}
|
||||||
pub fn rel(rel: impl UiNum) -> Len {
|
pub fn leftover(ratio: impl UiNum) -> LayoutLen {
|
||||||
Len {
|
LayoutLen::leftover(ratio)
|
||||||
abs: 0.0,
|
|
||||||
rel: rel.to_f32(),
|
|
||||||
rest: 0.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn rest(ratio: impl UiNum) -> Len {
|
|
||||||
Len {
|
|
||||||
abs: 0.0,
|
|
||||||
rel: 0.0,
|
|
||||||
rest: ratio.to_f32(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_op!(Len Add add; abs rel rest);
|
impl_op!(same LayoutLen Add add; px rel leftover);
|
||||||
impl_op!(Len Sub sub; abs rel rest);
|
impl_op!(same LayoutLen Sub sub; px rel leftover);
|
||||||
|
|
||||||
impl_op!(Size Add add; x y);
|
impl_op!(same Size Add add; x y);
|
||||||
impl_op!(Size Sub sub; x y);
|
impl_op!(same Size Sub sub; x y);
|
||||||
|
|
||||||
impl Default for Len {
|
impl Default for LayoutLen {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::rest(1.0)
|
Self::leftover(1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,17 +245,19 @@ impl std::fmt::Display for Size {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Len {
|
impl std::fmt::Display for LayoutLen {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
if self.abs != 0.0 {
|
if self.px != Px::ZERO {
|
||||||
write!(f, "{} abs;", self.abs)?;
|
write!(f, "{} px;", self.px)?;
|
||||||
}
|
}
|
||||||
if self.rel != 0.0 {
|
if self.rel != Rel::ZERO {
|
||||||
write!(f, "{} rel;", self.rel)?;
|
write!(f, "{} rel;", self.rel)?;
|
||||||
}
|
}
|
||||||
if self.rest != 0.0 {
|
if self.leftover != Weight::ZERO {
|
||||||
write!(f, "{} rest;", self.rest)?;
|
write!(f, "{} leftover;", self.leftover)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl_axis_index!(Size => LayoutLen);
|
||||||
+127
-174
@@ -1,41 +1,47 @@
|
|||||||
use std::{fmt::Display, hash::Hash, marker::Destruct};
|
use crate::util::impl_axis_index;
|
||||||
|
use std::{fmt::Display, marker::Destruct};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{Px, PxVec2, Rel, UiNum, util::impl_op};
|
||||||
UiNum,
|
|
||||||
util::{LerpUtil, impl_op},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||||
pub struct UiVec2 {
|
pub struct UiVec2 {
|
||||||
pub x: UiScalar,
|
pub x: Len,
|
||||||
pub y: UiScalar,
|
pub y: Len,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiVec2 {
|
impl UiVec2 {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
x: UiScalar::ZERO,
|
x: Len::ZERO,
|
||||||
y: UiScalar::ZERO,
|
y: Len::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const fn new(x: UiScalar, y: UiScalar) -> Self {
|
pub const fn new(x: Len, y: Len) -> Self {
|
||||||
Self { x, y }
|
Self { x, y }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn abs(abs: impl const Into<Vec2>) -> Self {
|
pub const fn px(px: impl const Into<Vec2>) -> Self {
|
||||||
let abs = abs.into();
|
let px = px.into();
|
||||||
Self {
|
Self {
|
||||||
x: UiScalar::abs(abs.x),
|
x: Len::px(px.x),
|
||||||
y: UiScalar::abs(abs.y),
|
y: Len::px(px.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From lengths already on the grid, with no fraction of a box.
|
||||||
|
pub const fn from_px(px: PxVec2) -> Self {
|
||||||
|
Self {
|
||||||
|
x: Len::from_parts(Rel::ZERO, px.x),
|
||||||
|
y: Len::from_parts(Rel::ZERO, px.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn rel(rel: impl const Into<Vec2>) -> Self {
|
pub const fn rel(rel: impl const Into<Vec2>) -> Self {
|
||||||
let rel = rel.into();
|
let rel = rel.into();
|
||||||
Self {
|
Self {
|
||||||
x: UiScalar::rel(rel.x),
|
x: Len::rel(rel.x),
|
||||||
y: UiScalar::rel(rel.y),
|
y: Len::rel(rel.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,37 +62,15 @@ impl UiVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn outside(&self, region: &UiRegion) -> UiVec2 {
|
/// Resolved against a box of `size`, which is where a fraction stops
|
||||||
UiVec2 {
|
/// being one and becomes a place.
|
||||||
x: self.x.outside(®ion.x),
|
pub fn to_px(&self, size: PxVec2) -> PxVec2 {
|
||||||
y: self.y.outside(®ion.y),
|
PxVec2::new(self.x.to_px(size.x), self.y.to_px(size.y))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar {
|
|
||||||
match axis {
|
|
||||||
Axis::X => &mut self.x,
|
|
||||||
Axis::Y => &mut self.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn axis(&self, axis: Axis) -> UiScalar {
|
|
||||||
match axis {
|
|
||||||
Axis::X => self.x,
|
|
||||||
Axis::Y => self.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_abs(&self, rel: Vec2) -> Vec2 {
|
|
||||||
Vec2 {
|
|
||||||
x: self.x.to_abs(rel.x),
|
|
||||||
y: self.y.to_abs(rel.y),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const FULL_SIZE: Self = Self::rel(Vec2::ONE);
|
pub const FULL_SIZE: Self = Self::rel(Vec2::ONE);
|
||||||
|
|
||||||
pub const fn from_axis(axis: Axis, aligned: UiScalar, ortho: UiScalar) -> Self {
|
pub const fn from_axis(axis: Axis, aligned: Len, ortho: Len) -> Self {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => Self {
|
Axis::X => Self {
|
||||||
x: aligned,
|
x: aligned,
|
||||||
@@ -99,34 +83,27 @@ impl UiVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_abs(&self) -> Vec2 {
|
pub fn get_px(&self) -> Vec2 {
|
||||||
(self.x.abs, self.y.abs).into()
|
(self.x.px.to_f32(), self.y.px.to_f32()).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rel(&self) -> Vec2 {
|
pub fn get_rel(&self) -> Vec2 {
|
||||||
(self.x.rel, self.y.rel).into()
|
(self.x.rel.to_f32(), self.y.rel.to_f32()).into()
|
||||||
}
|
|
||||||
|
|
||||||
pub fn abs_mut(&mut self) -> Vec2View<'_> {
|
|
||||||
Vec2View {
|
|
||||||
x: &mut self.x.abs,
|
|
||||||
y: &mut self.y.abs,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for UiVec2 {
|
impl Display for UiVec2 {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "rel{};abs{}", self.get_rel(), self.get_abs())
|
write!(f, "rel{};px{}", self.get_rel(), self.get_px())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_op!(UiVec2 Add add; x y);
|
impl_op!(same UiVec2 Add add; x y);
|
||||||
impl_op!(UiVec2 Sub sub; x y);
|
impl_op!(same UiVec2 Sub sub; x y);
|
||||||
|
|
||||||
const impl From<Vec2> for UiVec2 {
|
const impl From<Vec2> for UiVec2 {
|
||||||
fn from(abs: Vec2) -> Self {
|
fn from(px: Vec2) -> Self {
|
||||||
Self::abs(abs)
|
Self::px(px)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,133 +111,137 @@ const impl<T: const UiNum, U: const UiNum> From<(T, U)> for UiVec2
|
|||||||
where
|
where
|
||||||
(T, U): const Destruct,
|
(T, U): const Destruct,
|
||||||
{
|
{
|
||||||
fn from(abs: (T, U)) -> Self {
|
fn from(px: (T, U)) -> Self {
|
||||||
Self::abs(abs)
|
Self::px(px)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A length along one axis: a fraction of the box it is measured in plus an
|
||||||
|
/// offset, `rel * box + px`. A position is the same number -- the length from
|
||||||
|
/// the start of the box to the point -- which is why a [`UiSpan`] is two of
|
||||||
|
/// these. Both parts are fixed point, so composing one through a chain of
|
||||||
|
/// boxes rounds only where it multiplies, and lands on the same number as any
|
||||||
|
/// other route to the same place.
|
||||||
|
///
|
||||||
|
/// It carries no claim on what a container has left over. That is
|
||||||
|
/// [`crate::LayoutLen`], which is this plus a weight, and which means nothing
|
||||||
|
/// to anyone but whoever divides the room.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, Default, bytemuck::Zeroable)]
|
||||||
pub struct UiScalar {
|
pub struct Len {
|
||||||
pub rel: f32,
|
pub rel: Rel,
|
||||||
pub abs: f32,
|
pub px: Px,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for UiScalar {}
|
impl_op!(same Len Add add; rel px);
|
||||||
impl Hash for UiScalar {
|
impl_op!(same Len Sub sub; rel px);
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
||||||
state.write_u32(self.rel.to_bits());
|
impl Len {
|
||||||
state.write_u32(self.abs.to_bits());
|
pub const ZERO: Self = Self {
|
||||||
}
|
rel: Rel::ZERO,
|
||||||
|
px: Px::ZERO,
|
||||||
|
};
|
||||||
|
pub const FULL: Self = Self {
|
||||||
|
rel: Rel::ONE,
|
||||||
|
px: Px::ZERO,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const fn new(rel: f32, px: f32) -> Self {
|
||||||
|
Self::from_parts(Rel::from_f32(rel), Px::from_f32(px))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_op!(UiScalar Add add; rel abs);
|
/// From parts already on the grid, rather than numbers to be put on it.
|
||||||
impl_op!(UiScalar Sub sub; rel abs);
|
pub const fn from_parts(rel: Rel, px: Px) -> Self {
|
||||||
|
Self { rel, px }
|
||||||
impl UiScalar {
|
|
||||||
pub const ZERO: Self = Self { rel: 0.0, abs: 0.0 };
|
|
||||||
pub const FULL: Self = Self { rel: 1.0, abs: 0.0 };
|
|
||||||
|
|
||||||
pub const fn new(rel: f32, abs: f32) -> Self {
|
|
||||||
Self { rel, abs }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn rel(rel: f32) -> Self {
|
pub const fn rel(rel: f32) -> Self {
|
||||||
Self { rel, abs: 0.0 }
|
Self::from_parts(Rel::from_f32(rel), Px::ZERO)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn abs(abs: f32) -> Self {
|
pub const fn px(px: f32) -> Self {
|
||||||
Self { rel: 0.0, abs }
|
Self::from_parts(Rel::ZERO, Px::from_f32(px))
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn rel_min() -> Self {
|
|
||||||
Self::new(0.0, 0.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn rel_max() -> Self {
|
|
||||||
Self::new(1.0, 0.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn max(&self, other: Self) -> Self {
|
pub const fn max(&self, other: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rel: self.rel.max(other.rel),
|
rel: self.rel.max(other.rel),
|
||||||
abs: self.abs.max(other.abs),
|
px: self.px.max(other.px),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn min(&self, other: Self) -> Self {
|
pub const fn min(&self, other: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rel: self.rel.min(other.rel),
|
rel: self.rel.min(other.rel),
|
||||||
abs: self.abs.min(other.abs),
|
px: self.px.min(other.px),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn offset(mut self, amt: f32) -> Self {
|
/// Both parts by the same fraction, which is what a part of a length
|
||||||
self.abs += amt;
|
/// means when the length is part pixels and part a fraction of a box.
|
||||||
|
pub const fn scale(&self, by: Rel) -> Self {
|
||||||
|
Self {
|
||||||
|
rel: self.rel.mul(by),
|
||||||
|
px: self.px.mul(by),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn offset(mut self, amt: Px) -> Self {
|
||||||
|
self.px = self.px.add(amt);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn within(&self, span: &UiSpan) -> Self {
|
pub const fn within(&self, span: &UiSpan) -> Self {
|
||||||
let anchor = self.rel.lerp(span.start.rel, span.end.rel);
|
|
||||||
let offset = self.abs + self.rel.lerp(span.start.abs, span.end.abs);
|
|
||||||
Self {
|
Self {
|
||||||
rel: anchor,
|
rel: self.rel.lerp(span.start.rel, span.end.rel),
|
||||||
abs: offset,
|
px: self.px.add(self.rel.lerp(span.start.px, span.end.px)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn outside(&self, span: &UiSpan) -> Self {
|
pub const fn within_len(&self, len: Len) -> Self {
|
||||||
let rel = self.rel.lerp_inv(span.start.rel, span.end.rel);
|
|
||||||
let abs = self.abs - rel.lerp(span.start.abs, span.end.abs);
|
|
||||||
Self { rel, abs }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn within_len(&self, len: UiScalar) -> Self {
|
|
||||||
self.within(&UiSpan {
|
self.within(&UiSpan {
|
||||||
start: UiScalar::ZERO,
|
start: Len::ZERO,
|
||||||
end: len,
|
end: len,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_len(&self, len: UiScalar) -> Self {
|
|
||||||
len.within_len(*self)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn flip(&mut self) {
|
pub const fn flip(&mut self) {
|
||||||
self.rel = 1.0 - self.rel;
|
self.rel = Rel::ONE.sub(self.rel);
|
||||||
self.abs = -self.abs;
|
self.px = self.px.neg();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn to(&self, end: Self) -> UiSpan {
|
pub const fn to(&self, end: Self) -> UiSpan {
|
||||||
UiSpan { start: *self, end }
|
UiSpan { start: *self, end }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn to_abs(&self, rel: f32) -> f32 {
|
/// Resolved against a box of `len`, which is the only place a fraction
|
||||||
self.rel * rel + self.abs
|
/// becomes a number of pixels.
|
||||||
|
pub const fn to_px(&self, len: Px) -> Px {
|
||||||
|
self.px.add(len.mul(self.rel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct UiSpan {
|
pub struct UiSpan {
|
||||||
pub start: UiScalar,
|
pub start: Len,
|
||||||
pub end: UiScalar,
|
pub end: Len,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiSpan {
|
impl UiSpan {
|
||||||
pub const FULL: Self = Self {
|
pub const FULL: Self = Self {
|
||||||
start: UiScalar::ZERO,
|
start: Len::ZERO,
|
||||||
end: UiScalar::FULL,
|
end: Len::FULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const fn rel(rel: f32) -> Self {
|
pub const fn rel(rel: f32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
start: UiScalar::rel(rel),
|
start: Len::rel(rel),
|
||||||
end: UiScalar::rel(rel),
|
end: Len::rel(rel),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn new(start: UiScalar, end: UiScalar) -> Self {
|
pub const fn new(start: Len, end: Len) -> Self {
|
||||||
Self { start, end }
|
Self { start, end }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,14 +249,19 @@ impl UiSpan {
|
|||||||
self.start.flip();
|
self.start.flip();
|
||||||
self.end.flip();
|
self.end.flip();
|
||||||
std::mem::swap(&mut self.start.rel, &mut self.end.rel);
|
std::mem::swap(&mut self.start.rel, &mut self.end.rel);
|
||||||
std::mem::swap(&mut self.start.abs, &mut self.end.abs);
|
std::mem::swap(&mut self.start.px, &mut self.end.px);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn shift(&mut self, offset: UiScalar) {
|
pub const fn shift(&mut self, offset: Len) {
|
||||||
self.start += offset;
|
self.start += offset;
|
||||||
self.end += offset;
|
self.end += offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Composing a box through the one it sits in, and the hottest line in
|
||||||
|
/// layout. It used to skip the multiplies where a span was the whole of
|
||||||
|
/// its parent or the parent the whole of its own; both come out of the
|
||||||
|
/// multiply unchanged anyway, and the body those comparisons cost was
|
||||||
|
/// what kept the inliner from taking this at all.
|
||||||
pub const fn within(&self, parent: &Self) -> Self {
|
pub const fn within(&self, parent: &Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
start: self.start.within(parent),
|
start: self.start.within(parent),
|
||||||
@@ -283,14 +269,16 @@ impl UiSpan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn outside(&self, parent: &Self) -> Self {
|
/// A box `len` long inside this one, on the side `align` says. Both must
|
||||||
Self {
|
/// be lengths of the same rel base: it subtracts one from the other
|
||||||
start: self.start.outside(parent),
|
/// rather than composing it in, which is what keeps a fraction the same
|
||||||
end: self.end.outside(parent),
|
/// 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) -> UiScalar {
|
pub const fn len(&self) -> Len {
|
||||||
self.end - self.start
|
self.end - self.start
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,27 +312,6 @@ impl UiRegion {
|
|||||||
y: self.y.within(&parent.y),
|
y: self.y.within(&parent.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub const fn outside(&self, parent: &Self) -> Self {
|
|
||||||
Self {
|
|
||||||
x: self.x.outside(&parent.x),
|
|
||||||
y: self.y.outside(&parent.y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn axis(&mut 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(),
|
||||||
@@ -363,10 +330,10 @@ impl UiRegion {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_px(&self, size: Vec2) -> PixelRegion {
|
pub fn to_px(&self, size: PxVec2) -> PixelRegion {
|
||||||
PixelRegion {
|
PixelRegion {
|
||||||
top_left: self.top_left().get_rel() * size + self.top_left().get_abs(),
|
top_left: self.top_left().to_px(size),
|
||||||
bot_right: self.bot_right().get_rel() * size + self.bot_right().get_abs(),
|
bot_right: self.bot_right().to_px(size),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,21 +388,21 @@ impl Display for UiRegion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct PixelRegion {
|
pub struct PixelRegion {
|
||||||
pub top_left: Vec2,
|
pub top_left: PxVec2,
|
||||||
pub bot_right: Vec2,
|
pub bot_right: PxVec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PixelRegion {
|
impl PixelRegion {
|
||||||
pub fn contains(&self, pos: Vec2) -> bool {
|
pub fn contains(&self, pos: PxVec2) -> bool {
|
||||||
pos.x >= self.top_left.x
|
pos.x >= self.top_left.x
|
||||||
&& pos.x <= self.bot_right.x
|
&& pos.x <= self.bot_right.x
|
||||||
&& pos.y >= self.top_left.y
|
&& pos.y >= self.top_left.y
|
||||||
&& pos.y <= self.bot_right.y
|
&& pos.y <= self.bot_right.y
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn size(&self) -> Vec2 {
|
pub fn size(&self) -> PxVec2 {
|
||||||
self.bot_right - self.top_left
|
self.bot_right - self.top_left
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -446,19 +413,5 @@ impl Display for PixelRegion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Vec2View<'a> {
|
impl_axis_index!(UiVec2 => Len);
|
||||||
pub x: &'a mut f32,
|
impl_axis_index!(UiRegion => UiSpan);
|
||||||
pub y: &'a mut f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Vec2View<'_> {
|
|
||||||
pub fn set(&mut self, other: Vec2) {
|
|
||||||
*self.x = other.x;
|
|
||||||
*self.y = other.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(&mut self, other: Vec2) {
|
|
||||||
*self.x += other.x;
|
|
||||||
*self.y += other.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -120,6 +120,10 @@ impl<T: Default> Layers<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DrawLayers {
|
impl DrawLayers {
|
||||||
|
/// Inlined on purpose: it is one call per glyph, the innermost thing a
|
||||||
|
/// frame does, and whether the inliner takes it turns out to depend on
|
||||||
|
/// unrelated code elsewhere in the crate -- 12% of a resize frame.
|
||||||
|
#[inline]
|
||||||
pub fn write<P: Primitive>(
|
pub fn write<P: Primitive>(
|
||||||
&mut self,
|
&mut self,
|
||||||
layer: LayerId,
|
layer: LayerId,
|
||||||
|
|||||||
+169
-15
@@ -1,11 +1,17 @@
|
|||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
use crate::layout_diagnostics::{self as diag, Counter, TimerKind};
|
||||||
use crate::{
|
use crate::{
|
||||||
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
|
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, Px, PxVec2, RegionAlign, UiColor,
|
||||||
|
util::Vec2,
|
||||||
};
|
};
|
||||||
use parley::{
|
use parley::{
|
||||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
||||||
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||||
};
|
};
|
||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
|
};
|
||||||
use swash::{
|
use swash::{
|
||||||
FontRef,
|
FontRef,
|
||||||
scale::{Render, ScaleContext, Source, StrikeWith},
|
scale::{Render, ScaleContext, Source, StrikeWith},
|
||||||
@@ -17,8 +23,32 @@ pub struct TextData {
|
|||||||
pub layout_ctx: LayoutContext<UiColor>,
|
pub layout_ctx: LayoutContext<UiColor>,
|
||||||
scale_ctx: ScaleContext,
|
scale_ctx: ScaleContext,
|
||||||
pub atlas: GlyphAtlas,
|
pub atlas: GlyphAtlas,
|
||||||
|
spare: VecDeque<Placed>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The glyphs of one text at one width. A buffer holds the ones it is drawn
|
||||||
|
/// as; these are the ones it had before, kept because a container measures a
|
||||||
|
/// child by drawing it in a box it may not keep, and so comes back to widths
|
||||||
|
/// it has already asked for.
|
||||||
|
struct Placed {
|
||||||
|
/// Where the glyphs land is a function of these three and nothing else,
|
||||||
|
/// so no widget or buffer identity is involved and two texts of the same
|
||||||
|
/// words share an answer.
|
||||||
|
text: String,
|
||||||
|
key: LayoutKey,
|
||||||
|
glyphs: RenderedText,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many to keep. Bounding the whole store rather than each buffer is what
|
||||||
|
/// makes this a fixed cost instead of one a tree of ten thousand texts pays
|
||||||
|
/// ten thousand times; the re-asks come from laying out one subtree, so they
|
||||||
|
/// are close together and few are needed. Instructions over 500 resize frames
|
||||||
|
/// of `tests/revision_cost.rs`, both the repeating widths and the sweep that
|
||||||
|
/// cannot hit across frames: 13.7B at 32, 12.1B at 64, 10.4B and 12.1B at 128,
|
||||||
|
/// and nothing past that -- so 128, which is no worse in the case that never
|
||||||
|
/// repeats and better in the one that does.
|
||||||
|
const SPARE_PLACED: usize = 128;
|
||||||
|
|
||||||
impl Default for TextData {
|
impl Default for TextData {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -26,6 +56,7 @@ impl Default for TextData {
|
|||||||
layout_ctx: LayoutContext::new(),
|
layout_ctx: LayoutContext::new(),
|
||||||
scale_ctx: ScaleContext::new(),
|
scale_ctx: ScaleContext::new(),
|
||||||
atlas: GlyphAtlas::default(),
|
atlas: GlyphAtlas::default(),
|
||||||
|
spare: VecDeque::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,6 +112,9 @@ pub struct TextBuffer {
|
|||||||
text: String,
|
text: String,
|
||||||
layout: Layout<UiColor>,
|
layout: Layout<UiColor>,
|
||||||
layout_key: Option<LayoutKey>,
|
layout_key: Option<LayoutKey>,
|
||||||
|
/// The glyphs placed from `layout`, so drawing this text again at the
|
||||||
|
/// width it already has places them once.
|
||||||
|
placed: Option<RenderedText>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
@@ -95,13 +129,10 @@ impl TextBuffer {
|
|||||||
text: text.into(),
|
text: text.into(),
|
||||||
layout: Layout::new(),
|
layout: Layout::new(),
|
||||||
layout_key: None,
|
layout_key: None,
|
||||||
|
placed: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_empty() -> Self {
|
|
||||||
Self::new("")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn text(&self) -> &str {
|
pub fn text(&self) -> &str {
|
||||||
&self.text
|
&self.text
|
||||||
}
|
}
|
||||||
@@ -119,15 +150,45 @@ impl TextBuffer {
|
|||||||
if text != self.text {
|
if text != self.text {
|
||||||
self.text = text;
|
self.text = text;
|
||||||
self.layout_key = None;
|
self.layout_key = None;
|
||||||
|
self.placed = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Invalidates the layout and returns the underlying string for editing.
|
/// Invalidates the layout and returns the underlying string for editing.
|
||||||
pub fn edit(&mut self) -> &mut String {
|
pub fn edit(&mut self) -> &mut String {
|
||||||
self.layout_key = None;
|
self.layout_key = None;
|
||||||
|
self.placed = None;
|
||||||
&mut self.text
|
&mut self.text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The glyphs of the shaping it is drawn as, once they are placed.
|
||||||
|
pub fn rendered(&self) -> Option<&RenderedText> {
|
||||||
|
self.placed.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The width its shaping wraps at, and `None` where it does not wrap or
|
||||||
|
/// has not been shaped.
|
||||||
|
pub fn wrap_width(&self) -> Option<f32> {
|
||||||
|
self.layout_key.as_ref()?.max_width
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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())
|
||||||
}
|
}
|
||||||
@@ -138,8 +199,62 @@ impl TextBuffer {
|
|||||||
max_width: width,
|
max_width: width,
|
||||||
};
|
};
|
||||||
if self.layout_key.as_ref() == Some(&layout_key) {
|
if self.layout_key.as_ref() == Some(&layout_key) {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::TextShapeHits);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// A greedy break at one width is the same break at every width down
|
||||||
|
// to the longest line it produced: each line still fits, and none can
|
||||||
|
// take a word that would not fit in the wider box. So the layout in
|
||||||
|
// hand already answers, and re-breaking would only be work.
|
||||||
|
//
|
||||||
|
// At the longest line exactly, with no margin below it. A narrower
|
||||||
|
// width really does break differently, so answering one from the
|
||||||
|
// break in hand is how a warm tree keeps lines a cold tree would
|
||||||
|
// never produce. The margin was here because a text reports the
|
||||||
|
// width it used and a parent hands that back; the report is the step
|
||||||
|
// at or above its longest line now, so what comes back fits.
|
||||||
|
if let Some(key) = &self.layout_key
|
||||||
|
&& key.attrs == *attrs
|
||||||
|
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
|
||||||
|
&& want <= broke_at
|
||||||
|
&& want >= self.layout.width()
|
||||||
|
{
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::TextShapeHits);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let same_shaping = self
|
||||||
|
.layout_key
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|key| key.attrs == *attrs);
|
||||||
|
let old_key = self.layout_key.replace(layout_key);
|
||||||
|
// The glyphs it holds are of the width it held, which the layout may
|
||||||
|
// well come back to.
|
||||||
|
if let Some(key) = old_key
|
||||||
|
&& let Some(glyphs) = self.placed.take()
|
||||||
|
{
|
||||||
|
data.keep_placed(Placed {
|
||||||
|
text: self.text.clone(),
|
||||||
|
key,
|
||||||
|
glyphs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Only the line breaking depends on the width: the shaped runs under
|
||||||
|
// it are a function of the text and the attrs, and parley re-breaks
|
||||||
|
// them in place. So a new width is a break, not a shaping.
|
||||||
|
if same_shaping {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::TextBreaks);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _break = diag::timer(TimerKind::TextBreak);
|
||||||
|
self.break_lines(width);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::TextShapes);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _shape = diag::timer(TimerKind::TextShape);
|
||||||
let mut builder = data
|
let mut builder = data
|
||||||
.layout_ctx
|
.layout_ctx
|
||||||
.ranged_builder(&mut data.font_ctx, &self.text, 1.0, true);
|
.ranged_builder(&mut data.font_ctx, &self.text, 1.0, true);
|
||||||
@@ -150,10 +265,13 @@ impl TextBuffer {
|
|||||||
)));
|
)));
|
||||||
builder.push_default(StyleProperty::Brush(attrs.color));
|
builder.push_default(StyleProperty::Brush(attrs.color));
|
||||||
builder.build_into(&mut self.layout, &self.text);
|
builder.build_into(&mut self.layout, &self.text);
|
||||||
|
self.break_lines(width);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn break_lines(&mut self, width: Option<f32>) {
|
||||||
self.layout.break_all_lines(width);
|
self.layout.break_all_lines(width);
|
||||||
self.layout
|
self.layout
|
||||||
.align(Alignment::Start, AlignmentOptions::default());
|
.align(Alignment::Start, AlignmentOptions::default());
|
||||||
self.layout_key = Some(layout_key);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,9 +314,9 @@ impl TextData {
|
|||||||
};
|
};
|
||||||
placed.push(PlacedGlyph {
|
placed.push(PlacedGlyph {
|
||||||
entry,
|
entry,
|
||||||
offset: Vec2::new(
|
offset: PxVec2::new(
|
||||||
glyph.x.floor() + entry.left as f32,
|
Px::from_int(glyph.x.floor() as i32 + entry.left),
|
||||||
glyph.y.floor() - entry.top as f32,
|
Px::from_int(glyph.y.floor() as i32 - entry.top),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -265,18 +383,54 @@ pub struct RenderedText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TextData {
|
impl TextData {
|
||||||
pub fn render(
|
/// The glyphs of this text at this width, taken out of what is kept.
|
||||||
|
fn take_placed(&mut self, text: &str, key: &LayoutKey) -> Option<RenderedText> {
|
||||||
|
// From the newest, since a re-ask is usually of something recent.
|
||||||
|
let at = self
|
||||||
|
.spare
|
||||||
|
.iter()
|
||||||
|
.rposition(|spare| spare.key == *key && spare.text == text)?;
|
||||||
|
self.spare.remove(at).map(|spare| spare.glyphs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keep_placed(&mut self, placed: Placed) {
|
||||||
|
if self.spare.len() >= SPARE_PLACED {
|
||||||
|
self.spare.pop_front();
|
||||||
|
}
|
||||||
|
self.spare.push_back(placed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<'b>(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: &mut TextBuffer,
|
buffer: &'b mut TextBuffer,
|
||||||
attrs: &TextAttrs,
|
attrs: &TextAttrs,
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
) -> RenderedText {
|
) -> &'b RenderedText {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::TextRenders);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _render = diag::timer(TimerKind::TextRender);
|
||||||
buffer.shape(self, attrs, width);
|
buffer.shape(self, attrs, width);
|
||||||
let glyphs = self.place(buffer);
|
// Only asked for when the buffer no longer holds them: taking one out
|
||||||
|
// of the store to then drop it would throw an answer away.
|
||||||
|
let placed = buffer.placed.take().or_else(|| {
|
||||||
|
let key = buffer.layout_key.as_ref()?;
|
||||||
|
self.take_placed(&buffer.text, key)
|
||||||
|
});
|
||||||
|
let placed = match placed {
|
||||||
|
Some(placed) => placed,
|
||||||
|
None => {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::GlyphPlacements);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _place = diag::timer(TimerKind::GlyphPlacement);
|
||||||
RenderedText {
|
RenderedText {
|
||||||
glyphs,
|
glyphs: self.place(buffer),
|
||||||
size: buffer.size(),
|
size: buffer.size(),
|
||||||
color: attrs.color,
|
color: attrs.color,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
buffer.placed.insert(placed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
PatchRect,
|
PatchRect, PxVec2,
|
||||||
util::{HashMap, Vec2},
|
util::{HashMap, Vec2},
|
||||||
};
|
};
|
||||||
use image::RgbaImage;
|
use image::RgbaImage;
|
||||||
@@ -167,10 +167,6 @@ impl GlyphAtlas {
|
|||||||
pub fn page_count(&self) -> u32 {
|
pub fn page_count(&self) -> u32 {
|
||||||
self.pages.len() as u32
|
self.pages.len() as u32
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn glyph_count(&self) -> usize {
|
|
||||||
self.entries.len()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
@@ -241,5 +237,7 @@ fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
|
|||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct PlacedGlyph {
|
pub struct PlacedGlyph {
|
||||||
pub entry: GlyphEntry,
|
pub entry: GlyphEntry,
|
||||||
pub offset: Vec2,
|
/// Whole pixels from the origin of the text to this glyph's top-left,
|
||||||
|
/// on the grid once here rather than on every frame that draws it.
|
||||||
|
pub offset: PxVec2,
|
||||||
}
|
}
|
||||||
+52
-8
@@ -1,11 +1,10 @@
|
|||||||
use crate::{UiRegion, util::Id};
|
use crate::{UiRegion, util::Id, util::Vec2};
|
||||||
use wgpu::*;
|
use wgpu::*;
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||||
pub struct WindowUniform {
|
pub struct WindowUniform {
|
||||||
pub width: f32,
|
pub dim: Vec2,
|
||||||
pub height: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -13,15 +12,19 @@ pub struct WindowUniform {
|
|||||||
pub struct PrimitiveInstance {
|
pub struct PrimitiveInstance {
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
pub mask_idx: MaskIdx,
|
pub mask_idx: MaskIdx,
|
||||||
|
pub move_idx: MoveIdx,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrimitiveInstance {
|
impl PrimitiveInstance {
|
||||||
const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![
|
// The region's four scalars, each a `Rel` beside a `Px`: whole counts
|
||||||
0 => Float32x2,
|
// that the shader decodes, rather than the numbers themselves.
|
||||||
1 => Float32x2,
|
const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![
|
||||||
2 => Float32x2,
|
0 => Sint32x2,
|
||||||
3 => Float32x2,
|
1 => Sint32x2,
|
||||||
|
2 => Sint32x2,
|
||||||
|
3 => Sint32x2,
|
||||||
4 => Uint32,
|
4 => Uint32,
|
||||||
|
5 => Uint32,
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn desc() -> VertexBufferLayout<'static> {
|
pub fn desc() -> VertexBufferLayout<'static> {
|
||||||
@@ -43,4 +46,45 @@ impl MaskIdx {
|
|||||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct Mask {
|
pub struct Mask {
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
|
pub move_idx: MoveIdx,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Its own type rather than another `Id<u32>`, because it sits beside
|
||||||
|
/// `MaskIdx` in an instance and the two must not be swappable.
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
pub struct MoveIdx(u32);
|
||||||
|
|
||||||
|
impl MoveIdx {
|
||||||
|
pub const NONE: Self = Self(u32::MAX);
|
||||||
|
|
||||||
|
pub(crate) fn slot(idx: usize) -> Self {
|
||||||
|
Self(idx as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn idx(self) -> usize {
|
||||||
|
self.0 as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One link of the chain a primitive's position is resolved through: the box
|
||||||
|
/// its contents are placed within, given in the coordinates of the slot it
|
||||||
|
/// names. Moving or resizing a subtree writes its own slot and nothing else.
|
||||||
|
///
|
||||||
|
/// The identity is `UiRegion::FULL`, not zero: a zeroed entry is a box of no
|
||||||
|
/// extent, which collapses everything under it to a point.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct MoveOffset {
|
||||||
|
pub region: UiRegion,
|
||||||
|
pub parent: MoveIdx,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl bytemuck::Pod for MoveOffset {}
|
||||||
|
unsafe impl bytemuck::Zeroable for MoveOffset {}
|
||||||
|
|
||||||
|
impl MoveOffset {
|
||||||
|
pub fn new(parent: MoveIdx, region: UiRegion) -> Self {
|
||||||
|
Self { region, parent }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+96
-16
@@ -17,11 +17,31 @@ mod texture;
|
|||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
pub use atlas::*;
|
pub use atlas::*;
|
||||||
pub use data::{Mask, MaskIdx};
|
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset};
|
||||||
pub use primitive::*;
|
pub use primitive::*;
|
||||||
|
|
||||||
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
||||||
|
|
||||||
|
fn module_source(wgsl: &str) -> String {
|
||||||
|
// Every number both sides count in, written once here rather than a
|
||||||
|
// second time in the shader: a grid the two disagree about puts every
|
||||||
|
// coordinate somewhere else, and a sentinel they disagree about makes one
|
||||||
|
// of them walk a chain from a slot the other says is not there.
|
||||||
|
format!(
|
||||||
|
"const PX_STEP: f32 = 1.0 / {}.0;\n\
|
||||||
|
const REL_STEP: f32 = 1.0 / {}.0;\n\
|
||||||
|
const MASK_NONE: u32 = {}u;\n\
|
||||||
|
const MOVE_NONE: u32 = {}u;\n\
|
||||||
|
const CHAIN_LIMIT: u32 = {}u;\n\
|
||||||
|
{PRELUDE}\n{wgsl}",
|
||||||
|
1u32 << crate::PX_SHIFT,
|
||||||
|
1u32 << crate::REL_SHIFT,
|
||||||
|
MaskIdx::NONE.idx(),
|
||||||
|
MoveIdx::NONE.idx(),
|
||||||
|
crate::CHAIN_LIMIT,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub struct UiRenderNode {
|
pub struct UiRenderNode {
|
||||||
shared_layout: BindGroupLayout,
|
shared_layout: BindGroupLayout,
|
||||||
shared_group: BindGroup,
|
shared_group: BindGroup,
|
||||||
@@ -34,6 +54,7 @@ pub struct UiRenderNode {
|
|||||||
active: Vec<usize>,
|
active: Vec<usize>,
|
||||||
window_buffer: Buffer,
|
window_buffer: Buffer,
|
||||||
masks: ArrBuf<Mask>,
|
masks: ArrBuf<Mask>,
|
||||||
|
moves: ArrBuf<MoveOffset>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RenderLayer {
|
struct RenderLayer {
|
||||||
@@ -94,7 +115,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;
|
||||||
@@ -127,32 +149,35 @@ impl UiRenderNode {
|
|||||||
for primitive in &mut self.primitives {
|
for primitive in &mut self.primitives {
|
||||||
primitive.render.update(ui);
|
primitive.render.update(ui);
|
||||||
}
|
}
|
||||||
|
let mut regroup = false;
|
||||||
if ui.masks.changed {
|
if ui.masks.changed {
|
||||||
ui.masks.changed = false;
|
ui.masks.changed = false;
|
||||||
if self.masks.update(device, queue, &ui.masks[..]) {
|
regroup |= self.masks.update(device, queue, &ui.masks[..]);
|
||||||
|
}
|
||||||
|
if ui_render.moves.changed {
|
||||||
|
ui_render.moves.changed = false;
|
||||||
|
regroup |= self.moves.update(device, queue, ui_render.moves.entries());
|
||||||
|
}
|
||||||
|
if regroup {
|
||||||
self.shared_group = Self::shared_group(
|
self.shared_group = Self::shared_group(
|
||||||
device,
|
device,
|
||||||
&self.shared_layout,
|
&self.shared_layout,
|
||||||
&self.window_buffer,
|
&self.window_buffer,
|
||||||
&self.masks,
|
&self.masks,
|
||||||
|
&self.moves,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
||||||
let size = size.into();
|
let size = size.into();
|
||||||
let slice = &[WindowUniform {
|
let slice = &[WindowUniform { dim: size }];
|
||||||
width: size.x,
|
|
||||||
height: size.y,
|
|
||||||
}];
|
|
||||||
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
|
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
|
||||||
let window_uniform = WindowUniform {
|
let window_uniform = WindowUniform {
|
||||||
width: config.width as f32,
|
dim: Vec2::new(config.width as f32, config.height as f32),
|
||||||
height: config.height as f32,
|
|
||||||
};
|
};
|
||||||
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
||||||
label: Some("window"),
|
label: Some("window"),
|
||||||
@@ -166,7 +191,13 @@ impl UiRenderNode {
|
|||||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||||
"ui masks",
|
"ui masks",
|
||||||
);
|
);
|
||||||
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
|
let moves = ArrBuf::new(
|
||||||
|
device,
|
||||||
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||||
|
"ui move offsets",
|
||||||
|
);
|
||||||
|
let shared_group =
|
||||||
|
Self::shared_group(device, &shared_layout, &window_buffer, &masks, &moves);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
shared_layout,
|
shared_layout,
|
||||||
@@ -177,6 +208,7 @@ impl UiRenderNode {
|
|||||||
layers: HashMap::default(),
|
layers: HashMap::default(),
|
||||||
active: Vec::new(),
|
active: Vec::new(),
|
||||||
masks,
|
masks,
|
||||||
|
moves,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,8 +218,8 @@ impl UiRenderNode {
|
|||||||
for source in ®istry.sources()[self.primitives.len()..] {
|
for source in ®istry.sources()[self.primitives.len()..] {
|
||||||
let render = (source.render)(device, queue);
|
let render = (source.render)(device, queue);
|
||||||
let data_layout = Self::data_layout(device, source.stride);
|
let data_layout = Self::data_layout(device, source.stride);
|
||||||
let mut groups = vec![&self.shared_layout, &data_layout];
|
let mut groups = vec![Some(&self.shared_layout), Some(&data_layout)];
|
||||||
groups.extend(render.layout());
|
groups.extend(render.layout().map(Some));
|
||||||
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||||
label: Some(source.label),
|
label: Some(source.label),
|
||||||
bind_group_layouts: &groups,
|
bind_group_layouts: &groups,
|
||||||
@@ -211,7 +243,7 @@ impl UiRenderNode {
|
|||||||
) -> RenderPipeline {
|
) -> RenderPipeline {
|
||||||
let module = device.create_shader_module(ShaderModuleDescriptor {
|
let module = device.create_shader_module(ShaderModuleDescriptor {
|
||||||
label: Some(label),
|
label: Some(label),
|
||||||
source: ShaderSource::Wgsl(format!("{PRELUDE}\n{wgsl}").into()),
|
source: ShaderSource::Wgsl(module_source(wgsl).into()),
|
||||||
});
|
});
|
||||||
device.create_render_pipeline(&RenderPipelineDescriptor {
|
device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||||
label: Some(label),
|
label: Some(label),
|
||||||
@@ -219,7 +251,7 @@ impl UiRenderNode {
|
|||||||
vertex: VertexState {
|
vertex: VertexState {
|
||||||
module: &module,
|
module: &module,
|
||||||
entry_point: Some("vs_main"),
|
entry_point: Some("vs_main"),
|
||||||
buffers: &[PrimitiveInstance::desc()],
|
buffers: &[Some(PrimitiveInstance::desc())],
|
||||||
compilation_options: Default::default(),
|
compilation_options: Default::default(),
|
||||||
},
|
},
|
||||||
fragment: Some(FragmentState {
|
fragment: Some(FragmentState {
|
||||||
@@ -252,7 +284,8 @@ impl UiRenderNode {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What every draw in the ui is given: the window and the masks.
|
/// What every draw in the ui is given: the window, the masks and the
|
||||||
|
/// move chain every position is resolved through.
|
||||||
fn shared_layout(device: &Device) -> BindGroupLayout {
|
fn shared_layout(device: &Device) -> BindGroupLayout {
|
||||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||||
entries: &[
|
entries: &[
|
||||||
@@ -276,6 +309,16 @@ impl UiRenderNode {
|
|||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
},
|
},
|
||||||
|
BindGroupLayoutEntry {
|
||||||
|
binding: 2,
|
||||||
|
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
||||||
|
ty: BindingType::Buffer {
|
||||||
|
ty: BufferBindingType::Storage { read_only: true },
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: BufferSize::new(size_of::<MoveOffset>() as u64),
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
label: Some("ui shared"),
|
label: Some("ui shared"),
|
||||||
})
|
})
|
||||||
@@ -286,6 +329,7 @@ impl UiRenderNode {
|
|||||||
layout: &BindGroupLayout,
|
layout: &BindGroupLayout,
|
||||||
window: &Buffer,
|
window: &Buffer,
|
||||||
masks: &ArrBuf<Mask>,
|
masks: &ArrBuf<Mask>,
|
||||||
|
moves: &ArrBuf<MoveOffset>,
|
||||||
) -> BindGroup {
|
) -> BindGroup {
|
||||||
device.create_bind_group(&BindGroupDescriptor {
|
device.create_bind_group(&BindGroupDescriptor {
|
||||||
layout,
|
layout,
|
||||||
@@ -298,6 +342,10 @@ impl UiRenderNode {
|
|||||||
binding: 1,
|
binding: 1,
|
||||||
resource: masks.buffer.as_entire_binding(),
|
resource: masks.buffer.as_entire_binding(),
|
||||||
},
|
},
|
||||||
|
BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: moves.buffer.as_entire_binding(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
label: Some("ui shared"),
|
label: Some("ui shared"),
|
||||||
})
|
})
|
||||||
@@ -374,3 +422,35 @@ impl ListBuffers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::module_source;
|
||||||
|
use wgpu::naga::{
|
||||||
|
front::wgsl,
|
||||||
|
valid::{Capabilities, ValidationFlags, Validator},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Every shader file, composed as the renderer composes it, parses and
|
||||||
|
/// validates with no device -- so an edit that breaks one fails here and
|
||||||
|
/// not in the first window opened.
|
||||||
|
#[test]
|
||||||
|
fn every_shader_validates() {
|
||||||
|
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src/render/shader");
|
||||||
|
let mut checked = 0;
|
||||||
|
for entry in std::fs::read_dir(dir).unwrap() {
|
||||||
|
let path = entry.unwrap().path();
|
||||||
|
if path.extension().is_none_or(|e| e != "wgsl") || path.ends_with("prelude.wgsl") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let source = module_source(&std::fs::read_to_string(&path).unwrap());
|
||||||
|
let module = wgsl::parse_str(&source)
|
||||||
|
.unwrap_or_else(|e| panic!("{}: {}", path.display(), e.emit_to_string(&source)));
|
||||||
|
Validator::new(ValidationFlags::all(), Capabilities::all())
|
||||||
|
.validate(&module)
|
||||||
|
.unwrap_or_else(|e| panic!("{}: {e:?}", path.display()));
|
||||||
|
checked += 1;
|
||||||
|
}
|
||||||
|
assert!(checked > 0, "no shaders found in {dir}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ use std::{any::TypeId, marker::PhantomData};
|
|||||||
use crate::{
|
use crate::{
|
||||||
Color, TextureHandle, UiData, UiRegion, WidgetId,
|
Color, TextureHandle, UiData, UiRegion, WidgetId,
|
||||||
render::{
|
render::{
|
||||||
data::{MaskIdx, PrimitiveInstance},
|
data::{MaskIdx, MoveIdx, PrimitiveInstance},
|
||||||
page::GlyphRender,
|
page::GlyphRender,
|
||||||
texture::ImageRender,
|
texture::ImageRender,
|
||||||
},
|
},
|
||||||
@@ -246,6 +246,7 @@ impl LayerDraws {
|
|||||||
primitive,
|
primitive,
|
||||||
region,
|
region,
|
||||||
mask_idx,
|
mask_idx,
|
||||||
|
move_idx,
|
||||||
}: PrimitiveInst<P>,
|
}: PrimitiveInst<P>,
|
||||||
) -> PrimitiveHandle {
|
) -> PrimitiveHandle {
|
||||||
self.updated = true;
|
self.updated = true;
|
||||||
@@ -258,7 +259,11 @@ impl LayerDraws {
|
|||||||
.get_or_insert_with(InstanceList::new::<P>)
|
.get_or_insert_with(InstanceList::new::<P>)
|
||||||
.push(
|
.push(
|
||||||
id,
|
id,
|
||||||
PrimitiveInstance { region, mask_idx },
|
PrimitiveInstance {
|
||||||
|
region,
|
||||||
|
mask_idx,
|
||||||
|
move_idx,
|
||||||
|
},
|
||||||
bytemuck::bytes_of(&primitive),
|
bytemuck::bytes_of(&primitive),
|
||||||
);
|
);
|
||||||
PrimitiveHandle {
|
PrimitiveHandle {
|
||||||
@@ -304,6 +309,7 @@ pub struct PrimitiveInst<P> {
|
|||||||
pub primitive: P,
|
pub primitive: P,
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
pub mask_idx: MaskIdx,
|
pub mask_idx: MaskIdx,
|
||||||
|
pub move_idx: MoveIdx,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PrimitiveChange {
|
pub struct PrimitiveChange {
|
||||||
@@ -347,7 +353,7 @@ impl RectPrimitive {
|
|||||||
|
|
||||||
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
|
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
|
||||||
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
|
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
|
||||||
#[repr(C, align(8))]
|
#[repr(C)]
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct GlyphPrimitive {
|
pub struct GlyphPrimitive {
|
||||||
pub uv_min: Vec2,
|
pub uv_min: Vec2,
|
||||||
@@ -358,8 +364,8 @@ pub struct GlyphPrimitive {
|
|||||||
pub flags: u32,
|
pub flags: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual rather than derived: the align(8) leaves four bytes of padding, which
|
// Manual rather than derived: `Vec2`'s alignment leaves four bytes of padding
|
||||||
// is how WGSL lays the struct out.
|
// here, which is how WGSL lays the struct out.
|
||||||
unsafe impl bytemuck::Pod for GlyphPrimitive {}
|
unsafe impl bytemuck::Pod for GlyphPrimitive {}
|
||||||
unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
|
unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
|
||||||
impl Primitive for GlyphPrimitive {
|
impl Primitive for GlyphPrimitive {
|
||||||
|
|||||||
@@ -7,32 +7,117 @@
|
|||||||
var<uniform> window: WindowUniform;
|
var<uniform> window: WindowUniform;
|
||||||
@group(0) @binding(1)
|
@group(0) @binding(1)
|
||||||
var<storage> masks: array<Mask>;
|
var<storage> masks: array<Mask>;
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<storage> move_offsets: array<MoveOffset>;
|
||||||
|
|
||||||
struct WindowUniform {
|
struct WindowUniform {
|
||||||
dim: vec2<f32>,
|
dim: vec2<f32>,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Mask {
|
struct Mask {
|
||||||
|
x: RawSpan,
|
||||||
|
y: RawSpan,
|
||||||
|
move_idx: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MoveOffset {
|
||||||
|
x: RawSpan,
|
||||||
|
y: RawSpan,
|
||||||
|
parent: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// `PX_STEP`, `REL_STEP`, `MASK_NONE`, `MOVE_NONE` and `CHAIN_LIMIT` are
|
||||||
|
// prepended from `iris_core`'s own constants, so none of them is written
|
||||||
|
// twice. What the CPU stores is a whole count of each step, and both steps
|
||||||
|
// are powers of two, so decoding is exact and the number here is the number
|
||||||
|
// the CPU decided.
|
||||||
|
|
||||||
|
// Every coordinate the CPU decided is a whole count of `PX_STEP`, so one that
|
||||||
|
// composes to within half a step of a pixel boundary is on that boundary and
|
||||||
|
// belongs to the pixel above it. Flooring the product instead drops a pixel
|
||||||
|
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of
|
||||||
|
// `REL_STEP` as 383.99998, and five tabs each lose their last column.
|
||||||
|
//
|
||||||
|
// Taken over the whole coordinate, fraction and pixels summed, since a floor
|
||||||
|
// does not distribute over a sum: floored apart, a half of one and a half of
|
||||||
|
// the other lose the pixel the two together make.
|
||||||
|
fn snap_floor(v: vec2<f32>) -> vec2<f32> {
|
||||||
|
return floor(v + PX_STEP * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RawScalar {
|
||||||
|
rel: i32,
|
||||||
|
px: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RawSpan {
|
||||||
|
start: RawScalar,
|
||||||
|
end: RawScalar,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scalar_of(raw: RawScalar) -> Len {
|
||||||
|
return Len(f32(raw.rel) * REL_STEP, f32(raw.px) * PX_STEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span_of(raw: RawSpan) -> UiSpan {
|
||||||
|
return UiSpan(scalar_of(raw.start), scalar_of(raw.end));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scalar_of_pair(raw: vec2<i32>) -> Len {
|
||||||
|
return Len(f32(raw.x) * REL_STEP, f32(raw.y) * PX_STEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Region {
|
||||||
x: UiSpan,
|
x: UiSpan,
|
||||||
y: UiSpan,
|
y: UiSpan,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UiSpan {
|
// The same expression `Len::within` uses, in floats rather than on the
|
||||||
start: UiScalar,
|
// CPU's grid: a move is resolved here so that scrolling a subtree writes one
|
||||||
end: UiScalar,
|
// entry instead of walking it. What has to hold is that this agrees with
|
||||||
|
// itself frame to frame, not that it matches the CPU to the last bit.
|
||||||
|
fn scalar_within(s: Len, p: UiSpan) -> Len {
|
||||||
|
return Len(
|
||||||
|
p.start.rel + (p.end.rel - p.start.rel) * s.rel,
|
||||||
|
s.px + (p.start.px + (p.end.px - p.start.px) * s.rel),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UiScalar {
|
fn span_within(s: UiSpan, p: UiSpan) -> UiSpan {
|
||||||
|
return UiSpan(scalar_within(s.start, p), scalar_within(s.end, p));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_move(idx: u32, local: Region) -> Region {
|
||||||
|
var r = local;
|
||||||
|
var at = idx;
|
||||||
|
for (var step = 0u; step < CHAIN_LIMIT; step++) {
|
||||||
|
if at == MOVE_NONE {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let entry = move_offsets[at];
|
||||||
|
r = Region(span_within(r.x, span_of(entry.x)), span_within(r.y, span_of(entry.y)));
|
||||||
|
at = entry.parent;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UiSpan {
|
||||||
|
start: Len,
|
||||||
|
end: Len,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Len {
|
||||||
rel: f32,
|
rel: f32,
|
||||||
abs: f32,
|
px: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InstanceInput {
|
struct InstanceInput {
|
||||||
@location(0) x_start: vec2<f32>,
|
@location(0) x_start: vec2<i32>,
|
||||||
@location(1) x_end: vec2<f32>,
|
@location(1) x_end: vec2<i32>,
|
||||||
@location(2) y_start: vec2<f32>,
|
@location(2) y_start: vec2<i32>,
|
||||||
@location(3) y_end: vec2<f32>,
|
@location(3) y_end: vec2<i32>,
|
||||||
@location(4) mask_idx: u32,
|
@location(4) mask_idx: u32,
|
||||||
|
@location(5) move_idx: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@@ -52,13 +137,18 @@ fn vs_main(
|
|||||||
) -> VertexOutput {
|
) -> VertexOutput {
|
||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
|
|
||||||
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
|
let local = Region(
|
||||||
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
|
UiSpan(scalar_of_pair(in.x_start), scalar_of_pair(in.x_end)),
|
||||||
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
|
UiSpan(scalar_of_pair(in.y_start), scalar_of_pair(in.y_end)),
|
||||||
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
|
);
|
||||||
|
let r = resolve_move(in.move_idx, local);
|
||||||
|
let top_left_rel = vec2(r.x.start.rel, r.y.start.rel);
|
||||||
|
let top_left_px = vec2(r.x.start.px, r.y.start.px);
|
||||||
|
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
|
||||||
|
let bot_right_px = vec2(r.x.end.px, r.y.end.px);
|
||||||
|
|
||||||
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
|
let top_left = snap_floor(top_left_rel * window.dim + top_left_px);
|
||||||
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
|
let bot_right = snap_floor(bot_right_rel * window.dim + bot_right_px);
|
||||||
let size = bot_right - top_left;
|
let size = bot_right - top_left;
|
||||||
|
|
||||||
let uv = vec2<f32>(
|
let uv = vec2<f32>(
|
||||||
@@ -77,17 +167,20 @@ fn vs_main(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
||||||
if in.mask_idx == 4294967295u {
|
if in.mask_idx == MASK_NONE {
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
let mask = masks[in.mask_idx];
|
let mask = masks[in.mask_idx];
|
||||||
let tl = vec2(mask.x.start.rel, mask.y.start.rel);
|
// Its own chain, not the drawn primitive's, so a stationary viewport
|
||||||
let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs);
|
// clips content that moves inside it.
|
||||||
let br = vec2(mask.x.end.rel, mask.y.end.rel);
|
let m = resolve_move(mask.move_idx, Region(span_of(mask.x), span_of(mask.y)));
|
||||||
let br_abs = vec2(mask.x.end.abs, mask.y.end.abs);
|
let tl = vec2(m.x.start.rel, m.y.start.rel);
|
||||||
|
let tl_px = vec2(m.x.start.px, m.y.start.px);
|
||||||
|
let br = vec2(m.x.end.rel, m.y.end.rel);
|
||||||
|
let br_px = vec2(m.x.end.px, m.y.end.px);
|
||||||
|
|
||||||
let top_left = floor(tl * window.dim) + floor(tl_abs);
|
let top_left = snap_floor(tl * window.dim + tl_px);
|
||||||
let bot_right = floor(br * window.dim) + floor(br_abs);
|
let bot_right = snap_floor(br * window.dim + br_px);
|
||||||
let pos = in.clip_position.xy;
|
let pos = in.clip_position.xy;
|
||||||
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
||||||
return color * 0.0;
|
return color * 0.0;
|
||||||
|
|||||||
+103
-3
@@ -1,14 +1,114 @@
|
|||||||
use crate::{LayerId, MaskIdx, PrimitiveHandle, TextureHandle, UiRegion, WidgetId};
|
use crate::{
|
||||||
|
Bounds, Declared, LayerId, LayoutHolds, MaskIdx, MoveIdx, PlaceDesc, RegionAlign,
|
||||||
|
RetainedPrimitive, Size, TextureHandle, UiRegion, UiVec2, WidgetId,
|
||||||
|
};
|
||||||
|
|
||||||
/// important non rendering data for retained drawing
|
/// What is kept of a widget its parent has asked about. `drawn` says whether
|
||||||
|
/// it currently draws; one that does not is kept so that a change to it, or
|
||||||
|
/// under it, still reaches whoever asked.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ActiveData {
|
pub struct ActiveData {
|
||||||
pub id: WidgetId,
|
pub id: WidgetId,
|
||||||
|
/// 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 measured answer and its dependencies. A hint-only dependency or
|
||||||
|
/// a widget first encountered during placement has no measurement yet.
|
||||||
|
pub answer: Option<Answer>,
|
||||||
|
/// Asked more than once in its parent's last draw -- measured in one box
|
||||||
|
/// and then asked in the one the parent decided. The parent's layout
|
||||||
|
/// rests on the first answer and its drawing on the last, so only the
|
||||||
|
/// parent can ask either again.
|
||||||
|
pub re_asked: bool,
|
||||||
|
/// What the widget reported, in window-unit lengths.
|
||||||
|
pub size: Size,
|
||||||
|
/// The window and region reads that this drawing holds for, and the
|
||||||
|
/// rel base and region it pinned.
|
||||||
|
pub holds: LayoutHolds,
|
||||||
|
pub drawn: bool,
|
||||||
pub parent: Option<WidgetId>,
|
pub parent: Option<WidgetId>,
|
||||||
|
/// How far down the tree it was drawn, the root being 1. Carried down a
|
||||||
|
/// draw rather than worked out by walking up, so it is right for every
|
||||||
|
/// widget a frame visits and cannot drift while one is being drawn.
|
||||||
|
pub depth: usize,
|
||||||
pub textures: Vec<TextureHandle>,
|
pub textures: Vec<TextureHandle>,
|
||||||
pub primitives: Vec<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>,
|
||||||
|
pub request_deps: Vec<WidgetId>,
|
||||||
|
pub(crate) scratch: DrawScratch,
|
||||||
|
/// The movable region its primitives are positioned through: its own when
|
||||||
|
/// opted in, otherwise the nearest ancestor's.
|
||||||
|
pub move_idx: MoveIdx,
|
||||||
|
/// The declared lengths whoever drew this widget resolved into its rel base.
|
||||||
|
/// A change to one moves a box this widget cannot fix by drawing again,
|
||||||
|
/// and comparing them is what says so.
|
||||||
|
pub declared: Declared,
|
||||||
|
/// Its bounds, resolved the same way. The answer is held to these where
|
||||||
|
/// the box was not, so a change to one changes what it answers even
|
||||||
|
/// where its declared lengths stand.
|
||||||
|
pub bounds: Bounds,
|
||||||
|
/// Its alignment when it was last drawn, which a change to the property
|
||||||
|
/// is found against.
|
||||||
|
pub own_align: RegionAlign,
|
||||||
|
/// The movable region whose coordinates its placement is in when this
|
||||||
|
/// widget does not own a region node.
|
||||||
|
pub parent_move: MoveIdx,
|
||||||
|
/// The mask its drawing is clipped to: one it set itself, or the one it
|
||||||
|
/// inherited from whoever drew it.
|
||||||
pub mask: MaskIdx,
|
pub mask: MaskIdx,
|
||||||
|
/// That inherited one. The two differ exactly where the widget set a
|
||||||
|
/// mask of its own, which is the one it owns and the one a move rewrites
|
||||||
|
/// -- and the one a redraw of it must not be handed back, since setting
|
||||||
|
/// a mask asserts there is none.
|
||||||
|
pub parent_mask: MaskIdx,
|
||||||
pub layer: LayerId,
|
pub layer: LayerId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ActiveData {
|
||||||
|
/// What it answered when its parent asked, where it has been asked at
|
||||||
|
/// all. Not `size`, which is what its last drawing reported: a drawing
|
||||||
|
/// re-expressed in the box that answer chose is not a second answer.
|
||||||
|
pub fn measured(&self) -> Option<Size> {
|
||||||
|
self.answer.map(|answer| answer.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether it owns a region node rather than sharing the one it was drawn
|
||||||
|
/// under, which is what its two move indices being different says.
|
||||||
|
pub fn is_region_node(&self) -> bool {
|
||||||
|
self.move_idx != self.parent_move
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a widget answered when it was asked: the size it reported, and the
|
||||||
|
/// boxes and windows that answer holds for.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Answer {
|
||||||
|
pub size: Size,
|
||||||
|
pub holds: LayoutHolds,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct DrawScratch {
|
||||||
|
pub children: Vec<WidgetId>,
|
||||||
|
pub size_deps: Vec<WidgetId>,
|
||||||
|
pub under: Vec<(WidgetId, LayoutHolds)>,
|
||||||
|
pub requests: Vec<crate::RequestedLen>,
|
||||||
|
pub lengths: Vec<crate::Px>,
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
use crate::{BothAxis, Len, UiVec2, WidgetId, util::HashMap};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Cache {
|
|
||||||
pub size: BothAxis<HashMap<WidgetId, (UiVec2, Len)>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cache {
|
|
||||||
pub fn remove(&mut self, id: WidgetId) {
|
|
||||||
self.size.x.remove(&id);
|
|
||||||
self.size.y.remove(&id);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.size.x.clear();
|
|
||||||
self.size.y.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
use crate::{Bound, Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow};
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
|
/// The lengths of a box, in pixels, that one drawing of a widget holds for:
|
||||||
|
/// give the widget any box in this range and it draws the same thing and
|
||||||
|
/// reports the same size. A widget that never reads its box in pixels holds
|
||||||
|
/// for every length; one that does holds for the one it read unless it says
|
||||||
|
/// otherwise, and a parent holds for whatever keeps every child it asked
|
||||||
|
/// about or drew inside its own range.
|
||||||
|
///
|
||||||
|
/// The ends are lengths on the grid rather than floats with a tolerance
|
||||||
|
/// around them: a box offered back at the length a widget reported comes back
|
||||||
|
/// as the same number, so a range means what it says. The one place a range
|
||||||
|
/// is wider than the length it came from is [`Self::through`], and what it is
|
||||||
|
/// wider by is the floor that inverting a fraction undoes.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Holds {
|
||||||
|
pub lo: Px,
|
||||||
|
pub hi: Px,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Len {
|
||||||
|
/// Whether this is longer than `than` in a window this wide, and the
|
||||||
|
/// windows that answer holds for.
|
||||||
|
///
|
||||||
|
/// Which is longer is a question in pixels -- `rel(0.5)` is longer than
|
||||||
|
/// 300 px at a box of 600 and shorter at 400 -- and it is asked of the
|
||||||
|
/// difference and answered back through that same difference, so the
|
||||||
|
/// boundary is the comparison's own rather than a second way of finding
|
||||||
|
/// it.
|
||||||
|
pub fn longer_than(&self, than: Len, window: Px) -> (bool, Holds) {
|
||||||
|
let over = *self - than;
|
||||||
|
let longer = over.to_px(window) > Px::ZERO;
|
||||||
|
let side = match longer {
|
||||||
|
true => Px::STEP..=Px::MAX,
|
||||||
|
false => Px::MIN..=Px::ZERO,
|
||||||
|
};
|
||||||
|
(longer, Holds::from(side).through(over))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bound {
|
||||||
|
/// The end of this bound `len` falls outside, which is the length it
|
||||||
|
/// gets instead of its own, and the windows that answer holds for.
|
||||||
|
/// Nothing where it is inside, which is the answer wherever there is no
|
||||||
|
/// bound at all.
|
||||||
|
///
|
||||||
|
/// `len` and this bound are lengths of the same thing, whichever that
|
||||||
|
/// is: a box in window lengths wants the bound resolved, and a length a
|
||||||
|
/// widget declares of its rel base wants it as the rule wrote it. Both
|
||||||
|
/// comparisons are in pixels, so each is a question about this window,
|
||||||
|
/// and the box is decided again on the other side of a crossing.
|
||||||
|
pub fn outside(&self, len: Len, window: Px) -> (Option<Len>, Holds) {
|
||||||
|
let mut held = None;
|
||||||
|
let mut holds = Holds::ANY;
|
||||||
|
if let Some(min) = self.min {
|
||||||
|
let (shorter, kept) = min.longer_than(len, window);
|
||||||
|
holds = holds.and(kept);
|
||||||
|
if shorter {
|
||||||
|
held = Some(min);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(max) = self.max {
|
||||||
|
let (longer, kept) = held.unwrap_or(len).longer_than(max, window);
|
||||||
|
holds = holds.and(kept);
|
||||||
|
if longer {
|
||||||
|
debug_assert!(
|
||||||
|
held.is_none(),
|
||||||
|
"a floor of {:?} over a cap of {max:?} bounds nothing",
|
||||||
|
self.min,
|
||||||
|
);
|
||||||
|
held = Some(max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(held, holds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Holds {
|
||||||
|
pub const ANY: Self = Self {
|
||||||
|
lo: Px::MIN,
|
||||||
|
hi: Px::MAX,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const fn at(len: Px) -> Self {
|
||||||
|
Self { lo: len, hi: len }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn contains(&self, len: Px) -> bool {
|
||||||
|
len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every length `other` holds for is one this holds for, so a drawing
|
||||||
|
/// made under this range is still good wherever `other` is.
|
||||||
|
pub const fn covers(&self, other: Self) -> bool {
|
||||||
|
self.lo.raw() <= other.lo.raw() && self.hi.raw() >= other.hi.raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn and(&self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
lo: self.lo.max(other.lo),
|
||||||
|
hi: self.hi.min(other.hi),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a box has to be for a part of it, `len` of the box long, to stay
|
||||||
|
/// in this range: the exact preimage of `px + floor(rel * box)`, which is
|
||||||
|
/// the one way a box in pixels is reached. A part with no relative extent
|
||||||
|
/// is a fixed length -- it was drawn at that length and any box keeps it
|
||||||
|
/// there.
|
||||||
|
///
|
||||||
|
/// The answer is an interval even where this range is a single length,
|
||||||
|
/// because the multiply on the way in drops to the step below and many
|
||||||
|
/// boxes therefore give one length. That is a floor rather than an
|
||||||
|
/// allowance: inverting it is two divisions and nothing else, and the
|
||||||
|
/// whole of a box maps back to itself.
|
||||||
|
pub const fn through(&self, len: Len) -> Self {
|
||||||
|
if self.lo.raw() == Px::MIN.raw() && self.hi.raw() == Px::MAX.raw() {
|
||||||
|
return Self::ANY;
|
||||||
|
}
|
||||||
|
let rel = len.rel.raw() as i64;
|
||||||
|
if rel == 0 {
|
||||||
|
return Self::ANY;
|
||||||
|
}
|
||||||
|
let px = len.px.raw() as i64;
|
||||||
|
// `floor(rel * box) >= lo - px` is `rel * box >= (lo - px) << REL`, and
|
||||||
|
// `floor(rel * box) <= hi - px` is `rel * box < (hi - px + 1) << REL`.
|
||||||
|
let lo = (self.lo.raw() as i64 - px) << REL_SHIFT;
|
||||||
|
let hi = (((self.hi.raw() as i64 - px) + 1) << REL_SHIFT) - 1;
|
||||||
|
// Dividing by a negative fraction turns the ends around, so which
|
||||||
|
// bound each comes from is decided before dividing rather than by
|
||||||
|
// taking the min and max of four divisions.
|
||||||
|
match rel > 0 {
|
||||||
|
true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)),
|
||||||
|
false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn raws(lo: i64, hi: i64) -> Self {
|
||||||
|
Self {
|
||||||
|
lo: Px::from_raw(narrow(lo)),
|
||||||
|
hi: Px::from_raw(narrow(hi)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RangeInclusive<Px>> for Holds {
|
||||||
|
fn from(range: RangeInclusive<Px>) -> Self {
|
||||||
|
Self {
|
||||||
|
lo: *range.start(),
|
||||||
|
hi: *range.end(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Rel;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_unrestricted_range_stays_unrestricted_through_any_length() {
|
||||||
|
for rel in [-2.0, -0.5, 0.0, 0.5, 1.0, 2.0] {
|
||||||
|
for px in [-8, 0, 8] {
|
||||||
|
let len = Len::from_parts(Rel::from_f32(rel), Px::from_int(px));
|
||||||
|
assert_eq!(Holds::ANY.through(len), Holds::ANY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn through_reverses_a_range_for_a_negative_fraction() {
|
||||||
|
// `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
|
||||||
|
let part = Len::from_parts(Rel::from_f32(-0.5), Px::from_int(10));
|
||||||
|
let holds = Holds::from(Px::from_int(20)..=Px::from_int(40)).through(part);
|
||||||
|
assert!(holds.contains(Px::from_int(-60)) && holds.contains(Px::from_int(-20)));
|
||||||
|
assert!(!holds.contains(Px::from_int(-61)) && !holds.contains(Px::from_int(-19)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The case the widening is for: a part that holds only for the length it
|
||||||
|
/// was drawn at has to hold for the box it was drawn in, and a third of a
|
||||||
|
/// box is not a whole number of steps.
|
||||||
|
#[test]
|
||||||
|
fn a_part_maps_back_onto_the_box_it_was_measured_in() {
|
||||||
|
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
|
||||||
|
for box_len in (440..460).map(Px::from_int) {
|
||||||
|
let holds = Holds::at(part.to_px(box_len)).through(part);
|
||||||
|
assert!(holds.contains(box_len), "{box_len:?} left out by {holds:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget handed the whole of its parent's box, with or without pixels
|
||||||
|
/// taken off it, has no fraction to invert: multiplying by one is exact
|
||||||
|
/// and taking the pixels off again is too, so the box maps back to
|
||||||
|
/// itself. Allowing for anything here compounded a step a level down a
|
||||||
|
/// chain of widgets each taking the whole of its parent.
|
||||||
|
#[test]
|
||||||
|
fn the_whole_of_a_box_maps_back_to_itself() {
|
||||||
|
let at = Px::from_int(956);
|
||||||
|
assert_eq!(Holds::at(at).through(Len::FULL), Holds::at(at));
|
||||||
|
let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8));
|
||||||
|
assert_eq!(
|
||||||
|
Holds::at(at).through(less_eight),
|
||||||
|
Holds::at(at + Px::from_int(8))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The range is the exact preimage at both ends, so a box one step
|
||||||
|
/// outside it really does give a length outside this range. What a wider
|
||||||
|
/// range costs is a drawing reused where it does not hold.
|
||||||
|
#[test]
|
||||||
|
fn a_box_one_step_outside_the_range_is_outside_it() {
|
||||||
|
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
|
||||||
|
let at = Px::from_int(300);
|
||||||
|
let holds = Holds::at(at).through(part);
|
||||||
|
for inside in [holds.lo, holds.hi] {
|
||||||
|
assert_eq!(part.to_px(inside), at, "{inside:?} left out of {holds:?}");
|
||||||
|
}
|
||||||
|
for outside in [holds.lo.next_down(), holds.hi.next_up()] {
|
||||||
|
assert_ne!(part.to_px(outside), at, "{outside:?} admitted by {holds:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A truncating multiply only ever drops, so the step it needs allowing
|
||||||
|
/// for on the way in belongs at the top of the range and not the bottom.
|
||||||
|
#[test]
|
||||||
|
fn a_fraction_widens_further_up_than_down() {
|
||||||
|
let half = Len::from_parts(Rel::from_f32(0.5), Px::ZERO);
|
||||||
|
let holds = Holds::at(Px::from_int(100)).through(half);
|
||||||
|
let box_len = Px::from_int(200);
|
||||||
|
assert!(holds.hi - box_len > box_len - holds.lo, "{holds:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_boundary_the_next_step_along_does_not_admit_it() {
|
||||||
|
let boundary = Px::from_int(10);
|
||||||
|
let above = Holds::from(boundary.next_up()..=Px::MAX);
|
||||||
|
assert!(!above.contains(boundary));
|
||||||
|
assert!(above.contains(boundary.next_up()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
+102
-4
@@ -1,17 +1,27 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, UiRegion, WeakWidget,
|
||||||
|
WidgetId, Widgets,
|
||||||
|
util::{Arena, Id, TrackedArena},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// How far the shader will walk a move chain. It bounds a malformed cycle
|
||||||
|
/// rather than any real tree; `Moves::resolve` uses the same number so the
|
||||||
|
/// two agree on what a deep tree resolves to.
|
||||||
|
pub const CHAIN_LIMIT: u32 = 64;
|
||||||
|
|
||||||
mod active;
|
mod active;
|
||||||
mod cache;
|
mod holds;
|
||||||
|
mod layout_holds;
|
||||||
mod painter;
|
mod painter;
|
||||||
|
mod place;
|
||||||
mod render_state;
|
mod render_state;
|
||||||
mod size;
|
|
||||||
|
|
||||||
pub use active::*;
|
pub use active::*;
|
||||||
|
pub use holds::*;
|
||||||
|
pub use layout_holds::*;
|
||||||
pub use painter::{Painter, PrimitiveLike};
|
pub use painter::{Painter, PrimitiveLike};
|
||||||
|
pub use place::{PlaceDesc, PlaceDescAxis, PlaceFit, RetainedPrimitive};
|
||||||
pub use render_state::*;
|
pub use render_state::*;
|
||||||
pub use size::*;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct UiData {
|
pub struct UiData {
|
||||||
@@ -23,6 +33,94 @@ pub struct UiData {
|
|||||||
pub masks: TrackedArena<Mask, u32>,
|
pub masks: TrackedArena<Mask, u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where each widget's drawing sits relative to its parent's slot, so moving
|
||||||
|
/// a subtree writes one entry rather than every descendant's primitives.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Moves {
|
||||||
|
arena: Arena<MoveOffset, u32>,
|
||||||
|
pub changed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Moves {
|
||||||
|
pub fn push(&mut self, parent: MoveIdx, region: UiRegion) -> MoveIdx {
|
||||||
|
self.changed = true;
|
||||||
|
MoveIdx::slot(self.arena.push(MoveOffset::new(parent, region)).idx())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-points a slot at a different parent, for a widget drawn somewhere
|
||||||
|
/// else in the tree than it was.
|
||||||
|
pub fn set_parent(&mut self, idx: MoveIdx, parent: MoveIdx) {
|
||||||
|
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
|
||||||
|
if entry.parent != parent {
|
||||||
|
entry.parent = parent;
|
||||||
|
self.changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, idx: MoveIdx) {
|
||||||
|
self.changed = true;
|
||||||
|
self.arena.remove(Id::preset(idx.idx() as u32));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the box a slot's contents are placed within, itself given in the
|
||||||
|
/// coordinates of its parent slot.
|
||||||
|
pub fn set(&mut self, idx: MoveIdx, region: UiRegion) {
|
||||||
|
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
|
||||||
|
if entry.region != region {
|
||||||
|
entry.region = region;
|
||||||
|
self.changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same walk the vertex shader does, in the same `Len` the shader is
|
||||||
|
/// handed, for asking where a drawing will actually land -- hit testing,
|
||||||
|
/// and nothing layout decides on. Layout threads its lengths down the
|
||||||
|
/// draw instead, so no box it compares is composed back up this chain.
|
||||||
|
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
|
||||||
|
let mut region = local;
|
||||||
|
self.walk(idx, |entry| region = region.within(entry));
|
||||||
|
region
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk(&self, idx: MoveIdx, mut step: impl FnMut(&UiRegion)) {
|
||||||
|
let mut at = idx;
|
||||||
|
for _ in 0..CHAIN_LIMIT {
|
||||||
|
if at == MoveIdx::NONE {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let entry = &self.arena[at.idx()];
|
||||||
|
step(&entry.region);
|
||||||
|
at = entry.parent;
|
||||||
|
}
|
||||||
|
debug_assert!(
|
||||||
|
at == MoveIdx::NONE,
|
||||||
|
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
|
||||||
|
and the shader stops at the same depth"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many slots a region in `idx` is composed through, which is what
|
||||||
|
/// the shader's walk costs per primitive.
|
||||||
|
pub fn depth(&self, idx: MoveIdx) -> usize {
|
||||||
|
let mut depth = 0;
|
||||||
|
let mut at = idx;
|
||||||
|
while at != MoveIdx::NONE && depth < CHAIN_LIMIT as usize {
|
||||||
|
at = self.arena[at.idx()].parent;
|
||||||
|
depth += 1;
|
||||||
|
}
|
||||||
|
depth
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entries(&self) -> &[MoveOffset] {
|
||||||
|
&self.arena
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.changed = true;
|
||||||
|
self.arena = Arena::default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait UiRsc {
|
pub trait UiRsc {
|
||||||
fn ui(&self) -> &UiData;
|
fn ui(&self) -> &UiData;
|
||||||
fn ui_mut(&mut self) -> &mut UiData;
|
fn ui_mut(&mut self) -> &mut UiData;
|
||||||
|
|||||||
+911
-53
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,234 @@
|
|||||||
|
use crate::util::impl_axis_index;
|
||||||
|
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan, UiVec2};
|
||||||
|
|
||||||
|
/// How a child's region along one axis comes from the region of the widget
|
||||||
|
/// asking, and what its fractions are of.
|
||||||
|
///
|
||||||
|
/// The three ways of saying a region are the three the geometry already has:
|
||||||
|
/// a span composed into the caller's box, a span shifted to where that box
|
||||||
|
/// starts, and a length placed in it by alignment. Which one is meant cannot
|
||||||
|
/// be read off the numbers, since two of them take the same span and apply
|
||||||
|
/// it differently, so it is said here.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct PlaceDescAxis {
|
||||||
|
pub span: PlaceSpan,
|
||||||
|
pub fit: PlaceFit,
|
||||||
|
pub rel_base: RelBase,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum PlaceFit {
|
||||||
|
Align,
|
||||||
|
Fill,
|
||||||
|
/// The parent has already evaluated the child's size request.
|
||||||
|
Allocated,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaceFit {
|
||||||
|
pub fn fills(&self) -> bool {
|
||||||
|
!matches!(self, Self::Align)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum PlaceSpan {
|
||||||
|
Within(UiSpan),
|
||||||
|
Shifted(UiSpan),
|
||||||
|
Sized(Len),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a child's fractions are of. [`PlaceSpan::Sized`] is a length the
|
||||||
|
/// caller named, which is always its own base, so nothing here constructs one
|
||||||
|
/// beside anything but [`Self::Len`].
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum RelBase {
|
||||||
|
/// The caller's own, unchanged.
|
||||||
|
Inherit,
|
||||||
|
/// The caller's own, narrowed the way the region is.
|
||||||
|
WithRegion,
|
||||||
|
/// This length of the window.
|
||||||
|
Len(Len),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaceDescAxis {
|
||||||
|
/// The whole of the caller's box.
|
||||||
|
pub const WHOLE: Self = UiSpan::FULL.within_desc();
|
||||||
|
|
||||||
|
/// This region is the child's placement: its answer is not placed inside
|
||||||
|
/// it again. A container uses it where it hands back exactly what the
|
||||||
|
/// child asked for -- a row placing a child at the length it reported.
|
||||||
|
pub const fn fills(mut self) -> Self {
|
||||||
|
self.fit = PlaceFit::Fill;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A final allocation, including any comparisons in the child's request.
|
||||||
|
pub const fn allocated(mut self) -> Self {
|
||||||
|
self.fit = PlaceFit::Allocated;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This along `axis`, and the whole of the caller's box across it: what
|
||||||
|
/// a container dividing one axis says, since nothing divides the other.
|
||||||
|
/// [`PlaceDesc::from_axis`] says the across one where it is not the
|
||||||
|
/// whole.
|
||||||
|
pub const fn on_axis(self, axis: Axis) -> PlaceDesc {
|
||||||
|
PlaceDesc::from_axis(axis, self, Self::WHOLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the child's fractions are of, as a length of the window: a
|
||||||
|
/// resolved share, or a box a sibling's answer decided.
|
||||||
|
pub const fn rel_base(mut self, len: Len) -> Self {
|
||||||
|
self.rel_base = RelBase::Len(len);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where it lands in the coordinates `own` is in.
|
||||||
|
pub fn of(self, own: UiSpan, align: AxisAlign) -> UiSpan {
|
||||||
|
match self.span {
|
||||||
|
PlaceSpan::Within(span) => span.within(&own),
|
||||||
|
PlaceSpan::Shifted(mut span) => {
|
||||||
|
span.shift(own.start);
|
||||||
|
span
|
||||||
|
}
|
||||||
|
PlaceSpan::Sized(len) => own.place(len, align),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a child is asked, on both axes. A [`UiRegion`] converts into the
|
||||||
|
/// common case: that box of the caller's own, the answer placed inside it.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct PlaceDesc {
|
||||||
|
pub x: PlaceDescAxis,
|
||||||
|
pub y: PlaceDescAxis,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaceDesc {
|
||||||
|
/// The whole of the caller's box, on both axes.
|
||||||
|
pub const WHOLE: Self = Self::splat(PlaceDescAxis::WHOLE);
|
||||||
|
|
||||||
|
pub const fn new(x: PlaceDescAxis, y: PlaceDescAxis) -> Self {
|
||||||
|
Self { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same on both axes.
|
||||||
|
pub const fn splat(place: PlaceDescAxis) -> Self {
|
||||||
|
Self { x: place, y: place }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A description per axis, where the two differ and neither is the
|
||||||
|
/// axis a container divides.
|
||||||
|
pub fn from_axes(f: impl Fn(Axis) -> PlaceDescAxis) -> Self {
|
||||||
|
Self::new(f(Axis::X), f(Axis::Y))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `aligned` on `axis` and `ortho` on the other, which is how a
|
||||||
|
/// container that divides one axis says what it is doing.
|
||||||
|
pub const fn from_axis(axis: Axis, aligned: PlaceDescAxis, ortho: PlaceDescAxis) -> Self {
|
||||||
|
match axis {
|
||||||
|
Axis::X => Self::new(aligned, ortho),
|
||||||
|
Axis::Y => Self::new(ortho, aligned),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Both regions are the child's placement. See [`PlaceDescAxis::fills`].
|
||||||
|
pub const fn fills(self) -> Self {
|
||||||
|
Self::new(self.x.fills(), self.y.fills())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The child's rel base on one axis. See [`PlaceDescAxis::rel_base`].
|
||||||
|
pub const fn rel_base(mut self, axis: Axis, len: Len) -> Self {
|
||||||
|
self[axis] = self[axis].rel_base(len);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a child's fractions on one axis are of, as a length of the
|
||||||
|
/// window: a length this place names, or the rel base of the widget
|
||||||
|
/// giving it, which is `parent_rel_base`.
|
||||||
|
pub(super) fn base(&self, axis: Axis, parent_rel_base: UiVec2) -> Len {
|
||||||
|
match self[axis].rel_base {
|
||||||
|
RelBase::Len(len) => len,
|
||||||
|
RelBase::Inherit | RelBase::WithRegion => parent_rel_base[axis],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The box each axis names, in the coordinates `own` is in.
|
||||||
|
pub fn of(self, own: UiRegion, align: RegionAlign) -> UiRegion {
|
||||||
|
UiRegion::new(self.x.of(own.x, align.x), self.y.of(own.y, align.y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiSpan {
|
||||||
|
/// This span composed into the caller's own box, so it moves and scales
|
||||||
|
/// with it: [`UiSpan::within`], which is what a container that insets
|
||||||
|
/// one speaks. Taking eleven pixels off the end needs no length, where
|
||||||
|
/// saying the same thing in window lengths would make the container read
|
||||||
|
/// its own box -- and a box chosen from its own answer then feeds back
|
||||||
|
/// into the answer.
|
||||||
|
///
|
||||||
|
/// The child's rel base is narrowed the same way, so padding takes its
|
||||||
|
/// pixels off both and `rel(1)` under it fills the caller rather than
|
||||||
|
/// overflowing it.
|
||||||
|
pub const fn within_desc(self) -> PlaceDescAxis {
|
||||||
|
PlaceDescAxis {
|
||||||
|
span: PlaceSpan::Within(self),
|
||||||
|
fit: PlaceFit::Align,
|
||||||
|
rel_base: RelBase::WithRegion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This span shifted to where the caller's own box starts: window
|
||||||
|
/// lengths along a cursor, which is what a container dividing room among
|
||||||
|
/// its children speaks. A child's report is a window length, so the
|
||||||
|
/// cursor that sums those reports is one too, and a moved box re-places
|
||||||
|
/// every child by re-adding its start, exactly.
|
||||||
|
///
|
||||||
|
/// The child's rel base passes through: how far along the cursor a child
|
||||||
|
/// sits says nothing about what a fraction under it is of. The same span
|
||||||
|
/// says [`Self::within_desc`] as a part of that box instead, and which is
|
||||||
|
/// meant cannot be read off the numbers.
|
||||||
|
pub const fn shifted_desc(self) -> PlaceDescAxis {
|
||||||
|
PlaceDescAxis {
|
||||||
|
span: PlaceSpan::Shifted(self),
|
||||||
|
fit: PlaceFit::Align,
|
||||||
|
rel_base: RelBase::Inherit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Len {
|
||||||
|
/// A box this long, placed in the caller's own by the child's alignment:
|
||||||
|
/// the rule that places an answer, with the length given from above
|
||||||
|
/// rather than reported. What a stack's sizing child decides for the
|
||||||
|
/// rest. It is the child's rel base too.
|
||||||
|
pub const fn as_desc(self) -> PlaceDescAxis {
|
||||||
|
PlaceDescAxis {
|
||||||
|
span: PlaceSpan::Sized(self),
|
||||||
|
fit: PlaceFit::Align,
|
||||||
|
rel_base: RelBase::Len(self),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UiRegion> for PlaceDesc {
|
||||||
|
fn from(region: UiRegion) -> Self {
|
||||||
|
Self::new(region.x.within_desc(), region.y.within_desc())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PlaceDescAxis> for PlaceDesc {
|
||||||
|
fn from(place: PlaceDescAxis) -> Self {
|
||||||
|
Self::splat(place)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A primitive as it was written: its box in the widget's own box's
|
||||||
|
/// coordinates, which is what a move of that box re-composes from.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RetainedPrimitive {
|
||||||
|
pub handle: PrimitiveHandle,
|
||||||
|
pub region: UiRegion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_axis_index!(PlaceDesc => PlaceDescAxis);
|
||||||
+1169
-144
File diff suppressed because it is too large.
Load diff
@@ -1,89 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2,
|
|
||||||
WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct SizeCtx<'a> {
|
|
||||||
pub text: &'a mut TextData,
|
|
||||||
pub(super) source: WidgetId,
|
|
||||||
pub(super) widgets: &'a Widgets,
|
|
||||||
pub(super) cache: &'a mut Cache,
|
|
||||||
/// TODO: should this be pub? rn used for sized
|
|
||||||
pub outer: UiVec2,
|
|
||||||
pub(super) output_size: Vec2,
|
|
||||||
pub(super) id: WidgetId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SizeCtx<'_> {
|
|
||||||
pub fn id(&self) -> &WidgetId {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn source(&self) -> &WidgetId {
|
|
||||||
&self.source
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn len_inner<A: const AxisT>(&mut self, id: WidgetId) -> Len {
|
|
||||||
if let Some((_, len)) = self.cache.size.axis::<A>().get(&id) {
|
|
||||||
return *len;
|
|
||||||
}
|
|
||||||
let len = self
|
|
||||||
.widgets
|
|
||||||
.get_dyn_dynamic(id)
|
|
||||||
.desired_len::<A>(&mut SizeCtx {
|
|
||||||
text: self.text,
|
|
||||||
source: self.source,
|
|
||||||
widgets: self.widgets,
|
|
||||||
cache: self.cache,
|
|
||||||
outer: self.outer,
|
|
||||||
output_size: self.output_size,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
self.cache.size.axis::<A>().insert(id, (self.outer, len));
|
|
||||||
len
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn width(&mut self, id: impl IdLike) -> Len {
|
|
||||||
self.len_inner::<XAxis>(id.id())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn height(&mut self, id: impl IdLike) -> Len {
|
|
||||||
self.len_inner::<YAxis>(id.id())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn len_axis(&mut self, id: impl IdLike, axis: Axis) -> Len {
|
|
||||||
match axis {
|
|
||||||
Axis::X => self.width(id),
|
|
||||||
Axis::Y => self.height(id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn size(&mut self, id: impl IdLike) -> Size {
|
|
||||||
let id = id.id();
|
|
||||||
Size {
|
|
||||||
x: self.width(id),
|
|
||||||
y: self.height(id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn px_size(&mut self) -> Vec2 {
|
|
||||||
self.outer.to_abs(self.output_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn output_size(&mut self) -> Vec2 {
|
|
||||||
self.output_size
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_text(
|
|
||||||
&mut self,
|
|
||||||
buffer: &mut TextBuffer,
|
|
||||||
attrs: &TextAttrs,
|
|
||||||
width: Option<f32>,
|
|
||||||
) -> RenderedText {
|
|
||||||
self.text.render(buffer, attrs, width)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn label(&self, id: WidgetId) -> &String {
|
|
||||||
self.widgets.label(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -34,6 +34,10 @@ impl<T, I: IdNum> Arena<T, I> {
|
|||||||
self.tracker.free(id);
|
self.tracker.free(id);
|
||||||
self.data[i]
|
self.data[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
||||||
|
&mut self.data[id.idx()]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, I: IdNum> Default for Arena<T, I> {
|
impl<T, I: IdNum> Default for Arena<T, I> {
|
||||||
@@ -71,6 +75,11 @@ impl<T, I: IdNum> TrackedArena<T, I> {
|
|||||||
self.refs[i.idx()] += 1;
|
self.refs[i.idx()] += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
||||||
|
self.changed = true;
|
||||||
|
self.inner.get_mut(id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, id: Id<I>) -> T
|
pub fn remove(&mut self, id: Id<I>) -> T
|
||||||
where
|
where
|
||||||
T: Copy,
|
T: Copy,
|
||||||
|
|||||||
+57
-22
@@ -1,34 +1,13 @@
|
|||||||
use std::ops::*;
|
|
||||||
|
|
||||||
pub const trait LerpUtil {
|
pub const trait LerpUtil {
|
||||||
fn lerp(self, from: Self, to: Self) -> Self;
|
fn lerp(self, from: Self, to: Self) -> Self;
|
||||||
fn lerp_inv(self, from: Self, to: Self) -> Self;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const trait DivOr {
|
const impl LerpUtil for f32 {
|
||||||
fn div_or(self, rhs: Self, other: Self) -> Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
const impl DivOr for f32 {
|
|
||||||
fn div_or(self, rhs: Self, other: Self) -> Self {
|
|
||||||
let res = self / rhs;
|
|
||||||
if res.is_nan() { other } else { res }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const impl<
|
|
||||||
T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy,
|
|
||||||
> LerpUtil for T
|
|
||||||
{
|
|
||||||
/// linear interpolation
|
/// linear interpolation
|
||||||
/// from * (1.0 - self) + to * self
|
/// from * (1.0 - self) + to * self
|
||||||
fn lerp(self, from: Self, to: Self) -> Self {
|
fn lerp(self, from: Self, to: Self) -> Self {
|
||||||
from + (to - from) * self
|
from + (to - from) * self
|
||||||
}
|
}
|
||||||
/// inverse of lerp
|
|
||||||
fn lerp_inv(self, from: Self, to: Self) -> Self {
|
|
||||||
(self - from).div_or(to - from, from)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! impl_op {
|
macro_rules! impl_op {
|
||||||
@@ -77,6 +56,34 @@ macro_rules! impl_op {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Without the `f32` operations, for a type whose fields are not all the
|
||||||
|
// same kind of number: there is nothing a bare float means to a fraction
|
||||||
|
// and an offset at once.
|
||||||
|
(same $T:ident $op:ident $fn:ident $opa:ident $fna:ident; $($field:ident)*) => {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod ${concat($T, _op_, $fn, _same_impl)} {
|
||||||
|
use super::*;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::ops::*;
|
||||||
|
const impl $op for $T {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn $fn(self, rhs: Self) -> Self::Output {
|
||||||
|
Self {
|
||||||
|
$($field: self.$field.$fn(rhs.$field),)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const impl $opa for $T {
|
||||||
|
fn $fna(&mut self, rhs: Self) {
|
||||||
|
*self = self.$fn(rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(same $T:ident $op:ident $fn:ident; $($field:ident)*) => {
|
||||||
|
impl_op!(same $T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
|
||||||
|
};
|
||||||
($T:ident $op:ident $fn:ident; $($field:ident)*) => {
|
($T:ident $op:ident $fn:ident; $($field:ident)*) => {
|
||||||
impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
|
impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
|
||||||
};
|
};
|
||||||
@@ -86,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,8 +1,3 @@
|
|||||||
#[allow(clippy::missing_safety_doc)]
|
|
||||||
pub(crate) unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
|
|
||||||
unsafe { std::mem::transmute::<&T, &T>(x) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::missing_safety_doc)]
|
#[allow(clippy::missing_safety_doc)]
|
||||||
pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
|
pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
|
||||||
unsafe { std::mem::transmute::<&mut T, &mut T>(x) }
|
unsafe { std::mem::transmute::<&mut T, &mut T>(x) }
|
||||||
|
|||||||
+6
-11
@@ -1,7 +1,11 @@
|
|||||||
use crate::util::{DivOr, impl_op};
|
use crate::util::impl_op;
|
||||||
use std::{hash::Hash, ops::*};
|
use std::{hash::Hash, ops::*};
|
||||||
|
|
||||||
#[repr(C)]
|
/// `align(8)` because that is WGSL's alignment for a `vec2<f32>`, so any GPU
|
||||||
|
/// struct holding one is laid out the way its shader reads it without having
|
||||||
|
/// to say so itself. Those structs still need a manual `unsafe impl Pod`,
|
||||||
|
/// since the trailing padding this introduces is what `derive(Pod)` refuses.
|
||||||
|
#[repr(C, align(8))]
|
||||||
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct Vec2 {
|
pub struct Vec2 {
|
||||||
pub x: f32,
|
pub x: f32,
|
||||||
@@ -67,15 +71,6 @@ impl_op!(Vec2 Sub sub; x y);
|
|||||||
impl_op!(Vec2 Mul mul; x y);
|
impl_op!(Vec2 Mul mul; x y);
|
||||||
impl_op!(Vec2 Div div; x y);
|
impl_op!(Vec2 Div div; x y);
|
||||||
|
|
||||||
const impl DivOr for Vec2 {
|
|
||||||
fn div_or(self, rhs: Self, other: Self) -> Self {
|
|
||||||
Self {
|
|
||||||
x: self.x.div_or(rhs.x, other.x),
|
|
||||||
y: self.y.div_or(rhs.y, other.y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Neg for Vec2 {
|
impl Neg for Vec2 {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
use crate::Widget;
|
use crate::{RegionAlign, SizeRules, Widget};
|
||||||
|
|
||||||
pub struct WidgetData {
|
pub struct WidgetData {
|
||||||
pub widget: Box<dyn Widget>,
|
pub widget: Box<dyn Widget>,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
|
pub(super) region_node: bool,
|
||||||
|
pub(super) size: SizeRules,
|
||||||
|
pub(super) align: RegionAlign,
|
||||||
/// dynamic borrow checking
|
/// dynamic borrow checking
|
||||||
pub borrowed: bool,
|
pub borrowed: bool,
|
||||||
}
|
}
|
||||||
@@ -16,6 +19,9 @@ impl WidgetData {
|
|||||||
Self {
|
Self {
|
||||||
widget: Box::new(widget),
|
widget: Box::new(widget),
|
||||||
label,
|
label,
|
||||||
|
region_node: false,
|
||||||
|
size: SizeRules::default(),
|
||||||
|
align: RegionAlign::default(),
|
||||||
borrowed: false,
|
borrowed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-19
@@ -1,9 +1,11 @@
|
|||||||
use crate::{Axis, AxisT, Len, Painter, SizeCtx};
|
use crate::{Axis, LayoutLen, Painter, Size};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
mod data;
|
mod data;
|
||||||
mod handle;
|
mod handle;
|
||||||
mod like;
|
mod like;
|
||||||
|
mod request;
|
||||||
|
mod size_rule;
|
||||||
mod tag;
|
mod tag;
|
||||||
mod view;
|
mod view;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
@@ -11,36 +13,38 @@ mod widgets;
|
|||||||
pub use data::*;
|
pub use data::*;
|
||||||
pub use handle::*;
|
pub use handle::*;
|
||||||
pub use like::*;
|
pub use like::*;
|
||||||
|
pub use request::*;
|
||||||
|
pub use size_rule::*;
|
||||||
pub use tag::*;
|
pub use tag::*;
|
||||||
pub use view::*;
|
pub use view::*;
|
||||||
pub use widgets::*;
|
pub use widgets::*;
|
||||||
|
|
||||||
pub trait Widget: Any {
|
pub trait Widget: Any {
|
||||||
fn draw(&mut self, painter: &mut Painter);
|
/// Draws the widget, and returns what it used of the box it was given.
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len;
|
fn draw(&mut self, painter: &mut Painter) -> Size;
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len;
|
|
||||||
|
/// Describes an axis before painting. Return `None` when discovering it
|
||||||
|
/// needs a concrete box or work performed by `draw`.
|
||||||
|
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||||
|
self.size_hint(axis).map(|len| requests.length(len))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WidgetAxisFns {
|
/// An exact length the widget can give without a painter or its children.
|
||||||
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len;
|
/// Optional, and saves a draw rather than changing one: a hint that
|
||||||
}
|
/// disagrees with the eventual draw fails a debug assertion.
|
||||||
|
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
|
||||||
impl<W: Widget + ?Sized> WidgetAxisFns for W {
|
None
|
||||||
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
match A::get() {
|
|
||||||
Axis::X => self.desired_width(ctx),
|
|
||||||
Axis::Y => self.desired_height(ctx),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for () {
|
impl Widget for () {
|
||||||
fn draw(&mut self, _: &mut Painter) {}
|
/// A gap: nothing drawn, at the default length, so a span gives it a share.
|
||||||
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
fn draw(&mut self, _: &mut Painter) -> Size {
|
||||||
Len::ZERO
|
Size::default()
|
||||||
}
|
}
|
||||||
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
|
||||||
Len::ZERO
|
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
|
||||||
|
Some(LayoutLen::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,449 @@
|
|||||||
|
use crate::{
|
||||||
|
ActiveData, Axis, Bound, LayoutLen, Len, Px, Rel, SizeRule, StrongWidget, UiNum, Weight,
|
||||||
|
WidgetId, Widgets, util::HashMap,
|
||||||
|
};
|
||||||
|
use std::{cmp::Ordering, sync::Arc};
|
||||||
|
|
||||||
|
impl<N: UiNum> From<N> for SizeRequest {
|
||||||
|
fn from(value: N) -> Self {
|
||||||
|
LayoutLen::px(value).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutLen {
|
||||||
|
pub fn min(self, other: impl Into<SizeRequest>) -> SizeRequest {
|
||||||
|
SizeRequest::from(self).min(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max(self, other: impl Into<SizeRequest>) -> SizeRequest {
|
||||||
|
SizeRequest::from(self).max(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clamp(self, min: impl Into<SizeRequest>, max: impl Into<SizeRequest>) -> SizeRequest {
|
||||||
|
SizeRequest::from(self).clamp(min, max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A size request before a container has divided its leftover space.
|
||||||
|
/// Comparisons keep both operands until the share is known.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum SizeRequest {
|
||||||
|
Linear(LayoutLen),
|
||||||
|
Sum(Arc<(Self, Self)>),
|
||||||
|
Min(Arc<(Self, Self)>),
|
||||||
|
Max(Arc<(Self, Self)>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LayoutLen> for SizeRequest {
|
||||||
|
fn from(len: LayoutLen) -> Self {
|
||||||
|
Self::Linear(len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Len> for SizeRequest {
|
||||||
|
fn from(len: Len) -> Self {
|
||||||
|
LayoutLen::from(len).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SizeRequest {
|
||||||
|
pub fn min(self, other: impl Into<Self>) -> Self {
|
||||||
|
let other = other.into();
|
||||||
|
if let (Self::Linear(a), Self::Linear(b)) = (&self, &other)
|
||||||
|
&& let Some(order) = independent_order(*a, *b)
|
||||||
|
{
|
||||||
|
return if !order.is_gt() { self } else { other };
|
||||||
|
}
|
||||||
|
if self == other {
|
||||||
|
self
|
||||||
|
} else {
|
||||||
|
Self::Min(Arc::new((self, other)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max(self, other: impl Into<Self>) -> Self {
|
||||||
|
let other = other.into();
|
||||||
|
if let (Self::Linear(a), Self::Linear(b)) = (&self, &other)
|
||||||
|
&& let Some(order) = independent_order(*a, *b)
|
||||||
|
{
|
||||||
|
return if !order.is_lt() { self } else { other };
|
||||||
|
}
|
||||||
|
if self == other {
|
||||||
|
self
|
||||||
|
} else {
|
||||||
|
Self::Max(Arc::new((self, other)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clamp(self, min: impl Into<Self>, max: impl Into<Self>) -> Self {
|
||||||
|
self.max(min).min(max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add for SizeRequest {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, other: Self) -> Self {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::Linear(a), Self::Linear(b)) => Self::Linear(a + b),
|
||||||
|
(a, b) => Self::Sum(Arc::new((a, b))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A discovered length. Deferred values belong to the current layout pass;
|
||||||
|
/// widgets must not retain them. Ordinary requests remain inline lengths.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct RequestedLen(RequestValue);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
enum RequestValue {
|
||||||
|
Linear(LayoutLen),
|
||||||
|
Deferred {
|
||||||
|
index: usize,
|
||||||
|
epoch: u64,
|
||||||
|
leftover: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LayoutLen> for RequestedLen {
|
||||||
|
fn from(len: LayoutLen) -> Self {
|
||||||
|
Self(RequestValue::Linear(len))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Len> for RequestedLen {
|
||||||
|
fn from(len: Len) -> Self {
|
||||||
|
LayoutLen::from(len).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl RequestedLen {
|
||||||
|
/// The length itself, where no comparison is waiting on an allocation.
|
||||||
|
pub fn linear(&self) -> Option<LayoutLen> {
|
||||||
|
match self.0 {
|
||||||
|
RequestValue::Linear(len) => Some(len),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn has_leftover(&self) -> bool {
|
||||||
|
match self.0 {
|
||||||
|
RequestValue::Linear(len) => len.leftover > Weight::ZERO,
|
||||||
|
RequestValue::Deferred { leftover, .. } => leftover,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum Op {
|
||||||
|
Sum,
|
||||||
|
Min,
|
||||||
|
Max,
|
||||||
|
}
|
||||||
|
struct Node {
|
||||||
|
op: Op,
|
||||||
|
a: RequestedLen,
|
||||||
|
b: RequestedLen,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct RequestArena {
|
||||||
|
nodes: Vec<Node>,
|
||||||
|
epoch: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestArena {
|
||||||
|
pub(crate) fn reset(&mut self) {
|
||||||
|
self.nodes.clear();
|
||||||
|
self.epoch = self
|
||||||
|
.epoch
|
||||||
|
.checked_add(1)
|
||||||
|
.expect("layout generation exhausted");
|
||||||
|
}
|
||||||
|
pub(crate) fn import(&mut self, request: &SizeRequest, base: Len) -> RequestedLen {
|
||||||
|
let (op, pair) = match request {
|
||||||
|
SizeRequest::Linear(len) => return len.within_len(base).into(),
|
||||||
|
SizeRequest::Sum(pair) => (Op::Sum, pair),
|
||||||
|
SizeRequest::Min(pair) => (Op::Min, pair),
|
||||||
|
SizeRequest::Max(pair) => (Op::Max, pair),
|
||||||
|
};
|
||||||
|
let a = self.import(&pair.0, base);
|
||||||
|
let b = self.import(&pair.1, base);
|
||||||
|
self.combine(op, a, b)
|
||||||
|
}
|
||||||
|
fn combine(&mut self, op: Op, a: RequestedLen, b: RequestedLen) -> RequestedLen {
|
||||||
|
if let (Some(x), Some(y)) = (a.linear(), b.linear()) {
|
||||||
|
if matches!(op, Op::Sum) {
|
||||||
|
return (x + y).into();
|
||||||
|
}
|
||||||
|
let order = independent_order(x, y);
|
||||||
|
if let Some(order) = order {
|
||||||
|
let take_a = match op {
|
||||||
|
Op::Min => !order.is_gt(),
|
||||||
|
_ => !order.is_lt(),
|
||||||
|
};
|
||||||
|
return if take_a { a } else { b };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a == b && !matches!(op, Op::Sum) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
let index = self.nodes.len();
|
||||||
|
self.nodes.push(Node { op, a, b });
|
||||||
|
RequestedLen(RequestValue::Deferred {
|
||||||
|
index,
|
||||||
|
epoch: self.epoch,
|
||||||
|
leftover: a.has_leftover() || b.has_leftover(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub(crate) fn minimum(&self, request: RequestedLen, window: Px) -> Px {
|
||||||
|
Px::from_raw(self.segment(request, Ratio::ZERO, window).fixed as i32)
|
||||||
|
}
|
||||||
|
fn segment(&self, request: RequestedLen, at: Ratio, window: Px) -> Segment {
|
||||||
|
match request.0 {
|
||||||
|
RequestValue::Linear(len) => {
|
||||||
|
debug_assert!(
|
||||||
|
len.leftover >= Weight::ZERO,
|
||||||
|
"a leftover weight cannot be negative"
|
||||||
|
);
|
||||||
|
Segment {
|
||||||
|
fixed: i64::from(len.without_leftover().to_px(window).raw()),
|
||||||
|
weight: i64::from(len.leftover.raw()),
|
||||||
|
end: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RequestValue::Deferred { index, epoch, .. } => {
|
||||||
|
assert_eq!(epoch, self.epoch, "request retained beyond its layout pass");
|
||||||
|
let Node { op, a, b } = self.nodes[index];
|
||||||
|
let a = self.segment(a, at, window);
|
||||||
|
let b = self.segment(b, at, window);
|
||||||
|
if matches!(op, Op::Sum) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
// At a crossing choose the branch to its right, so the next
|
||||||
|
// iteration advances rather than selecting that crossing again.
|
||||||
|
let order = a.value(at).cmp(&b.value(at)).then(a.weight.cmp(&b.weight));
|
||||||
|
let take_a = match op {
|
||||||
|
Op::Min => !order.is_gt(),
|
||||||
|
_ => !order.is_lt(),
|
||||||
|
};
|
||||||
|
let mut selected = if take_a { a } else { b };
|
||||||
|
selected.end = first(a.end, b.end);
|
||||||
|
if a.weight != b.weight {
|
||||||
|
let crossing = Ratio::new(b.fixed - a.fixed, a.weight - b.weight);
|
||||||
|
if crossing > at {
|
||||||
|
selected.end = first(selected.end, Some(crossing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Divides `room` between requests whose weights are nonnegative, one
|
||||||
|
/// length per request. A floor can overflow the room and a cap can leave
|
||||||
|
/// part of it unused, so the lengths need not come to `room`. Each edge
|
||||||
|
/// is rounded from the running total rather than from the length before
|
||||||
|
/// it, so two neighbouring slots meet exactly.
|
||||||
|
pub(crate) fn allocate<'a>(
|
||||||
|
&'a self,
|
||||||
|
requests: &'a [RequestedLen],
|
||||||
|
room: Px,
|
||||||
|
window: Px,
|
||||||
|
) -> impl Iterator<Item = Px> + 'a {
|
||||||
|
let mut at = Ratio::ZERO;
|
||||||
|
loop {
|
||||||
|
let total = requests.iter().fold(Segment::ZERO, |total, request| {
|
||||||
|
total + self.segment(*request, at, window)
|
||||||
|
});
|
||||||
|
if total.value(at) >= i128::from(room.raw()) * i128::from(at.den) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if total.weight != 0 {
|
||||||
|
let solution = Ratio::new(i64::from(room.raw()) - total.fixed, total.weight);
|
||||||
|
if total.end.is_none_or(|end| solution <= end) {
|
||||||
|
at = solution;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match total.end {
|
||||||
|
Some(end) => at = end,
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut prefix = 0_i128;
|
||||||
|
let mut previous = 0_i128;
|
||||||
|
requests.iter().map(move |request| {
|
||||||
|
prefix += self.segment(*request, at, window).value(at);
|
||||||
|
let den = i128::from(at.den);
|
||||||
|
// Half away from zero, which is what `Fixed` rounds a division
|
||||||
|
// to: the two decide the same edge, and a change to one of them
|
||||||
|
// is a change to the other.
|
||||||
|
let edge = prefix.signum() * ((prefix.abs() + den / 2) / den);
|
||||||
|
let len = Px::from_raw((edge - previous) as i32);
|
||||||
|
previous = edge;
|
||||||
|
len
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq)]
|
||||||
|
struct Ratio {
|
||||||
|
num: i64,
|
||||||
|
den: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Ratio {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.cmp(other).is_eq()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ratio {
|
||||||
|
const ZERO: Self = Self { num: 0, den: 1 };
|
||||||
|
|
||||||
|
fn new(num: i64, den: i64) -> Self {
|
||||||
|
debug_assert_ne!(den, 0, "a ratio of nothing");
|
||||||
|
if den < 0 {
|
||||||
|
Self {
|
||||||
|
num: -num,
|
||||||
|
den: -den,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self { num, den }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Ratio {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
(i128::from(self.num) * i128::from(other.den))
|
||||||
|
.cmp(&(i128::from(other.num) * i128::from(self.den)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Ratio {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Segment {
|
||||||
|
fixed: i64,
|
||||||
|
weight: i64,
|
||||||
|
end: Option<Ratio>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Segment {
|
||||||
|
const ZERO: Self = Self {
|
||||||
|
fixed: 0,
|
||||||
|
weight: 0,
|
||||||
|
end: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn value(self, at: Ratio) -> i128 {
|
||||||
|
i128::from(self.fixed) * i128::from(at.den) + i128::from(self.weight) * i128::from(at.num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add for Segment {
|
||||||
|
type Output = Self;
|
||||||
|
fn add(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
fixed: self.fixed + other.fixed,
|
||||||
|
weight: self.weight + other.weight,
|
||||||
|
end: first(self.end, other.end),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first(a: Option<Ratio>, b: Option<Ratio>) -> Option<Ratio> {
|
||||||
|
match (a, b) {
|
||||||
|
(Some(a), Some(b)) => Some(a.min(b)),
|
||||||
|
(a, b) => a.or(b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-only discovery of requests through a widget's children. A request is
|
||||||
|
/// expressed in window lengths; `rel_base` supplies the base for declarations.
|
||||||
|
pub struct SizeRequests<'a> {
|
||||||
|
pub(crate) arena: &'a mut RequestArena,
|
||||||
|
pub(crate) measured: Option<&'a HashMap<WidgetId, ActiveData>>,
|
||||||
|
pub(crate) widgets: &'a Widgets,
|
||||||
|
pub(crate) dependencies: &'a mut Vec<WidgetId>,
|
||||||
|
pub(crate) rel_base: Len,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SizeRequests<'_> {
|
||||||
|
pub fn sum(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
|
||||||
|
self.arena.combine(Op::Sum, a, b)
|
||||||
|
}
|
||||||
|
pub fn min(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
|
||||||
|
self.arena.combine(Op::Min, a, b)
|
||||||
|
}
|
||||||
|
pub fn max(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
|
||||||
|
self.arena.combine(Op::Max, a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn widget<W: ?Sized>(
|
||||||
|
&mut self,
|
||||||
|
child: &StrongWidget<W>,
|
||||||
|
axis: Axis,
|
||||||
|
) -> Option<RequestedLen> {
|
||||||
|
self.dependencies.push(child.id());
|
||||||
|
let rules = self.widgets.size_rules(child.id());
|
||||||
|
let rule = &rules[axis];
|
||||||
|
if let SizeRule::Request(request) = rule {
|
||||||
|
return Some(self.arena.import(request, self.rel_base));
|
||||||
|
}
|
||||||
|
if let Some(exact) = rule.exact() {
|
||||||
|
return Some(exact.within_len(self.rel_base).into());
|
||||||
|
}
|
||||||
|
let widget = self.widgets.get_dyn(child.id())?;
|
||||||
|
let request = widget.size_request(self, axis).or_else(|| {
|
||||||
|
self.measured?
|
||||||
|
.get(&child.id())?
|
||||||
|
.measured()
|
||||||
|
.map(|size| size[axis].into())
|
||||||
|
})?;
|
||||||
|
Some(self.bounded(request, rule.bound()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn bounded(&mut self, request: RequestedLen, bound: Bound) -> RequestedLen {
|
||||||
|
let bound = bound.within_len(self.rel_base);
|
||||||
|
let request = match bound.min {
|
||||||
|
Some(min) => self.max(request, min.into()),
|
||||||
|
None => request,
|
||||||
|
};
|
||||||
|
match bound.max {
|
||||||
|
Some(max) => self.min(request, max.into()),
|
||||||
|
None => request,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn length(&self, len: LayoutLen) -> RequestedLen {
|
||||||
|
len.within_len(self.rel_base).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inset<W: ?Sized>(
|
||||||
|
&mut self,
|
||||||
|
child: &StrongWidget<W>,
|
||||||
|
axis: Axis,
|
||||||
|
padding: Px,
|
||||||
|
) -> Option<RequestedLen> {
|
||||||
|
let base = self.rel_base;
|
||||||
|
self.rel_base.px -= padding;
|
||||||
|
let request = self.widget(child, axis);
|
||||||
|
self.rel_base = base;
|
||||||
|
request.map(|request| self.sum(request, Len::from_parts(Rel::ZERO, padding).into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal fractions keep this valid even when padding makes a rel base negative.
|
||||||
|
fn independent_order(a: LayoutLen, b: LayoutLen) -> Option<Ordering> {
|
||||||
|
if a.rel == b.rel && a.leftover == b.leftover {
|
||||||
|
Some(a.px.cmp(&b.px))
|
||||||
|
} else if a.px == b.px && a.rel == b.rel {
|
||||||
|
Some(a.leftover.cmp(&b.leftover))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
use crate::util::impl_axis_index;
|
||||||
|
use crate::{Axis, LayoutLen, Len, Rel, SizeRequest};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// What a widget's length on one axis is, as a rule its parent applies where
|
||||||
|
/// it draws it rather than an answer the widget gives about itself.
|
||||||
|
///
|
||||||
|
/// A rule and a drawn size are not two opinions to reconcile: a rule wins on
|
||||||
|
/// the axis it names, and the `Size` returned by `draw` answers only the axes
|
||||||
|
/// with no rule. That is what lets a span divide its space around a length
|
||||||
|
/// nobody has drawn yet, and it is why a rule lives beside the widget rather
|
||||||
|
/// than inside it -- the widget under the rule never has to know about it.
|
||||||
|
///
|
||||||
|
/// Exact expressions can bound a share before allocation. Bounds on an
|
||||||
|
/// intrinsic answer are applied after that answer becomes known.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub enum SizeRule {
|
||||||
|
/// Whatever the widget reports from drawing.
|
||||||
|
#[default]
|
||||||
|
Free,
|
||||||
|
/// This length, whatever the widget reports.
|
||||||
|
Exact(LayoutLen),
|
||||||
|
/// An exact request whose comparisons await the parent's allocation.
|
||||||
|
Request(Arc<SizeRequest>),
|
||||||
|
/// At least this long, and otherwise whatever the box gives it.
|
||||||
|
Min(Len),
|
||||||
|
/// At most this long.
|
||||||
|
Max(Len),
|
||||||
|
/// Between the two.
|
||||||
|
Clamp { min: Len, max: Len },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SizeRule {
|
||||||
|
/// What this rule allows the length to be where it does not give one
|
||||||
|
/// outright.
|
||||||
|
pub fn bound(&self) -> Bound {
|
||||||
|
match *self {
|
||||||
|
Self::Free | Self::Exact(_) | Self::Request(_) => Bound::ANY,
|
||||||
|
Self::Min(min) => Bound {
|
||||||
|
min: Some(min),
|
||||||
|
max: None,
|
||||||
|
},
|
||||||
|
Self::Max(max) => Bound {
|
||||||
|
min: None,
|
||||||
|
max: Some(max),
|
||||||
|
},
|
||||||
|
Self::Clamp { min, max } => Bound {
|
||||||
|
min: Some(min),
|
||||||
|
max: Some(max),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether what this rule says is a fraction of the rel base, so that
|
||||||
|
/// the same rule against a different one is a different length.
|
||||||
|
pub fn has_fraction(&self) -> bool {
|
||||||
|
self.exact().is_some_and(|len| len.rel != Rel::ZERO) || self.bound().has_fraction()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This rule with a floor under it, which is the whole of it where there
|
||||||
|
/// was no rule.
|
||||||
|
pub fn at_least(&self, min: Len) -> Self {
|
||||||
|
match *self {
|
||||||
|
Self::Free | Self::Min(_) => Self::Min(min),
|
||||||
|
Self::Max(max) | Self::Clamp { max, .. } => Self::Clamp { min, max },
|
||||||
|
Self::Request(ref request) => request.as_ref().clone().max(min).into(),
|
||||||
|
Self::Exact(len) => len.max(min).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This rule with a cap over it, which is the whole of it where there was
|
||||||
|
/// no rule.
|
||||||
|
pub fn at_most(&self, max: Len) -> Self {
|
||||||
|
match *self {
|
||||||
|
Self::Free | Self::Max(_) => Self::Max(max),
|
||||||
|
Self::Min(min) | Self::Clamp { min, .. } => Self::Clamp { min, max },
|
||||||
|
Self::Request(ref request) => request.as_ref().clone().min(max).into(),
|
||||||
|
Self::Exact(len) => len.min(max).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The length this rule gives without the widget being drawn, if it can
|
||||||
|
/// give one.
|
||||||
|
pub fn declared(&self) -> Option<Len> {
|
||||||
|
self.exact().and_then(|len| len.declared())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The length this rule gives outright, whatever the widget reports --
|
||||||
|
/// which makes the widget's answer on that axis moot. A share counts: it
|
||||||
|
/// is a length the widget's parent still has to divide, so it is exact
|
||||||
|
/// here and resolved there, unlike `declared`, which is only the ones
|
||||||
|
/// that give a box directly.
|
||||||
|
pub fn exact(&self) -> Option<LayoutLen> {
|
||||||
|
match self {
|
||||||
|
Self::Exact(len) => Some(*len),
|
||||||
|
Self::Free | Self::Request(_) | Self::Min(_) | Self::Max(_) | Self::Clamp { .. } => {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a rule allows a length to be where it does not give one outright: a
|
||||||
|
/// floor, a cap, or both. Each is a length of the rel base the widget is
|
||||||
|
/// asked with, which is the base a declared length is a fraction of too.
|
||||||
|
///
|
||||||
|
/// A bound is a [`Len`]. Comparisons involving shares are [`SizeRequest`]s.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub struct Bound {
|
||||||
|
pub min: Option<Len>,
|
||||||
|
pub max: Option<Len>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bound {
|
||||||
|
/// Every length.
|
||||||
|
pub const ANY: Self = Self {
|
||||||
|
min: None,
|
||||||
|
max: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Whether either end is a fraction of the rel base, so that the same
|
||||||
|
/// bound against a different one binds at a different length.
|
||||||
|
pub fn has_fraction(&self) -> bool {
|
||||||
|
[self.min, self.max]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.any(|len| len.rel != Rel::ZERO)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This bound as lengths of the window, from lengths of a rel base that
|
||||||
|
/// long.
|
||||||
|
pub fn within_len(&self, len: Len) -> Self {
|
||||||
|
Self {
|
||||||
|
min: self.min.map(|min| min.within_len(len)),
|
||||||
|
max: self.max.map(|max| max.within_len(len)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One bound per axis, as [`SizeRules`] is one rule per axis.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub struct Bounds {
|
||||||
|
pub x: Bound,
|
||||||
|
pub y: Bound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bounds {
|
||||||
|
pub const ANY: Self = Self {
|
||||||
|
x: Bound::ANY,
|
||||||
|
y: Bound::ANY,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn from_axes(f: impl Fn(Axis) -> Bound) -> Self {
|
||||||
|
Self {
|
||||||
|
x: f(Axis::X),
|
||||||
|
y: f(Axis::Y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_axis_index!(Bounds => Bound);
|
||||||
|
|
||||||
|
impl From<LayoutLen> for SizeRule {
|
||||||
|
fn from(len: LayoutLen) -> Self {
|
||||||
|
Self::Exact(len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SizeRequest> for SizeRule {
|
||||||
|
fn from(request: SizeRequest) -> Self {
|
||||||
|
match request {
|
||||||
|
SizeRequest::Linear(len) => Self::Exact(len),
|
||||||
|
request => Self::Request(Arc::new(request)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<LayoutLen>> for SizeRule {
|
||||||
|
fn from(len: Option<LayoutLen>) -> Self {
|
||||||
|
len.map_or(Self::Free, Self::Exact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One rule per axis, which is how a widget carries a length on one axis and
|
||||||
|
/// leaves the other to whatever it draws.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub struct SizeRules {
|
||||||
|
pub x: SizeRule,
|
||||||
|
pub y: SizeRule,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_axis_index!(SizeRules => SizeRule);
|
||||||
|
|
||||||
|
/// What a widget's box is on each axis where something says so outright,
|
||||||
|
/// before it is drawn: a rule beside it, or a hint it gives about itself.
|
||||||
|
/// Whoever draws the widget resolves these against its rel base.
|
||||||
|
///
|
||||||
|
/// A [`Len`] rather than a [`LayoutLen`], because a share can never be one
|
||||||
|
/// -- see [`LayoutLen::declared`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Declared {
|
||||||
|
pub x: Option<Len>,
|
||||||
|
pub y: Option<Len>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Declared {
|
||||||
|
pub const NONE: Self = Self { x: None, y: None };
|
||||||
|
|
||||||
|
pub fn from_axes(f: impl Fn(Axis) -> Option<Len>) -> Self {
|
||||||
|
Self {
|
||||||
|
x: f(Axis::X),
|
||||||
|
y: f(Axis::Y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_axis_index!(Declared => Option<Len>);
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
IdLike, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
|
Axis, AxisAlign, IdLike, Len, RegionAlign, SizeRule, SizeRules, StrongWidget, WeakWidget,
|
||||||
|
Widget, WidgetData, WidgetId,
|
||||||
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,6 +30,14 @@ impl Widgets {
|
|||||||
!self.needs_redraw.is_empty()
|
!self.needs_redraw.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Marks this widget for the next frame to draw again, with nothing about
|
||||||
|
/// it changed. Taking a widget mutably marks it too, which is the ordinary
|
||||||
|
/// content-change signal; this is for a change the borrow cannot express,
|
||||||
|
/// and for asking for the same tree over again.
|
||||||
|
pub fn mark_for_redraw(&mut self, id: impl IdLike) {
|
||||||
|
self.needs_redraw.insert(id.id());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> {
|
pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> {
|
||||||
Some(self.vec.get(id)?.widget.as_ref())
|
Some(self.vec.get(id)?.widget.as_ref())
|
||||||
}
|
}
|
||||||
@@ -40,14 +49,14 @@ impl Widgets {
|
|||||||
|
|
||||||
/// get_dyn but dynamic borrow checking of widgets
|
/// get_dyn but dynamic borrow checking of widgets
|
||||||
/// lets you do recursive (tree) operations, like the painter does
|
/// lets you do recursive (tree) operations, like the painter does
|
||||||
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> {
|
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> DynBorrower<'a, dyn Widget> {
|
||||||
// SAFETY: must guarantee no other mutable references to this widget exist
|
// SAFETY: must guarantee no other mutable references to this widget exist
|
||||||
// done through the borrow variable
|
// done through the borrow variable
|
||||||
let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) };
|
let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) };
|
||||||
if data.borrowed {
|
if data.borrowed {
|
||||||
panic!("tried to mutably borrow the same widget twice");
|
panic!("tried to mutably borrow the same widget twice");
|
||||||
}
|
}
|
||||||
WidgetWrapper::new(data.widget.as_mut(), &mut data.borrowed)
|
DynBorrower::new(data.widget.as_mut(), &mut data.borrowed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget>
|
pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget>
|
||||||
@@ -100,6 +109,87 @@ impl Widgets {
|
|||||||
self.data_mut(id.id()).unwrap().label = label;
|
self.data_mut(id.id()).unwrap().label = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this widget owns a movable retained region.
|
||||||
|
pub fn is_region_node(&self, id: impl IdLike) -> bool {
|
||||||
|
self.data(id).unwrap().region_node
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chooses whether this widget's retained drawing has one movable region
|
||||||
|
/// of its own. Changing the boundary redraws the subtree once so every
|
||||||
|
/// primitive names the right coordinate space.
|
||||||
|
pub fn set_region_node(&mut self, id: impl IdLike, region_node: bool) {
|
||||||
|
let id = id.id();
|
||||||
|
let data = self.data_mut(id).unwrap();
|
||||||
|
if data.region_node == region_node {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.region_node = region_node;
|
||||||
|
self.needs_redraw.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The length rules whoever draws this widget applies to its box.
|
||||||
|
pub fn size_rules(&self, id: impl IdLike) -> &SizeRules {
|
||||||
|
&self.data(id).unwrap().size
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets one axis's rule. The widget is marked rather than its parent
|
||||||
|
/// because the parent is not known here; `redraw` escalates a changed
|
||||||
|
/// declared length to whoever resolves it.
|
||||||
|
pub fn set_size_rule(&mut self, id: impl IdLike, axis: Axis, rule: SizeRule) {
|
||||||
|
let id = id.id();
|
||||||
|
let data = self.data_mut(id).unwrap();
|
||||||
|
if data.size[axis] == rule {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.size[axis] = rule;
|
||||||
|
self.needs_redraw.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts a floor under this widget's length on one axis, keeping a cap it
|
||||||
|
/// already had. See [`SizeRule::at_least`].
|
||||||
|
pub fn set_min_len(&mut self, id: impl IdLike, axis: Axis, min: Len) {
|
||||||
|
let id = id.id();
|
||||||
|
let rule = self.size_rules(id)[axis].at_least(min);
|
||||||
|
self.set_size_rule(id, axis, rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts a cap over it, keeping a floor it already had. See
|
||||||
|
/// [`SizeRule::at_most`].
|
||||||
|
pub fn set_max_len(&mut self, id: impl IdLike, axis: Axis, max: Len) {
|
||||||
|
let id = id.id();
|
||||||
|
let rule = self.size_rules(id)[axis].at_most(max);
|
||||||
|
self.set_size_rule(id, axis, rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where this widget sits in a box longer than the length it takes.
|
||||||
|
pub fn alignment(&self, id: impl IdLike) -> RegionAlign {
|
||||||
|
self.data(id).unwrap().align
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets one axis's alignment. Which box a widget ends up in is its
|
||||||
|
/// parent's to decide, so this is escalated the way a length rule is.
|
||||||
|
pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) {
|
||||||
|
let id = id.id();
|
||||||
|
let data = self.data_mut(id).unwrap();
|
||||||
|
if data.align[axis] == align {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.align[axis] = align;
|
||||||
|
self.needs_redraw.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Both axes at once.
|
||||||
|
pub fn set_size_rules(
|
||||||
|
&mut self,
|
||||||
|
id: impl IdLike,
|
||||||
|
x: impl Into<SizeRule>,
|
||||||
|
y: impl Into<SizeRule>,
|
||||||
|
) {
|
||||||
|
let id = id.id();
|
||||||
|
self.set_size_rule(id, Axis::X, x.into());
|
||||||
|
self.set_size_rule(id, Axis::Y, y.into());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> {
|
pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> {
|
||||||
self.vec.get_mut(id.id())
|
self.vec.get_mut(id.id())
|
||||||
}
|
}
|
||||||
@@ -122,8 +212,6 @@ impl Default for Widgets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type WidgetWrapper<'a> = DynBorrower<'a, dyn Widget>;
|
|
||||||
|
|
||||||
impl<I: IdLike> std::ops::Index<I> for Widgets
|
impl<I: IdLike> std::ops::Index<I> for Widgets
|
||||||
where
|
where
|
||||||
I::Widget: Sized + Widget,
|
I::Widget: Sized + Widget,
|
||||||
|
|||||||
+1
-5
@@ -10,11 +10,7 @@ struct State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DefaultAppState for State {
|
impl DefaultAppState for State {
|
||||||
fn new(
|
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||||
mut ui_state: DefaultUiState,
|
|
||||||
rsc: &mut DefaultRsc<Self>,
|
|
||||||
_: Proxy<Self::Event>,
|
|
||||||
) -> Self {
|
|
||||||
rect(Color::RED).set_root(rsc, &mut ui_state);
|
rect(Color::RED).set_root(rsc, &mut ui_state);
|
||||||
Self { ui_state }
|
Self { ui_state }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//! The seeded random tree `tests/generated.rs` checks, drawn so it can be
|
||||||
|
//! looked at. `IRIS_SEED` and `IRIS_DEPTH` choose which one.
|
||||||
|
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris::random::Edits;
|
||||||
|
|
||||||
|
fn env(name: &str, fallback: u64) -> u64 {
|
||||||
|
std::env::var(name)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
DefaultApp::<State>::run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DefaultUiState)]
|
||||||
|
struct State {
|
||||||
|
ui_state: DefaultUiState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DefaultAppState for State {
|
||||||
|
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||||
|
let seed = env("IRIS_SEED", 1);
|
||||||
|
let depth = env("IRIS_DEPTH", 4) as usize;
|
||||||
|
let (root, _) = iris::random::grow(rsc, seed, depth, &Edits::default());
|
||||||
|
ui_state.set_root(root);
|
||||||
|
Self { ui_state }
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-11
@@ -15,31 +15,31 @@ pub struct Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DefaultAppState for Client {
|
impl DefaultAppState for Client {
|
||||||
fn new(
|
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||||
mut ui_state: DefaultUiState,
|
|
||||||
rsc: &mut DefaultRsc<Self>,
|
|
||||||
_: Proxy<Self::Event>,
|
|
||||||
) -> Self {
|
|
||||||
let rrect = rect(Color::WHITE).radius(20);
|
let rrect = rect(Color::WHITE).radius(20);
|
||||||
let pad_test = (
|
let pad_test = (
|
||||||
rrect.color(Color::BLUE),
|
rrect.color(Color::BLUE),
|
||||||
(
|
(
|
||||||
|
// The square is one widget and the two shares of the row it
|
||||||
|
// sits centred in are another: a length is a property of a
|
||||||
|
// widget, so `.width` here would overwrite the `.sized`.
|
||||||
rrect
|
rrect
|
||||||
.color(Color::RED)
|
.color(Color::RED)
|
||||||
.sized((100, 100))
|
.sized((100, 100))
|
||||||
.center()
|
.center()
|
||||||
.width(rest(2)),
|
.wrapper()
|
||||||
|
.width(leftover(2)),
|
||||||
(
|
(
|
||||||
rrect.color(Color::ORANGE),
|
rrect.color(Color::ORANGE),
|
||||||
rrect.color(Color::LIME).pad(10.0),
|
rrect.color(Color::LIME).pad(10.0),
|
||||||
)
|
)
|
||||||
.span(Dir::RIGHT)
|
.span(Dir::RIGHT)
|
||||||
.width(rest(2)),
|
.width(leftover(2)),
|
||||||
rrect.color(Color::YELLOW),
|
rrect.color(Color::YELLOW),
|
||||||
)
|
)
|
||||||
.span(Dir::RIGHT)
|
.span(Dir::RIGHT)
|
||||||
.pad(10)
|
.pad(10)
|
||||||
.width(rest(3)),
|
.width(leftover(3)),
|
||||||
)
|
)
|
||||||
.span(Dir::RIGHT)
|
.span(Dir::RIGHT)
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
@@ -125,11 +125,11 @@ impl DefaultAppState for Client {
|
|||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let text_edit_scroll = (
|
let text_edit_scroll = (
|
||||||
msg_area.height(rest(1)),
|
msg_area.height(leftover(1)),
|
||||||
(
|
(
|
||||||
Rect::new(Color::WHITE.darker(0.9)),
|
Rect::new(Color::WHITE.darker(0.9)),
|
||||||
(
|
(
|
||||||
add_text.width(rest(1)),
|
add_text.width(leftover(1)),
|
||||||
Rect::new(Color::GREEN)
|
Rect::new(Color::GREEN)
|
||||||
.on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| {
|
.on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| {
|
||||||
rsc.run_event::<Submit>(add_text, (), ctx.state);
|
rsc.run_event::<Submit>(add_text, (), ctx.state);
|
||||||
@@ -147,7 +147,7 @@ impl DefaultAppState for Client {
|
|||||||
.span(Dir::DOWN)
|
.span(Dir::DOWN)
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let main = WidgetPtr::new().add(rsc);
|
let main = Wrapper::new().add(rsc);
|
||||||
|
|
||||||
let vals = Rc::new(RefCell::new((0, Vec::new())));
|
let vals = Rc::new(RefCell::new((0, Vec::new())));
|
||||||
let mut switch_button = |color, to: WeakWidget, label| {
|
let mut switch_button = |color, to: WeakWidget, label| {
|
||||||
|
|||||||
+1
-5
@@ -11,11 +11,7 @@ struct State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DefaultAppState for State {
|
impl DefaultAppState for State {
|
||||||
fn new(
|
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||||
mut ui_state: DefaultUiState,
|
|
||||||
rsc: &mut DefaultRsc<Self>,
|
|
||||||
_: Proxy<Self::Event>,
|
|
||||||
) -> Self {
|
|
||||||
let rect = rect(Color::RED).add(rsc);
|
let rect = rect(Color::RED).add(rsc);
|
||||||
rect.task_on(CursorSense::click(), async move |mut ctx| {
|
rect.task_on(CursorSense::click(), async move |mut ctx| {
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
//! Text sizing: wrapped text reads the width it is offered, fixed text does
|
||||||
|
//! not, and both report a height their container lays out around.
|
||||||
|
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
DefaultApp::<State>::run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DefaultUiState)]
|
||||||
|
struct State {
|
||||||
|
ui_state: DefaultUiState,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SAMPLE: &str = "Wrapping shapes one source into as many lines as its container \
|
||||||
|
leaves room for, so the height of a paragraph is an answer rather than a setting, and \
|
||||||
|
the same words in a narrower box come back taller. Resize the window and watch the \
|
||||||
|
text below reflow into a different number of lines while nothing about it changes.";
|
||||||
|
|
||||||
|
impl DefaultAppState for State {
|
||||||
|
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||||
|
let panel = || rect(Color::WHITE.darker(0.85));
|
||||||
|
|
||||||
|
let wrapped = wtext(SAMPLE)
|
||||||
|
.size(28)
|
||||||
|
.wrap(true)
|
||||||
|
.text_align(Align::LEFT)
|
||||||
|
.pad(16)
|
||||||
|
.background(panel());
|
||||||
|
|
||||||
|
// Each one takes the whole width, because `text_align` puts the
|
||||||
|
// glyphs somewhere in the box the text is given and a text that
|
||||||
|
// reports the width of its own glyphs is given exactly that.
|
||||||
|
let label = |text: &str, align| wtext(text).size(24).text_align(align).width(rel(1.0));
|
||||||
|
let aligned = (
|
||||||
|
label("left", Align::LEFT),
|
||||||
|
label("centred", Align::H_CENTER),
|
||||||
|
label("right", Align::RIGHT),
|
||||||
|
)
|
||||||
|
.span(Dir::DOWN)
|
||||||
|
.gap(8)
|
||||||
|
.pad(16)
|
||||||
|
.background(panel());
|
||||||
|
|
||||||
|
// The same words in half the width, which is a different number of
|
||||||
|
// lines and so a different height. A declared width only holds along
|
||||||
|
// a span's own axis, hence the row.
|
||||||
|
let narrow = (
|
||||||
|
wtext(SAMPLE)
|
||||||
|
.size(20)
|
||||||
|
.wrap(true)
|
||||||
|
.pad(16)
|
||||||
|
.background(panel())
|
||||||
|
.align(Align::TOP)
|
||||||
|
.width(rel(0.5)),
|
||||||
|
rect(Color::WHITE.darker(0.95)),
|
||||||
|
)
|
||||||
|
.span(Dir::RIGHT);
|
||||||
|
|
||||||
|
(wrapped, aligned, narrow)
|
||||||
|
.span(Dir::DOWN)
|
||||||
|
.gap(12)
|
||||||
|
.pad(12)
|
||||||
|
.set_root(rsc, &mut ui_state);
|
||||||
|
Self { ui_state }
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-5
@@ -36,11 +36,7 @@ impl Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DefaultAppState for State {
|
impl DefaultAppState for State {
|
||||||
fn new(
|
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||||
mut ui_state: DefaultUiState,
|
|
||||||
rsc: &mut DefaultRsc<Self>,
|
|
||||||
_: Proxy<Self::Event>,
|
|
||||||
) -> Self {
|
|
||||||
let test = Test::new(rsc);
|
let test = Test::new(rsc);
|
||||||
|
|
||||||
test.on(CursorSense::click(), move |_, rsc| {
|
test.on(CursorSense::click(), move |_, rsc| {
|
||||||
|
|||||||
+10
-3
@@ -18,6 +18,7 @@ struct Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct InputFn {
|
struct InputFn {
|
||||||
|
attrs: Vec<Attribute>,
|
||||||
sig: Signature,
|
sig: Signature,
|
||||||
body: Block,
|
body: Block,
|
||||||
}
|
}
|
||||||
@@ -32,9 +33,10 @@ impl Parse for Input {
|
|||||||
input.parse::<Token![;]>()?;
|
input.parse::<Token![;]>()?;
|
||||||
let mut fns = Vec::new();
|
let mut fns = Vec::new();
|
||||||
while !input.is_empty() {
|
while !input.is_empty() {
|
||||||
|
let attrs = input.call(Attribute::parse_outer)?;
|
||||||
let sig = input.parse()?;
|
let sig = input.parse()?;
|
||||||
let body = input.parse()?;
|
let body = input.parse()?;
|
||||||
fns.push(InputFn { sig, body })
|
fns.push(InputFn { attrs, sig, body })
|
||||||
}
|
}
|
||||||
if !input.is_empty() {
|
if !input.is_empty() {
|
||||||
input.error("function expected");
|
input.error("function expected");
|
||||||
@@ -59,10 +61,15 @@ pub fn widget_trait(input: TokenStream) -> TokenStream {
|
|||||||
fns,
|
fns,
|
||||||
} = parse_macro_input!(input as Input);
|
} = parse_macro_input!(input as Input);
|
||||||
|
|
||||||
let sigs: Vec<_> = fns.iter().map(|f| f.sig.clone()).collect();
|
// What a method says about itself belongs on the trait, where a reader
|
||||||
|
// looks it up; the implementation is the same text and says it again.
|
||||||
|
let sigs: Vec<_> = fns
|
||||||
|
.iter()
|
||||||
|
.map(|InputFn { attrs, sig, .. }| quote! { #(#attrs)* #sig })
|
||||||
|
.collect();
|
||||||
let impls: Vec<_> = fns
|
let impls: Vec<_> = fns
|
||||||
.iter()
|
.iter()
|
||||||
.map(|InputFn { sig, body }| quote! { #sig #body })
|
.map(|InputFn { attrs, sig, body }| quote! { #(#attrs)* #sig #body })
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let Some(GenericParam::Type(state)) = generics.params.first() else {
|
let Some(GenericParam::Type(state)) = generics.params.first() else {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "rig-input"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
# Replays `.touch` recordings through Wayland's virtual-pointer protocol;
|
||||||
|
# headless sway has no input devices for coordinate-driving tools to move.
|
||||||
|
[[bin]]
|
||||||
|
name = "replay-touch"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Share the harness parser so both ways of replaying read a file the same.
|
||||||
|
iris = { path = ".." }
|
||||||
|
wayland-client = { workspace = true }
|
||||||
|
wayland-protocols-wlr = { workspace = true }
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
use iris::harness::{TouchAction, TouchScript};
|
||||||
|
use std::time::Duration;
|
||||||
|
use wayland_client::protocol::wl_pointer::ButtonState;
|
||||||
|
use wayland_client::protocol::{wl_registry, wl_seat};
|
||||||
|
use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
|
||||||
|
use wayland_protocols_wlr::virtual_pointer::v1::client::{
|
||||||
|
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1,
|
||||||
|
zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const BTN_LEFT: u32 = 0x110;
|
||||||
|
|
||||||
|
const SETTLE: Duration = Duration::from_millis(200);
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Globals {
|
||||||
|
seat: Option<wl_seat::WlSeat>,
|
||||||
|
manager: Option<ZwlrVirtualPointerManagerV1>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<wl_registry::WlRegistry, ()> for Globals {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
registry: &wl_registry::WlRegistry,
|
||||||
|
event: wl_registry::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
let wl_registry::Event::Global {
|
||||||
|
name,
|
||||||
|
interface,
|
||||||
|
version,
|
||||||
|
} = event
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match interface.as_str() {
|
||||||
|
"wl_seat" => {
|
||||||
|
state.seat = Some(registry.bind(name, version.min(7), qh, ()));
|
||||||
|
}
|
||||||
|
"zwlr_virtual_pointer_manager_v1" => {
|
||||||
|
state.manager = Some(registry.bind(name, version.min(2), qh, ()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate_noop!(Globals: ignore wl_seat::WlSeat);
|
||||||
|
delegate_noop!(Globals: ZwlrVirtualPointerManagerV1);
|
||||||
|
delegate_noop!(Globals: ZwlrVirtualPointerV1);
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
let [width, height, path] = args.as_slice() else {
|
||||||
|
eprintln!("usage: replay-touch WIDTH HEIGHT FILE");
|
||||||
|
std::process::exit(2);
|
||||||
|
};
|
||||||
|
let (width, height) = (parse(width, "WIDTH"), parse(height, "HEIGHT"));
|
||||||
|
let text = std::fs::read_to_string(path)
|
||||||
|
.unwrap_or_else(|e| fail(&format!("could not read {path}: {e}")));
|
||||||
|
let script = TouchScript::parse(&text).unwrap_or_else(|e| fail(&e));
|
||||||
|
|
||||||
|
let conn = Connection::connect_to_env().unwrap_or_else(|e| {
|
||||||
|
fail(&format!(
|
||||||
|
"no wayland display ({e}); is WAYLAND_DISPLAY set?"
|
||||||
|
))
|
||||||
|
});
|
||||||
|
let mut queue = conn.new_event_queue();
|
||||||
|
let qh = queue.handle();
|
||||||
|
let display = conn.display();
|
||||||
|
display.get_registry(&qh, ());
|
||||||
|
let mut globals = Globals::default();
|
||||||
|
queue
|
||||||
|
.roundtrip(&mut globals)
|
||||||
|
.unwrap_or_else(|e| fail(&format!("wayland roundtrip failed: {e}")));
|
||||||
|
|
||||||
|
let manager = globals.manager.as_ref().unwrap_or_else(|| {
|
||||||
|
fail(
|
||||||
|
"this compositor does not offer zwlr_virtual_pointer_manager_v1, so a pointer cannot \
|
||||||
|
be synthesised; sway and every wlroots compositor do",
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let pointer = manager.create_virtual_pointer(globals.seat.as_ref(), &qh, ());
|
||||||
|
|
||||||
|
// Put the pointer where the gesture starts and let the compositor
|
||||||
|
// settle before anything is pressed. Without this the press is
|
||||||
|
// dropped: sway has just learned about this pointer, and a button
|
||||||
|
// sent in the same breath as the motion that first puts it over a
|
||||||
|
// window arrives before there is a focused surface to send it to --
|
||||||
|
// winit sees `CursorEntered`, the moves and the *release*, never the
|
||||||
|
// press, so the gesture reads as a hover and nothing scrolls. Found
|
||||||
|
// by printing winit's own events; the settle is what fixed it.
|
||||||
|
if let Some(first) = script.samples.first() {
|
||||||
|
pointer.motion_absolute(0, first.pos.x as u32, first.pos.y as u32, width, height);
|
||||||
|
pointer.frame();
|
||||||
|
conn.flush()
|
||||||
|
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
|
||||||
|
std::thread::sleep(SETTLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut previous = 0;
|
||||||
|
for sample in &script.samples {
|
||||||
|
std::thread::sleep(Duration::from_millis(sample.t_ms - previous));
|
||||||
|
previous = sample.t_ms;
|
||||||
|
let t = sample.t_ms as u32;
|
||||||
|
pointer.motion_absolute(t, sample.pos.x as u32, sample.pos.y as u32, width, height);
|
||||||
|
pointer.frame();
|
||||||
|
// The button goes in a frame of its own, *after* the motion has
|
||||||
|
// been committed. Sent in the same frame as the motion that
|
||||||
|
// first puts the pointer over the window, sway drops it: the
|
||||||
|
// client sees `CursorEntered` and the moves but never a
|
||||||
|
// `MouseInput { state: Pressed }`, so the whole gesture reads as
|
||||||
|
// a hover and nothing scrolls. Found exactly that way, by
|
||||||
|
// printing winit's events.
|
||||||
|
let state = match sample.action {
|
||||||
|
TouchAction::Down => Some(ButtonState::Pressed),
|
||||||
|
TouchAction::Up => Some(ButtonState::Released),
|
||||||
|
TouchAction::Move => None,
|
||||||
|
};
|
||||||
|
if let Some(state) = state {
|
||||||
|
pointer.button(t, BTN_LEFT, state);
|
||||||
|
pointer.frame();
|
||||||
|
}
|
||||||
|
conn.flush()
|
||||||
|
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
|
||||||
|
}
|
||||||
|
pointer.destroy();
|
||||||
|
conn.flush().ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(text: &str, what: &str) -> u32 {
|
||||||
|
text.parse()
|
||||||
|
.unwrap_or_else(|_| fail(&format!("{what} is not a whole number: {text:?}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fail(message: &str) -> ! {
|
||||||
|
eprintln!("replay-touch: {message}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# The compositor `scripts/run-headless.sh` starts, so that an example has a
|
||||||
|
# surface where there is no display. Nothing here is meant to be looked at
|
||||||
|
# directly; `grim` is.
|
||||||
|
#
|
||||||
|
# No Xwayland: winit talks Wayland natively, so an X server is a second thing
|
||||||
|
# to go wrong for no gain.
|
||||||
|
xwayland disable
|
||||||
|
|
||||||
|
# The default output, overridden per run by `--mode`. Larger than the window
|
||||||
|
# an example opens, so nothing is scaled or clipped.
|
||||||
|
output HEADLESS-1 mode 1920x1200@60Hz
|
||||||
|
|
||||||
|
default_border none
|
||||||
|
focus_follows_mouse no
|
||||||
Executable
+187
@@ -0,0 +1,187 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Run an iris example on a machine with no display.
|
||||||
|
#
|
||||||
|
# ./scripts/run-headless.sh tabs
|
||||||
|
# ./scripts/run-headless.sh tabs --shot /tmp/tabs.png --seconds 4
|
||||||
|
# ./scripts/run-headless.sh tabs --replay taps.touch --shot /tmp/tabs.png
|
||||||
|
# ./scripts/run-headless.sh app --dir ../elsewhere --mode 1080x2424@120Hz
|
||||||
|
#
|
||||||
|
# `--dir DIR` names the workspace to build in, defaulting to this one, so a
|
||||||
|
# project that depends on iris can be run through the same rig. `--bin` runs a
|
||||||
|
# crate binary rather than an example, and takes its own argv from
|
||||||
|
# `$RUN_HEADLESS_ARGS`, word-split on purpose.
|
||||||
|
#
|
||||||
|
# `--mode` sets the output, for running something at a size other than a
|
||||||
|
# desktop's -- a phone's, say. Set every run rather than only when it changes:
|
||||||
|
# the compositor is reused between runs, so a default-shaped run after a
|
||||||
|
# custom one would otherwise inherit the other's output and quietly screenshot
|
||||||
|
# the wrong size.
|
||||||
|
#
|
||||||
|
# `--resize WxH@Hz` changes the output under the app once it is up, then
|
||||||
|
# screenshots. A resize is its own case: what it has to match is a cold start
|
||||||
|
# at that size, byte for byte, and nothing in `cargo test` can see it.
|
||||||
|
#
|
||||||
|
# `--replay FILE` drives a `.touch` recording into the window through
|
||||||
|
# `replay-touch`, which reads it with the same parser `iris::harness` uses. A
|
||||||
|
# recording is `<ms> down|move|up <x> <y>` in the output's own pixels. With
|
||||||
|
# `--shot` it also writes `<shot>-before.png` from just before the gesture,
|
||||||
|
# since "it moved" is a claim about two pictures.
|
||||||
|
#
|
||||||
|
# What it supplies is a compositor for winit to open a surface on: a headless
|
||||||
|
# sway, and `grim` to screenshot it. Sway gets its own socket and runtime
|
||||||
|
# directory rather than joining whatever else is running, because it tiles --
|
||||||
|
# adding a window to someone else's compositor resizes theirs.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
need() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || {
|
||||||
|
echo "run-headless: $1 is not installed ($2)" >&2
|
||||||
|
exit 127
|
||||||
|
}
|
||||||
|
}
|
||||||
|
need sway "the compositor an example opens its window on"
|
||||||
|
need swaymsg "sway's control socket"
|
||||||
|
|
||||||
|
scripts=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
root=$(cd "$scripts/.." && pwd)
|
||||||
|
workdir="$root"
|
||||||
|
cd "$root"
|
||||||
|
run="${XDG_RUNTIME_DIR:-/tmp}/iris-headless"
|
||||||
|
seconds=3
|
||||||
|
shot=""
|
||||||
|
replay=""
|
||||||
|
resize=""
|
||||||
|
example=""
|
||||||
|
kind=example
|
||||||
|
mode=1920x1200@60Hz
|
||||||
|
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--shot) shot=$2; shift 2 ;;
|
||||||
|
--seconds) seconds=$2; shift 2 ;;
|
||||||
|
--bin) kind=bin; shift ;;
|
||||||
|
--mode) mode=$2; shift 2 ;;
|
||||||
|
--resize) resize=$2; shift 2 ;;
|
||||||
|
--replay) replay=$2; shift 2 ;;
|
||||||
|
--dir) workdir=$(cd "$2" && pwd); shift 2 ;;
|
||||||
|
--) shift; break ;;
|
||||||
|
*) example=$1; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ -n "$example" ] || { echo "usage: $0 NAME [--bin] [--dir DIR] [--mode WxH@Hz] [--resize WxH@Hz] [--replay TOUCH] [--shot PNG] [--seconds N] [-- cargo args]" >&2; exit 2; }
|
||||||
|
[ -z "$replay" ] || [ -f "$replay" ] || { echo "run-headless: no touch script at $replay" >&2; exit 2; }
|
||||||
|
[ -z "$shot" ] || need grim "the screenshot --shot writes"
|
||||||
|
|
||||||
|
mkdir -p "$run"
|
||||||
|
export SWAYSOCK="$run/sway.sock"
|
||||||
|
|
||||||
|
# Named rather than left to sway's pid-based default, so a second run reuses
|
||||||
|
# this compositor instead of starting another beside it.
|
||||||
|
if ! swaymsg -t get_version >/dev/null 2>&1; then
|
||||||
|
rm -f "$SWAYSOCK"
|
||||||
|
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 LIBSEAT_BACKEND=noop \
|
||||||
|
setsid sway -c "$scripts/headless.conf" >"$run/sway.log" 2>&1 &
|
||||||
|
i=0
|
||||||
|
while [ $i -lt 20 ]; do
|
||||||
|
swaymsg -t get_version >/dev/null 2>&1 && break
|
||||||
|
i=$((i + 1)); sleep 0.5
|
||||||
|
done
|
||||||
|
swaymsg -t get_version >/dev/null 2>&1 || {
|
||||||
|
echo "run-headless: compositor did not start; see $run/sway.log" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$run/display"
|
||||||
|
swaymsg exec -- "sh -c 'printf %s \"\$WAYLAND_DISPLAY\" > $run/display'" >/dev/null
|
||||||
|
i=0
|
||||||
|
while [ $i -lt 20 ]; do
|
||||||
|
[ -s "$run/display" ] && break
|
||||||
|
i=$((i + 1)); sleep 0.5
|
||||||
|
done
|
||||||
|
[ -s "$run/display" ] || { echo "run-headless: could not read WAYLAND_DISPLAY" >&2; exit 1; }
|
||||||
|
WAYLAND_DISPLAY=$(cat "$run/display")
|
||||||
|
export WAYLAND_DISPLAY
|
||||||
|
|
||||||
|
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
|
||||||
|
|
||||||
|
# The extent `replay-touch` positions against, so a script's coordinates are
|
||||||
|
# the output's own pixels. Set beside every mode change, since a gesture
|
||||||
|
# scaled against a mode the output no longer has lands somewhere else and
|
||||||
|
# still looks like a run that worked.
|
||||||
|
set_mode() {
|
||||||
|
swaymsg output HEADLESS-1 mode "$1" >/dev/null
|
||||||
|
out_w=${1%x*}
|
||||||
|
out_h=${1#*x}; out_h=${out_h%@*}
|
||||||
|
}
|
||||||
|
set_mode "$mode"
|
||||||
|
|
||||||
|
# Built before the app starts, so a compile error is not reported as a
|
||||||
|
# window that failed to move.
|
||||||
|
[ -z "$replay" ] || (cd "$root" && cargo build --bin replay-touch -p rig-input) >&2
|
||||||
|
|
||||||
|
cd "$workdir"
|
||||||
|
if [ "$kind" = bin ]; then
|
||||||
|
cargo build --bin "$example" "$@" >&2
|
||||||
|
bin="$workdir/target/debug/$example"
|
||||||
|
else
|
||||||
|
cargo build --example "$example" "$@" >&2
|
||||||
|
bin="$workdir/target/debug/examples/$example"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Deliberately word-split: this is the binary's own argv, not a single path.
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
"$bin" ${RUN_HEADLESS_ARGS:-} >"$run/$example.log" 2>&1 &
|
||||||
|
pid=$!
|
||||||
|
trap 'kill "$pid" 2>/dev/null || true' EXIT INT TERM
|
||||||
|
|
||||||
|
# Wait for the window to be mapped rather than for a number of seconds. A
|
||||||
|
# fixed sleep took an all-black screenshot the first time this ran, when sway
|
||||||
|
# had started in the same invocation and had not composited its output yet --
|
||||||
|
# which is indistinguishable from an app that draws nothing.
|
||||||
|
i=0
|
||||||
|
while [ $i -lt 40 ]; do
|
||||||
|
kill -0 "$pid" 2>/dev/null || break
|
||||||
|
swaymsg -t get_tree --raw 2>/dev/null | grep -q "\"pid\":$pid," && break
|
||||||
|
i=$((i + 1)); sleep 0.25
|
||||||
|
done
|
||||||
|
|
||||||
|
i=0
|
||||||
|
while [ $i -lt "$((seconds * 2))" ]; do
|
||||||
|
kill -0 "$pid" 2>/dev/null || break
|
||||||
|
i=$((i + 1)); sleep 0.5
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then
|
||||||
|
set_mode "$resize"
|
||||||
|
echo "run-headless: resized to $resize" >&2
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$replay" ] && kill -0 "$pid" 2>/dev/null; then
|
||||||
|
if [ -n "$shot" ]; then
|
||||||
|
grim "${shot%.png}-before.png"
|
||||||
|
echo "run-headless: wrote ${shot%.png}-before.png (before the gesture)" >&2
|
||||||
|
fi
|
||||||
|
"$root/target/debug/replay-touch" "$out_w" "$out_h" "$replay"
|
||||||
|
# A fling outlives the finger: the gesture's own last sample is not
|
||||||
|
# when the list stops. Long enough for Android's spline to settle
|
||||||
|
# (`FlingCalculator::duration` tops out around a second and a half).
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
|
[ -n "$shot" ] && grim "$shot" && echo "run-headless: wrote $shot" >&2
|
||||||
|
kill "$pid" 2>/dev/null || true
|
||||||
|
wait "$pid" 2>/dev/null || true
|
||||||
|
status=0
|
||||||
|
else
|
||||||
|
wait "$pid" 2>/dev/null || status=$?
|
||||||
|
echo "run-headless: $example exited early (status ${status:-0})" >&2
|
||||||
|
status=${status:-1}
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "--- $example output ---" >&2
|
||||||
|
cat "$run/$example.log" >&2
|
||||||
|
exit "$status"
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 191 B |
+7
-4
@@ -15,8 +15,11 @@ where
|
|||||||
let region = ctx.data.render.window_region(&id).unwrap();
|
let region = ctx.data.render.window_region(&id).unwrap();
|
||||||
let id_pos = region.top_left;
|
let id_pos = region.top_left;
|
||||||
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
|
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
|
||||||
let pos = ctx.data.pos + container_pos - id_pos;
|
// The two regions are on the grid and the pointer is not, so the
|
||||||
let size = region.size();
|
// step between them is taken there and the pointer keeps the
|
||||||
|
// precision the platform gave it.
|
||||||
|
let pos = ctx.data.pos + (container_pos - id_pos).to_f32();
|
||||||
|
let size = region.size().to_f32();
|
||||||
select(
|
select(
|
||||||
rsc,
|
rsc,
|
||||||
ctx.data.render,
|
ctx.data.render,
|
||||||
@@ -70,8 +73,8 @@ fn select(
|
|||||||
if let Some(region) = render.window_region(&id) {
|
if let Some(region) = render.window_region(&id) {
|
||||||
state.window.set_ime_allowed(true);
|
state.window.set_ime_allowed(true);
|
||||||
state.window.set_ime_cursor_area(
|
state.window.set_ime_cursor_area(
|
||||||
LogicalPosition::<f32>::from(region.top_left.tuple()),
|
LogicalPosition::<f32>::from(region.top_left.to_f32().tuple()),
|
||||||
LogicalSize::<f32>::from(region.size().tuple()),
|
LogicalSize::<f32>::from(region.size().to_f32().tuple()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
state.focus = Some(id);
|
state.focus = Some(id);
|
||||||
|
|||||||
+58
-45
@@ -1,10 +1,6 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use arboard::Clipboard;
|
use arboard::Clipboard;
|
||||||
use std::{
|
use std::{marker::PhantomData, sync::Arc, time::Instant};
|
||||||
marker::{PhantomData, Sized},
|
|
||||||
sync::Arc,
|
|
||||||
time::Instant,
|
|
||||||
};
|
|
||||||
use winit::{
|
use winit::{
|
||||||
event::{Ime, WindowEvent},
|
event::{Ime, WindowEvent},
|
||||||
event_loop::{ActiveEventLoop, EventLoopProxy},
|
event_loop::{ActiveEventLoop, EventLoopProxy},
|
||||||
@@ -29,7 +25,35 @@ pub use sense::*;
|
|||||||
pub use state::*;
|
pub use state::*;
|
||||||
pub use task::*;
|
pub use task::*;
|
||||||
|
|
||||||
pub type Proxy<Event> = EventLoopProxy<Event>;
|
/// Sends an application's own events to its event loop. It wraps the proxy
|
||||||
|
/// rather than being one because task updates travel the same way: what an
|
||||||
|
/// application sends is its `Event`, not the loop's whole message type.
|
||||||
|
pub struct Proxy<State: DefaultAppState>(EventLoopProxy<DefaultEvent<State>>);
|
||||||
|
|
||||||
|
impl<State: DefaultAppState> Clone for Proxy<State> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: DefaultAppState> Proxy<State> {
|
||||||
|
pub fn send_event(&self, event: State::Event) {
|
||||||
|
let _ = self.0.send_event(DefaultEvent::User(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the event loop carries: the application's own events, and the
|
||||||
|
/// updates tasks send back to the ui thread.
|
||||||
|
pub enum DefaultEvent<State: DefaultAppState> {
|
||||||
|
User(State::Event),
|
||||||
|
Update(Box<dyn TaskUpdate<DefaultRsc<State>>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: DefaultAppState> TaskQueue<DefaultRsc<State>> for Proxy<State> {
|
||||||
|
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<State>>>) {
|
||||||
|
let _ = self.0.send_event(DefaultEvent::Update(update));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct DefaultUiState {
|
pub struct DefaultUiState {
|
||||||
pub root: Option<StrongWidget>,
|
pub root: Option<StrongWidget>,
|
||||||
@@ -70,9 +94,8 @@ pub trait HasDefaultUiState: Sized + 'static {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait DefaultAppState: HasDefaultUiState {
|
pub trait DefaultAppState: HasDefaultUiState {
|
||||||
type Event = ();
|
type Event: Send = ();
|
||||||
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>)
|
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self>) -> Self;
|
||||||
-> Self;
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn event(
|
fn event(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -105,18 +128,14 @@ pub struct DefaultRsc<State: 'static> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<State> DefaultRsc<State> {
|
impl<State> DefaultRsc<State> {
|
||||||
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) {
|
pub fn init(queue: Arc<dyn TaskQueue<Self>>) -> Self {
|
||||||
let (tasks, recv) = Tasks::init(window);
|
|
||||||
(
|
|
||||||
Self {
|
Self {
|
||||||
ui: Default::default(),
|
ui: Default::default(),
|
||||||
events: Default::default(),
|
events: Default::default(),
|
||||||
tasks,
|
tasks: Tasks::init(queue),
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
_state: Default::default(),
|
_state: Default::default(),
|
||||||
},
|
}
|
||||||
recv,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
|
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
|
||||||
@@ -181,43 +200,32 @@ pub struct DefaultApp<State: DefaultAppState> {
|
|||||||
rsc: DefaultRsc<State>,
|
rsc: DefaultRsc<State>,
|
||||||
render: UiRenderState,
|
render: UiRenderState,
|
||||||
state: State,
|
state: State,
|
||||||
task_recv: TaskMsgReceiver<DefaultRsc<State>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||||
type Event = State::Event;
|
type Event = DefaultEvent<State>;
|
||||||
|
|
||||||
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
|
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
|
||||||
let window = event_loop
|
let window = event_loop
|
||||||
.create_window(State::window_attributes())
|
.create_window(State::window_attributes())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let default_state = DefaultUiState::new(window);
|
let default_state = DefaultUiState::new(window);
|
||||||
let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone());
|
let mut rsc = DefaultRsc::init(Arc::new(Proxy(proxy.clone())));
|
||||||
let state = State::new(default_state, &mut rsc, proxy);
|
let state = State::new(default_state, &mut rsc, Proxy(proxy));
|
||||||
let render = UiRenderState::new();
|
let render = UiRenderState::new();
|
||||||
Self {
|
Self { rsc, state, render }
|
||||||
rsc,
|
|
||||||
state,
|
|
||||||
render,
|
|
||||||
task_recv,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
|
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
|
||||||
self.state.event(event, &mut self.rsc, &mut self.render);
|
match event {
|
||||||
|
DefaultEvent::User(event) => self.state.event(event, &mut self.rsc, &mut self.render),
|
||||||
|
DefaultEvent::Update(update) => update(&mut self.state, &mut self.rsc),
|
||||||
|
}
|
||||||
|
self.request_redraw_if_needed();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
|
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
|
||||||
let Self {
|
let Self { rsc, render, state } = self;
|
||||||
rsc,
|
|
||||||
render,
|
|
||||||
state,
|
|
||||||
task_recv,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
for update in task_recv.try_iter() {
|
|
||||||
update(state, rsc);
|
|
||||||
}
|
|
||||||
|
|
||||||
let ui_state = state.default_state_mut();
|
let ui_state = state.default_state_mut();
|
||||||
let input_changed = ui_state.input.event(&event);
|
let input_changed = ui_state.input.event(&event);
|
||||||
@@ -227,8 +235,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
ui_state.focus = None;
|
ui_state.focus = None;
|
||||||
}
|
}
|
||||||
if input_changed {
|
if input_changed {
|
||||||
let window_size = ui_state.window_size();
|
render.run_sensors(rsc, state, cursor_state);
|
||||||
render.run_sensors(rsc, state, cursor_state, window_size);
|
|
||||||
}
|
}
|
||||||
let ui_state = state.default_state_mut();
|
let ui_state = state.default_state_mut();
|
||||||
if old != ui_state.focus
|
if old != ui_state.focus
|
||||||
@@ -244,7 +251,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
ui_state.renderer.draw();
|
ui_state.renderer.draw();
|
||||||
}
|
}
|
||||||
WindowEvent::Resized(size) => {
|
WindowEvent::Resized(size) => {
|
||||||
render.resize((size.width, size.height));
|
render.resize((size.width, size.height), rsc.widgets_mut());
|
||||||
ui_state.renderer.resize(size)
|
ui_state.renderer.resize(size)
|
||||||
}
|
}
|
||||||
WindowEvent::KeyboardInput { event, .. } => {
|
WindowEvent::KeyboardInput { event, .. } => {
|
||||||
@@ -297,11 +304,8 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
state.window_event(event, rsc, render);
|
state.window_event(event, rsc, render);
|
||||||
let ui_state = self.state.default_state_mut();
|
self.request_redraw_if_needed();
|
||||||
if render.needs_redraw(&ui_state.root, rsc.widgets()) {
|
self.state.default_state_mut().input.end_frame();
|
||||||
ui_state.renderer.window().request_redraw();
|
|
||||||
}
|
|
||||||
ui_state.input.end_frame();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit(&mut self) {
|
fn exit(&mut self) {
|
||||||
@@ -309,6 +313,15 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<State: DefaultAppState> DefaultApp<State> {
|
||||||
|
fn request_redraw_if_needed(&mut self) {
|
||||||
|
let ui_state = self.state.default_state_mut();
|
||||||
|
if self.render.needs_redraw(&ui_state.root, self.rsc.widgets()) {
|
||||||
|
ui_state.renderer.window().request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait RscIdx<Rsc> {
|
pub trait RscIdx<Rsc> {
|
||||||
type Output;
|
type Output;
|
||||||
fn get(self, rsc: &Rsc) -> &Self::Output;
|
fn get(self, rsc: &Rsc) -> &Self::Output;
|
||||||
|
|||||||
+23
-4
@@ -22,7 +22,20 @@ impl UiRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(&mut self) {
|
pub fn draw(&mut self) {
|
||||||
let output = self.surface.get_current_texture().unwrap();
|
let (output, suboptimal) = match self.surface.get_current_texture() {
|
||||||
|
CurrentSurfaceTexture::Success(texture) => (texture, false),
|
||||||
|
// Used for this frame, and the swapchain rebuilt after it has
|
||||||
|
// been presented: configuring the surface while a texture it
|
||||||
|
// handed out is still alive panics.
|
||||||
|
CurrentSurfaceTexture::Suboptimal(texture) => (texture, true),
|
||||||
|
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
|
||||||
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CurrentSurfaceTexture::Timeout
|
||||||
|
| CurrentSurfaceTexture::Occluded
|
||||||
|
| CurrentSurfaceTexture::Validation => return,
|
||||||
|
};
|
||||||
let view = output
|
let view = output
|
||||||
.texture
|
.texture
|
||||||
.create_view(&TextureViewDescriptor::default());
|
.create_view(&TextureViewDescriptor::default());
|
||||||
@@ -46,7 +59,10 @@ 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();
|
||||||
output.present();
|
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>) {
|
||||||
@@ -65,9 +81,10 @@ impl UiRenderer {
|
|||||||
pub fn new(window: Arc<Window>) -> Self {
|
pub fn new(window: Arc<Window>) -> Self {
|
||||||
let size = window.inner_size();
|
let size = window.inner_size();
|
||||||
|
|
||||||
let instance = Instance::new(&InstanceDescriptor {
|
let instance = Instance::new(InstanceDescriptor {
|
||||||
backends: Backends::PRIMARY,
|
backends: Backends::PRIMARY,
|
||||||
..Default::default()
|
display: Some(Box::new(window.clone())),
|
||||||
|
..InstanceDescriptor::new_without_display_handle()
|
||||||
});
|
});
|
||||||
|
|
||||||
let surface = instance
|
let surface = instance
|
||||||
@@ -79,6 +96,7 @@ impl UiRenderer {
|
|||||||
power_preference: PowerPreference::default(),
|
power_preference: PowerPreference::default(),
|
||||||
compatible_surface: Some(&surface),
|
compatible_surface: Some(&surface),
|
||||||
force_fallback_adapter: false,
|
force_fallback_adapter: false,
|
||||||
|
apply_limit_buckets: false,
|
||||||
})
|
})
|
||||||
.block_on()
|
.block_on()
|
||||||
.expect("Could not get adapter!");
|
.expect("Could not get adapter!");
|
||||||
@@ -105,6 +123,7 @@ impl UiRenderer {
|
|||||||
let config = SurfaceConfiguration {
|
let config = SurfaceConfiguration {
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||||
format: surface_format,
|
format: surface_format,
|
||||||
|
color_space: SurfaceColorSpace::Auto,
|
||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height,
|
height: size.height,
|
||||||
present_mode: PresentMode::AutoVsync,
|
present_mode: PresentMode::AutoVsync,
|
||||||
|
|||||||
+103
-35
@@ -4,14 +4,14 @@ use std::{
|
|||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum CursorButton {
|
pub enum CursorButton {
|
||||||
Left,
|
Left,
|
||||||
Right,
|
Right,
|
||||||
Middle,
|
Middle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum CursorSense {
|
pub enum CursorSense {
|
||||||
PressStart(CursorButton),
|
PressStart(CursorButton),
|
||||||
Pressing(CursorButton),
|
Pressing(CursorButton),
|
||||||
@@ -27,7 +27,7 @@ pub struct CursorSenses(Vec<CursorSense>);
|
|||||||
|
|
||||||
impl Event for CursorSenses {
|
impl Event for CursorSenses {
|
||||||
type Data<'a> = CursorData<'a>;
|
type Data<'a> = CursorData<'a>;
|
||||||
type State = SensorState;
|
type Global = Hovered;
|
||||||
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
||||||
if let Some(sense) = should_run(self, &data.cursor, data.hover) {
|
if let Some(sense) = should_run(self, &data.cursor, data.hover) {
|
||||||
let mut data = data.clone();
|
let mut data = data.clone();
|
||||||
@@ -37,6 +37,24 @@ impl Event for CursorSenses {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A press or a scroll is used up by whatever answered it, so it stops
|
||||||
|
/// there. Hovering is not: a cursor resting somewhere goes on resting.
|
||||||
|
fn consumes(&self, data: &Self::Data<'_>) -> bool {
|
||||||
|
!data.sense.position_only()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Who the cursor was inside, before and after an input. The difference is
|
||||||
|
/// whose hover has ended -- including a widget a higher layer has covered,
|
||||||
|
/// which the walk stops before reaching.
|
||||||
|
///
|
||||||
|
/// Two buffers that swap rather than one rebuilt, so an input allocates
|
||||||
|
/// nothing once they have grown.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Hovered {
|
||||||
|
was: Vec<WidgetId>,
|
||||||
|
now: Vec<WidgetId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CursorSense {
|
impl CursorSense {
|
||||||
@@ -52,6 +70,12 @@ impl CursorSense {
|
|||||||
pub fn is_dragging(&self) -> bool {
|
pub fn is_dragging(&self) -> bool {
|
||||||
matches!(self, CursorSense::Pressing(CursorButton::Left))
|
matches!(self, CursorSense::Pressing(CursorButton::Left))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// False if the sense is a button or a scroll, true if it is only about
|
||||||
|
/// where the cursor is.
|
||||||
|
fn position_only(&self) -> bool {
|
||||||
|
matches!(self, Self::HoverStart | Self::Hovering | Self::HoverEnd)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
@@ -96,6 +120,12 @@ impl CursorButtons {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CursorState {
|
impl CursorState {
|
||||||
|
/// True if the cursor is only reporting where it is: no button and no
|
||||||
|
/// scroll this frame.
|
||||||
|
pub fn position_only(&self) -> bool {
|
||||||
|
self.scroll_delta == Vec2::ZERO && self.buttons.iter().all(|(_, state)| state.is_off())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn end_frame(&mut self) {
|
pub fn end_frame(&mut self) {
|
||||||
self.buttons.end_frame();
|
self.buttons.end_frame();
|
||||||
self.scroll_delta = Vec2::ZERO;
|
self.scroll_delta = Vec2::ZERO;
|
||||||
@@ -123,11 +153,6 @@ pub struct Sensor<Ctx: HasEvents, Data> {
|
|||||||
|
|
||||||
pub type SenseShape = UiRegion;
|
pub type SenseShape = UiRegion;
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct SensorState {
|
|
||||||
pub hover: ActivationState,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CursorData<'a> {
|
pub struct CursorData<'a> {
|
||||||
/// where this widget was hit
|
/// where this widget was hit
|
||||||
@@ -147,7 +172,6 @@ pub trait SensorUi {
|
|||||||
rsc: &mut Rsc,
|
rsc: &mut Rsc,
|
||||||
state: &mut Rsc::State,
|
state: &mut Rsc::State,
|
||||||
cursor: CursorState,
|
cursor: CursorState,
|
||||||
window_size: Vec2,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,47 +181,86 @@ impl SensorUi for UiRenderState {
|
|||||||
rsc: &mut Rsc,
|
rsc: &mut Rsc,
|
||||||
state: &mut Rsc::State,
|
state: &mut Rsc::State,
|
||||||
cursor: CursorState,
|
cursor: CursorState,
|
||||||
window_size: Vec2,
|
|
||||||
) {
|
) {
|
||||||
// in order to remove this take, need to store active list in UiRenderState somehow
|
// in order to remove this take, need to store active list in UiRenderState somehow
|
||||||
// this would probably be done through a generic parameter that adds yet another rsc /
|
// this would probably be done through a generic parameter that adds yet another rsc /
|
||||||
// state like thing, but local to render state, and is passed to UiRsc events so you can
|
// state like thing, but local to render state, and is passed to UiRsc events so you can
|
||||||
// update it there?
|
// update it there?
|
||||||
let mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
|
let active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
|
||||||
|
let mut hovered = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().global);
|
||||||
|
hovered.now.clear();
|
||||||
|
let position_only = cursor.position_only();
|
||||||
|
let region_of = |id| self.window_region(&id);
|
||||||
|
|
||||||
for layer in self.layers.indices().rev() {
|
for layer in self.layers.indices().rev() {
|
||||||
let mut sensed = false;
|
let mut consumed = false;
|
||||||
for (id, sensor) in active.get_mut(&layer).into_flat_iter() {
|
for id in active.get(&layer).into_flat_iter().map(|(id, _)| *id) {
|
||||||
let shape = self.active.get(id).unwrap().region;
|
let Some(region) = region_of(id) else {
|
||||||
let region = shape.to_px(window_size);
|
continue;
|
||||||
let in_shape = cursor.exists && region.contains(cursor.pos);
|
};
|
||||||
sensor.hover.update(in_shape);
|
if !cursor.exists || !region.contains(PxVec2::from_f32(cursor.pos)) {
|
||||||
if sensor.hover == ActivationState::Off {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
sensed = true;
|
hovered.now.push(id);
|
||||||
|
let hover = match hovered.was.contains(&id) {
|
||||||
let cursor = cursor.clone();
|
true => ActivationState::On,
|
||||||
|
false => ActivationState::Start,
|
||||||
let data = CursorData {
|
|
||||||
pos: cursor.pos - region.top_left,
|
|
||||||
size: region.bot_right - region.top_left,
|
|
||||||
scroll_delta: cursor.scroll_delta,
|
|
||||||
hover: sensor.hover,
|
|
||||||
cursor,
|
|
||||||
// this does not have any meaning;
|
|
||||||
// might wanna set up Event to have a prepare stage
|
|
||||||
sense: CursorSense::Hovering,
|
|
||||||
render: self,
|
|
||||||
};
|
};
|
||||||
rsc.run_event::<CursorSense>(*id, data, state);
|
// A press or a scroll stops where something answered it, so a
|
||||||
|
// button over a list does not swallow the list's scrolling.
|
||||||
|
consumed |= deliver(self, rsc, state, id, hover, &cursor, region);
|
||||||
|
// A cursor doing neither stops at whatever it is over, so
|
||||||
|
// hovering does not reach through.
|
||||||
|
consumed |= position_only;
|
||||||
}
|
}
|
||||||
if sensed {
|
// Applied after the layer, never during it: senses on one layer do
|
||||||
|
// not block each other.
|
||||||
|
if consumed {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rsc.events_mut().get_type::<CursorSense>().active = active;
|
|
||||||
|
// Whatever the cursor was inside and is not now, whether it left or a
|
||||||
|
// layer above took the input before the walk reached it. A widget that
|
||||||
|
// stopped being drawn has no region to report and is simply dropped.
|
||||||
|
for &id in &hovered.was {
|
||||||
|
if !hovered.now.contains(&id)
|
||||||
|
&& let Some(region) = region_of(id)
|
||||||
|
{
|
||||||
|
deliver(self, rsc, state, id, ActivationState::End, &cursor, region);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
std::mem::swap(&mut hovered.was, &mut hovered.now);
|
||||||
|
|
||||||
|
let senses = rsc.events_mut().get_type::<CursorSense>();
|
||||||
|
senses.active = active;
|
||||||
|
senses.global = hovered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs one widget's cursor senses, and says whether they used up the input.
|
||||||
|
fn deliver<Rsc: HasEvents>(
|
||||||
|
render: &UiRenderState,
|
||||||
|
rsc: &mut Rsc,
|
||||||
|
state: &mut Rsc::State,
|
||||||
|
id: WidgetId,
|
||||||
|
hover: ActivationState,
|
||||||
|
cursor: &CursorState,
|
||||||
|
region: PixelRegion,
|
||||||
|
) -> bool {
|
||||||
|
let data = CursorData {
|
||||||
|
pos: cursor.pos - region.top_left.to_f32(),
|
||||||
|
size: region.size().to_f32(),
|
||||||
|
scroll_delta: cursor.scroll_delta,
|
||||||
|
hover,
|
||||||
|
cursor: cursor.clone(),
|
||||||
|
// this does not have any meaning;
|
||||||
|
// might wanna set up Event to have a prepare stage
|
||||||
|
sense: CursorSense::Hovering,
|
||||||
|
render,
|
||||||
|
};
|
||||||
|
rsc.run_event::<CursorSense>(id, data, state)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn should_run(
|
pub fn should_run(
|
||||||
senses: &CursorSenses,
|
senses: &CursorSenses,
|
||||||
@@ -205,6 +268,11 @@ pub fn should_run(
|
|||||||
hover: ActivationState,
|
hover: ActivationState,
|
||||||
) -> Option<CursorSense> {
|
) -> Option<CursorSense> {
|
||||||
for sense in senses.iter() {
|
for sense in senses.iter() {
|
||||||
|
// A widget the cursor is no longer inside senses only its position:
|
||||||
|
// the press that ended its hover landed on something else.
|
||||||
|
if !hover.is_on() && !sense.position_only() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if match sense {
|
if match sense {
|
||||||
CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(),
|
CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(),
|
||||||
CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(),
|
CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(),
|
||||||
|
|||||||
+16
-34
@@ -1,11 +1,5 @@
|
|||||||
use iris_core::HasState;
|
use iris_core::HasState;
|
||||||
use std::{
|
use std::{pin::Pin, sync::Arc};
|
||||||
pin::Pin,
|
|
||||||
sync::{
|
|
||||||
Arc,
|
|
||||||
mpsc::{Receiver as SyncReceiver, Sender as SyncSender, channel as sync_channel},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
runtime::Runtime,
|
runtime::Runtime,
|
||||||
sync::mpsc::{
|
sync::mpsc::{
|
||||||
@@ -13,64 +7,52 @@ use tokio::{
|
|||||||
unbounded_channel as async_channel,
|
unbounded_channel as async_channel,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use winit::window::Window;
|
|
||||||
|
|
||||||
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
|
|
||||||
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
|
|
||||||
|
|
||||||
pub trait TaskUpdate<Rsc: HasState>: FnOnce(&mut Rsc::State, &mut Rsc) + Send {}
|
pub trait TaskUpdate<Rsc: HasState>: FnOnce(&mut Rsc::State, &mut Rsc) + Send {}
|
||||||
impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {}
|
impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {}
|
||||||
|
|
||||||
|
/// Hands an update from a task to the thread that owns the ui. Delivery and
|
||||||
|
/// waking are one act: a host posts the update as a message its loop already
|
||||||
|
/// carries, so nothing has to wake the loop separately, or claim a redraw to
|
||||||
|
/// be looked at.
|
||||||
|
pub trait TaskQueue<Rsc: HasState>: Send + Sync + 'static {
|
||||||
|
fn send(&self, update: Box<dyn TaskUpdate<Rsc>>);
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Tasks<Rsc: HasState> {
|
pub struct Tasks<Rsc: HasState> {
|
||||||
start: AsyncSender<BoxTask>,
|
start: AsyncSender<BoxTask>,
|
||||||
window: Arc<Window>,
|
queue: Arc<dyn TaskQueue<Rsc>>,
|
||||||
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TaskCtx<Rsc: HasState> {
|
pub struct TaskCtx<Rsc: HasState> {
|
||||||
send: TaskMsgSender<Rsc>,
|
queue: Arc<dyn TaskQueue<Rsc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Rsc: HasState> TaskCtx<Rsc> {
|
impl<Rsc: HasState> TaskCtx<Rsc> {
|
||||||
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
|
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
|
||||||
let _ = self.send.send(Box::new(f));
|
self.queue.send(Box::new(f));
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
|
|
||||||
fn new(send: TaskMsgSender<Rsc>) -> Self {
|
|
||||||
Self { send }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
|
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
|
||||||
|
|
||||||
impl<Rsc: HasState> Tasks<Rsc> {
|
impl<Rsc: HasState> Tasks<Rsc> {
|
||||||
pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) {
|
pub fn init(queue: Arc<dyn TaskQueue<Rsc>>) -> Self {
|
||||||
let (start, start_recv) = async_channel();
|
let (start, start_recv) = async_channel();
|
||||||
let (msgs, msgs_recv) = sync_channel();
|
|
||||||
std::thread::spawn(|| {
|
std::thread::spawn(|| {
|
||||||
let rt = Runtime::new().unwrap();
|
let rt = Runtime::new().unwrap();
|
||||||
rt.block_on(listen(start_recv))
|
rt.block_on(listen(start_recv))
|
||||||
});
|
});
|
||||||
(
|
Self { start, queue }
|
||||||
Self {
|
|
||||||
start,
|
|
||||||
msg_send: msgs,
|
|
||||||
window,
|
|
||||||
},
|
|
||||||
msgs_recv,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
|
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
|
||||||
where
|
where
|
||||||
F::CallOnceFuture: Send,
|
F::CallOnceFuture: Send,
|
||||||
{
|
{
|
||||||
let send = self.msg_send.clone();
|
let queue = self.queue.clone();
|
||||||
let window = self.window.clone();
|
|
||||||
let _ = self.start.send(Box::pin(async move {
|
let _ = self.start.send(Box::pin(async move {
|
||||||
task(TaskCtx::new(send)).await;
|
task(TaskCtx { queue }).await;
|
||||||
window.request_redraw();
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+333
@@ -0,0 +1,333 @@
|
|||||||
|
//! A ui with no window: build a tree, run frames, move a pointer, and read
|
||||||
|
//! back where widgets landed.
|
||||||
|
//!
|
||||||
|
//! It does not draw. A claim about pixels still needs a real surface.
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
use std::{
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
mpsc::{Receiver, SyncSender, sync_channel},
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// There is no loop here to post to, so updates queue until the test asks
|
||||||
|
/// for them.
|
||||||
|
struct Queue(SyncSender<Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>>);
|
||||||
|
|
||||||
|
impl TaskQueue<DefaultRsc<HarnessState>> for Queue {
|
||||||
|
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>) {
|
||||||
|
let _ = self.0.send(update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `assert_eq!` for where a frame put a widget, written as its two corners.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! assert_corners {
|
||||||
|
($harness:expr, $id:expr, ($x0:expr, $y0:expr), ($x1:expr, $y1:expr)) => {
|
||||||
|
assert_eq!(
|
||||||
|
$harness.region(&$id).expect("widget drew nothing"),
|
||||||
|
$crate::core::PixelRegion {
|
||||||
|
top_left: $crate::core::PxVec2::new(
|
||||||
|
$crate::core::Px::from_f32($x0 as f32),
|
||||||
|
$crate::core::Px::from_f32($y0 as f32),
|
||||||
|
),
|
||||||
|
bot_right: $crate::core::PxVec2::new(
|
||||||
|
$crate::core::Px::from_f32($x1 as f32),
|
||||||
|
$crate::core::Px::from_f32($y1 as f32),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pub use crate::assert_corners;
|
||||||
|
|
||||||
|
/// One replayed pointer sample, cut down to what a window delivers: where the
|
||||||
|
/// pointer is, and whether the button changed.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum TouchAction {
|
||||||
|
Down,
|
||||||
|
Move,
|
||||||
|
Up,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TouchAction {
|
||||||
|
fn parse(word: &str) -> Option<Self> {
|
||||||
|
match word {
|
||||||
|
"down" => Some(Self::Down),
|
||||||
|
"move" => Some(Self::Move),
|
||||||
|
"up" => Some(Self::Up),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct TouchSample {
|
||||||
|
pub t_ms: u64,
|
||||||
|
pub action: TouchAction,
|
||||||
|
pub pos: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A recorded gesture, in the output's own pixels: `<ms> down|move|up <x> <y>`
|
||||||
|
/// a line, `#` to end of line ignored.
|
||||||
|
///
|
||||||
|
/// One parser for both ways of replaying a recording -- into a harness, and
|
||||||
|
/// into a real window -- so the two cannot read the same file differently.
|
||||||
|
pub struct TouchScript {
|
||||||
|
pub samples: Vec<TouchSample>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TouchScript {
|
||||||
|
pub fn parse(text: &str) -> Result<Self, String> {
|
||||||
|
let mut samples: Vec<TouchSample> = Vec::new();
|
||||||
|
for (i, line) in text.lines().enumerate() {
|
||||||
|
let line = line.split('#').next().unwrap_or("").trim();
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let at = |what: &str| format!("touch script line {}: {what}: {line:?}", i + 1);
|
||||||
|
let mut words = line.split_whitespace();
|
||||||
|
let (Some(t), Some(action), Some(x), Some(y), None) = (
|
||||||
|
words.next(),
|
||||||
|
words.next(),
|
||||||
|
words.next(),
|
||||||
|
words.next(),
|
||||||
|
words.next(),
|
||||||
|
) else {
|
||||||
|
return Err(at("expected `t_ms action x y`"));
|
||||||
|
};
|
||||||
|
let t_ms: u64 = t.parse().map_err(|_| at("t_ms is not a whole number"))?;
|
||||||
|
let action =
|
||||||
|
TouchAction::parse(action).ok_or_else(|| at("action is not down/move/up"))?;
|
||||||
|
let x: f32 = x.parse().map_err(|_| at("x is not a number"))?;
|
||||||
|
let y: f32 = y.parse().map_err(|_| at("y is not a number"))?;
|
||||||
|
if let Some(last) = samples.last()
|
||||||
|
&& t_ms < last.t_ms
|
||||||
|
{
|
||||||
|
return Err(at("samples must be in time order"));
|
||||||
|
}
|
||||||
|
samples.push(TouchSample {
|
||||||
|
t_ms,
|
||||||
|
action,
|
||||||
|
pos: Vec2::new(x, y),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Self { samples })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct HarnessState {
|
||||||
|
pub root: Option<StrongWidget>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasRoot for HarnessState {
|
||||||
|
fn set_root(&mut self, root: StrongWidget) {
|
||||||
|
self.root = Some(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Harness {
|
||||||
|
pub rsc: DefaultRsc<HarnessState>,
|
||||||
|
pub render: UiRenderState,
|
||||||
|
pub state: HarnessState,
|
||||||
|
updates: Receiver<Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>>,
|
||||||
|
cursor: CursorState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Harness {
|
||||||
|
/// `size` is the output in physical pixels.
|
||||||
|
pub fn new(size: impl Into<Vec2>) -> Self {
|
||||||
|
// A `TaskQueue` must be `Sync`, which `mpsc::Sender` is not; the
|
||||||
|
// bound that comes with `SyncSender` is far past anything a test
|
||||||
|
// leaves unread.
|
||||||
|
let (send, updates) = sync_channel(1024);
|
||||||
|
let mut rsc = DefaultRsc::init(Arc::new(Queue(send)));
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
render.resize(size, rsc.widgets_mut());
|
||||||
|
Self {
|
||||||
|
rsc,
|
||||||
|
render,
|
||||||
|
state: HarnessState::default(),
|
||||||
|
updates,
|
||||||
|
cursor: CursorState::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> Vec2 {
|
||||||
|
self.render.output_size().to_f32()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||||
|
self.render.resize(size, self.rsc.widgets_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes a length rule after the fact, the way `.width()` sets one.
|
||||||
|
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<LayoutLen>) {
|
||||||
|
self.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(id, axis, SizeRule::Exact(len.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the root and lays it out, so a pointer event has something to hit.
|
||||||
|
pub fn set_root<T>(&mut self, widget: impl WidgetLike<DefaultRsc<HarnessState>, T>) {
|
||||||
|
widget.set_root(&mut self.rsc, &mut self.state);
|
||||||
|
self.frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn needs_redraw(&self) -> bool {
|
||||||
|
self.render
|
||||||
|
.needs_redraw(&self.state.root, self.rsc.widgets())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_updates(&mut self) -> usize {
|
||||||
|
let mut applied = 0;
|
||||||
|
while let Ok(update) = self.updates.try_recv() {
|
||||||
|
update(&mut self.state, &mut self.rsc);
|
||||||
|
applied += 1;
|
||||||
|
}
|
||||||
|
applied
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for a task's first update, then applies everything waiting.
|
||||||
|
/// False if none arrived in time.
|
||||||
|
#[must_use]
|
||||||
|
pub fn await_update(&mut self, timeout: Duration) -> bool {
|
||||||
|
let Ok(update) = self.updates.recv_timeout(timeout) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
update(&mut self.state, &mut self.rsc);
|
||||||
|
self.apply_updates();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays the tree out and builds its primitives.
|
||||||
|
pub fn frame(&mut self) {
|
||||||
|
self.apply_updates();
|
||||||
|
self.render.update(&self.state.root, &mut self.rsc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the last frame put a widget, or `None` if it drew nothing.
|
||||||
|
pub fn region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
||||||
|
self.render.window_region(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_to(&mut self, pos: impl Into<Vec2>) {
|
||||||
|
self.cursor.pos = pos.into();
|
||||||
|
self.cursor.exists = true;
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leave(&mut self) {
|
||||||
|
self.cursor.exists = false;
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn press(&mut self, button: CursorButton) {
|
||||||
|
self.button(button).update(true);
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release(&mut self, button: CursorButton) {
|
||||||
|
self.button(button).update(false);
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wheel carries no position, so this goes wherever the cursor was last
|
||||||
|
/// moved to -- nowhere, until it has been moved.
|
||||||
|
pub fn scroll(&mut self, delta: impl Into<Vec2>) {
|
||||||
|
self.cursor.scroll_delta = delta.into();
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn click(&mut self, pos: impl Into<Vec2>) {
|
||||||
|
self.move_to(pos);
|
||||||
|
self.press(CursorButton::Left);
|
||||||
|
self.release(CursorButton::Left);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drives a recorded gesture through the harness.
|
||||||
|
pub fn replay(&mut self, script: &TouchScript) {
|
||||||
|
for sample in &script.samples {
|
||||||
|
match sample.action {
|
||||||
|
TouchAction::Down => {
|
||||||
|
self.move_to(sample.pos);
|
||||||
|
self.press(CursorButton::Left);
|
||||||
|
}
|
||||||
|
TouchAction::Move => self.move_to(sample.pos),
|
||||||
|
TouchAction::Up => {
|
||||||
|
self.move_to(sample.pos);
|
||||||
|
self.release(CursorButton::Left);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn button(&mut self, button: CursorButton) -> &mut ActivationState {
|
||||||
|
let buttons = &mut self.cursor.buttons;
|
||||||
|
match button {
|
||||||
|
CursorButton::Left => &mut buttons.left,
|
||||||
|
CursorButton::Middle => &mut buttons.middle,
|
||||||
|
CursorButton::Right => &mut buttons.right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatches against the layout of the last frame, which is what a
|
||||||
|
/// window delivers input against too.
|
||||||
|
fn sense(&mut self) {
|
||||||
|
let cursor = self.cursor.clone();
|
||||||
|
self.render
|
||||||
|
.run_sensors(&mut self.rsc, &mut self.state, cursor);
|
||||||
|
self.cursor.end_frame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn parse(text: &str) -> Result<Vec<(u64, TouchAction, f32, f32)>, String> {
|
||||||
|
Ok(TouchScript::parse(text)?
|
||||||
|
.samples
|
||||||
|
.iter()
|
||||||
|
.map(|s| (s.t_ms, s.action, s.pos.x, s.pos.y))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_recording_is_time_action_and_a_point() {
|
||||||
|
assert_eq!(
|
||||||
|
parse("0 down 10 20\n16 move 10.5 24\n32 up 10.5 24").unwrap(),
|
||||||
|
[
|
||||||
|
(0, TouchAction::Down, 10.0, 20.0),
|
||||||
|
(16, TouchAction::Move, 10.5, 24.0),
|
||||||
|
(32, TouchAction::Up, 10.5, 24.0),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blank_lines_and_comments_are_not_samples() {
|
||||||
|
assert_eq!(
|
||||||
|
parse("# a flick\n\n 0 down 1 2 # the finger lands\n\n").unwrap(),
|
||||||
|
[(0, TouchAction::Down, 1.0, 2.0)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_recording_that_goes_backwards_is_rejected() {
|
||||||
|
// Replay waits out the gap between samples, so time running backwards
|
||||||
|
// would silently become no wait at all.
|
||||||
|
let err = parse("16 down 1 2\n0 up 1 2").unwrap_err();
|
||||||
|
assert!(err.contains("time order"), "{err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_line_that_is_not_a_sample_says_which_line() {
|
||||||
|
let err = parse("0 down 1 2\n16 wiggle 1 2").unwrap_err();
|
||||||
|
assert!(err.contains("line 2"), "{err}");
|
||||||
|
assert!(err.contains("down/move/up"), "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
pub mod default;
|
pub mod default;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
pub mod harness;
|
||||||
|
pub mod random;
|
||||||
pub mod widget;
|
pub mod widget;
|
||||||
|
|
||||||
pub use iris_core as core;
|
pub use iris_core as core;
|
||||||
|
|||||||
+1005
File diff suppressed because it is too large.
Load diff
+11
-5
@@ -6,16 +6,22 @@ pub struct Image {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Image {
|
impl Widget for Image {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
painter.primitive(&self.handle);
|
painter.primitive(&self.handle);
|
||||||
|
Size::px(self.handle.size())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
fn size_hint(&self, axis: Axis) -> Option<LayoutLen> {
|
||||||
Len::abs(self.handle.size().x)
|
Some(LayoutLen::px(self.handle.size()[axis]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
impl Image {
|
||||||
Len::abs(self.handle.size().y)
|
/// One texture already uploaded, for a caller holding its handle: [`image()`]
|
||||||
|
/// uploads what it is given, and several widgets showing one picture want
|
||||||
|
/// one upload and one slot between them.
|
||||||
|
pub fn new(handle: TextureHandle) -> Self {
|
||||||
|
Self { handle }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-8
@@ -5,16 +5,18 @@ pub struct Masked {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Masked {
|
impl Widget for Masked {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
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`
|
||||||
|
// reports the same: it clips what is inside to that box, so it can
|
||||||
|
// neither take less of one nor honestly ask for more. Passing the
|
||||||
|
// inner size up instead asks to be placed at a length it does not
|
||||||
|
// draw, and the framework would place the drawing it clipped away.
|
||||||
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||||
ctx.width(&self.inner)
|
Some(LayoutLen::LEFTOVER)
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
ctx.height(&self.inner)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,15 +1,15 @@
|
|||||||
mod image;
|
mod image;
|
||||||
mod mask;
|
mod mask;
|
||||||
mod position;
|
mod position;
|
||||||
mod ptr;
|
|
||||||
mod rect;
|
mod rect;
|
||||||
mod text;
|
mod text;
|
||||||
mod trait_fns;
|
mod trait_fns;
|
||||||
|
mod wrapper;
|
||||||
|
|
||||||
pub use image::*;
|
pub use image::*;
|
||||||
pub use mask::*;
|
pub use mask::*;
|
||||||
pub use position::*;
|
pub use position::*;
|
||||||
pub use ptr::*;
|
|
||||||
pub use rect::*;
|
pub use rect::*;
|
||||||
pub use text::*;
|
pub use text::*;
|
||||||
pub use trait_fns::*;
|
pub use trait_fns::*;
|
||||||
|
pub use wrapper::*;
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
pub struct Aligned {
|
|
||||||
pub inner: StrongWidget,
|
|
||||||
pub align: Align,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Aligned {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
|
||||||
let region = match self.align.tuple() {
|
|
||||||
(Some(x), Some(y)) => painter
|
|
||||||
.size(&self.inner)
|
|
||||||
.to_uivec2()
|
|
||||||
.align(RegionAlign { x, y }),
|
|
||||||
(Some(x), None) => {
|
|
||||||
let x = painter.size_ctx().width(&self.inner).apply_rest().align(x);
|
|
||||||
UiRegion::new(x, UiSpan::FULL)
|
|
||||||
}
|
|
||||||
(None, Some(y)) => {
|
|
||||||
let y = painter.size_ctx().height(&self.inner).apply_rest().align(y);
|
|
||||||
UiRegion::new(UiSpan::FULL, y)
|
|
||||||
}
|
|
||||||
(None, None) => UiRegion::FULL,
|
|
||||||
};
|
|
||||||
painter.widget_within(&self.inner, region);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
ctx.width(&self.inner)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
ctx.height(&self.inner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,18 +6,14 @@ pub struct LayerOffset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for LayerOffset {
|
impl Widget for LayerOffset {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||||
|
requests.widget(&self.inner, axis)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
for _ in 0..self.offset {
|
for _ in 0..self.offset {
|
||||||
painter.next_layer();
|
painter.next_layer();
|
||||||
}
|
}
|
||||||
painter.widget(&self.inner);
|
painter.widget(&self.inner).size()
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
ctx.width(&self.inner)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
ctx.height(&self.inner)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,19 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Asks its child in the shorter of a cap and the box this widget was given,
|
||||||
|
/// and answers what the child used, held to the same cap.
|
||||||
|
///
|
||||||
|
/// A cap on the box is a widget rather than a [`SizeRule`] because a box is
|
||||||
|
/// whoever asked's to decide: a rule that read the box it was given would be
|
||||||
|
/// decided again by every path that hands a widget one, including the ones
|
||||||
|
/// that re-place a drawing without asking it anything, and the decision would
|
||||||
|
/// then depend on which path arrived last. A widget is drawn again whenever
|
||||||
|
/// its own box changes, so the comparison is made where the answer can be
|
||||||
|
/// kept -- `longer_than` narrows the windows this drawing holds for, and
|
||||||
|
/// `holds` says the box lengths.
|
||||||
|
///
|
||||||
|
/// The box is what a text wraps at and what a scroll takes its viewport from,
|
||||||
|
/// which is why capping the answer alone is not the same thing.
|
||||||
pub struct MaxSize {
|
pub struct MaxSize {
|
||||||
pub inner: StrongWidget,
|
pub inner: StrongWidget,
|
||||||
pub x: Option<Len>,
|
pub x: Option<Len>,
|
||||||
@@ -7,42 +21,48 @@ pub struct MaxSize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MaxSize {
|
impl MaxSize {
|
||||||
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
|
fn max(&self, axis: Axis) -> Option<Len> {
|
||||||
if let Some(x) = self.x {
|
match axis {
|
||||||
ctx.outer.x.select_len(x.apply_rest());
|
Axis::X => self.x,
|
||||||
}
|
Axis::Y => self.y,
|
||||||
if let Some(y) = self.y {
|
|
||||||
ctx.outer.y.select_len(y.apply_rest());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for MaxSize {
|
impl Widget for MaxSize {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||||
painter.widget(&self.inner);
|
let inner = requests.widget(&self.inner, axis)?;
|
||||||
|
Some(match self.max(axis) {
|
||||||
|
Some(max) => requests.min(inner, max.into()),
|
||||||
|
None => inner,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
self.apply_to_outer(ctx);
|
let align = painter.alignment();
|
||||||
let width = ctx.width(&self.inner);
|
let mut region = UiRegion::FULL;
|
||||||
if let Some(x) = self.x {
|
for axis in Axis::BOTH {
|
||||||
let width_px = width.apply_rest().to_abs(ctx.output_size().x);
|
let Some(max) = self.max(axis) else {
|
||||||
let x_px = x.apply_rest().to_abs(ctx.output_size().x);
|
continue;
|
||||||
if width_px > x_px { x } else { width }
|
};
|
||||||
} else {
|
let own = painter.region_len(axis);
|
||||||
width
|
if painter.longer_than(own, max, axis) {
|
||||||
|
region[axis] = max.align(align[axis]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let mut size = painter.widget_at(&self.inner, region).size();
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
for axis in Axis::BOTH {
|
||||||
self.apply_to_outer(ctx);
|
// The child may draw past the box it was given -- a text too tall
|
||||||
let height = ctx.height(&self.inner);
|
// for it -- and the cap is a promise about the length as well. A
|
||||||
if let Some(y) = self.y {
|
// share passes through: it is a length only to whoever divides
|
||||||
let height_px = height.apply_rest().to_abs(ctx.output_size().y);
|
// one, and that is this widget's parent rather than this widget,
|
||||||
let y_px = y.apply_rest().to_abs(ctx.output_size().y);
|
// which has already given the share the box the cap allows.
|
||||||
if height_px > y_px { y } else { height }
|
if let Some(max) = self.max(axis)
|
||||||
} else {
|
&& painter.longer_than(size[axis].without_leftover(), max, axis)
|
||||||
height
|
{
|
||||||
|
size[axis] = max.into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
mod align;
|
|
||||||
mod layer;
|
mod layer;
|
||||||
mod max_size;
|
mod max_size;
|
||||||
mod offset;
|
mod offset;
|
||||||
mod pad;
|
mod pad;
|
||||||
mod scroll;
|
mod scroll;
|
||||||
mod sized;
|
|
||||||
mod span;
|
mod span;
|
||||||
mod stack;
|
mod stack;
|
||||||
|
|
||||||
pub use align::*;
|
|
||||||
pub use layer::*;
|
pub use layer::*;
|
||||||
pub use max_size::*;
|
pub use max_size::*;
|
||||||
pub use offset::*;
|
pub use offset::*;
|
||||||
pub use pad::*;
|
pub use pad::*;
|
||||||
pub use scroll::*;
|
pub use scroll::*;
|
||||||
pub use sized::*;
|
|
||||||
pub use span::*;
|
pub use span::*;
|
||||||
pub use stack::*;
|
pub use stack::*;
|
||||||
@@ -6,16 +6,13 @@ pub struct Offset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Offset {
|
impl Widget for Offset {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||||
let region = UiRegion::FULL.offset(self.amt);
|
requests.widget(&self.inner, axis)
|
||||||
painter.widget_within(&self.inner, region);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
ctx.width(&self.inner)
|
painter
|
||||||
}
|
.widget_at(&self.inner, UiRegion::FULL.offset(self.amt))
|
||||||
|
.size()
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
ctx.height(&self.inner)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+67
-49
@@ -6,48 +6,54 @@ pub struct Pad {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Pad {
|
impl Widget for Pad {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||||
painter.widget_within(&self.inner, self.padding.region());
|
requests.inset(&self.inner, axis, self.padding.along(axis))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let width = self.padding.left + self.padding.right;
|
// The inner's own alignment, not the near edge. This reports the
|
||||||
let height = self.padding.top + self.padding.bottom;
|
// inner's size plus the padding, so where the box is that answer the
|
||||||
ctx.outer.x.abs -= width;
|
// inset box is exactly the inner and alignment has no room to move
|
||||||
ctx.outer.y.abs -= height;
|
// it; where the box is bigger -- a share of a row, a rule over this
|
||||||
let mut size = ctx.width(&self.inner);
|
// widget -- the slack is the inner's to sit in, and forcing the near
|
||||||
size.abs += width;
|
// edge pinned it to a corner it had not asked for.
|
||||||
size
|
//
|
||||||
|
// Padding is an inset of both: it comes off the rel base, so `rel(1)`
|
||||||
|
// under it fills this widget rather than overflowing it by the
|
||||||
|
// padding, and it comes off the box, so what is drawn sits inside.
|
||||||
|
// The two stay distinct -- the box can be narrower still, where a row
|
||||||
|
// asked this widget in the room left, and a text wraps at that.
|
||||||
|
let inner = painter.widget_at(&self.inner, self.padding.region()).size();
|
||||||
|
Size {
|
||||||
|
x: LayoutLen {
|
||||||
|
px: inner.x.px + self.padding.along(Axis::X),
|
||||||
|
..inner.x
|
||||||
|
},
|
||||||
|
y: LayoutLen {
|
||||||
|
px: inner.y.px + self.padding.along(Axis::Y),
|
||||||
|
..inner.y
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
let width = self.padding.left + self.padding.right;
|
|
||||||
let height = self.padding.top + self.padding.bottom;
|
|
||||||
ctx.outer.x.abs -= width;
|
|
||||||
ctx.outer.y.abs -= height;
|
|
||||||
let mut size = ctx.height(&self.inner);
|
|
||||||
size.abs += height;
|
|
||||||
size
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Padding {
|
pub struct Padding {
|
||||||
pub left: f32,
|
pub left: Px,
|
||||||
pub right: f32,
|
pub right: Px,
|
||||||
pub top: f32,
|
pub top: Px,
|
||||||
pub bottom: f32,
|
pub bottom: Px,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Padding {
|
impl Padding {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
left: 0.0,
|
left: Px::ZERO,
|
||||||
right: 0.0,
|
right: Px::ZERO,
|
||||||
top: 0.0,
|
top: Px::ZERO,
|
||||||
bottom: 0.0,
|
bottom: Px::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn uniform(amt: impl UiNum) -> Self {
|
pub fn uniform(amt: impl UiNum) -> Self {
|
||||||
let amt = amt.to_f32();
|
let amt = Px::from_num(amt);
|
||||||
Self {
|
Self {
|
||||||
left: amt,
|
left: amt,
|
||||||
right: amt,
|
right: amt,
|
||||||
@@ -55,80 +61,92 @@ impl Padding {
|
|||||||
bottom: amt,
|
bottom: amt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn region(&self) -> UiRegion {
|
|
||||||
let mut region = UiRegion::FULL;
|
/// Both sides of one axis together, which is what this padding takes
|
||||||
region.x.start.abs += self.left;
|
/// of a length along it.
|
||||||
region.y.start.abs += self.top;
|
pub fn along(&self, axis: Axis) -> Px {
|
||||||
region.x.end.abs -= self.right;
|
match axis {
|
||||||
region.y.end.abs -= self.bottom;
|
Axis::X => self.left + self.right,
|
||||||
|
Axis::Y => self.top + self.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `region` less this padding on each side.
|
||||||
|
pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
|
||||||
|
region.x.start.px += self.left;
|
||||||
|
region.y.start.px += self.top;
|
||||||
|
region.x.end.px -= self.right;
|
||||||
|
region.y.end.px -= self.bottom;
|
||||||
region
|
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 = amt.to_f32();
|
let amt = Px::from_num(amt);
|
||||||
Self {
|
Self {
|
||||||
left: amt,
|
left: amt,
|
||||||
right: amt,
|
right: amt,
|
||||||
top: 0.0,
|
..Self::ZERO
|
||||||
bottom: 0.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn y(amt: impl UiNum) -> Self {
|
pub fn y(amt: impl UiNum) -> Self {
|
||||||
let amt = amt.to_f32();
|
let amt = Px::from_num(amt);
|
||||||
Self {
|
Self {
|
||||||
left: 0.0,
|
|
||||||
right: 0.0,
|
|
||||||
top: amt,
|
top: amt,
|
||||||
bottom: amt,
|
bottom: amt,
|
||||||
|
..Self::ZERO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn top(amt: impl UiNum) -> Self {
|
pub fn top(amt: impl UiNum) -> Self {
|
||||||
let mut s = Self::ZERO;
|
let mut s = Self::ZERO;
|
||||||
s.top = amt.to_f32();
|
s.top = Px::from_num(amt);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bottom(amt: impl UiNum) -> Self {
|
pub fn bottom(amt: impl UiNum) -> Self {
|
||||||
let mut s = Self::ZERO;
|
let mut s = Self::ZERO;
|
||||||
s.bottom = amt.to_f32();
|
s.bottom = Px::from_num(amt);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left(amt: impl UiNum) -> Self {
|
pub fn left(amt: impl UiNum) -> Self {
|
||||||
let mut s = Self::ZERO;
|
let mut s = Self::ZERO;
|
||||||
s.left = amt.to_f32();
|
s.left = Px::from_num(amt);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn right(amt: impl UiNum) -> Self {
|
pub fn right(amt: impl UiNum) -> Self {
|
||||||
let mut s = Self::ZERO;
|
let mut s = Self::ZERO;
|
||||||
s.right = amt.to_f32();
|
s.right = Px::from_num(amt);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_top(mut self, amt: impl UiNum) -> Self {
|
pub fn with_top(mut self, amt: impl UiNum) -> Self {
|
||||||
self.top = amt.to_f32();
|
self.top = Px::from_num(amt);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_bottom(mut self, amt: impl UiNum) -> Self {
|
pub fn with_bottom(mut self, amt: impl UiNum) -> Self {
|
||||||
self.bottom = amt.to_f32();
|
self.bottom = Px::from_num(amt);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_left(mut self, amt: impl UiNum) -> Self {
|
pub fn with_left(mut self, amt: impl UiNum) -> Self {
|
||||||
self.left = amt.to_f32();
|
self.left = Px::from_num(amt);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_right(mut self, amt: impl UiNum) -> Self {
|
pub fn with_right(mut self, amt: impl UiNum) -> Self {
|
||||||
self.right = amt.to_f32();
|
self.right = Px::from_num(amt);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: UiNum> From<T> for Padding {
|
impl<T: UiNum> From<T> for Padding {
|
||||||
fn from(amt: T) -> Self {
|
fn from(amt: T) -> Self {
|
||||||
Self::uniform(amt.to_f32())
|
Self::uniform(amt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,40 +3,76 @@ use crate::prelude::*;
|
|||||||
pub struct Scroll {
|
pub struct Scroll {
|
||||||
inner: StrongWidget,
|
inner: StrongWidget,
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
amt: f32,
|
amt: Px,
|
||||||
snap_end: bool,
|
snap_end: bool,
|
||||||
container_len: f32,
|
container_len: Px,
|
||||||
content_len: f32,
|
content_len: Px,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Scroll {
|
impl Widget for Scroll {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let output_len = painter.output_size().axis(self.axis);
|
let container_len = painter.px_len(self.axis);
|
||||||
let container_len = painter.region().axis(self.axis).len();
|
// Asked in the whole viewport, then put at the scrolled offset.
|
||||||
let content_len = painter
|
let answer_len = painter
|
||||||
.len_axis(&self.inner, self.axis)
|
.widget_at(&self.inner, PlaceDesc::WHOLE.fills())
|
||||||
.apply_rest()
|
.len(self.axis);
|
||||||
.within_len(container_len)
|
let answer_px = painter.to_px(answer_len.without_leftover(), self.axis);
|
||||||
.to_abs(output_len);
|
self.container_len = container_len;
|
||||||
self.container_len = container_len.to_abs(output_len);
|
self.content_len = answer_px.max(container_len);
|
||||||
self.content_len = content_len;
|
|
||||||
|
|
||||||
if self.snap_end {
|
if self.snap_end {
|
||||||
self.amt = self.content_len - self.container_len;
|
self.amt = self.content_len - self.container_len;
|
||||||
}
|
}
|
||||||
self.update_amt();
|
self.update_amt();
|
||||||
|
// Reading the box in pixels above holds this drawing to that one
|
||||||
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
|
// length, so these two say where it holds more widely.
|
||||||
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
//
|
||||||
painter.widget_within(&self.inner, region);
|
// Content of a fixed length that fits is handed the whole box below,
|
||||||
|
// and nothing here reads the box again, so every longer box gives the
|
||||||
|
// same drawing: it holds from the length the content needs upwards,
|
||||||
|
// and shrinking past that is what changes it. Where it sits in a box
|
||||||
|
// longer than itself is not this widget's to say -- placing its
|
||||||
|
// answer in the whole box is its own alignment, and that placement is
|
||||||
|
// a fraction of the box, so it holds at every length too.
|
||||||
|
//
|
||||||
|
// One scrolled part way sits where it is until the box shrinks past
|
||||||
|
// what is left of it. Kept to the end, it moves with every length.
|
||||||
|
let answer_is_px = answer_len.is_px();
|
||||||
|
if answer_is_px && self.content_len <= self.container_len {
|
||||||
|
painter.holds(self.axis, answer_px..=Px::MAX);
|
||||||
|
} else if answer_is_px && !self.snap_end {
|
||||||
|
let left = self.content_len - self.amt;
|
||||||
|
painter.holds(self.axis, Px::MIN..=left);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
// Content that fills the viewport is the viewport, and is handed back
|
||||||
ctx.width(&self.inner)
|
// as it came -- it has nothing to scroll through, so the clamp above
|
||||||
|
// has already put `amt` at zero. Writing the same box as its own
|
||||||
|
// length in pixels is the same box in another form, and the two do
|
||||||
|
// not round alike: a part centred in `rel 1` lands a step from one
|
||||||
|
// centred in `px 900`, since halving a difference is not halving each
|
||||||
|
// part of it.
|
||||||
|
let content = match self.content_len > self.container_len {
|
||||||
|
true => {
|
||||||
|
let start = Len::from_parts(Rel::ZERO, -self.amt);
|
||||||
|
UiSpan::new(start, start.offset(self.content_len)).shifted_desc()
|
||||||
|
}
|
||||||
|
false => PlaceDescAxis::WHOLE,
|
||||||
|
};
|
||||||
|
// The viewport is the inner's rel base, so a fraction it declares or
|
||||||
|
// reports is a fraction of what is on screen rather than of the
|
||||||
|
// content box its own answer decided. Where it goes is the content
|
||||||
|
// box, scrolled: its drawing moved there, not made again there.
|
||||||
|
painter.place_at(&self.inner, content.on_axis(self.axis).fills());
|
||||||
|
// What it occupies is its box, on both axes: it clips its content to
|
||||||
|
// that box, so it can neither take less of one nor honestly ask for
|
||||||
|
// more. The content's length is what it scrolls through, not what it
|
||||||
|
// is.
|
||||||
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||||
ctx.height(&self.inner)
|
Some(LayoutLen::LEFTOVER)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,22 +81,24 @@ impl Scroll {
|
|||||||
Self {
|
Self {
|
||||||
inner,
|
inner,
|
||||||
axis,
|
axis,
|
||||||
amt: 0.0,
|
amt: Px::ZERO,
|
||||||
snap_end: true,
|
snap_end: true,
|
||||||
container_len: 0.0,
|
container_len: Px::ZERO,
|
||||||
content_len: 0.0,
|
content_len: Px::ZERO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_amt(&mut self) {
|
pub fn update_amt(&mut self) {
|
||||||
self.amt = self.amt.max(0.0);
|
self.amt = self.amt.max(Px::ZERO);
|
||||||
let len = (self.content_len - self.container_len).max(0.0);
|
let len = (self.content_len - self.container_len).max(Px::ZERO);
|
||||||
self.amt = self.amt.min(len);
|
self.amt = self.amt.min(len);
|
||||||
self.snap_end = self.amt == len;
|
self.snap_end = self.amt == len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scrolled by a distance the platform measures, which is the last place
|
||||||
|
/// a wheel notch or a finger is a float.
|
||||||
pub fn scroll(&mut self, amt: f32) {
|
pub fn scroll(&mut self, amt: f32) {
|
||||||
self.amt -= amt;
|
self.amt -= Px::from_f32(amt);
|
||||||
self.update_amt();
|
self.update_amt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
pub struct Sized {
|
|
||||||
pub inner: StrongWidget,
|
|
||||||
pub x: Option<Len>,
|
|
||||||
pub y: Option<Len>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sized {
|
|
||||||
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
|
|
||||||
if let Some(x) = self.x {
|
|
||||||
ctx.outer.x.select_len(x.apply_rest());
|
|
||||||
}
|
|
||||||
if let Some(y) = self.y {
|
|
||||||
ctx.outer.y.select_len(y.apply_rest());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Sized {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
|
||||||
painter.widget(&self.inner);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
self.apply_to_outer(ctx);
|
|
||||||
self.x.unwrap_or_else(|| ctx.width(&self.inner))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
self.apply_to_outer(ctx);
|
|
||||||
self.y.unwrap_or_else(|| ctx.height(&self.inner))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+214
-120
@@ -4,61 +4,236 @@ use std::marker::PhantomData;
|
|||||||
pub struct Span {
|
pub struct Span {
|
||||||
pub children: Vec<StrongWidget>,
|
pub children: Vec<StrongWidget>,
|
||||||
pub dir: Dir,
|
pub dir: Dir,
|
||||||
pub gap: f32,
|
pub gap: Px,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Span {
|
impl Widget for Span {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||||
let total = self.len_sum(&mut painter.size_ctx());
|
if axis != self.dir.axis {
|
||||||
let mut start = UiScalar::rel_min();
|
// A share can be hidden when the other axis has no room. Its
|
||||||
|
// cross-axis length then contributes nothing to the drawn answer.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut total = RequestedLen::from(Len::from_parts(Rel::ZERO, self.gaps()));
|
||||||
for child in &self.children {
|
for child in &self.children {
|
||||||
let mut span = UiSpan::FULL;
|
let child = requests.widget(child, axis)?;
|
||||||
span.start = start;
|
total = requests.sum(total, child);
|
||||||
let len = painter.len_axis(child, self.dir.axis);
|
|
||||||
if len.rest > 0.0 {
|
|
||||||
let offset = UiScalar::new(total.rel, total.abs);
|
|
||||||
let rel_end = UiScalar::rel(len.rest / total.rest);
|
|
||||||
let end = (UiScalar::rel_max() + start) - offset;
|
|
||||||
start = rel_end.within(&start.to(end));
|
|
||||||
}
|
|
||||||
start.abs += len.abs;
|
|
||||||
start.rel += len.rel;
|
|
||||||
span.end = start;
|
|
||||||
let mut child_region = UiRegion::from_axis(self.dir.axis, span, UiSpan::FULL);
|
|
||||||
if self.dir.sign == Sign::Neg {
|
|
||||||
child_region.flip(self.dir.axis);
|
|
||||||
}
|
|
||||||
painter.widget_within(child, child_region);
|
|
||||||
start.abs += self.gap;
|
|
||||||
}
|
}
|
||||||
|
Some(total)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
match self.dir.axis {
|
painter.with_requests(|painter, lens, values| self.layout(painter, lens, values))
|
||||||
Axis::X => self.desired_len(ctx),
|
|
||||||
Axis::Y => self.desired_ortho(ctx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
match self.dir.axis {
|
|
||||||
Axis::X => self.desired_ortho(ctx),
|
|
||||||
Axis::Y => self.desired_len(ctx),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Span {
|
impl Span {
|
||||||
|
fn layout(
|
||||||
|
&self,
|
||||||
|
painter: &mut Painter,
|
||||||
|
lens: &mut Vec<RequestedLen>,
|
||||||
|
values: &mut Vec<Px>,
|
||||||
|
) -> Size {
|
||||||
|
let axis = self.dir.axis;
|
||||||
|
// The row this span lays its children out along, as a length of the
|
||||||
|
// rel base they are laid out against. Where it starts is nothing's
|
||||||
|
// business -- a slot is a length from there -- so what this reads is
|
||||||
|
// the length alone.
|
||||||
|
let row = painter.region_len(axis);
|
||||||
|
self.collect(painter, row, lens, true);
|
||||||
|
let gaps = self.gaps();
|
||||||
|
let fixed = lens
|
||||||
|
.iter()
|
||||||
|
.try_fold(Len::from_parts(Rel::ZERO, gaps), |sum, len| {
|
||||||
|
Some(sum + len.linear()?.without_leftover())
|
||||||
|
});
|
||||||
|
if let Some(fixed) = fixed
|
||||||
|
&& lens.iter().any(|len| len.has_leftover())
|
||||||
|
&& !painter.longer_than(row, fixed, axis)
|
||||||
|
{
|
||||||
|
// With no share to assign, intrinsic drawings keep the remaining
|
||||||
|
// offer, including overflow. Their answer is only moved into a slot.
|
||||||
|
self.collect(painter, row, lens, false);
|
||||||
|
}
|
||||||
|
let nonlinear = lens.iter().any(|len| len.linear().is_none());
|
||||||
|
if nonlinear {
|
||||||
|
painter.allocate(lens, row - Len::from_parts(Rel::ZERO, gaps), axis, values);
|
||||||
|
}
|
||||||
|
let allocated = nonlinear.then(|| &values[..]);
|
||||||
|
let total = match allocated {
|
||||||
|
Some(allocated) => LayoutLen {
|
||||||
|
px: allocated.iter().fold(gaps, |sum, len| sum + *len),
|
||||||
|
..LayoutLen::ZERO
|
||||||
|
},
|
||||||
|
None => lens.iter().fold(
|
||||||
|
LayoutLen {
|
||||||
|
px: gaps,
|
||||||
|
..LayoutLen::ZERO
|
||||||
|
},
|
||||||
|
|sum, len| sum + len.linear().unwrap(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let all_fixed = total.without_leftover();
|
||||||
|
let room = row - all_fixed;
|
||||||
|
let any_leftover = total.leftover > Weight::ZERO;
|
||||||
|
let has_room = any_leftover && painter.longer_than(row, all_fixed, axis);
|
||||||
|
|
||||||
|
// Across itself a span is as long as its longest child -- unless a
|
||||||
|
// rule beside it gives that length outright, and then reading them
|
||||||
|
// answers nothing and makes its size depend on theirs for it. A rule
|
||||||
|
// that only bounds the length does not count: the answer is still
|
||||||
|
// this span's to give.
|
||||||
|
let shrinks = !painter.has_exact_size(!axis);
|
||||||
|
// What the fixed parts and the gaps before here take, which is a sum
|
||||||
|
// of lengths and exact, and how much of the leftover weight is
|
||||||
|
// spoken for. Both ends of a slot are read from those two rather
|
||||||
|
// than stepped from the last child: the share of the room is
|
||||||
|
// rounded, and taking each end from the one before it would carry
|
||||||
|
// every rounding along the row.
|
||||||
|
let mut fixed = Len::ZERO;
|
||||||
|
let mut taken = Weight::ZERO;
|
||||||
|
let mut ortho = LayoutLen::ZERO;
|
||||||
|
// Nothing divides the room where no child asked for any of it, and a
|
||||||
|
// ratio of a whole of nothing has no answer.
|
||||||
|
let reached = |fixed: Len, taken: Weight| match any_leftover {
|
||||||
|
false => fixed,
|
||||||
|
true => fixed + room.scale(Rel::ratio(taken, total.leftover)),
|
||||||
|
};
|
||||||
|
for (index, (child, request)) in self.children.iter().zip(lens.iter()).enumerate() {
|
||||||
|
// An allocated row already has a length for every child; without
|
||||||
|
// one the request is the length and the room is divided here.
|
||||||
|
// Either way a child asking for nothing but a part of what is
|
||||||
|
// left over, when nothing is, is not drawn at all -- one that
|
||||||
|
// also asked for pixels or a fraction keeps those and overflows.
|
||||||
|
let (len, shares, nothing_left) = match allocated {
|
||||||
|
Some(allocated) => {
|
||||||
|
let len = LayoutLen {
|
||||||
|
px: allocated[index],
|
||||||
|
..LayoutLen::ZERO
|
||||||
|
};
|
||||||
|
let shares = request.has_leftover();
|
||||||
|
(len, shares, shares && len.px == Px::ZERO)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let len = request.linear().unwrap();
|
||||||
|
let shares = len.leftover > Weight::ZERO && has_room;
|
||||||
|
(len, shares, len.is_only_leftover() && !has_room)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if nothing_left {
|
||||||
|
painter.undraw(child);
|
||||||
|
fixed.px += self.gap;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let from = reached(fixed, taken);
|
||||||
|
if shares {
|
||||||
|
taken += len.leftover;
|
||||||
|
}
|
||||||
|
fixed += len.without_leftover();
|
||||||
|
let to = reached(fixed, taken);
|
||||||
|
// Along the row the span says where the child goes, and that slot
|
||||||
|
// is the child's box outright rather than something to place an
|
||||||
|
// answer inside again. A share is decided here and nowhere
|
||||||
|
// else: its slot narrows its rel base, and the child is asked in
|
||||||
|
// it, since a text wraps at the width it is actually given. A
|
||||||
|
// fixed child's slot is its own answer, so a drawing made in the
|
||||||
|
// room is put there as it is, and one not made yet is made here.
|
||||||
|
let slot = self.slot(row, from, to);
|
||||||
|
let mut place = slot.shifted_desc().allocated().on_axis(axis);
|
||||||
|
if shares {
|
||||||
|
place = place.rel_base(axis, slot.len());
|
||||||
|
}
|
||||||
|
let used = painter.place_at(child, place).len(!axis);
|
||||||
|
if shrinks {
|
||||||
|
// Choosing between a fixed and a relative length from the
|
||||||
|
// span's own eventual width admits multiple fixed points.
|
||||||
|
// A scalable child therefore makes the span scalable too;
|
||||||
|
// only fixed children are compared with one another.
|
||||||
|
if !used.is_px() {
|
||||||
|
ortho = LayoutLen::LEFTOVER;
|
||||||
|
} else if ortho.leftover == Weight::ZERO {
|
||||||
|
ortho.px = ortho.px.max(used.px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fixed.px += self.gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where nothing was allocated the weight is carried whole rather
|
||||||
|
// than collapsed to one share, so nesting spans divides the same
|
||||||
|
// space rather than re-dividing a share of it: four `leftover(1)`
|
||||||
|
// children under two spans under one span get a quarter each, which
|
||||||
|
// one share per level does not give. Resolution happens at the
|
||||||
|
// nearest ancestor with a length, and the root always has one --
|
||||||
|
// or, where a comparison deferred the row, at the ancestor whose
|
||||||
|
// allocation discovery carried these requests to, and `total` is
|
||||||
|
// pixels by the time it gets here.
|
||||||
|
let ortho = match shrinks {
|
||||||
|
true => ortho,
|
||||||
|
false => LayoutLen::rel(1.0),
|
||||||
|
};
|
||||||
|
Size::from_axis(axis, total, ortho)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Span {
|
||||||
|
/// What the gaps between this span's children take, which is a length of
|
||||||
|
/// the row before anything is divided.
|
||||||
|
fn gaps(&self) -> Px {
|
||||||
|
self.gap
|
||||||
|
.mul_int(self.children.len().saturating_sub(1) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect(
|
||||||
|
&self,
|
||||||
|
painter: &mut Painter,
|
||||||
|
row: Len,
|
||||||
|
lens: &mut Vec<RequestedLen>,
|
||||||
|
discover: bool,
|
||||||
|
) {
|
||||||
|
let axis = self.dir.axis;
|
||||||
|
let mut cursor = Len::ZERO;
|
||||||
|
lens.clear();
|
||||||
|
for child in &self.children {
|
||||||
|
let request = if discover {
|
||||||
|
painter.size_request(child, axis)
|
||||||
|
} else {
|
||||||
|
painter.size_hint(child, axis).map(Into::into)
|
||||||
|
};
|
||||||
|
let len = match request {
|
||||||
|
Some(len) => len,
|
||||||
|
None => {
|
||||||
|
let room = self.slot(row, cursor, row).shifted_desc().on_axis(axis);
|
||||||
|
let len = painter.widget_at(child, room).len(axis);
|
||||||
|
painter.measured_request(child, axis, len)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
cursor += painter.minimum_request(&len, axis);
|
||||||
|
cursor.px += self.gap;
|
||||||
|
lens.push(len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The stretch of the row between two distances from where this span
|
||||||
|
/// starts laying children out, as a span of its own box. A negative
|
||||||
|
/// direction lays out from the far end, so the same two distances mirror
|
||||||
|
/// in a row `row` long.
|
||||||
|
fn slot(&self, row: Len, from: Len, to: Len) -> UiSpan {
|
||||||
|
match self.dir.sign {
|
||||||
|
Sign::Pos => from.to(to),
|
||||||
|
Sign::Neg => (row - to).to(row - from),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn empty(dir: Dir) -> Self {
|
pub fn empty(dir: Dir) -> Self {
|
||||||
Self {
|
Self {
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
dir,
|
dir,
|
||||||
gap: 0.0,
|
gap: Px::ZERO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
||||||
self.gap = gap.to_f32();
|
self.gap = Px::from_num(gap);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,93 +244,12 @@ impl Span {
|
|||||||
pub fn pop(&mut self) -> Option<StrongWidget> {
|
pub fn pop(&mut self) -> Option<StrongWidget> {
|
||||||
self.children.pop()
|
self.children.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn len_sum(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
|
|
||||||
self.children.iter().fold(Len::abs(gap), |mut s, id| {
|
|
||||||
// it's tempting to subtract the abs & rel from the ctx outer,
|
|
||||||
// but that would create inconsistent sizing if you put
|
|
||||||
// a rest first vs last & only speed up in one direction.
|
|
||||||
// I think this is only solvable by restricting how you can
|
|
||||||
// compute size, bc currently you need child to define parent's
|
|
||||||
// sectioning and you need parent's sectioning to define child.
|
|
||||||
// Fortunately, that doesn't matter in most cases
|
|
||||||
let len = ctx.len_axis(id, self.dir.axis);
|
|
||||||
s += len;
|
|
||||||
s
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_len(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
let len = self.len_sum(ctx);
|
|
||||||
if len.rest == 0.0 && len.rel == 0.0 {
|
|
||||||
len
|
|
||||||
} else {
|
|
||||||
Len::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_ortho(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
// this is a weird hack to get text wrapping to work properly when in a downward span
|
|
||||||
// the correct solution here is to add a function to widget that lets them
|
|
||||||
// request that ctx.outer has an axis "resolved" before checking the other,
|
|
||||||
// and panicking or warning if two request opposite axis (unsolvable in that case)
|
|
||||||
let outer = ctx.outer.axis(self.dir.axis);
|
|
||||||
if self.dir.axis == Axis::X {
|
|
||||||
// so....... this literally copies draw so that the lengths are correctly set in the
|
|
||||||
// context, which makes this slow and not cool
|
|
||||||
let total = self.len_sum(ctx);
|
|
||||||
let mut start = UiScalar::rel_min();
|
|
||||||
let mut ortho_len = Len::ZERO;
|
|
||||||
for child in &self.children {
|
|
||||||
let mut span = UiSpan::FULL;
|
|
||||||
span.start = start;
|
|
||||||
let len = ctx.len_axis(child, self.dir.axis);
|
|
||||||
if len.rest > 0.0 {
|
|
||||||
let offset = UiScalar::new(total.rel, total.abs);
|
|
||||||
let rel_end = UiScalar::rel(len.rest / total.rest);
|
|
||||||
let end = (UiScalar::rel_max() + start) - offset;
|
|
||||||
start = rel_end.within(&start.to(end));
|
|
||||||
}
|
|
||||||
start.abs += len.abs;
|
|
||||||
start.rel += len.rel;
|
|
||||||
span.end = start;
|
|
||||||
|
|
||||||
let scalar = span.len();
|
|
||||||
*ctx.outer.axis_mut(self.dir.axis) = outer.select_len(scalar);
|
|
||||||
let ortho = ctx.len_axis(child, !self.dir.axis);
|
|
||||||
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
|
|
||||||
if ortho.rel > 0.0 || ortho.rest > 0.0 {
|
|
||||||
ortho_len.rest = 1.0;
|
|
||||||
ortho_len.abs = 0.0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
ortho_len.abs = ortho_len.abs.max(ortho.abs);
|
|
||||||
start.abs += self.gap;
|
|
||||||
}
|
|
||||||
ortho_len
|
|
||||||
} else {
|
|
||||||
let mut ortho_len = Len::ZERO;
|
|
||||||
let ortho = !self.dir.axis;
|
|
||||||
for child in &self.children {
|
|
||||||
let len = ctx.len_axis(child, ortho);
|
|
||||||
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
|
|
||||||
if len.rel > 0.0 || len.rest > 0.0 {
|
|
||||||
ortho_len.rest = 1.0;
|
|
||||||
ortho_len.abs = 0.0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
ortho_len.abs = ortho_len.abs.max(len.abs);
|
|
||||||
}
|
|
||||||
ortho_len
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
|
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
|
||||||
pub children: Wa,
|
pub children: Wa,
|
||||||
pub dir: Dir,
|
pub dir: Dir,
|
||||||
pub gap: f32,
|
pub gap: Px,
|
||||||
_pd: PhantomData<(State, Tag)>,
|
_pd: PhantomData<(State, Tag)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,13 +275,13 @@ impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
|
|||||||
Self {
|
Self {
|
||||||
children,
|
children,
|
||||||
dir,
|
dir,
|
||||||
gap: 0.0,
|
gap: Px::ZERO,
|
||||||
_pd: PhantomData,
|
_pd: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
pub fn gap(mut self, gap: impl UiNum) -> Self {
|
||||||
self.gap = gap.to_f32();
|
self.gap = Px::from_num(gap);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,29 +8,65 @@ pub struct Stack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Stack {
|
impl Widget for Stack {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||||
let mut iter = self.children.iter();
|
let sizing = match self.size {
|
||||||
if let Some(child) = iter.next() {
|
StackSize::Default => None,
|
||||||
painter.child_layer();
|
StackSize::Child(i) => self.children.get(i),
|
||||||
painter.widget(child);
|
};
|
||||||
}
|
// With nothing sizing it a stack is a share of the box it is given,
|
||||||
for child in iter {
|
// which is what its draw answers too.
|
||||||
painter.next_layer();
|
match sizing {
|
||||||
painter.widget(child);
|
Some(child) => requests.widget(child, axis),
|
||||||
|
None => Some(LayoutLen::LEFTOVER.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
match self.size {
|
let sizing = match self.size {
|
||||||
StackSize::Default => Len::default(),
|
StackSize::Default => None,
|
||||||
StackSize::Child(i) => ctx.width(&self.children[i]),
|
StackSize::Child(i) => Some(i),
|
||||||
|
};
|
||||||
|
// Whichever child sizes the stack is given the stack's whole box --
|
||||||
|
// the stack is the length that child asked for, so placing that
|
||||||
|
// answer inside the box it decided would apply it twice.
|
||||||
|
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) {
|
||||||
|
// On the layer that child ends up on, so the ask below is a reuse
|
||||||
|
// rather than a second drawing of it somewhere else: a retained
|
||||||
|
// drawing belongs to the layer it was made on.
|
||||||
|
Some((i, child)) => {
|
||||||
|
painter.child_layer_at(i);
|
||||||
|
painter.widget_at(child, PlaceDesc::WHOLE.fills()).size()
|
||||||
}
|
}
|
||||||
|
None => Size::LEFTOVER,
|
||||||
|
};
|
||||||
|
// Every other child gets the box the sizing child decided: the
|
||||||
|
// stack is that length, so that is the box they are asked in, and a
|
||||||
|
// fraction under them is a fraction of it. A share leaves the axis
|
||||||
|
// to whoever gave the stack its box. Where a child sits in a box
|
||||||
|
// bigger than itself is its own business.
|
||||||
|
let place = PlaceDesc::from_axes(|axis| {
|
||||||
|
let len = size[axis];
|
||||||
|
match len.leftover == Weight::ZERO {
|
||||||
|
true => len.without_leftover().as_desc().fills(),
|
||||||
|
false => PlaceDescAxis::WHOLE,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (i, child) in self.children.iter().enumerate() {
|
||||||
|
if sizing == Some(i) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
painter.child_layer_at(i);
|
||||||
|
painter.widget_at(child, place);
|
||||||
|
}
|
||||||
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
/// 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 {
|
match self.size {
|
||||||
StackSize::Default => Len::default(),
|
StackSize::Default => Some(LayoutLen::LEFTOVER),
|
||||||
StackSize::Child(i) => ctx.height(&self.children[i]),
|
StackSize::Child(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
use crate::prelude::*;
|
|
||||||
use std::marker::{Sized, Unsize};
|
|
||||||
|
|
||||||
pub struct WidgetPtr {
|
|
||||||
pub inner: Option<StrongWidget>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for WidgetPtr {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
|
||||||
if let Some(id) = &self.inner {
|
|
||||||
painter.widget(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
if let Some(id) = &self.inner {
|
|
||||||
ctx.width(id)
|
|
||||||
} else {
|
|
||||||
Len::ZERO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
if let Some(id) = &self.inner {
|
|
||||||
ctx.height(id)
|
|
||||||
} else {
|
|
||||||
Len::ZERO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetPtr {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
pub fn empty() -> Self {
|
|
||||||
Self {
|
|
||||||
inner: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) {
|
|
||||||
self.inner = Some(to)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn replace<W: ?Sized + Unsize<dyn Widget>>(
|
|
||||||
&mut self,
|
|
||||||
to: StrongWidget<W>,
|
|
||||||
) -> Option<StrongWidget> {
|
|
||||||
self.inner.replace(to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for WidgetPtr {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-7
@@ -28,21 +28,18 @@ impl Rect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Rect {
|
impl Widget for Rect {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
painter.primitive(RectPrimitive {
|
painter.primitive(RectPrimitive {
|
||||||
color: self.color,
|
color: self.color,
|
||||||
radius: self.radius,
|
radius: self.radius,
|
||||||
thickness: self.thickness,
|
thickness: self.thickness,
|
||||||
inner_radius: self.inner_radius,
|
inner_radius: self.inner_radius,
|
||||||
});
|
});
|
||||||
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||||
Len::rest(1)
|
Some(LayoutLen::LEFTOVER)
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
|
||||||
Len::rest(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use std::marker::{PhantomData, Sized};
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
|
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
|||||||
+22
-23
@@ -55,44 +55,43 @@ impl TextEdit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for TextEdit {
|
impl Widget for TextEdit {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let base = painter.layer;
|
let base = painter.layer;
|
||||||
painter.child_layer();
|
painter.child_layer();
|
||||||
self.view.draw(painter);
|
let (_, size) = self.view.draw(painter);
|
||||||
painter.layer = base;
|
painter.layer = base;
|
||||||
let region = self.region();
|
let region = self.region();
|
||||||
|
|
||||||
let Some(selection) = self.selection else {
|
let Some(selection) = self.selection else {
|
||||||
return;
|
return size;
|
||||||
};
|
};
|
||||||
let layout = self.view.buf.layout();
|
let layout = self.view.buf.layout();
|
||||||
|
|
||||||
// parley reports selection as boxes in layout space, so bidi and
|
// parley reports selection as boxes in layout space, so bidi and
|
||||||
// wrapped lines come out right without this code knowing about either.
|
// wrapped lines come out right without this code knowing about either.
|
||||||
for (rect, _) in selection.geometry(layout) {
|
for (rect, _) in selection.geometry(layout) {
|
||||||
let size = vec2(rect.width() as f32, rect.height() as f32);
|
let rect_size = vec2(rect.width() as f32, rect.height() as f32);
|
||||||
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
|
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
|
||||||
painter.primitive_within(
|
painter.primitive_within(
|
||||||
RectPrimitive::color(Color::SKY),
|
RectPrimitive::color(Color::SKY),
|
||||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
rect_size
|
||||||
|
.align(Align::TOP_LEFT)
|
||||||
|
.offset(top_left)
|
||||||
|
.within(®ion),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let caret = selection.focus().geometry(layout, CARET_WIDTH);
|
let caret = selection.focus().geometry(layout, CARET_WIDTH);
|
||||||
let size = vec2(caret.width() as f32, caret.height() as f32);
|
let caret_size = vec2(caret.width() as f32, caret.height() as f32);
|
||||||
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
|
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
|
||||||
painter.primitive_within(
|
painter.primitive_within(
|
||||||
RectPrimitive::color(Color::WHITE),
|
RectPrimitive::color(Color::WHITE),
|
||||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
caret_size
|
||||||
|
.align(Align::TOP_LEFT)
|
||||||
|
.offset(top_left)
|
||||||
|
.within(®ion),
|
||||||
);
|
);
|
||||||
}
|
size
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
self.view.desired_width(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
self.view.desired_height(ctx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +126,6 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
pub fn set(&mut self, text: &str) {
|
pub fn set(&mut self, text: &str) {
|
||||||
let text = self.string(text);
|
let text = self.string(text);
|
||||||
self.text.view.buf.set_text(text);
|
self.text.view.buf.set_text(text);
|
||||||
self.text.view.buf.changed = true;
|
|
||||||
self.text.selection = None;
|
self.text.selection = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +172,6 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
};
|
};
|
||||||
let at = at.min(self.text.view.buf.text().len());
|
let at = at.min(self.text.view.buf.text().len());
|
||||||
self.text.view.buf.edit().insert_str(at, text);
|
self.text.view.buf.edit().insert_str(at, text);
|
||||||
self.text.view.buf.changed = true;
|
|
||||||
self.set_caret(at + text.len());
|
self.set_caret(at + text.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +184,6 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
}
|
}
|
||||||
let range = sel.text_range();
|
let range = sel.text_range();
|
||||||
self.text.view.buf.edit().replace_range(range.clone(), "");
|
self.text.view.buf.edit().replace_range(range.clone(), "");
|
||||||
self.text.view.buf.changed = true;
|
|
||||||
self.set_caret(range.start);
|
self.set_caret(range.start);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -265,7 +261,6 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
|
|
||||||
fn delete_range(&mut self, start: usize, end: usize) {
|
fn delete_range(&mut self, start: usize, end: usize) {
|
||||||
self.text.view.buf.edit().replace_range(start..end, "");
|
self.text.view.buf.edit().replace_range(start..end, "");
|
||||||
self.text.view.buf.changed = true;
|
|
||||||
self.set_caret(start);
|
self.set_caret(start);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +276,13 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
||||||
let pos = pos - self.text.region().top_left().to_abs(size);
|
let pos = pos
|
||||||
|
- self
|
||||||
|
.text
|
||||||
|
.region()
|
||||||
|
.top_left()
|
||||||
|
.to_px(PxVec2::from_f32(size))
|
||||||
|
.to_f32();
|
||||||
let prev_sel = self.text.selection;
|
let prev_sel = self.text.selection;
|
||||||
let prev_hit = self.text.double_hit;
|
let prev_hit = self.text.double_hit;
|
||||||
|
|
||||||
@@ -320,12 +321,10 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
let old = (self.text.view.buf.text().to_string(), self.text.selection);
|
let old = (self.text.view.buf.text().to_string(), self.text.selection);
|
||||||
let mut undo = false;
|
let mut undo = false;
|
||||||
let res = self.apply_event_inner(event, modifiers, &mut undo);
|
let res = self.apply_event_inner(event, modifiers, &mut undo);
|
||||||
if undo {
|
if undo && let Some((old, selection)) = self.text.history.pop() {
|
||||||
if let Some((old, selection)) = self.text.history.pop() {
|
|
||||||
self.set(&old);
|
self.set(&old);
|
||||||
self.text.selection = selection;
|
self.text.selection = selection;
|
||||||
self.clamp_selection_to_layout();
|
self.clamp_selection_to_layout();
|
||||||
}
|
|
||||||
} else if self.text.view.buf.text() != old.0 {
|
} else if self.text.view.buf.text() != old.0 {
|
||||||
self.text.history.push(old);
|
self.text.history.push(old);
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-73
@@ -14,11 +14,8 @@ pub struct Text {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct TextView {
|
pub struct TextView {
|
||||||
pub attrs: MutDetect<TextAttrs>,
|
pub attrs: TextAttrs,
|
||||||
pub buf: MutDetect<TextBuffer>,
|
pub buf: TextBuffer,
|
||||||
// cache
|
|
||||||
tex: Option<RenderedText>,
|
|
||||||
width: Option<f32>,
|
|
||||||
pub hint: Option<StrongWidget>,
|
pub hint: Option<StrongWidget>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,19 +25,13 @@ impl TextView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn wrap_width(&self) -> Option<f32> {
|
pub fn wrap_width(&self) -> Option<f32> {
|
||||||
self.width
|
self.buf.wrap_width()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextView {
|
impl TextView {
|
||||||
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
|
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
|
||||||
Self {
|
Self { attrs, buf, hint }
|
||||||
attrs: attrs.into(),
|
|
||||||
buf: buf.into(),
|
|
||||||
tex: None,
|
|
||||||
width: None,
|
|
||||||
hint,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// region where the text should be draw
|
/// region where the text should be draw
|
||||||
@@ -52,56 +43,45 @@ impl TextView {
|
|||||||
.align(self.align)
|
.align(self.align)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, ctx: &mut SizeCtx) -> &RenderedText {
|
/// The text shaped for the width it is drawn in. The buffer keeps its
|
||||||
let width = if self.attrs.wrap {
|
/// answers under the attrs too, so changing those asks a new question
|
||||||
Some(ctx.px_size().x)
|
/// rather than invalidating anything.
|
||||||
} else {
|
fn render(&mut self, painter: &mut Painter) -> &RenderedText {
|
||||||
None
|
let width = self.attrs.wrap.then(|| painter.px_len(Axis::X));
|
||||||
};
|
// The shaper measures in floats, which is where a glyph advance comes
|
||||||
if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed {
|
// from; what it answers goes back on the grid.
|
||||||
self.width = width;
|
painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32));
|
||||||
self.tex = Some(ctx.draw_text(&mut self.buf, &self.attrs, width));
|
if width.is_some() {
|
||||||
self.attrs.changed = false;
|
painter.holds(Axis::X, self.buf.width_holds());
|
||||||
self.buf.changed = false;
|
|
||||||
}
|
}
|
||||||
self.tex.as_ref().unwrap()
|
self.buf.rendered().expect("render_text placed the glyphs")
|
||||||
}
|
|
||||||
pub fn tex(&self) -> Option<&RenderedText> {
|
|
||||||
self.tex.as_ref()
|
|
||||||
}
|
|
||||||
pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
if self.is_empty()
|
|
||||||
&& let Some(hint) = &self.hint
|
|
||||||
{
|
|
||||||
ctx.width(hint)
|
|
||||||
} else {
|
|
||||||
Len::abs(self.render(ctx).size.x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
if self.is_empty()
|
|
||||||
&& let Some(hint) = &self.hint
|
|
||||||
{
|
|
||||||
ctx.height(hint)
|
|
||||||
} else {
|
|
||||||
Len::abs(self.render(ctx).size.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
|
|
||||||
let align = self.align;
|
|
||||||
if self.is_empty() && self.hint.is_some() {
|
|
||||||
let region = self.render(&mut painter.size_ctx()).size.align(align);
|
|
||||||
if let Some(hint) = &self.hint {
|
|
||||||
painter.widget(hint);
|
|
||||||
}
|
|
||||||
return region;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let tex = self.render(&mut painter.size_ctx());
|
pub fn tex(&self) -> Option<&RenderedText> {
|
||||||
|
self.buf.rendered()
|
||||||
|
}
|
||||||
|
/// Draws the text, and says where the glyphs went and what they use.
|
||||||
|
pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) {
|
||||||
|
let align = self.align;
|
||||||
|
if self.is_empty() && self.hint.is_some() {
|
||||||
|
let region = self.render(painter).size.align(align);
|
||||||
|
let size = match &self.hint {
|
||||||
|
Some(hint) => painter.widget(hint).size(),
|
||||||
|
None => Size::ZERO,
|
||||||
|
};
|
||||||
|
return (region, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tex = self.render(painter);
|
||||||
let region = tex.size.align(align);
|
let region = tex.size.align(align);
|
||||||
let within = region.within(&painter.region());
|
// The step at or above what the shaper measured, so a parent that
|
||||||
painter.glyphs(tex, within);
|
// hands back the length this reports hands back a box the longest
|
||||||
region
|
// line fits in. Rounded to the nearest step it is half the time a
|
||||||
|
// hair under that line, and the break made in it is not the break a
|
||||||
|
// cold layout makes there.
|
||||||
|
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
|
||||||
|
painter.glyphs(tex, region);
|
||||||
|
(region, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(&self) -> String {
|
pub fn content(&self) -> String {
|
||||||
@@ -117,7 +97,7 @@ impl Text {
|
|||||||
content: content.into(),
|
content: content.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn update_buf(&mut self, _ctx: &mut SizeCtx) {
|
fn update_buf(&mut self) {
|
||||||
if self.content.changed {
|
if self.content.changed {
|
||||||
self.content.changed = false;
|
self.content.changed = false;
|
||||||
self.view.buf.set_text(self.content.as_str());
|
self.view.buf.set_text(self.content.as_str());
|
||||||
@@ -126,19 +106,9 @@ impl Text {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Text {
|
impl Widget for Text {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
self.update_buf(&mut painter.size_ctx());
|
self.update_buf();
|
||||||
self.view.draw(painter);
|
self.view.draw(painter).1
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
self.update_buf(ctx);
|
|
||||||
self.view.desired_width(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
self.update_buf(ctx);
|
|
||||||
self.view.desired_height(ctx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+86
-29
@@ -12,14 +12,23 @@ widget_trait! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn align(self, align: impl Into<Align>) -> impl WidgetFn<Rsc, Aligned> {
|
fn align(self, align: impl Into<Align>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
move |state| Aligned {
|
// An axis left out keeps whatever it had, which is centered unless
|
||||||
inner: self.add_strong(state),
|
// something else set it.
|
||||||
align: align.into(),
|
let align = align.into();
|
||||||
|
move |state| {
|
||||||
|
let id = self.add(state);
|
||||||
|
let widgets = &mut state.ui_mut().widgets;
|
||||||
|
for axis in Axis::BOTH {
|
||||||
|
if let Some(align) = align[axis] {
|
||||||
|
widgets.set_alignment(id, axis, align);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn center(self) -> impl WidgetFn<Rsc, Aligned> {
|
fn center(self) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
self.align(Align::CENTER)
|
self.align(Align::CENTER)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,15 +40,63 @@ widget_trait! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, Sized> {
|
fn region_node(self) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
let size = size.into();
|
|state| {
|
||||||
move |state| Sized {
|
let id = self.add(state);
|
||||||
inner: self.add_strong(state),
|
state.ui_mut().widgets.set_region_node(id, true);
|
||||||
x: Some(size.x),
|
id
|
||||||
y: Some(size.y),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sized(self, size: impl Into<Size>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
|
let size = size.into();
|
||||||
|
move |state| {
|
||||||
|
let id = self.add(state);
|
||||||
|
let widgets = &mut state.ui_mut().widgets;
|
||||||
|
widgets.set_size_rule(id, Axis::X, SizeRule::Exact(size.x));
|
||||||
|
widgets.set_size_rule(id, Axis::Y, SizeRule::Exact(size.y));
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn width(self, len: impl Into<SizeRequest>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
|
let len = len.into();
|
||||||
|
move |state| {
|
||||||
|
let id = self.add(state);
|
||||||
|
state
|
||||||
|
.ui_mut()
|
||||||
|
.widgets
|
||||||
|
.set_size_rule(id, Axis::X, SizeRule::from(len));
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Answers at least this wide, whatever it drew: a rule beside the
|
||||||
|
/// widget, so what a row gives it is at least this even where the widget
|
||||||
|
/// itself wanted less. The box it draws in is untouched -- for that, see
|
||||||
|
/// [`MaxSize`].
|
||||||
|
fn min_width(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
|
let len = len.into();
|
||||||
|
move |state| {
|
||||||
|
let id = self.add(state);
|
||||||
|
state.ui_mut().widgets.set_min_len(id, Axis::X, len);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn min_height(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
|
let len = len.into();
|
||||||
|
move |state| {
|
||||||
|
let id = self.add(state);
|
||||||
|
state.ui_mut().widgets.set_min_len(id, Axis::Y, len);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts this in a [`MaxSize`]: it is asked in the shorter of the cap and
|
||||||
|
/// the box that widget was given, and is as long as it used, held to the
|
||||||
|
/// cap. A widget rather than a rule because the box is whoever asked's to
|
||||||
|
/// decide -- see [`MaxSize`].
|
||||||
fn max_width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
|
fn max_width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
|
||||||
let len = len.into();
|
let len = len.into();
|
||||||
move |state| MaxSize {
|
move |state| MaxSize {
|
||||||
@@ -58,21 +115,15 @@ widget_trait! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
|
fn height(self, len: impl Into<SizeRequest>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
let len = len.into();
|
let len = len.into();
|
||||||
move |state| Sized {
|
move |state| {
|
||||||
inner: self.add_strong(state),
|
let id = self.add(state);
|
||||||
x: Some(len),
|
state
|
||||||
y: None,
|
.ui_mut()
|
||||||
}
|
.widgets
|
||||||
}
|
.set_size_rule(id, Axis::Y, SizeRule::from(len));
|
||||||
|
id
|
||||||
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
|
|
||||||
let len = len.into();
|
|
||||||
move |state| Sized {
|
|
||||||
inner: self.add_strong(state),
|
|
||||||
x: None,
|
|
||||||
y: Some(len),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +136,9 @@ widget_trait! {
|
|||||||
|
|
||||||
fn scrollable(self) -> impl WidgetIdFn<Rsc, Scroll> where Rsc: HasEvents {
|
fn scrollable(self) -> impl WidgetIdFn<Rsc, Scroll> where Rsc: HasEvents {
|
||||||
move |state| {
|
move |state| {
|
||||||
Scroll::new(self.add_strong(state), Axis::Y)
|
let inner = self.add(state);
|
||||||
|
state.ui_mut().widgets.set_region_node(inner, true);
|
||||||
|
Scroll::new(inner.upgrade(state), Axis::Y)
|
||||||
.on(CursorSense::Scroll, |ctx, rsc| {
|
.on(CursorSense::Scroll, |ctx, rsc| {
|
||||||
let delta = ctx.data.scroll_delta.y * 50.0;
|
let delta = ctx.data.scroll_delta.y * 50.0;
|
||||||
ctx.widget(rsc).scroll(delta);
|
ctx.widget(rsc).scroll(delta);
|
||||||
@@ -125,9 +178,13 @@ widget_trait! {
|
|||||||
|state| self.add(state)
|
|state| self.add(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_ptr(self, ptr: WeakWidget<WidgetPtr>, state: &mut Rsc) {
|
// Named for the type it makes rather than as `wrapped`, which would read
|
||||||
let id = self.add_strong(state);
|
// as the text setting. `widget_trait!` takes no attributes, so what it is
|
||||||
state.ui_mut().widgets[ptr].inner = Some(id);
|
// for is on `Wrapper` itself.
|
||||||
|
fn wrapper(self) -> impl WidgetFn<Rsc, Wrapper> {
|
||||||
|
|state| Wrapper {
|
||||||
|
inner: Some(self.add_strong(state)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
use std::marker::Unsize;
|
||||||
|
|
||||||
|
/// One widget in a box of its own, doing as little as possible on the way:
|
||||||
|
/// it draws its child in the whole of its box and reports back what the child
|
||||||
|
/// said. It exists because a length and an alignment are properties of one
|
||||||
|
/// widget, so a widget cannot both be 100 wide and take two shares of a row
|
||||||
|
/// -- the two lengths need two widgets, and this is the smaller one.
|
||||||
|
///
|
||||||
|
/// Its child is optional so it can also be the swappable slot a tab bar
|
||||||
|
/// needs, which is what it was written for.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Wrapper {
|
||||||
|
pub inner: Option<StrongWidget>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Wrapper {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
match &self.inner {
|
||||||
|
Some(id) => painter.widget(id).size(),
|
||||||
|
None => Size::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wrapper {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) {
|
||||||
|
self.inner = Some(to)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replace<W: ?Sized + Unsize<dyn Widget>>(
|
||||||
|
&mut self,
|
||||||
|
to: StrongWidget<W>,
|
||||||
|
) -> Option<StrongWidget> {
|
||||||
|
self.inner.replace(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
use iris::{harness::Harness, prelude::*};
|
||||||
|
use std::{
|
||||||
|
alloc::{GlobalAlloc, Layout, System},
|
||||||
|
cell::Cell,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Counting;
|
||||||
|
thread_local! {
|
||||||
|
static COUNT: Cell<Option<usize>> = const { Cell::new(None) };
|
||||||
|
}
|
||||||
|
fn count() {
|
||||||
|
COUNT.with(|count| {
|
||||||
|
if let Some(n) = count.get() {
|
||||||
|
count.set(Some(n + 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// The wrapper preserves System's allocation and deallocation contracts;
|
||||||
|
// observing calls here also counts allocations hidden inside layout helpers.
|
||||||
|
unsafe impl GlobalAlloc for Counting {
|
||||||
|
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||||
|
count();
|
||||||
|
unsafe { System.alloc(layout) }
|
||||||
|
}
|
||||||
|
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||||
|
unsafe { System.dealloc(ptr, layout) }
|
||||||
|
}
|
||||||
|
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 {
|
||||||
|
count();
|
||||||
|
unsafe { System.realloc(ptr, layout, size) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[global_allocator]
|
||||||
|
static ALLOCATOR: Counting = Counting;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unchanged_tree_reuses_layout_storage() {
|
||||||
|
for deferred in [false, true] {
|
||||||
|
let mut h = Harness::new((600, 200));
|
||||||
|
let mut children: Vec<StrongWidget> = Vec::new();
|
||||||
|
for _ in 0..8 {
|
||||||
|
let a = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
if deferred {
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(a, Axis::X, leftover(1).clamp(20, 80).into());
|
||||||
|
}
|
||||||
|
let row = (a, rect(Color::BLUE))
|
||||||
|
.span(Dir::RIGHT)
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
children.push(row);
|
||||||
|
}
|
||||||
|
let root = h.rsc.widgets_mut().add_strong(Span {
|
||||||
|
children,
|
||||||
|
dir: Dir::DOWN,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
});
|
||||||
|
h.state.root = Some(root);
|
||||||
|
h.frame();
|
||||||
|
let ids: Vec<_> = h.render.active.keys().copied().collect();
|
||||||
|
for frame in 0..8 {
|
||||||
|
for &id in &ids {
|
||||||
|
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||||
|
}
|
||||||
|
h.resize((600 + frame % 2, 200));
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
|
COUNT.set(Some(0));
|
||||||
|
for frame in 0..100 {
|
||||||
|
for &id in &ids {
|
||||||
|
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||||
|
}
|
||||||
|
h.resize((600 + frame % 2, 200));
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
|
let allocations = COUNT.replace(None).unwrap();
|
||||||
|
println!("deferred={deferred}: {allocations} allocations over 100 resize frames");
|
||||||
|
assert_eq!(allocations, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
use std::{cell::Cell, rc::Rc};
|
||||||
|
|
||||||
|
use iris::harness::{Harness, assert_corners};
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
struct Counted {
|
||||||
|
draws: Rc<Cell<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Counted {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
self.draws.set(self.draws.get() + 1);
|
||||||
|
painter.px_size();
|
||||||
|
painter.primitive(RectPrimitive::color(Color::RED));
|
||||||
|
Size::LEFTOVER
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||||
|
Some(LayoutLen::LEFTOVER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_capped_share_returns_room_to_its_sibling() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let first = rect(Color::RED).max_width(80).add(&mut h.rsc);
|
||||||
|
let second = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((first, second).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, first, (0, 0), (80, 100));
|
||||||
|
assert_corners!(h, second, (80, 0), (300, 100));
|
||||||
|
h.resize((100, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, first, (0, 0), (50, 100));
|
||||||
|
assert_corners!(h, second, (50, 0), (100, 100));
|
||||||
|
h.resize((300, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, first, (0, 0), (80, 100));
|
||||||
|
assert_corners!(h, second, (80, 0), (300, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_shares_are_discovered_without_provisional_paint() {
|
||||||
|
let mut h = Harness::new((400, 100));
|
||||||
|
let draws = Rc::new(Cell::new(0));
|
||||||
|
let leaf = h.rsc.ui_mut().widgets.add_strong(Counted {
|
||||||
|
draws: draws.clone(),
|
||||||
|
});
|
||||||
|
let leaf_id = leaf.id();
|
||||||
|
let mut inner: StrongWidget = leaf;
|
||||||
|
for _ in 0..8 {
|
||||||
|
let sibling = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
inner = h.rsc.ui_mut().widgets.add_strong(Span {
|
||||||
|
children: vec![inner, sibling],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
h.state.root = Some(inner);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(draws.get(), 1);
|
||||||
|
assert!(h.region(&leaf_id).is_some());
|
||||||
|
h.resize((800, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(draws.get(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_bounds_are_resolved_in_the_outer_allocation() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let a = rect(Color::RED).max_width(40).add(&mut h.rsc);
|
||||||
|
let b = rect(Color::GREEN).max_width(60).add(&mut h.rsc);
|
||||||
|
let inner = (a, b).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let tail = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((inner, tail).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, a, (0, 0), (40, 100));
|
||||||
|
assert_corners!(h, b, (40, 0), (100, 100));
|
||||||
|
assert_corners!(h, tail, (100, 0), (300, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_edits_in_a_nested_child_reach_the_allocator() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let a = rect(Color::RED).max_width(80).add(&mut h.rsc);
|
||||||
|
let inner = (a,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let tail = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((inner, tail).span(Dir::RIGHT));
|
||||||
|
h.rsc.ui_mut().widgets.get_mut(&a).unwrap().x = Some(Len::px(40.0));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, a, (0, 0), (40, 100));
|
||||||
|
assert_corners!(h, tail, (40, 0), (300, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adding_a_bound_to_a_previously_unbounded_share_reallocates_the_row() {
|
||||||
|
for hinted in [true, false] {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let a = if hinted {
|
||||||
|
rect(Color::RED).add_strong(&mut h.rsc).any()
|
||||||
|
} else {
|
||||||
|
h.rsc.widgets_mut().add_strong(Unhinted).any()
|
||||||
|
};
|
||||||
|
let id = a.id();
|
||||||
|
let b = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let mut row = Span::empty(Dir::RIGHT);
|
||||||
|
row.push(a);
|
||||||
|
row.push(b.add_strong(&mut h.rsc));
|
||||||
|
h.set_root(row);
|
||||||
|
h.resize((400, 100));
|
||||||
|
h.frame();
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(id, Axis::X, SizeRule::Max(Len::px(80.0)));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, id, (0, 0), (80, 100));
|
||||||
|
assert_corners!(h, b, (80, 0), (400, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_deferred_comparison_can_compare_two_different_weights() {
|
||||||
|
let a = SizeRequest::from(leftover(1.0) + px(30.0)).min(leftover(2.0));
|
||||||
|
let b = SizeRequest::from(leftover(1.0)).clamp(px(20.0), px(100.0));
|
||||||
|
let mut h = Harness::new((60, 100));
|
||||||
|
let a = rect(Color::RED).width(a).add(&mut h.rsc);
|
||||||
|
let b = rect(Color::BLUE).width(b).add(&mut h.rsc);
|
||||||
|
h.set_root((a, b).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, a, (0, 0), (40, 100));
|
||||||
|
assert_corners!(h, b, (40, 0), (60, 100));
|
||||||
|
h.resize((300, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, a, (0, 0), (200, 100));
|
||||||
|
assert_corners!(h, b, (200, 0), (300, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_length_expression_is_resolved_before_wrapping_text() {
|
||||||
|
let mut h = Harness::new((300, 500));
|
||||||
|
let text = wtext("one two three four five six seven eight nine ten")
|
||||||
|
.size(16)
|
||||||
|
.wrap(true)
|
||||||
|
.width(leftover(1).clamp(40, 80))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let other = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((text, other).span(Dir::RIGHT));
|
||||||
|
let box_ = h.region(&text).unwrap();
|
||||||
|
assert_eq!(box_.top_left.x, Px::ZERO);
|
||||||
|
assert_eq!(box_.bot_right.x, Px::from_int(80));
|
||||||
|
assert!(box_.bot_right.y - box_.top_left.y > Px::from_int(30));
|
||||||
|
assert_corners!(h, other, (80, 0), (300, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_bounds_keep_the_allocators_base() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let head = rect(Color::BLUE).width(30).add(&mut h.rsc);
|
||||||
|
let bounded = rect(Color::RED)
|
||||||
|
.width(leftover(1).min(rel(0.25)))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let tail = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
|
h.set_root((head, bounded, tail).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, bounded, (30, 0), (105, 100));
|
||||||
|
assert_corners!(h, tail, (105, 0), (300, 100));
|
||||||
|
h.resize((400, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, bounded, (30, 0), (130, 100));
|
||||||
|
assert_corners!(h, tail, (130, 0), (400, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_root_resolves_a_deferred_request_again_after_resize() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let bounded = rect(Color::RED)
|
||||||
|
.width(leftover(1).min(rel(0.25)))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(bounded);
|
||||||
|
assert_corners!(h, bounded, (112.5, 0), (187.5, 100));
|
||||||
|
h.resize((400, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, bounded, (150, 0), (250, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filling_a_stack_does_not_mean_its_sizing_child_was_already_allocated() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let child = rect(Color::RED).width(leftover(1).min(80)).add(&mut h.rsc);
|
||||||
|
let overlay = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let children: Vec<StrongWidget> =
|
||||||
|
vec![child.add_strong(&mut h.rsc), overlay.add_strong(&mut h.rsc)];
|
||||||
|
h.set_root(Stack {
|
||||||
|
children,
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
});
|
||||||
|
assert_corners!(h, child, (110, 0), (190, 100));
|
||||||
|
assert_corners!(h, overlay, (110, 0), (190, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Unhinted;
|
||||||
|
impl Widget for Unhinted {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
painter.px_size();
|
||||||
|
painter.primitive(RectPrimitive::color(Color::RED));
|
||||||
|
Size::LEFTOVER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bounds_also_apply_to_shares_discovered_by_drawing() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let a = h.rsc.widgets_mut().add_strong(Unhinted);
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(a.id(), Axis::X, SizeRule::Max(Len::px(80.0)));
|
||||||
|
let id = a.id();
|
||||||
|
let b = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let b_id = b.id();
|
||||||
|
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
|
||||||
|
children: vec![a, b],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, id, (0, 0), (80, 100));
|
||||||
|
assert_corners!(h, b_id, (80, 0), (300, 100));
|
||||||
|
h.resize((100, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, id, (0, 0), (50, 100));
|
||||||
|
assert_corners!(h, b_id, (50, 0), (100, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comparisons_with_a_known_order_remain_plain_lengths() {
|
||||||
|
assert_eq!(leftover(2).max(leftover(5)), SizeRequest::from(leftover(5)));
|
||||||
|
assert_eq!(leftover(2).min(leftover(5)), SizeRequest::from(leftover(2)));
|
||||||
|
assert_eq!(
|
||||||
|
(px(10) + rel(0.5)).max(px(30) + rel(0.5)),
|
||||||
|
SizeRequest::from(px(30) + rel(0.5))
|
||||||
|
);
|
||||||
|
assert_eq!(LayoutLen::px(10).clamp(20, 80), SizeRequest::from(20));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_measured_nested_share_keeps_its_comparison_for_the_outer_span() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let a = h.rsc.widgets_mut().add_strong(Unhinted);
|
||||||
|
let a_id = a.id();
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(a_id, Axis::X, SizeRule::Max(Len::px(80.0)));
|
||||||
|
let b = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let b_id = b.id();
|
||||||
|
let inner = h.rsc.widgets_mut().add_strong(Span {
|
||||||
|
children: vec![a, b],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
});
|
||||||
|
let c = rect(Color::GREEN).add_strong(&mut h.rsc);
|
||||||
|
let c_id = c.id();
|
||||||
|
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
|
||||||
|
children: vec![inner, c],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, a_id, (0, 0), (80, 100));
|
||||||
|
assert_corners!(h, b_id, (80, 0), (190, 100));
|
||||||
|
assert_corners!(h, c_id, (190, 0), (300, 100));
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.mark_for_redraw(h.state.root.as_ref().unwrap().id());
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, c_id, (190, 0), (300, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Natural;
|
||||||
|
impl Widget for Natural {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
painter.primitive(RectPrimitive::color(Color::RED));
|
||||||
|
Size::from_axis(Axis::X, LayoutLen::px(64), LayoutLen::px(64))
|
||||||
|
}
|
||||||
|
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||||
|
Some(LayoutLen::px(64))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_bounds_on_a_hinted_child_track_the_offer_before_its_declared_size() {
|
||||||
|
fn tree(h: &mut Harness) -> WidgetId {
|
||||||
|
let natural = h.rsc.widgets_mut().add_strong(Natural);
|
||||||
|
let id = natural.id();
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(id, Axis::X, SizeRule::Max(Len::rel(0.75)));
|
||||||
|
h.rsc.widgets_mut().set_size_rule(
|
||||||
|
id,
|
||||||
|
Axis::Y,
|
||||||
|
SizeRule::Clamp {
|
||||||
|
min: Len::rel(0.25),
|
||||||
|
max: Len::rel(0.75),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let inner = h.rsc.widgets_mut().add_strong(Stack {
|
||||||
|
children: vec![natural],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
});
|
||||||
|
let inner_id = inner.id();
|
||||||
|
let fill = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let overlay = h.rsc.widgets_mut().add_strong(Stack {
|
||||||
|
children: vec![fill, inner],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
});
|
||||||
|
let share = rect(Color::GREEN).add_strong(&mut h.rsc);
|
||||||
|
let bounded = rect(Color::GREEN).add_strong(&mut h.rsc);
|
||||||
|
h.rsc.widgets_mut().set_size_rule(
|
||||||
|
bounded.id(),
|
||||||
|
Axis::X,
|
||||||
|
SizeRule::Clamp {
|
||||||
|
min: Len::rel(0.25),
|
||||||
|
max: Len::rel(0.75),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let fixed = wtext("one line, overflowing whatever it is given")
|
||||||
|
.size(16)
|
||||||
|
.wrap(false)
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
|
||||||
|
children: vec![share, bounded, fixed, overlay],
|
||||||
|
dir: Dir::LEFT,
|
||||||
|
gap: Px::from_int(8),
|
||||||
|
}));
|
||||||
|
h.rsc.widgets_mut().set_size_rule(
|
||||||
|
h.state.root.as_ref().unwrap().id(),
|
||||||
|
Axis::Y,
|
||||||
|
LayoutLen::rel(1).into(),
|
||||||
|
);
|
||||||
|
h.frame();
|
||||||
|
inner_id
|
||||||
|
}
|
||||||
|
let mut warm = Harness::new((1920, 1200));
|
||||||
|
let a = tree(&mut warm);
|
||||||
|
warm.resize((640, 900));
|
||||||
|
warm.frame();
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let b = tree(&mut cold);
|
||||||
|
assert_eq!(warm.region(&a), cold.region(&b));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_bound_can_extend_an_explicit_share_request() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let a = rect(Color::RED)
|
||||||
|
.width(leftover(1))
|
||||||
|
.min_width(100)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let b = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((a, b).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, a, (0, 0), (150, 100));
|
||||||
|
h.resize((120, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, a, (0, 0), (100, 100));
|
||||||
|
assert_corners!(h, b, (100, 0), (120, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn moving_scroll_content_preserves_its_resolved_expression_size() {
|
||||||
|
let mut h = Harness::new((300, 300));
|
||||||
|
let leaf = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let leaf_id = leaf.id();
|
||||||
|
let content = h.rsc.widgets_mut().add_strong(Stack {
|
||||||
|
children: vec![leaf],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
});
|
||||||
|
let content_id = content.id();
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(content_id, Axis::Y, leftover(1).min(120).into());
|
||||||
|
let scroll = h
|
||||||
|
.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.add_strong(Scroll::new(content, Axis::Y));
|
||||||
|
let scroll_id = scroll.id();
|
||||||
|
h.state.root = Some(scroll);
|
||||||
|
for _ in 0..3 {
|
||||||
|
h.rsc.widgets_mut().mark_for_redraw(scroll_id);
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, content_id, (0, 90), (300, 210));
|
||||||
|
assert_corners!(h, leaf_id, (0, 90), (300, 210));
|
||||||
|
}
|
||||||
|
h.resize((300, 600));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, leaf_id, (0, 240), (300, 360));
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
//! A measurement that decides control flow.
|
||||||
|
//!
|
||||||
|
//! Comparing boxes catches a widget that moved. It does not catch a widget
|
||||||
|
//! that measured a child, believed a different answer from the one a cold
|
||||||
|
//! start would give, and took the other branch -- which is the same defect
|
||||||
|
//! arriving somewhere it cannot be ignored. A widget here branches on what it
|
||||||
|
//! measured, so a disagreement shows up as a different tree.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
/// Measures `probe` across `axis` and draws one of two children on the
|
||||||
|
/// answer. Its own configuration never changes, so which child is drawn is a
|
||||||
|
/// property of the layout alone.
|
||||||
|
struct BranchesOnMeasurement {
|
||||||
|
probe: StrongWidget,
|
||||||
|
wide: StrongWidget,
|
||||||
|
narrow: StrongWidget,
|
||||||
|
threshold: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for BranchesOnMeasurement {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
|
||||||
|
let top = UiSpan::new(Len::ZERO, cut).shifted_desc();
|
||||||
|
let measured = painter
|
||||||
|
.widget_at(&self.probe, top.on_axis(Axis::Y))
|
||||||
|
.len(Axis::X);
|
||||||
|
let px = painter.to_px(measured.apply_leftover(), Axis::X);
|
||||||
|
|
||||||
|
let below = UiSpan::new(cut, painter.region_len(Axis::Y)).shifted_desc();
|
||||||
|
let place = below.on_axis(Axis::Y);
|
||||||
|
match px > Px::from_f32(self.threshold) {
|
||||||
|
true => painter.widget_at(&self.wide, place),
|
||||||
|
false => painter.widget_at(&self.narrow, place),
|
||||||
|
};
|
||||||
|
Size::LEFTOVER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plant(h: &mut Harness, threshold: f32) -> (WidgetId, WidgetId) {
|
||||||
|
let words = "the quick brown fox jumps over the lazy dog and keeps running";
|
||||||
|
let probe = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let wide = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let narrow = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let branch = BranchesOnMeasurement {
|
||||||
|
probe: probe.add_strong(&mut h.rsc),
|
||||||
|
wide: wide.add_strong(&mut h.rsc),
|
||||||
|
narrow: narrow.add_strong(&mut h.rsc),
|
||||||
|
threshold,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let side = rect(Color::GREEN).width(120).add(&mut h.rsc);
|
||||||
|
h.set_root((side, branch).span(Dir::RIGHT));
|
||||||
|
(wide.id(), narrow.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which of the two branches drew, as a pair a test can compare.
|
||||||
|
fn taken(h: &Harness, wide: WidgetId, narrow: WidgetId) -> (bool, bool) {
|
||||||
|
(h.region(&wide).is_some(), h.region(&narrow).is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_branch_taken_on_a_measurement_holds_across_repaints() {
|
||||||
|
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
|
||||||
|
let mut h = Harness::new((900, 600));
|
||||||
|
let (wide, narrow) = plant(&mut h, threshold);
|
||||||
|
let first = taken(&h, wide, narrow);
|
||||||
|
assert_ne!(first, (false, false), "threshold {threshold}: neither drew");
|
||||||
|
|
||||||
|
for frame in 0..4 {
|
||||||
|
h.rsc.widgets_mut().mark_for_redraw(wide);
|
||||||
|
h.rsc.widgets_mut().mark_for_redraw(narrow);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(
|
||||||
|
taken(&h, wide, narrow),
|
||||||
|
first,
|
||||||
|
"threshold {threshold}, repaint {frame}: the branch moved when nothing did"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_branch_taken_on_a_measurement_is_the_one_a_cold_start_takes() {
|
||||||
|
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
|
||||||
|
let mut warm = Harness::new((900, 600));
|
||||||
|
let (wide, narrow) = plant(&mut warm, threshold);
|
||||||
|
warm.resize((640, 480));
|
||||||
|
warm.frame();
|
||||||
|
warm.rsc.widgets_mut().mark_for_redraw(wide);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 480));
|
||||||
|
let (cwide, cnarrow) = plant(&mut cold, threshold);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
taken(&warm, wide, narrow),
|
||||||
|
taken(&cold, cwide, cnarrow),
|
||||||
|
"threshold {threshold}: warm and cold took different branches"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//! What a retained drawing costs in accuracy when it is moved instead of made
|
||||||
|
//! again. A subtree's stored regions are the only record of where it is, so a
|
||||||
|
//! move that works from the last answer rather than from the box it is now in
|
||||||
|
//! integrates its own rounding, and nothing later recomputes it. Re-expressing
|
||||||
|
//! each part as the same fraction of the new box is what keeps a long-lived
|
||||||
|
//! layout on the one a cold start produces.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
/// A row of a fixed height under a bar, so changing the bar's height moves the
|
||||||
|
/// row without changing the box it is given: the move path, repeatedly.
|
||||||
|
fn plant(h: &mut Harness, bar_height: f32) -> (WeakWidget<Rect>, WeakWidget<Rect>) {
|
||||||
|
let bar = rect(Color::RED).height(bar_height).add(&mut h.rsc);
|
||||||
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let row = (inner, rect(Color::GREEN)).span(Dir::RIGHT).height(100);
|
||||||
|
h.set_root((bar, row).span(Dir::DOWN));
|
||||||
|
(bar, inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enough moves to pass the 0.05 physical pixels layout treats as the same
|
||||||
|
/// place, for a move that adds an offset to the last answer. Measured on this
|
||||||
|
/// fixture on 2026-09-15: adding the offset to both ends of a span shortened
|
||||||
|
/// the row by 0.071 over this many moves and by 0.712 over ten times as many,
|
||||||
|
/// growing with the count rather than settling. Placing the far end from the
|
||||||
|
/// near one instead left 0.069, because the length is re-derived either way.
|
||||||
|
const MOVES: usize = 20_000;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_subtree_moved_many_times_stays_where_a_cold_layout_puts_it() {
|
||||||
|
let mut warm = Harness::new((640, 900));
|
||||||
|
let (bar, inner) = plant(&mut warm, 40.0);
|
||||||
|
let mut height = 40.0;
|
||||||
|
for step in 0..MOVES {
|
||||||
|
height = 40.0 + (step % 300) as f32 * 0.37;
|
||||||
|
warm.set_len(bar, Axis::Y, height);
|
||||||
|
warm.frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let (_, cold_inner) = plant(&mut cold, height);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
assert_eq!(warm.region(&inner), cold.region(&cold_inner));
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
//! Whether measuring a widget and then giving it the length it reported is a
|
||||||
|
//! fixed point, which is what a span that sizes to its children needs.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_wrapping_text_in_a_span_settles_on_one_width() {
|
||||||
|
let mut h = Harness::new((900, 600));
|
||||||
|
let words = "the quick brown fox jumps over the lazy dog and keeps on running \
|
||||||
|
until it reaches the end of a rather long line of text";
|
||||||
|
let t = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let filler = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((t, filler).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
let mut widths = Vec::new();
|
||||||
|
for _ in 0..6 {
|
||||||
|
let r = h.region(&t.id()).unwrap();
|
||||||
|
widths.push(r.bot_right.x - r.top_left.x);
|
||||||
|
// Redrawing it changes nothing about the state, so nothing may move.
|
||||||
|
h.rsc.widgets_mut().mark_for_redraw(t.id());
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
|
println!("widths over six frames: {widths:?}");
|
||||||
|
assert!(
|
||||||
|
widths.windows(2).all(|w| w[0] == w[1]),
|
||||||
|
"a repaint that changed nothing moved it: {widths:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,156 @@
|
|||||||
|
//! The tree a seed describes, as a value rather than as widgets.
|
||||||
|
//!
|
||||||
|
//! Two things have to hold for a plan to be worth having. Editing a plan has
|
||||||
|
//! to mean what growing with those edits means, or a scenario reads one thing
|
||||||
|
//! and the oracle another. And reducing a plan has to end, or a shrinker
|
||||||
|
//! searching for the smallest counterexample never returns.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, grow, plan};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
fn some_edits(seed: u64, of: &Plan) -> Edits {
|
||||||
|
let mut rng = Rng::new(seed);
|
||||||
|
let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0);
|
||||||
|
let mut of = of.clone();
|
||||||
|
of.walk_mut(&mut |p| {
|
||||||
|
if matches!(p.kind, Kind::Span { .. }) {
|
||||||
|
spans += 1;
|
||||||
|
}
|
||||||
|
sized += p.size.is_some() as usize;
|
||||||
|
aligned += p.align.is_some() as usize;
|
||||||
|
nodes += p.region_node.is_some() as usize;
|
||||||
|
});
|
||||||
|
let pick =
|
||||||
|
|n: usize, rng: &mut Rng| -> Vec<usize> { (0..n).filter(|_| rng.chance()).collect() };
|
||||||
|
Edits {
|
||||||
|
sizes: pick(sized, &mut rng)
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| {
|
||||||
|
(
|
||||||
|
i,
|
||||||
|
SizeRules {
|
||||||
|
x: SizeRule::Exact(LayoutLen::LEFTOVER),
|
||||||
|
y: SizeRule::Free,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
aligns: pick(aligned, &mut rng)
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| {
|
||||||
|
(
|
||||||
|
i,
|
||||||
|
Align {
|
||||||
|
x: Some(AxisAlign::POS),
|
||||||
|
y: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
nodes: pick(nodes, &mut rng)
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| (i, true))
|
||||||
|
.collect(),
|
||||||
|
spans: pick(spans, &mut rng)
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| {
|
||||||
|
(
|
||||||
|
i,
|
||||||
|
SpanEdit {
|
||||||
|
detach: vec![0],
|
||||||
|
attach: 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<HashMap<_, _>>(),
|
||||||
|
fixed_branches: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The two routes to an edited tree are one tree. `plan` resolves edits out
|
||||||
|
/// of the random stream as it draws; `edited` puts them on a tree that
|
||||||
|
/// already exists, which is the only route a shrunk plan has, since no seed
|
||||||
|
/// grows one. A scenario written against either has to read the same.
|
||||||
|
#[test]
|
||||||
|
fn editing_a_plan_is_growing_one_with_those_edits() {
|
||||||
|
for seed in 1..=60 {
|
||||||
|
let bare = plan(seed, 5, &Edits::default());
|
||||||
|
let edits = some_edits(seed, &bare);
|
||||||
|
assert_eq!(
|
||||||
|
bare.edited(&edits),
|
||||||
|
plan(seed, 5, &edits),
|
||||||
|
"seed {seed}: edited and grown-with-edits disagree"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No simplification is larger, which is the half of "the shrinker stops" a
|
||||||
|
/// widget count can see. Most are not smaller either -- a dropped alignment
|
||||||
|
/// and a simpler leaf both keep the count -- so what rules out circling is
|
||||||
|
/// that those are one-way too: a `Some` becomes a `None`, and a kind steps
|
||||||
|
/// down a ladder with no way back up.
|
||||||
|
#[test]
|
||||||
|
fn no_simplification_of_a_plan_is_larger_than_it() {
|
||||||
|
for seed in 1..=60 {
|
||||||
|
let tree = plan(seed, 4, &Edits::default());
|
||||||
|
let mut queue = vec![tree];
|
||||||
|
let mut seen = 0;
|
||||||
|
while let Some(node) = queue.pop() {
|
||||||
|
seen += 1;
|
||||||
|
if seen > 400 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for small in node.smaller() {
|
||||||
|
assert!(
|
||||||
|
small.size() <= node.size(),
|
||||||
|
"seed {seed}: a simplification grew from {} to {}",
|
||||||
|
node.size(),
|
||||||
|
small.size()
|
||||||
|
);
|
||||||
|
if small.size() < node.size() {
|
||||||
|
queue.push(small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reducing until nothing reduces ends, and ends at something small enough to
|
||||||
|
/// read rather than at the tree it started from.
|
||||||
|
#[test]
|
||||||
|
fn reducing_a_plan_all_the_way_ends() {
|
||||||
|
for seed in 1..=30 {
|
||||||
|
let mut node = plan(seed, 5, &Edits::default());
|
||||||
|
let grown = node.size();
|
||||||
|
let mut steps = 0;
|
||||||
|
while let Some(next) = node.smaller().into_iter().next() {
|
||||||
|
node = next;
|
||||||
|
steps += 1;
|
||||||
|
assert!(steps < 10_000, "seed {seed}: reducing did not end");
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
node.size() < grown.max(2),
|
||||||
|
"seed {seed}: reduced {grown} widgets to {}",
|
||||||
|
node.size()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every image in a tree is the same picture, and a handle is a reference to
|
||||||
|
/// the texture rather than a copy of it, so one upload and one slot serve all
|
||||||
|
/// of them however many a tree grows -- and the trees are grown in hundreds.
|
||||||
|
#[test]
|
||||||
|
fn a_tree_of_images_uploads_one_texture() {
|
||||||
|
let mut images = 0;
|
||||||
|
let mut tree = plan(1, 4, &Edits::default());
|
||||||
|
tree.walk_mut(&mut |p| images += (p.kind == Kind::Image) as usize);
|
||||||
|
assert!(images > 1, "a tree of {images} images tests nothing");
|
||||||
|
|
||||||
|
let mut h = Harness::new((900, 1200));
|
||||||
|
let (root, _) = grow(&mut h.rsc, 1, 4, &Edits::default());
|
||||||
|
h.state.root = Some(root);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(h.rsc.ui().textures.count(), 1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
//! Which widget an input reaches.
|
||||||
|
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use iris::harness::{Harness, TouchScript};
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_press_reaches_only_the_widget_under_the_cursor() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let clicks = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
|
||||||
|
let (on_left, on_right) = (clicks.clone(), clicks.clone());
|
||||||
|
let left = rect(Color::RED)
|
||||||
|
.width(100)
|
||||||
|
.on(CursorSense::click(), move |_, _| {
|
||||||
|
on_left.borrow_mut().push("left")
|
||||||
|
})
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let right = rect(Color::BLUE)
|
||||||
|
.on(CursorSense::click(), move |_, _| {
|
||||||
|
on_right.borrow_mut().push("right")
|
||||||
|
})
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root((left, right).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
h.click((50, 100));
|
||||||
|
assert_eq!(*clicks.borrow(), ["left"]);
|
||||||
|
|
||||||
|
h.click((300, 100));
|
||||||
|
assert_eq!(*clicks.borrow(), ["left", "right"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_ends_when_the_cursor_leaves_the_window() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let hovered = Rc::new(RefCell::new(0));
|
||||||
|
let ended = Rc::new(RefCell::new(0));
|
||||||
|
|
||||||
|
let (h_count, e_count) = (hovered.clone(), ended.clone());
|
||||||
|
let widget = rect(Color::RED)
|
||||||
|
.on(CursorSense::HoverStart, move |_, _| {
|
||||||
|
*h_count.borrow_mut() += 1
|
||||||
|
})
|
||||||
|
.on(CursorSense::HoverEnd, move |_, _| {
|
||||||
|
*e_count.borrow_mut() += 1
|
||||||
|
})
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(widget);
|
||||||
|
|
||||||
|
h.move_to((200, 100));
|
||||||
|
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
|
||||||
|
|
||||||
|
// A second sample inside the same widget is not a second hover.
|
||||||
|
h.move_to((210, 100));
|
||||||
|
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
|
||||||
|
|
||||||
|
h.leave();
|
||||||
|
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_recorded_gesture_presses_where_it_says() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let clicks = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
|
||||||
|
let (on_left, on_right) = (clicks.clone(), clicks.clone());
|
||||||
|
let left = rect(Color::RED)
|
||||||
|
.width(100)
|
||||||
|
.on(CursorSense::click(), move |_, _| {
|
||||||
|
on_left.borrow_mut().push("left")
|
||||||
|
})
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let right = rect(Color::BLUE)
|
||||||
|
.on(CursorSense::click(), move |_, _| {
|
||||||
|
on_right.borrow_mut().push("right")
|
||||||
|
})
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root((left, right).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
let script = TouchScript::parse("0 down 300 100\n80 up 300 100").unwrap();
|
||||||
|
h.replay(&script);
|
||||||
|
|
||||||
|
assert_eq!(*clicks.borrow(), ["right"]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
//! Input across layers: what stops at a layer, what passes through it, and
|
||||||
|
//! where hovering stops.
|
||||||
|
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
const WINDOW: f32 = 100.0;
|
||||||
|
|
||||||
|
/// Every sense that has fired on one widget since it was last read.
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct Fired(Rc<RefCell<Vec<CursorSense>>>);
|
||||||
|
|
||||||
|
impl Fired {
|
||||||
|
fn take(&self) -> Vec<CursorSense> {
|
||||||
|
std::mem::take(&mut self.0.borrow_mut())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget filling whatever it is given, recording the senses it is sent.
|
||||||
|
fn listener(h: &mut Harness, senses: impl Into<CursorSenses>) -> (WeakWidget<Rect>, Fired) {
|
||||||
|
let fired = Fired::default();
|
||||||
|
let record = fired.clone();
|
||||||
|
let id = rect(Color::WHITE)
|
||||||
|
.on(senses.into(), move |ctx, _| {
|
||||||
|
record.0.borrow_mut().push(ctx.data.sense)
|
||||||
|
})
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
(id, fired)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget with no senses of its own, to leave a gap beside one that has.
|
||||||
|
fn blank(h: &mut Harness) -> WeakWidget<Rect> {
|
||||||
|
rect(Color::WHITE).add(&mut h.rsc)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn harness() -> Harness {
|
||||||
|
Harness::new((WINDOW, WINDOW))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_stops_at_the_topmost_widget() {
|
||||||
|
let mut h = harness();
|
||||||
|
let (bottom, bottom_hover) = listener(&mut h, CursorSense::HoverStart);
|
||||||
|
let (middle, middle_hover) = listener(&mut h, CursorSense::HoverStart);
|
||||||
|
let (top, top_hover) = listener(&mut h, CursorSense::HoverStart);
|
||||||
|
h.set_root((bottom, middle, top).stack());
|
||||||
|
|
||||||
|
h.move_to((50, 50));
|
||||||
|
|
||||||
|
assert_eq!(top_hover.take(), [CursorSense::HoverStart]);
|
||||||
|
assert_eq!(
|
||||||
|
middle_hover.take(),
|
||||||
|
[],
|
||||||
|
"hover is not shared with a layer below"
|
||||||
|
);
|
||||||
|
assert_eq!(bottom_hover.take(), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_scroll_passes_through_every_widget_that_does_not_want_it() {
|
||||||
|
let mut h = harness();
|
||||||
|
let (list, scrolled) = listener(&mut h, CursorSense::Scroll);
|
||||||
|
let (button, clicked) = listener(&mut h, CursorSense::click());
|
||||||
|
let (overlay, overlay_clicked) = listener(&mut h, CursorSense::click());
|
||||||
|
h.set_root((list, button, overlay).stack());
|
||||||
|
|
||||||
|
h.move_to((50, 50));
|
||||||
|
h.scroll((0, 10));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
scrolled.take(),
|
||||||
|
[CursorSense::Scroll],
|
||||||
|
"two layers of click-only widgets do not stop a scroll"
|
||||||
|
);
|
||||||
|
assert_eq!(clicked.take(), []);
|
||||||
|
assert_eq!(overlay_clicked.take(), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hovering_a_button_above_does_not_stop_a_later_scroll() {
|
||||||
|
let mut h = harness();
|
||||||
|
let (list, scrolled) = listener(&mut h, CursorSense::Scroll);
|
||||||
|
let (button, _clicked) = listener(&mut h, CursorSense::click());
|
||||||
|
h.set_root((list, button).stack());
|
||||||
|
|
||||||
|
// The hover arrives in its own frame, as a window delivers it.
|
||||||
|
h.move_to((50, 50));
|
||||||
|
assert_eq!(scrolled.take(), []);
|
||||||
|
|
||||||
|
h.scroll((0, 10));
|
||||||
|
assert_eq!(
|
||||||
|
scrolled.take(),
|
||||||
|
[CursorSense::Scroll],
|
||||||
|
"a hover already resting on the button must not consume the wheel"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_the_topmost_listener_takes_a_press() {
|
||||||
|
let mut h = harness();
|
||||||
|
let (below, below_clicked) = listener(&mut h, CursorSense::click());
|
||||||
|
let (above, above_clicked) = listener(&mut h, CursorSense::click());
|
||||||
|
h.set_root((below, above).stack());
|
||||||
|
|
||||||
|
h.click((50, 50));
|
||||||
|
|
||||||
|
assert_eq!(above_clicked.take(), [CursorSense::click()]);
|
||||||
|
assert_eq!(below_clicked.take(), [], "one press goes to one widget");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_press_beside_the_button_reaches_the_layer_below() {
|
||||||
|
let mut h = harness();
|
||||||
|
let (list, list_clicked) = listener(&mut h, CursorSense::click());
|
||||||
|
// The row above the list covers it, but only its left half is the button.
|
||||||
|
let (button, button_clicked) = listener(&mut h, CursorSense::click());
|
||||||
|
let row = (button, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.set_root((list, row).stack());
|
||||||
|
|
||||||
|
h.click((20, 50));
|
||||||
|
assert_eq!(button_clicked.take(), [CursorSense::click()]);
|
||||||
|
assert_eq!(list_clicked.take(), []);
|
||||||
|
|
||||||
|
h.click((80, 50));
|
||||||
|
assert_eq!(button_clicked.take(), [], "the cursor is not on the button");
|
||||||
|
assert_eq!(
|
||||||
|
list_clicked.take(),
|
||||||
|
[CursorSense::click()],
|
||||||
|
"a press beside the button belongs to what is under it"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leaving_a_widget_still_ends_its_hover() {
|
||||||
|
let mut h = harness();
|
||||||
|
let (widget, hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
|
||||||
|
h.set_root(widget);
|
||||||
|
|
||||||
|
h.move_to((50, 50));
|
||||||
|
assert_eq!(hover.take(), [CursorSense::HoverStart]);
|
||||||
|
|
||||||
|
h.leave();
|
||||||
|
assert_eq!(hover.take(), [CursorSense::HoverEnd]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leaving_a_widget_does_not_block_the_layer_below() {
|
||||||
|
let mut h = harness();
|
||||||
|
let (below, below_hover) = listener(&mut h, CursorSense::HoverStart);
|
||||||
|
// Only the left half of the layer above is a widget, so the cursor can
|
||||||
|
// leave it without leaving the one underneath.
|
||||||
|
let (above, above_hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
|
||||||
|
let row = (above, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.set_root((below, row).stack());
|
||||||
|
|
||||||
|
h.move_to((20, 50));
|
||||||
|
assert_eq!(above_hover.take(), [CursorSense::HoverStart]);
|
||||||
|
assert_eq!(below_hover.take(), [], "the layer above is over it");
|
||||||
|
|
||||||
|
h.move_to((80, 50));
|
||||||
|
assert_eq!(above_hover.take(), [CursorSense::HoverEnd]);
|
||||||
|
assert_eq!(
|
||||||
|
below_hover.take(),
|
||||||
|
[CursorSense::HoverStart],
|
||||||
|
"ending a hover above must not stop the hover below"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn covering_a_widget_ends_its_hover() {
|
||||||
|
let mut h = harness();
|
||||||
|
let (below, below_hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
|
||||||
|
let (above, above_hover) = listener(&mut h, CursorSense::HoverStart);
|
||||||
|
let row = (above, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.set_root((below, row).stack());
|
||||||
|
|
||||||
|
h.move_to((80, 50));
|
||||||
|
assert_eq!(below_hover.take(), [CursorSense::HoverStart]);
|
||||||
|
|
||||||
|
h.move_to((20, 50));
|
||||||
|
assert_eq!(above_hover.take(), [CursorSense::HoverStart]);
|
||||||
|
assert_eq!(
|
||||||
|
below_hover.take(),
|
||||||
|
[CursorSense::HoverEnd],
|
||||||
|
"a widget covered by one that took the input is no longer hovered"
|
||||||
|
);
|
||||||
|
|
||||||
|
h.move_to((80, 50));
|
||||||
|
assert_eq!(
|
||||||
|
below_hover.take(),
|
||||||
|
[CursorSense::HoverStart],
|
||||||
|
"uncovering it hovers it again"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_starts_and_ends_once_each() {
|
||||||
|
let mut h = harness();
|
||||||
|
// Only the left half is the widget, so the cursor can leave it without
|
||||||
|
// leaving the window.
|
||||||
|
let (widget, hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
|
||||||
|
let row = (widget, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.set_root(row);
|
||||||
|
|
||||||
|
h.move_to((20, 50));
|
||||||
|
assert_eq!(hover.take(), [CursorSense::HoverStart]);
|
||||||
|
|
||||||
|
h.move_to((30, 50));
|
||||||
|
assert_eq!(hover.take(), [], "staying inside is not a second start");
|
||||||
|
|
||||||
|
h.move_to((80, 50));
|
||||||
|
assert_eq!(hover.take(), [CursorSense::HoverEnd]);
|
||||||
|
|
||||||
|
h.move_to((90, 50));
|
||||||
|
assert_eq!(hover.take(), [], "an ended hover does not end again");
|
||||||
|
|
||||||
|
h.move_to((20, 50));
|
||||||
|
assert_eq!(
|
||||||
|
hover.take(),
|
||||||
|
[CursorSense::HoverStart],
|
||||||
|
"re-entering starts it"
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,186 @@
|
|||||||
|
//! Scrolling moves content and stops at its ends.
|
||||||
|
|
||||||
|
use iris::harness::{Harness, assert_corners};
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scrollable_enables_a_region_node_but_raw_scroll_does_not() {
|
||||||
|
let mut h = Harness::new((100, 100));
|
||||||
|
let default_child = ().add(&mut h.rsc);
|
||||||
|
let _default = default_child.scrollable().add(&mut h.rsc);
|
||||||
|
assert!(h.rsc.widgets().is_region_node(default_child));
|
||||||
|
h.rsc.widgets_mut().set_region_node(default_child, false);
|
||||||
|
assert!(!h.rsc.widgets().is_region_node(default_child));
|
||||||
|
|
||||||
|
let raw_child = ().add(&mut h.rsc);
|
||||||
|
let _raw = Scroll::new(raw_child.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
||||||
|
assert!(!h.rsc.widgets().is_region_node(raw_child));
|
||||||
|
|
||||||
|
let explicit = ().region_node().add(&mut h.rsc);
|
||||||
|
assert!(h.rsc.widgets().is_region_node(explicit));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_scrollable_child_can_drop_its_region_node() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let top = rect(Color::RED).height(200).add(&mut h.rsc);
|
||||||
|
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
|
||||||
|
let content = (top, bottom).span(Dir::DOWN).add(&mut h.rsc);
|
||||||
|
h.set_root(content.scrollable());
|
||||||
|
h.rsc.widgets_mut().set_region_node(content, false);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
h.move_to((200, 100));
|
||||||
|
h.scroll((0, 1));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert!(!h.rsc.widgets().is_region_node(content));
|
||||||
|
assert_corners!(h, top, (0, -150), (400, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
// Twice the window's height, so there is 200 to scroll.
|
||||||
|
let top = rect(Color::RED).height(200).add(&mut h.rsc);
|
||||||
|
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
|
||||||
|
h.set_root((top, bottom).span(Dir::DOWN).scrollable());
|
||||||
|
h.move_to((200, 100));
|
||||||
|
|
||||||
|
// `Scroll` starts snapped to the end.
|
||||||
|
assert_corners!(h, top, (0, -200), (400, 0));
|
||||||
|
|
||||||
|
// The handler scales a wheel line by 50.
|
||||||
|
h.scroll((0, 1));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, top, (0, -150), (400, 50));
|
||||||
|
|
||||||
|
h.scroll((0, 10));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, top, (0, 0), (400, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_content_and_a_share_fill_one_viewport() {
|
||||||
|
let mut h = Harness::new((900, 100));
|
||||||
|
let content = rect(Color::RED)
|
||||||
|
.width(LayoutLen {
|
||||||
|
px: Px::from_int(600),
|
||||||
|
rel: Rel::ZERO,
|
||||||
|
leftover: Weight::ONE,
|
||||||
|
})
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
||||||
|
h.set_root(scroll);
|
||||||
|
|
||||||
|
assert_corners!(h, content, (0, 0), (900, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_content_wider_than_the_viewport_still_scrolls() {
|
||||||
|
let mut h = Harness::new((900, 100));
|
||||||
|
let content = rect(Color::RED).width(1200).add(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
||||||
|
h.set_root(scroll);
|
||||||
|
|
||||||
|
assert_corners!(h, content, (-300, 0), (900, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_lone_share_fills_without_scrolling() {
|
||||||
|
let mut h = Harness::new((900, 100));
|
||||||
|
let content = rect(Color::RED).width(LayoutLen::LEFTOVER).add(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
||||||
|
h.set_root(scroll);
|
||||||
|
|
||||||
|
assert_corners!(h, content, (0, 0), (900, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrapping_content_beside_a_fixed_length_is_stable_warm_and_cold() {
|
||||||
|
fn plant(h: &mut Harness) -> (WidgetId, WidgetId) {
|
||||||
|
let fixed = rect(Color::RED).width(600).add(&mut h.rsc);
|
||||||
|
let text = wtext("Wrapping shapes one source into as many lines as the box leaves room for, so a paragraph's height is an answer and not a setting.")
|
||||||
|
.size(16)
|
||||||
|
.wrap(true)
|
||||||
|
.width(LayoutLen::LEFTOVER)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let content = (fixed, text).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
||||||
|
h.set_root(scroll);
|
||||||
|
(text.id(), content.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut warm = Harness::new((900, 300));
|
||||||
|
let (text, content) = plant(&mut warm);
|
||||||
|
warm.rsc.widgets_mut().mark_for_redraw(text);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 300));
|
||||||
|
let (cold_text, cold_content) = plant(&mut cold);
|
||||||
|
|
||||||
|
assert_eq!(warm.region(&text), cold.region(&cold_text));
|
||||||
|
assert_eq!(warm.region(&content), cold.region(&cold_content));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that clips to its box may not report more than the box: its
|
||||||
|
/// parent would place the part it cut off, and the framework would put a
|
||||||
|
/// drawing longer than its box somewhere. `Masked` is the second of these
|
||||||
|
/// after `Scroll`, and the assertion in `draw_at` is what says so.
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "clips to"]
|
||||||
|
fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
|
||||||
|
struct Clipper(StrongWidget);
|
||||||
|
|
||||||
|
impl Widget for Clipper {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
painter.set_mask(UiRegion::FULL);
|
||||||
|
painter.widget(&self.0).size()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut h = Harness::new((100, 100));
|
||||||
|
let tall = rect(Color::RED).height(400).add_strong(&mut h.rsc);
|
||||||
|
let clipper = Clipper(tall).add(&mut h.rsc);
|
||||||
|
// `set_root` lays the tree out, so this is where it is caught.
|
||||||
|
h.set_root(clipper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Content that fits sits in the viewport, not in a box of the window's
|
||||||
|
/// length anchored at the viewport's start. `Part::From` takes window
|
||||||
|
/// lengths, so a `rel(1.0)` span in one is the window, and only a scroll
|
||||||
|
/// filling the window would land right.
|
||||||
|
#[test]
|
||||||
|
fn content_that_fits_is_placed_in_the_viewport_and_not_in_the_window() {
|
||||||
|
let mut h = Harness::new((400, 400));
|
||||||
|
let head = rect(Color::RED).height(100).add(&mut h.rsc);
|
||||||
|
let inner = rect(Color::BLUE).height(50).add(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(inner.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
||||||
|
h.set_root((head, scroll).span(Dir::DOWN));
|
||||||
|
|
||||||
|
assert_corners!(h, scroll, (0, 100), (400, 400));
|
||||||
|
assert_corners!(h, inner, (0, 225), (400, 275));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cap narrows the box the widget is asked in, which is what a scroll
|
||||||
|
/// measures its viewport from: the content scrolls within the cap rather than
|
||||||
|
/// within the room the cap was cut from.
|
||||||
|
#[test]
|
||||||
|
fn a_capped_scroll_takes_its_viewport_from_the_cap() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let top = rect(Color::RED).height(200).add(&mut h.rsc);
|
||||||
|
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
|
||||||
|
let scroll = (top, bottom).span(Dir::DOWN).scrollable().add(&mut h.rsc);
|
||||||
|
let capped = scroll.max_height(100).add(&mut h.rsc);
|
||||||
|
h.set_root(capped);
|
||||||
|
h.move_to((200, 50));
|
||||||
|
|
||||||
|
// 400 of content in a viewport of 100, so 300 to scroll and the end
|
||||||
|
// showing: the top is 300 above the box, which the window centres.
|
||||||
|
assert_eq!(h.region(&scroll).unwrap().size().y, Px::from_int(100));
|
||||||
|
assert_corners!(h, top, (0, -250), (400, -50));
|
||||||
|
|
||||||
|
h.scroll((0, 1));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, top, (0, -200), (400, 0));
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//! What a background task can change, and how it gets back to the ui.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_task_update_reaches_the_tree() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let widget = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
h.set_root(widget.task_on(CursorSense::click(), async move |mut ctx| {
|
||||||
|
ctx.update(move |_, rsc| widget(rsc).color = Color::BLUE);
|
||||||
|
}));
|
||||||
|
|
||||||
|
h.click((200, 100));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
h.await_update(Duration::from_secs(5)),
|
||||||
|
"the task sent no update"
|
||||||
|
);
|
||||||
|
assert_eq!(h.rsc[widget].color, Color::BLUE);
|
||||||
|
}
|
||||||
File renamed without changes.
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,199 @@
|
|||||||
|
//! What the vertex shader's move-chain walk costs, against how many nested
|
||||||
|
//! region nodes a primitive resolves through.
|
||||||
|
//!
|
||||||
|
//! cargo test --release --test chain_cost -- --ignored --nocapture
|
||||||
|
//!
|
||||||
|
//! Timed on the GPU with timestamp queries rather than by the clock: wall time
|
||||||
|
//! here varied by 2x between runs of one unchanged binary. The pass is
|
||||||
|
//! submitted and waited on, so this is the GPU's cost and not the recording
|
||||||
|
//! loop's -- which is what `draw_cost.rs` measures instead.
|
||||||
|
//!
|
||||||
|
//! The instances are two pixels wide so that vertex work dominates; a chain
|
||||||
|
//! walk that does not show up against small quads will not show up against
|
||||||
|
//! anything.
|
||||||
|
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris_core::{
|
||||||
|
Len, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
|
||||||
|
UiRenderState, UiSpan,
|
||||||
|
};
|
||||||
|
use wgpu::{Color as GpuColor, *};
|
||||||
|
|
||||||
|
#[path = "gpu/mod.rs"]
|
||||||
|
mod gpu;
|
||||||
|
|
||||||
|
const SIZE: u32 = 1024;
|
||||||
|
const INSTANCES: usize = 200_000;
|
||||||
|
const FRAMES: u32 = 20;
|
||||||
|
/// Reported as the best of this many batches, since the mean moves by more
|
||||||
|
/// than the thing being measured.
|
||||||
|
const BATCHES: u32 = 8;
|
||||||
|
|
||||||
|
fn gpu() -> Option<(Device, Queue, f32)> {
|
||||||
|
let adapter = gpu::adapter()?;
|
||||||
|
if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
|
||||||
|
println!("no timestamp queries on {:?}", adapter.get_info().name);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
println!("adapter: {:?}", adapter.get_info().name);
|
||||||
|
let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor {
|
||||||
|
required_features: Features::TIMESTAMP_QUERY,
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.ok()?;
|
||||||
|
let period = queue.get_timestamp_period();
|
||||||
|
Some((device, queue, period))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A chain `depth` slots long, and instances that all resolve through its end.
|
||||||
|
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
||||||
|
let kind = ui.primitives.kind::<RectPrimitive>();
|
||||||
|
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
||||||
|
|
||||||
|
let mut slot = MoveIdx::NONE;
|
||||||
|
for _ in 0..depth {
|
||||||
|
slot = render.moves.push(slot, UiRegion::FULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
let px = |v: f32| Len::px(v);
|
||||||
|
for i in 0..INSTANCES {
|
||||||
|
let x = (i % (SIZE as usize / 2)) as f32 * 2.0;
|
||||||
|
let y = (i / (SIZE as usize / 2)) as f32;
|
||||||
|
render.layers.write(
|
||||||
|
0,
|
||||||
|
PrimitiveInst {
|
||||||
|
kind,
|
||||||
|
id,
|
||||||
|
primitive: RectPrimitive::color(UiColor::WHITE),
|
||||||
|
region: UiRegion::new(
|
||||||
|
UiSpan::new(px(x), px(x + 2.0)),
|
||||||
|
UiSpan::new(px(y), px(y + 1.0)),
|
||||||
|
),
|
||||||
|
mask_idx: MaskIdx::NONE,
|
||||||
|
move_idx: slot,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nanoseconds the pass took on the GPU, best of `BATCHES`.
|
||||||
|
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
|
||||||
|
let format = TextureFormat::Bgra8Unorm;
|
||||||
|
let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
|
||||||
|
let mut ui = UiData::default();
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
fill(&mut ui, &mut render, depth);
|
||||||
|
node.update(device, queue, &mut ui, &mut render);
|
||||||
|
|
||||||
|
let target = device.create_texture(&TextureDescriptor {
|
||||||
|
label: Some("chain cost"),
|
||||||
|
size: Extent3d {
|
||||||
|
width: SIZE,
|
||||||
|
height: SIZE,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: TextureDimension::D2,
|
||||||
|
format,
|
||||||
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let view = target.create_view(&TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
let queries = device.create_query_set(&QuerySetDescriptor {
|
||||||
|
label: Some("chain cost"),
|
||||||
|
ty: QueryType::Timestamp,
|
||||||
|
count: 2,
|
||||||
|
});
|
||||||
|
let resolved = device.create_buffer(&BufferDescriptor {
|
||||||
|
label: Some("resolved"),
|
||||||
|
size: 16,
|
||||||
|
usage: BufferUsages::QUERY_RESOLVE | BufferUsages::COPY_SRC,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
let readback = device.create_buffer(&BufferDescriptor {
|
||||||
|
label: Some("readback"),
|
||||||
|
size: 16,
|
||||||
|
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let frame = || {
|
||||||
|
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
|
||||||
|
{
|
||||||
|
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
|
||||||
|
label: None,
|
||||||
|
color_attachments: &[Some(RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: Operations {
|
||||||
|
load: LoadOp::Clear(GpuColor::BLACK),
|
||||||
|
store: StoreOp::Store,
|
||||||
|
},
|
||||||
|
depth_slice: None,
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
timestamp_writes: Some(RenderPassTimestampWrites {
|
||||||
|
query_set: &queries,
|
||||||
|
beginning_of_pass_write_index: Some(0),
|
||||||
|
end_of_pass_write_index: Some(1),
|
||||||
|
}),
|
||||||
|
occlusion_query_set: None,
|
||||||
|
multiview_mask: None,
|
||||||
|
});
|
||||||
|
node.draw(pass);
|
||||||
|
}
|
||||||
|
encoder.resolve_query_set(&queries, 0..2, &resolved, 0);
|
||||||
|
encoder.copy_buffer_to_buffer(&resolved, 0, &readback, 0, 16);
|
||||||
|
queue.submit(Some(encoder.finish()));
|
||||||
|
|
||||||
|
let slice = readback.slice(..);
|
||||||
|
slice.map_async(MapMode::Read, |_| {});
|
||||||
|
let _ = device.poll(PollType::Wait {
|
||||||
|
submission_index: None,
|
||||||
|
timeout: None,
|
||||||
|
});
|
||||||
|
let ns = {
|
||||||
|
let view = slice.get_mapped_range().expect("timestamps did not map");
|
||||||
|
let stamps: [u64; 2] = [
|
||||||
|
u64::from_le_bytes(view[..8].try_into().unwrap()),
|
||||||
|
u64::from_le_bytes(view[8..16].try_into().unwrap()),
|
||||||
|
];
|
||||||
|
(stamps[1].saturating_sub(stamps[0])) as f64 * period as f64
|
||||||
|
};
|
||||||
|
readback.unmap();
|
||||||
|
ns
|
||||||
|
};
|
||||||
|
|
||||||
|
frame();
|
||||||
|
let mut best = f64::MAX;
|
||||||
|
for _ in 0..BATCHES {
|
||||||
|
let mut total = 0.0;
|
||||||
|
for _ in 0..FRAMES {
|
||||||
|
total += frame();
|
||||||
|
}
|
||||||
|
best = best.min(total / FRAMES as f64);
|
||||||
|
}
|
||||||
|
best
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "measurement, not a check"]
|
||||||
|
fn chain_cost_by_depth() {
|
||||||
|
let Some((device, queue, period)) = gpu() else {
|
||||||
|
println!("no gpu with timestamps; nothing measured");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
println!("{INSTANCES} instances, {SIZE}x{SIZE}, best of {BATCHES} batches");
|
||||||
|
let mut base = None;
|
||||||
|
for depth in [1, 2, 4, 8, 16, 32, 64] {
|
||||||
|
let ns = pass_cost(&device, &queue, period, depth);
|
||||||
|
let base = *base.get_or_insert(ns);
|
||||||
|
println!(
|
||||||
|
"depth {depth:>3}: {:>9.1} us {:+6.1}% against depth 1",
|
||||||
|
ns / 1000.0,
|
||||||
|
(ns - base) / base * 100.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#[path = "scenario/mod.rs"]
|
||||||
|
mod scenario;
|
||||||
|
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris::random::{Edits, Plan, plan};
|
||||||
|
|
||||||
|
fn check_requests(edit: impl Fn(&mut Plan) + Sync) {
|
||||||
|
let count = scenario::env("IRIS_DEFERRED_SEEDS", 20_u64);
|
||||||
|
let depth = scenario::env("IRIS_DEFERRED_DEPTH", 4_usize);
|
||||||
|
let seeds = std::env::var("IRIS_DEFERRED_SEED")
|
||||||
|
.ok()
|
||||||
|
.and_then(|seed| seed.parse().ok())
|
||||||
|
.map_or_else(|| (1..=count).collect(), |seed| vec![seed]);
|
||||||
|
scenario::over_seeds(seeds, |seed| {
|
||||||
|
let mut grown = plan(seed, depth, &Edits::default());
|
||||||
|
edit(&mut grown);
|
||||||
|
for case in scenario::ALL {
|
||||||
|
if let Some(how) = scenario::diverges(&grown, case, seed) {
|
||||||
|
panic!(
|
||||||
|
"request seed {seed} depth {depth} after {}: {how}",
|
||||||
|
case.name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deferred_requests_agree_warm_and_cold() {
|
||||||
|
check_requests(|grown| {
|
||||||
|
let mut index = 0;
|
||||||
|
grown.walk_mut(&mut |node| {
|
||||||
|
if let Some(rules) = &mut node.size {
|
||||||
|
for axis in Axis::BOTH {
|
||||||
|
index += 1;
|
||||||
|
rules[axis] = match index % 7 {
|
||||||
|
0 => leftover(1).clamp(20, 120).into(),
|
||||||
|
1 => leftover(1).min(rel(0.5)).into(),
|
||||||
|
2 => (leftover(1) + px(30)).min(leftover(2)).into(),
|
||||||
|
_ => rules[axis].clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_intrinsic_bounds_agree_warm_and_cold() {
|
||||||
|
check_requests(|grown| {
|
||||||
|
grown.walk_mut(&mut |node| {
|
||||||
|
if let Some(rules) = &mut node.size {
|
||||||
|
for axis in Axis::BOTH {
|
||||||
|
rules[axis] = match rules[axis] {
|
||||||
|
SizeRule::Min(_) => SizeRule::Min(Len::rel(0.25)),
|
||||||
|
SizeRule::Max(_) => SizeRule::Max(Len::rel(0.75)),
|
||||||
|
SizeRule::Clamp { .. } => SizeRule::Clamp {
|
||||||
|
min: Len::rel(0.25),
|
||||||
|
max: Len::rel(0.75),
|
||||||
|
},
|
||||||
|
ref rule => rule.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+18
-44
@@ -4,69 +4,40 @@
|
|||||||
//!
|
//!
|
||||||
//! cargo test --release --test draw_cost -- --ignored --nocapture
|
//! cargo test --release --test draw_cost -- --ignored --nocapture
|
||||||
//!
|
//!
|
||||||
//! **Read the instruction count, not the clock.** Wall time here swings by 2x
|
//! Wall time is the wrong number to read for anything under a few percent --
|
||||||
//! between runs of one binary on this machine -- more under `cargo test` than
|
//! it varied by 2x between runs of one unchanged binary where instructions
|
||||||
//! run directly -- while instructions retired are stable to 0.1%:
|
//! retired varied by 0.1%. Count those instead:
|
||||||
//!
|
//!
|
||||||
//! perf stat -e instructions:u target/release/.../draw_cost-* --ignored
|
//! perf stat -e instructions:u target/release/.../draw_cost-* --ignored
|
||||||
//!
|
//!
|
||||||
//! Measured that way on 2026-09-13, drawing each primitive through its own
|
//! That is how `PrimitiveRender` was measured against a match in the renderer:
|
||||||
//! `PrimitiveRender` rather than a match in the renderer costs **6
|
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording
|
||||||
//! instructions per list drawn**, which is 0.1% of a frame at both 256 and
|
//! one.
|
||||||
//! 1024 layers. Recording one list into the pass costs wgpu ~5,400.
|
|
||||||
//!
|
|
||||||
//! The instance is leaked on purpose. Dropping the last one makes the Vulkan
|
|
||||||
//! loader unload Mesa's ICD, which faults when a thread that touched Vulkan
|
|
||||||
//! exits -- and libtest runs every test on a spawned thread.
|
|
||||||
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use iris_core::{
|
use iris_core::{
|
||||||
GlyphPrimitive, MaskIdx, PrimitiveInst, RectPrimitive, TextureHandle, TexturePrimitive, UiData,
|
GlyphPrimitive, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, TextureHandle,
|
||||||
UiRegion, UiRenderNode, UiRenderState,
|
TexturePrimitive, UiData, UiRegion, UiRenderNode, UiRenderState,
|
||||||
};
|
};
|
||||||
use wgpu::{Color as GpuColor, *};
|
use wgpu::{Color as GpuColor, *};
|
||||||
|
|
||||||
|
#[path = "gpu/mod.rs"]
|
||||||
|
mod gpu;
|
||||||
|
|
||||||
const SIZE: u32 = 1024;
|
const SIZE: u32 = 1024;
|
||||||
const FRAMES: u32 = 200;
|
const FRAMES: u32 = 200;
|
||||||
/// Reported as the best of this many batches. The mean moves by 15% between
|
/// Reported as the best of this many batches, since the mean moves by more
|
||||||
/// runs on this machine, which is more than the thing being measured.
|
/// than the thing being measured.
|
||||||
const BATCHES: u32 = 8;
|
const BATCHES: u32 = 8;
|
||||||
|
|
||||||
fn gpu() -> Option<(Device, Queue)> {
|
fn gpu() -> Option<(Device, Queue)> {
|
||||||
// Probed rather than assumed: this machine's Vulkan device comes and goes,
|
let adapter = gpu::adapter()?;
|
||||||
// and GL is what is left when it is gone.
|
|
||||||
let all = Instance::new(&InstanceDescriptor::default());
|
|
||||||
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
|
||||||
{
|
|
||||||
Ok(_) => all,
|
|
||||||
Err(_) => Instance::new(&InstanceDescriptor {
|
|
||||||
backends: Backends::GL,
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
// Leaked rather than dropped: see the note at the top of the file.
|
|
||||||
let instance: &'static Instance = Box::leak(Box::new(instance));
|
|
||||||
let adapter =
|
|
||||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
|
|
||||||
println!("adapter: {:?}", adapter.get_info());
|
println!("adapter: {:?}", adapter.get_info());
|
||||||
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
|
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config(format: TextureFormat) -> SurfaceConfiguration {
|
|
||||||
SurfaceConfiguration {
|
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
|
||||||
format,
|
|
||||||
width: SIZE,
|
|
||||||
height: SIZE,
|
|
||||||
present_mode: PresentMode::Fifo,
|
|
||||||
desired_maximum_frame_latency: 2,
|
|
||||||
alpha_mode: CompositeAlphaMode::Auto,
|
|
||||||
view_formats: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every layer draws all three primitives, so the renderer takes a different
|
/// Every layer draws all three primitives, so the renderer takes a different
|
||||||
/// path for each list it walks -- which is the case a single-primitive layer
|
/// path for each list it walks -- which is the case a single-primitive layer
|
||||||
/// would never exercise. Images are bound per instance, so there are few.
|
/// would never exercise. Images are bound per instance, so there are few.
|
||||||
@@ -95,6 +66,7 @@ fn fill(
|
|||||||
primitive: RectPrimitive::color(UiColor::WHITE),
|
primitive: RectPrimitive::color(UiColor::WHITE),
|
||||||
region: UiRegion::FULL,
|
region: UiRegion::FULL,
|
||||||
mask_idx: MaskIdx::NONE,
|
mask_idx: MaskIdx::NONE,
|
||||||
|
move_idx: MoveIdx::NONE,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
render.layers.write(
|
render.layers.write(
|
||||||
@@ -111,6 +83,7 @@ fn fill(
|
|||||||
},
|
},
|
||||||
region: UiRegion::FULL,
|
region: UiRegion::FULL,
|
||||||
mask_idx: MaskIdx::NONE,
|
mask_idx: MaskIdx::NONE,
|
||||||
|
move_idx: MoveIdx::NONE,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -123,6 +96,7 @@ fn fill(
|
|||||||
primitive: TexturePrimitive::from(h),
|
primitive: TexturePrimitive::from(h),
|
||||||
region: UiRegion::FULL,
|
region: UiRegion::FULL,
|
||||||
mask_idx: MaskIdx::NONE,
|
mask_idx: MaskIdx::NONE,
|
||||||
|
move_idx: MoveIdx::NONE,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -133,7 +107,7 @@ fn fill(
|
|||||||
|
|
||||||
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 {
|
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 {
|
||||||
let format = TextureFormat::Bgra8Unorm;
|
let format = TextureFormat::Bgra8Unorm;
|
||||||
let mut node = UiRenderNode::new(device, &config(format));
|
let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
|
||||||
let mut ui = UiData::default();
|
let mut ui = UiData::default();
|
||||||
let mut render = UiRenderState::new();
|
let mut render = UiRenderState::new();
|
||||||
let _handles = fill(&mut ui, &mut render, layers, per_layer);
|
let _handles = fill(&mut ui, &mut render, layers, per_layer);
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
//! Laying a tree out again has to land where growing it that way would.
|
||||||
|
//!
|
||||||
|
//! Every case is one of `scenario`'s, over the trees `iris::random` grows
|
||||||
|
//! from a seed. The fast test takes a handful of seeds and the ignored one
|
||||||
|
//! takes as many as it is asked for; both run the same cases the shrinker
|
||||||
|
//! does over the same trees, so a seed that fails here is reduced by
|
||||||
|
//!
|
||||||
|
//! SHRINK_SEED=<seed> SHRINK_DEPTH=<depth> SHRINK_CASE=<case> \
|
||||||
|
//! cargo test --release --test shrink -- --ignored --nocapture
|
||||||
|
//!
|
||||||
|
//! `IRIS_GENERATED_SEED`, `IRIS_GENERATED_SEEDS` and `IRIS_GENERATED_DEPTH`
|
||||||
|
//! select what the long run covers.
|
||||||
|
|
||||||
|
#[path = "scenario/mod.rs"]
|
||||||
|
mod scenario;
|
||||||
|
|
||||||
|
use iris::random::{Edits, Plan, plan};
|
||||||
|
use scenario::{ALL, Case, diverges, env, over_seeds};
|
||||||
|
|
||||||
|
/// How deep the generator branches. The generator widens two to four ways per
|
||||||
|
/// level, so depth is exponential in width and a deep narrow tree is not
|
||||||
|
/// reachable by raising this -- it buys more overlap between dependency
|
||||||
|
/// paths, not more ancestry.
|
||||||
|
fn depth() -> usize {
|
||||||
|
env("IRIS_GENERATED_DEPTH", 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The seeds the ordinary tests take: a corpus rather than a set of
|
||||||
|
/// regression cases, since a seed names a tree only for as long as the
|
||||||
|
/// generator draws the same things in the same order. Adding images to the
|
||||||
|
/// leaves moved every one of them, so 20 and 86 -- which once caught a widget
|
||||||
|
/// placed twice in a box its parent had already placed it in, and a `Scroll`
|
||||||
|
/// fixed point settling differently -- no longer grow those trees. Both
|
||||||
|
/// defects are pinned by the shrunk fixtures in `cases/unsettled.rs`, which
|
||||||
|
/// are trees rather than numbers.
|
||||||
|
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
|
||||||
|
|
||||||
|
fn check(seed: u64, depth: usize, case: Case) {
|
||||||
|
check_plan(&plan(seed, depth, &Edits::default()), seed, depth, case);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_plan(grown: &Plan, seed: u64, depth: usize, case: Case) {
|
||||||
|
if let Some(how) = diverges(grown, case, seed) {
|
||||||
|
panic!(
|
||||||
|
"seed {seed} at depth {depth} differs after {}: {how}\n\
|
||||||
|
reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \
|
||||||
|
SHRINK_CASE={} cargo test --release --test shrink -- --ignored --nocapture",
|
||||||
|
case.name(),
|
||||||
|
case.name(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A test per case, and the list of which cases have one, from the same
|
||||||
|
/// place. A case the ordinary suite leaves out runs only in the long scan,
|
||||||
|
/// which nobody runs by hand.
|
||||||
|
macro_rules! cases {
|
||||||
|
($($name:ident = $case:expr,)*) => {
|
||||||
|
$(
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
for seed in SEEDS {
|
||||||
|
check(seed, depth(), $case);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
|
||||||
|
const NAMED: [Case; [$($case,)*].len()] = [$($case,)*];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cases! {
|
||||||
|
many_widgets_redrawing_at_once_leaves_every_box_where_it_was = Case::RepaintSome,
|
||||||
|
everything_redrawing_at_once_leaves_every_box_where_it_was = Case::Repaint,
|
||||||
|
a_resize_lands_where_starting_at_that_size_would = Case::Resize,
|
||||||
|
a_resize_and_a_repaint_land_where_starting_that_way_would = Case::ResizeRepaint,
|
||||||
|
a_size_change_after_a_resize_lands_the_same_way = Case::ResizeSize,
|
||||||
|
a_resize_after_a_size_change_lands_the_same_way = Case::SizeResize,
|
||||||
|
a_size_change_lands_where_growing_it_that_way_would = Case::Size,
|
||||||
|
every_size_changing_at_once_lands_where_growing_it_that_way_would = Case::EverySize,
|
||||||
|
an_alignment_change_lands_where_growing_it_that_way_would = Case::Align,
|
||||||
|
giving_and_taking_a_movable_region_rebuilds_what_resolves_it = Case::RegionNode,
|
||||||
|
reordering_a_span_lands_where_growing_it_that_way_would = Case::Reorder,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The shuffles are one test between them, so they are the only cases `ALL`
|
||||||
|
/// may hold without a test of their own.
|
||||||
|
#[test]
|
||||||
|
fn every_case_runs_without_the_long_scan() {
|
||||||
|
for case in ALL {
|
||||||
|
assert!(
|
||||||
|
NAMED.contains(&case) || matches!(case, Case::Shuffle(_)),
|
||||||
|
"{} runs only in the long seed scan; give it a case here",
|
||||||
|
case.name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
|
||||||
|
for case in ALL {
|
||||||
|
if matches!(case, Case::Shuffle(_)) {
|
||||||
|
for seed in SEEDS {
|
||||||
|
check(seed, depth(), case);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "as many seeds as it is asked for, rather than the ten the others check"]
|
||||||
|
fn a_long_run_of_seeds_agrees() {
|
||||||
|
let depth = depth();
|
||||||
|
let seeds: Vec<u64> = match std::env::var("IRIS_GENERATED_SEED")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
{
|
||||||
|
Some(seed) => vec![seed],
|
||||||
|
None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(),
|
||||||
|
};
|
||||||
|
over_seeds(seeds, |seed| {
|
||||||
|
let grown = plan(seed, depth, &Edits::default());
|
||||||
|
for case in ALL {
|
||||||
|
check_plan(&grown, seed, depth, case);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
//! The adapter and the surface configuration the GPU measurement rigs share,
|
||||||
|
//! so the two cannot probe for a device in two different ways.
|
||||||
|
|
||||||
|
use wgpu::*;
|
||||||
|
|
||||||
|
/// An adapter on whatever this machine has, or `None` where there is none.
|
||||||
|
///
|
||||||
|
/// Probed rather than assumed: there may be no Vulkan adapter, and GL is what
|
||||||
|
/// is left when there is not.
|
||||||
|
///
|
||||||
|
/// The instance is leaked deliberately. A Vulkan loader may unload the driver
|
||||||
|
/// when the last one drops, which can fault as a thread that used it exits --
|
||||||
|
/// and every test runs on a spawned thread.
|
||||||
|
pub fn adapter() -> Option<Adapter> {
|
||||||
|
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
|
||||||
|
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
||||||
|
{
|
||||||
|
Ok(_) => all,
|
||||||
|
Err(_) => Instance::new(InstanceDescriptor {
|
||||||
|
backends: Backends::GL,
|
||||||
|
..InstanceDescriptor::new_without_display_handle()
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let instance: &'static Instance = Box::leak(Box::new(instance));
|
||||||
|
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config(format: TextureFormat, size: u32) -> SurfaceConfiguration {
|
||||||
|
SurfaceConfiguration {
|
||||||
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format,
|
||||||
|
color_space: SurfaceColorSpace::Auto,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
present_mode: PresentMode::Fifo,
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
alpha_mode: CompositeAlphaMode::Auto,
|
||||||
|
view_formats: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
//! Retained CPU-layout diagnostics on one reproducible random tree.
|
||||||
|
//!
|
||||||
|
//! Counters and phase timers:
|
||||||
|
//!
|
||||||
|
//! cargo test --release --features layout-diagnostics \
|
||||||
|
//! --test layout_diagnostics -- --ignored --nocapture
|
||||||
|
//!
|
||||||
|
//! Build the uninstrumented test with `cargo test --release --test
|
||||||
|
//! layout_diagnostics --no-run`, then run the emitted executable directly:
|
||||||
|
//!
|
||||||
|
//! IRIS_PHASE=resize IRIS_FRAMES=10000 perf stat -r 7 \
|
||||||
|
//! -e cycles:u,instructions:u /path/to/layout_diagnostics --ignored --nocapture
|
||||||
|
//!
|
||||||
|
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
|
||||||
|
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
|
||||||
|
//! `IRIS_DIRTY` how many widgets `many` marks at once. `IRIS_UNBOUNDED=1`
|
||||||
|
//! removes intrinsic bounds while preserving the rest of the generated tree.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris::random::{Edits, Tree, build, plan};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
|
||||||
|
|
||||||
|
/// A scroll whose content fits is the same drawing in every box it still
|
||||||
|
/// fits in, so a longer or shorter one relays out nothing. Where the content
|
||||||
|
/// sits in that box is decided by placing its answer in the whole of it,
|
||||||
|
/// which is a fraction of the box and holds at every length -- so the
|
||||||
|
/// contract must not turn on the alignment. It did, and at the default
|
||||||
|
/// alignment, which is the middle, every box change redrew the scroll.
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
#[test]
|
||||||
|
fn a_fitting_scroll_holds_for_every_box_its_content_fits_in() {
|
||||||
|
use iris::core::layout_diagnostics as diag;
|
||||||
|
|
||||||
|
for align in [Align::TOP_LEFT, Align::CENTER, Align::BOT_RIGHT] {
|
||||||
|
let mut harness = Harness::new((400, 200));
|
||||||
|
let inner = rect(Color::RED).height(50).add(&mut harness.rsc);
|
||||||
|
harness.set_root(inner.scrollable().align(align));
|
||||||
|
harness.frame();
|
||||||
|
let _ = diag::take();
|
||||||
|
// Still far longer than the 50 the content needs.
|
||||||
|
harness.resize((400, 180));
|
||||||
|
harness.frame();
|
||||||
|
assert_eq!(diag::take().distinct_widgets(), 0, "{align:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
#[test]
|
||||||
|
fn a_selected_widget_retains_its_layout_events() {
|
||||||
|
use iris::core::layout_diagnostics::{self as diagnostics, TraceEvent};
|
||||||
|
|
||||||
|
diagnostics::clear_traced_widgets();
|
||||||
|
let _ = diagnostics::take();
|
||||||
|
let mut harness = Harness::new((400, 200));
|
||||||
|
let leaf = rect(Color::RED).region_node().add(&mut harness.rsc);
|
||||||
|
let other = rect(Color::BLUE).add(&mut harness.rsc);
|
||||||
|
let root = (leaf, other).span(Dir::RIGHT).add(&mut harness.rsc);
|
||||||
|
harness.set_root(root);
|
||||||
|
diagnostics::trace_widget(leaf.id());
|
||||||
|
let _ = diagnostics::take();
|
||||||
|
|
||||||
|
harness.rsc.widgets_mut().mark_for_redraw(root.id());
|
||||||
|
harness.rsc.widgets_mut().mark_for_redraw(leaf.id());
|
||||||
|
harness.frame();
|
||||||
|
|
||||||
|
let report = diagnostics::take();
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.traces()
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, TraceEvent::RegionNode { id, .. } if *id == leaf.id()))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.traces()
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, TraceEvent::DrawRequest { id, .. } if *id == leaf.id()))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.traces()
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, TraceEvent::SizeRead { id, .. } if *id == leaf.id()))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.traces()
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, TraceEvent::SizeReported { id, .. } if *id == leaf.id()))
|
||||||
|
);
|
||||||
|
diagnostics::clear_traced_widgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||||
|
std::env::var(name)
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.parse().ok())
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
fn trace_selected(tree: &Tree) {
|
||||||
|
let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let index = value
|
||||||
|
.parse::<usize>()
|
||||||
|
.expect("IRIS_TRACE_INDEX must be a tree.ids index");
|
||||||
|
let id = tree.ids[index];
|
||||||
|
iris::core::layout_diagnostics::trace_widget(id);
|
||||||
|
println!("tracing tree.ids[{index}] = {id:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "layout-diagnostics"))]
|
||||||
|
fn trace_selected(_: &Tree) {}
|
||||||
|
|
||||||
|
/// The shape a cost is measured on must not depend on what layout measured,
|
||||||
|
/// or two commits are compared on two different trees. See `Edits`.
|
||||||
|
fn rig_edits() -> Edits {
|
||||||
|
Edits {
|
||||||
|
fixed_branches: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture(harness: &mut Harness, seed: u64, depth: usize) -> (StrongWidget, Tree) {
|
||||||
|
let mut plan = plan(seed, depth, &rig_edits());
|
||||||
|
if env("IRIS_UNBOUNDED", 0_u8) != 0 {
|
||||||
|
plan.drop_bounds();
|
||||||
|
}
|
||||||
|
build(&mut harness.rsc, &plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
||||||
|
let mut harness = Harness::new(OUTPUT);
|
||||||
|
let (root, tree) = fixture(&mut harness, seed, depth);
|
||||||
|
harness.state.root = Some(root);
|
||||||
|
harness.frame();
|
||||||
|
println!(
|
||||||
|
"fixture: seed {seed}, depth {depth}, {} widgets, {} active",
|
||||||
|
tree.ids.len(),
|
||||||
|
harness.render.active_widgets()
|
||||||
|
);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _ = iris::core::layout_diagnostics::take();
|
||||||
|
(harness, tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
|
||||||
|
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
|
let frames = elapsed.len();
|
||||||
|
// The worst frame is the stutter somebody sees, so it goes beside the
|
||||||
|
// median; p99 says whether it is the load or a single interruption.
|
||||||
|
println!(
|
||||||
|
"{label}: {frames} frame(s), min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
|
||||||
|
max {:.3} ms, total {:.1} ms",
|
||||||
|
elapsed[0],
|
||||||
|
elapsed[frames / 2],
|
||||||
|
elapsed[frames * 99 / 100],
|
||||||
|
elapsed[frames - 1],
|
||||||
|
elapsed.iter().sum::<f64>(),
|
||||||
|
);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
{
|
||||||
|
let diagnostics = iris::core::layout_diagnostics::take();
|
||||||
|
print!("{}", diagnostics.per_frame(frames));
|
||||||
|
for event in diagnostics.traces() {
|
||||||
|
println!(" {event:?}");
|
||||||
|
}
|
||||||
|
for callsite in diagnostics.hot_text().iter().take(3) {
|
||||||
|
let mut ancestry = Vec::new();
|
||||||
|
let mut id = Some(callsite.id);
|
||||||
|
while let Some(widget) = id {
|
||||||
|
ancestry.push(_harness.rsc.widgets().label(widget).as_str());
|
||||||
|
id = _harness
|
||||||
|
.render
|
||||||
|
.active
|
||||||
|
.get(&widget)
|
||||||
|
.and_then(|active| active.parent);
|
||||||
|
}
|
||||||
|
println!(" text ancestry: {}", ancestry.join(" < "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
label: &str,
|
||||||
|
frames: usize,
|
||||||
|
harness: &mut Harness,
|
||||||
|
mut change: impl FnMut(&mut Harness, usize),
|
||||||
|
) {
|
||||||
|
let mut elapsed = Vec::with_capacity(frames);
|
||||||
|
for frame in 0..frames {
|
||||||
|
change(harness, frame);
|
||||||
|
let start = Instant::now();
|
||||||
|
harness.frame();
|
||||||
|
elapsed.push(start.elapsed().as_secs_f64() * 1_000.0);
|
||||||
|
}
|
||||||
|
report(label, elapsed, harness);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "measurement, not a check"]
|
||||||
|
fn layout_cost() {
|
||||||
|
let seed = env("IRIS_SEED", 1_u64);
|
||||||
|
let depth = env("IRIS_DEPTH", 7_usize);
|
||||||
|
let frames = env("IRIS_FRAMES", 100_usize);
|
||||||
|
assert!(frames > 0, "IRIS_FRAMES must be greater than zero");
|
||||||
|
let phase = env("IRIS_PHASE", String::from("all"));
|
||||||
|
assert!(
|
||||||
|
["all", "cold", "repaint", "many", "size", "scroll", "resize"].contains(&phase.as_str()),
|
||||||
|
"unknown IRIS_PHASE {phase:?}"
|
||||||
|
);
|
||||||
|
let selected = |name| phase == "all" || phase == name;
|
||||||
|
|
||||||
|
if selected("cold") {
|
||||||
|
let mut harness = Harness::new(OUTPUT);
|
||||||
|
let (root, tree) = fixture(&mut harness, seed, depth);
|
||||||
|
harness.state.root = Some(root);
|
||||||
|
println!(
|
||||||
|
"fixture: seed {seed}, depth {depth}, {} widgets",
|
||||||
|
tree.ids.len()
|
||||||
|
);
|
||||||
|
trace_selected(&tree);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _ = iris::core::layout_diagnostics::take();
|
||||||
|
run("cold", 1, &mut harness, |_, _| {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected("repaint") {
|
||||||
|
let (mut harness, tree) = warm(seed, depth);
|
||||||
|
trace_selected(&tree);
|
||||||
|
let leaf = tree.ids[0];
|
||||||
|
run("repaint", frames, &mut harness, move |harness, _| {
|
||||||
|
harness.rsc.widgets_mut().mark_for_redraw(leaf);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected("many") {
|
||||||
|
let (mut harness, tree) = warm(seed, depth);
|
||||||
|
trace_selected(&tree);
|
||||||
|
// Spread through the tree rather than taken from one subtree, so the
|
||||||
|
// dependency paths the frame settles overlap.
|
||||||
|
let wanted = env("IRIS_DIRTY", 32_usize).max(1);
|
||||||
|
let step = (tree.ids.len() / wanted).max(1);
|
||||||
|
let dirty: Vec<_> = tree.ids.iter().copied().step_by(step).collect();
|
||||||
|
println!("marking {} of {} widgets", dirty.len(), tree.ids.len());
|
||||||
|
run("many", frames, &mut harness, move |harness, _| {
|
||||||
|
for &id in &dirty {
|
||||||
|
harness.rsc.widgets_mut().mark_for_redraw(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected("size") {
|
||||||
|
let (mut harness, tree) = warm(seed, depth);
|
||||||
|
trace_selected(&tree);
|
||||||
|
let sized = tree.sized[0];
|
||||||
|
run("size", frames, &mut harness, move |harness, frame| {
|
||||||
|
let len = LayoutLen::px(100.0 + (frame % 2) as f32 * 40.0);
|
||||||
|
harness
|
||||||
|
.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(sized, Axis::X, SizeRule::Exact(len));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected("scroll") {
|
||||||
|
let (mut harness, tree) = warm(seed, depth);
|
||||||
|
trace_selected(&tree);
|
||||||
|
let scroll = tree.scrolls[0];
|
||||||
|
run("scroll", frames, &mut harness, move |harness, frame| {
|
||||||
|
harness.rsc[scroll].scroll(if frame % 2 == 0 { 12.0 } else { -12.0 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected("resize") {
|
||||||
|
let (mut harness, tree) = warm(seed, depth);
|
||||||
|
trace_selected(&tree);
|
||||||
|
run("resize", frames, &mut harness, |harness, frame| {
|
||||||
|
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
#[test]
|
||||||
|
fn repainting_measured_text_does_not_invalidate_its_span() {
|
||||||
|
use iris::core::layout_diagnostics as diag;
|
||||||
|
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let text = wtext("a paragraph that fits").wrap(true).add(&mut h.rsc);
|
||||||
|
h.set_root((text, wtext("another paragraph")).span(Dir::DOWN));
|
||||||
|
let _ = diag::take();
|
||||||
|
h.rsc.widgets_mut().mark_for_redraw(text);
|
||||||
|
h.frame();
|
||||||
|
let report = diag::take();
|
||||||
|
assert_eq!(report.distinct_widgets(), 1);
|
||||||
|
assert_eq!(report.hot_widgets()[0].id, text.id());
|
||||||
|
}
|
||||||
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user