Compare commits

..
Author SHA1 Message Date
iris 7dc7614ae6 Swap two hover buffers, and drive the tests from the harness
The set of hovered widgets is two vectors that trade places, so an input
allocates nothing once they have grown, instead of building a fresh one
each time.

`run_sensors` no longer takes a `window_size`: `UiRenderState` already
holds the output size, and passing it back in was one more thing that
could disagree.

`tests/pointer_routing.rs` drops its own `Rsc`, event manager and layer
scaffolding for `iris::harness`, which is what it was standing in for.
Presses now arrive as a move and then a press, and a scroll after the
hover that precedes it, because that is what the harness delivers and
what a window does.
2026-09-13 21:58:39 -04:00
iris e865467a3f Merge upstream/main (#15) into split/12-pointer-routing 2026-09-13 21:55:53 -04:00
iris 23376aef25 Say what these do without naming what calls them
A doc comment that describes another function goes stale when that
function changes, and nobody editing it looks here.
2026-09-13 21:37:21 -04:00
iris 36fec09d11 Track who is hovered, apart from what consumes
Hover was per-sensor state that only changed when the walk reached that
sensor, so ending it depended on the walk, which consumption cuts short.
`CursorSenses` now keeps the set of widgets the cursor was inside, in a
new `Event::Global` slot for state a whole event type owns rather than
each widget -- which is also where the input restructure keeps its pointer
capture.

The walk visits only widgets the cursor is inside and stops at the layer
that consumes, as before. Whoever was in the set and is not now has been
left or covered, and gets its `HoverEnd` afterwards, however early the
walk stopped.

Two things fall out. `SensorState` is gone: whether a hover is starting,
on or ending is the difference between the two sets. And the consumption
line loses its `&& in_shape`, since being inside is now the reason the
widget is looked at rather than something to test again.

Nine tests, five of which fail on `main`. `hover_starts_and_ends_once_each`
pins the lifecycle, and `covering_a_widget_ends_its_hover` now returns the
cursor so an uncovered widget hovers again.
2026-09-13 21:34:01 -04:00
iris 5494642dec End the hover of a widget that gets covered
Breaking out of the layer loop left every sensor below the consuming
layer untouched, so one that was hovered stayed hovered: moving onto a
widget in a layer above never ended the hover of what it covered, and
nothing ever would.

Consumption now carries into the hit test rather than stopping the walk.
A covered widget is simply not in shape, so its hover ends and its
`HoverEnd` runs; `should_run` already refuses non-position senses once
the hover is not on, so nothing else reaches it. It is applied after a
layer rather than during one, so senses on the same layer still do not
block each other.
2026-09-13 21:27:35 -04:00
iris 8cac927438 Pin the hover-then-scroll case, and say what the line means
A wheel makes `position_only` false, so a button already hovered in a
layer above does not consume it -- but the line read as though it might.
`hovering_a_button_above_does_not_stop_a_later_scroll` is that case in the
two frames a window actually delivers it in, and the comment now leads
with it. `resting` is renamed to `position_only`, so the same word is used
throughout.
2026-09-13 21:07:48 -04:00
iris 827d317f41 Report consumption from run_event
`Event::consumes` says whether having run uses up what triggered it,
defaulting to no. `run_fn` already calls `should_run` per registration,
so it ors that across everything that ran and hands it back through
`run_event`. `CursorSenses` answers it with the sense it matched: a press
or a scroll is used up, hovering is not.

That drops `TypeEventManager::registered` and the second pass over a
widget's senses -- the match that decides consumption is now the same one
that decides whether the handler runs.

A cursor that is only resting still stops at the layer it is over, which
`run_event` cannot report because nothing need answer for it to be true.
It must not stop at a widget it has merely left, though, or ending a hover
above blocks the hover below: `leaving_a_widget_does_not_block_the_layer_below`
is that case, and it fails on `main` too.
2026-09-13 20:53:34 -04:00
iris e53ce585e6 Say position-only, and stop falsifying the cursor
`is_momentary` becomes `position_only` on both the sense and the cursor,
inverted so it reads as what it tests.

A widget the cursor has left was being handed a blanked cursor so its
press senses would not match. `should_run` now skips non-position senses
when the pointer is not inside, which is the same rule without lying
about the input: the widget still gets the real cursor with its hover
ending.

`consumes` loses its `momentary` argument, since the cursor answers that
itself.
2026-09-13 20:43:38 -04:00
iris f3fd9417d4 Consume by layer, not by widget
Replaces the taking mechanism with `CursorSenses::consumes`, which
decides only whether a layer stops the input reaching the layer below.
Nothing is removed from the cursor, and senses on one layer no longer
block each other: every sensor the pointer is inside runs.

Where the cursor rests stops at the top layer under it. Something
happening to the cursor stops only at a widget that answers to it, so a
click-only child does not swallow a scroll -- which is what `main` gets
wrong, where any hovered sensor blocks the layer below.

A widget the cursor has left still hears its hover ending, but is handed
no press or scroll: that input landed somewhere else. This is a hit test
rather than a consumption rule, and without it a press beside a button
fires the button it just left.

`a_click_and_a_scroll_in_one_frame_go_to_different_widgets` goes with the
per-kind taking it tested. Of the five that remain, two fail on `main`.
2026-09-13 20:20:25 -04:00
iris 3ab9c922fd Merge upstream/main (#14) into split/12-pointer-routing 2026-09-13 20:17:06 -04:00
irisandClaude Opus 5 71ba3723ff Keep momentary input on the widget the cursor is on
Tests across layers, as asked, and the fifth one found a defect older than
this branch: a press fired on a widget the cursor had just left, because the
frame its hover ends is a frame it still gets dispatched on, and `should_run`
only ever looked at the cursor. A button in the corner of a list therefore
clicked when the press landed anywhere else in the row.

A widget that is not under the cursor now sees a cursor with nothing
momentary in it, which settles both halves of the question at once: it is not
its press to receive, and not its press to take from the layers below.

`CursorSense` and `CursorButton` derive `Debug`, so a failure says which
sense fired rather than `left != right`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 19:22:56 -04:00
iris 0a14df2cc3 Merge canonical main after the wgpu 30 upgrade 2026-09-13 19:16:16 -04:00
irisandClaude Opus 5 0e7076a01c Take input per kind, rather than deciding it once a frame
Reviewing this against the process we agreed: the title claimed per-kind
routing and the code decided it once for the whole frame. A scroll and a
click in the same frame both went to the button, because a widget that
matched any momentary sense consumed everything.

Consumption is now removing an input from the cursor the layers below see.
`CursorSense::take` states what each sense takes -- exhaustively, so a new
sense has to answer the question rather than inherit a default -- and
`is_momentary` is gone with the enumeration it was written on. `should_run`
and consumption share one matcher instead of two copies of the table.

Two tests, each checked to fail without the change: a click and a scroll in
one frame reach different widgets, and leaving a widget still ends its hover.
The second is a regression this review caught in its own first draft, where
the skip condition used `is_off`, which counts `End` -- the one frame a
hover-end handler has to run on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 19:09:50 -04:00
iris f62131eecf Merge canonical main after #11 2026-09-13 19:03:03 -04:00
iris 028521b419 Route pointer input per kind, so a scroll falls through a hovered button
`run_sensors` decided that a widget had consumed the frame's input from
hover alone: if the cursor was inside its shape, no lower layer saw
anything. So a button sitting over a list swallowed the list's scroll,
having registered nothing but `click()`.

Being in shape still runs a widget -- a hover highlight has to fire on the
topmost thing under the cursor regardless -- but consuming is now judged
per input kind. With nothing momentary happening the behaviour is
unchanged and the topmost widget wins the hover; with a scroll or a press
happening, only a widget that registered a matching momentary sense
consumes it.

`TypeEventManager::registered` is what makes that askable: what a widget
would match is a different question from dispatching to it, and `run_fn`
can only answer the second.

tests/pointer_routing.rs drives `run_sensors` directly, with no GPU and no
window. It fails on the unfixed code with "a scroll over the button must
still reach the list underneath it".
2026-09-13 04:01:22 -04:00
37 changed files with 628 additions and 1428 deletions

No files matched your search

Generated
+16 -25
View File
@@ -567,9 +567,9 @@ dependencies = [
[[package]] [[package]]
name = "dlib" name = "dlib"
version = "0.5.3" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [ dependencies = [
"libloading", "libloading",
] ]
@@ -2158,9 +2158,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.41.0" version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -2353,15 +2353,6 @@ version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
[[package]]
name = "rig-input"
version = "0.1.0"
dependencies = [
"iris",
"wayland-client",
"wayland-protocols-wlr",
]
[[package]] [[package]]
name = "roxmltree" name = "roxmltree"
version = "0.21.1" version = "0.21.1"
@@ -2918,9 +2909,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-backend" name = "wayland-backend"
version = "0.3.17" version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9"
dependencies = [ dependencies = [
"cc", "cc",
"downcast-rs", "downcast-rs",
@@ -2932,9 +2923,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-client" name = "wayland-client"
version = "0.31.15" version = "0.31.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"rustix 1.1.3", "rustix 1.1.3",
@@ -2966,9 +2957,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-protocols" name = "wayland-protocols"
version = "0.32.13" version = "0.32.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"wayland-backend", "wayland-backend",
@@ -2991,9 +2982,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-protocols-wlr" name = "wayland-protocols-wlr"
version = "0.3.12" version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"wayland-backend", "wayland-backend",
@@ -3004,9 +2995,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-scanner" name = "wayland-scanner"
version = "0.31.11" version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quick-xml", "quick-xml",
@@ -3015,9 +3006,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-sys" name = "wayland-sys"
version = "0.31.11" version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd"
dependencies = [ dependencies = [
"dlib", "dlib",
"log", "log",
+1 -3
View File
@@ -20,7 +20,7 @@ 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", "rig-input"] members = ["core", "macro"]
[workspace.package] [workspace.package]
version = "0.1.0" version = "0.1.0"
@@ -40,5 +40,3 @@ 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"] }
+23 -55
View File
@@ -56,6 +56,13 @@ impl UiVec2 {
} }
} }
pub const fn outside(&self, region: &UiRegion) -> UiVec2 {
UiVec2 {
x: self.x.outside(&region.x),
y: self.y.outside(&region.y),
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar { pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar {
match axis { match axis {
Axis::X => &mut self.x, Axis::X => &mut self.x,
@@ -202,12 +209,10 @@ impl UiScalar {
} }
} }
/// Undoes `within`, and `None` where the span has a fixed length: every pub const fn outside(&self, span: &UiSpan) -> Self {
/// fraction of it lands on the same `rel`, so none can be told apart. let rel = self.rel.lerp_inv(span.start.rel, span.end.rel);
pub fn outside(&self, span: &UiSpan) -> Option<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); let abs = self.abs - rel.lerp(span.start.abs, span.end.abs);
Some(Self { rel, abs }) Self { rel, abs }
} }
pub fn within_len(&self, len: UiScalar) -> Self { pub fn within_len(&self, len: UiScalar) -> Self {
@@ -278,11 +283,11 @@ impl UiSpan {
} }
} }
pub fn outside(&self, parent: &Self) -> Option<Self> { pub const fn outside(&self, parent: &Self) -> Self {
Some(Self { Self {
start: self.start.outside(parent)?, start: self.start.outside(parent),
end: self.end.outside(parent)?, end: self.end.outside(parent),
}) }
} }
pub const fn len(&self) -> UiScalar { pub const fn len(&self) -> UiScalar {
@@ -319,7 +324,14 @@ impl UiRegion {
y: self.y.within(&parent.y), y: self.y.within(&parent.y),
} }
} }
pub const fn axis(&self, axis: Axis) -> &UiSpan { 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 { match axis {
Axis::X => &self.x, Axis::X => &self.x,
Axis::Y => &self.y, Axis::Y => &self.y,
@@ -397,50 +409,6 @@ impl UiRegion {
} }
} }
/// Taking a drawing out of one box and putting it in another, checked once
/// for a whole subtree so that applying it cannot fail.
///
/// A box of a fixed length holds each part as an offset from its start rather
/// than as a fraction of it, so those parts can be carried to a box of the
/// same length but never stretched to a different one.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Remap {
from: UiRegion,
to: UiRegion,
}
impl Remap {
pub fn new(from: UiRegion, to: UiRegion) -> Option<Self> {
[Axis::X, Axis::Y]
.into_iter()
.all(|axis| {
let (from, to) = (from.axis(axis), to.axis(axis));
from.start.rel != from.end.rel || from.len() == to.len()
})
.then_some(Self { from, to })
}
pub fn apply(&self, region: UiRegion) -> UiRegion {
UiRegion {
x: Self::span(region.x, self.from.x, self.to.x),
y: Self::span(region.y, self.from.y, self.to.y),
}
}
fn span(span: UiSpan, from: UiSpan, to: UiSpan) -> UiSpan {
match span.outside(&from) {
Some(out) => out.within(&to),
// `new` admits this only where the two are the same length, so
// the difference between their starts is the whole move.
None => {
let mut span = span;
span.shift(to.start - from.start);
span
}
}
}
}
impl Display for UiRegion { impl Display for UiRegion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
+1 -7
View File
@@ -1,20 +1,14 @@
use crate::{LayerId, MaskIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId}; use crate::{LayerId, MaskIdx, PrimitiveHandle, TextureHandle, UiRegion, WidgetId};
/// important non rendering data for retained drawing /// important non rendering data for retained drawing
#[derive(Debug)] #[derive(Debug)]
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
pub region: UiRegion, pub region: UiRegion,
/// What the widget said it used of `region`, the last time it drew.
pub size: Size,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>, pub primitives: Vec<PrimitiveHandle>,
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>,
/// Whether it read the output's size, and so is wrong when that changes.
pub reads_output: bool,
pub mask: MaskIdx, pub mask: MaskIdx,
pub layer: LayerId, pub layer: LayerId,
} }
+18
View File
@@ -0,0 +1,18 @@
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();
}
}
+3
View File
@@ -3,12 +3,15 @@ use crate::{
}; };
mod active; mod active;
mod cache;
mod painter; mod painter;
mod render_state; mod render_state;
mod size;
pub use active::*; pub use active::*;
pub use painter::{Painter, PrimitiveLike}; pub use painter::{Painter, PrimitiveLike};
pub use render_state::*; pub use render_state::*;
pub use size::*;
#[derive(Default)] #[derive(Default)]
pub struct UiData { pub struct UiData {
+24 -69
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind, GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
TexturePrimitive, TexturePrimitive,
@@ -18,9 +18,6 @@ pub struct Painter<'a> {
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>, pub(super) primitives: Vec<PrimitiveHandle>,
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>,
pub(super) reads_output: bool,
pub layer: usize, pub layer: usize,
pub(super) id: WidgetId, pub(super) id: WidgetId,
} }
@@ -71,31 +68,19 @@ impl<'a> Painter<'a> {
} }
/// Draws a widget within this widget's region. /// Draws a widget within this widget's region.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> { pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.widget_at(id, self.region) self.widget_at(id, self.region);
} }
/// Draws a widget somewhere within this one. Drawing one a second time /// Draws a widget somewhere within this one.
/// gives it a new box, keeping the drawing it already has where it can. /// Useful for drawing child widgets in select areas.
pub fn widget_within<'s, W: ?Sized>( pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
&'s mut self, self.widget_at(id, region.within(&self.region));
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
let region = region.within(&self.region);
self.widget_at(id, region)
} }
fn widget_at<'s, W: ?Sized>( fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
// A child listed twice would be moved twice.
if !self.children.contains(&id.id()) {
self.children.push(id.id()); self.children.push(id.id());
} self.state.draw_inner(
let size = self.state.draw_inner(
self.layer, self.layer,
id.id(), id.id(),
region, region,
@@ -104,25 +89,6 @@ impl<'a> Painter<'a> {
None, None,
self.rsc, self.rsc,
); );
DrawResult {
child: id,
painter: self,
size,
}
}
/// What a child says its length is without being drawn, if it can say.
/// Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
let hint = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis)?;
self.depend_on_size(id);
Some(hint)
}
fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id());
}
} }
pub fn render_text( pub fn render_text(
@@ -163,17 +129,22 @@ impl<'a> Painter<'a> {
self.region self.region
} }
/// The output's size in pixels. A widget that reads it draws again when pub fn size<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>) -> Size {
/// the output changes, since nothing else can put that right. self.size_ctx().size(id)
pub fn output_size(&mut self) -> Vec2 { }
self.reads_output = true;
pub fn len_axis<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Len {
match axis {
Axis::X => self.size_ctx().width(id),
Axis::Y => self.size_ctx().height(id),
}
}
pub fn output_size(&self) -> Vec2 {
self.state.output_size self.state.output_size
} }
/// This widget's box in pixels. Resolved against the output's size, so a
/// widget that reads it draws again when the output changes.
pub fn px_size(&mut self) -> Vec2 { pub fn px_size(&mut self) -> Vec2 {
self.reads_output = true;
self.region.size().to_abs(self.state.output_size) self.region.size().to_abs(self.state.output_size)
} }
@@ -196,25 +167,9 @@ impl<'a> Painter<'a> {
pub fn id(&self) -> &WidgetId { pub fn id(&self) -> &WidgetId {
&self.id &self.id
} }
}
/// A child that has just been drawn. Reading its size records that this pub fn size_ctx(&mut self) -> SizeCtx<'_> {
/// widget's own size depends on it; dropping it without reading draws the self.state.size_ctx(self.id, self.region.size(), self.rsc)
/// child and leaves the parent independent of what it came to.
pub struct DrawResult<'p, 'a, W: ?Sized> {
painter: &'p mut Painter<'a>,
child: &'p StrongWidget<W>,
size: Size,
}
impl<W: ?Sized> DrawResult<'_, '_, W> {
pub fn size(self) -> Size {
self.painter.depend_on_size(self.child);
self.size
}
pub fn len(self, axis: Axis) -> Len {
self.size().axis(axis)
} }
} }
+61 -123
View File
@@ -1,6 +1,7 @@
use crate::{ use crate::{
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, OnResize, Painter, PixelRegion, Remap, Size, ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget,
StrongWidget, UiRegion, UiRsc, WidgetId, Widgets, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
ui::cache::Cache,
util::{HashMap, HashSet, Vec2, forget_ref}, util::{HashMap, HashSet, Vec2, forget_ref},
}; };
@@ -8,6 +9,7 @@ pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>, pub active: HashMap<WidgetId, ActiveData>,
pub layers: DrawLayers, pub layers: DrawLayers,
pub(super) output_size: Vec2, pub(super) output_size: Vec2,
pub cache: Cache,
old_root: Option<WidgetId>, old_root: Option<WidgetId>,
resized: bool, resized: bool,
@@ -19,6 +21,7 @@ impl UiRenderState {
Self { Self {
active: Default::default(), active: Default::default(),
layers: Default::default(), layers: Default::default(),
cache: Default::default(),
output_size: Vec2::ZERO, output_size: Vec2::ZERO,
old_root: None, old_root: None,
resized: false, resized: false,
@@ -53,21 +56,11 @@ impl UiRenderState {
); );
} }
let root = root.into(); let root = root.into();
if self.root_changed(root) { if self.needs_full_redraw(root) {
self.redraw_all(root, rsc); self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id()); self.old_root = root.map(|r| r.id());
} else if self.resized {
// A region is a fraction of the output plus an offset, resolved
// against the window in the shader, so a resize moves the whole
// drawing on its own. Only a widget that read pixels can be wrong.
for (&id, active) in &self.active {
if active.reads_output {
rsc.widgets_mut().needs_redraw.insert(id);
}
}
}
self.resized = false; self.resized = false;
if rsc.widgets().has_updates() { } else if rsc.widgets().has_updates() {
self.redraw_updates(rsc); self.redraw_updates(rsc);
} }
} }
@@ -91,11 +84,19 @@ impl UiRenderState {
mask: MaskIdx, mask: MaskIdx,
old_children: Option<Vec<WidgetId>>, old_children: Option<Vec<WidgetId>>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> Size { ) {
let mut old_children = old_children.unwrap_or_default(); let mut old_children = old_children.unwrap_or_default();
if self.active.contains_key(&id) { if let Some(active) = self.active.get_mut(&id)
if let Some(size) = self.try_reuse(id, region, rsc) { && !rsc.widgets().needs_redraw.contains(&id)
return size; {
// check to see if we can skip drawing first
if active.region == region {
return;
} else if active.region.size() == region.size() {
// TODO: epsilon?
let from = active.region;
self.mov(id, from, region);
return;
} }
// if not, then maintain resize and track old children to remove unneeded // if not, then maintain resize and track old children to remove unneeded
let active = self.remove(id, false, rsc).unwrap(); let active = self.remove(id, false, rsc).unwrap();
@@ -103,7 +104,6 @@ impl UiRenderState {
} }
// draw widget // draw widget
rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.insert(id); self.draw_started.insert(id);
let mut painter = Painter { let mut painter = Painter {
@@ -115,13 +115,11 @@ impl UiRenderState {
textures: Vec::new(), textures: Vec::new(),
primitives: Vec::new(), primitives: Vec::new(),
children: Vec::new(), children: Vec::new(),
size_deps: Vec::new(),
reads_output: false,
rsc, rsc,
}; };
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id); let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
let size = widget.draw(&mut painter); widget.draw(&mut painter);
drop(widget); drop(widget);
let Painter { let Painter {
@@ -132,29 +130,18 @@ impl UiRenderState {
textures, textures,
primitives, primitives,
children, children,
size_deps,
reads_output,
layer, layer,
id, id,
} = painter; } = painter;
debug_assert!(
Self::hints_agree(id, size, rsc),
"'{}' ({id:?}) drew a size its size_hint disagrees with",
rsc.widgets().label(id)
);
// add to active // add to active
let active = ActiveData { let active = ActiveData {
id, id,
region, region,
size,
parent, parent,
textures, textures,
primitives, primitives,
children, children,
size_deps,
reads_output,
mask, mask,
layer, layer,
}; };
@@ -168,77 +155,19 @@ impl UiRenderState {
rsc.on_draw(&active); rsc.on_draw(&active);
self.active.insert(id, active); self.active.insert(id, active);
size
} }
/// The drawing a widget already has, kept for a new box if the box has not fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
/// changed in a way it depends on.
fn try_reuse(&mut self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> Option<Size> {
if rsc.widgets().needs_redraw.contains(&id) {
return None;
}
let active = self.active.get(&id)?;
let (size, old) = (active.size, active.region);
if old == region {
return Some(size);
}
// TODO: epsilon?
if old.size() != region.size() && !self.reusable(id, region, rsc) {
return None;
}
// Its drawing stands, if the new box can be reached from the old one.
self.mov(id, &Remap::new(old, region)?);
Some(size)
}
/// Whether the widget can keep the drawing it has and be given `region`
/// instead, asked one axis at a time: a change on an axis it does not
/// depend on costs nothing, whatever it depends on elsewhere.
fn reusable(&self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> bool {
let Some(active) = self.active.get(&id) else {
return false;
};
let Some(widget) = rsc.widgets().get_dyn(id) else {
return false;
};
[Axis::X, Axis::Y].into_iter().all(|axis| {
let offered = region.axis(axis).len();
let had = active.region.axis(axis).len();
match widget.on_resize(axis) {
OnResize::Scale => true,
// `Translate` is not acted on yet, and cannot be until a
// drawing can sit somewhere other than its box. `region` is
// both the box a widget was given and the box its primitives
// are in, and `mov` remaps from it -- so carrying a drawing at
// its old size while the box grows makes the next move stretch
// it. The offset chain is what separates the two.
OnResize::Translate | OnResize::Redraw => offered == had,
}
})
}
fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool {
let Some(widget) = rsc.widgets().get_dyn(id) else {
return true;
};
[Axis::X, Axis::Y].into_iter().all(|axis| {
widget
.size_hint(axis)
.is_none_or(|hint| hint == size.axis(axis))
})
}
fn mov(&mut self, id: WidgetId, remap: &Remap) {
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
for h in &active.primitives { for h in &active.primitives {
let region = self.layers[h.layer].region_mut(h); let region = self.layers[h.layer].region_mut(h);
*region = remap.apply(*region); *region = region.outside(&from).within(&to);
} }
active.region = remap.apply(active.region); active.region = active.region.outside(&from).within(&to);
// SAFETY: children cannot be recursive // SAFETY: children cannot be recursive
let children = unsafe { forget_ref(&active.children) }; let children = unsafe { forget_ref(&active.children) };
for child in children { for child in children {
self.mov(*child, remap); self.mov(*child, from, to);
} }
} }
@@ -262,6 +191,7 @@ impl UiRenderState {
} }
fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> { fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
self.cache.remove(id);
let inst = self.remove(id, true, rsc); let inst = self.remove(id, true, rsc);
if let Some(inst) = &inst { if let Some(inst) = &inst {
for c in &inst.children { for c in &inst.children {
@@ -275,6 +205,7 @@ impl UiRenderState {
for (_, active) in self.active.drain() { for (_, active) in self.active.drain() {
rsc.on_undraw(&active); rsc.on_undraw(&active);
} }
self.cache.clear();
self.layers.clear(); self.layers.clear();
rsc.widgets_mut().needs_redraw.clear(); rsc.widgets_mut().needs_redraw.clear();
rsc.free(); rsc.free();
@@ -291,12 +222,17 @@ impl UiRenderState {
root.into().map(|r| r.id()) != self.old_root root.into().map(|r| r.id()) != self.old_root
} }
// Scheduling and drawing must use the same full-redraw predicate.
fn needs_full_redraw<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
self.root_changed(root) || self.resized
}
pub fn needs_redraw<'a>( pub fn needs_redraw<'a>(
&self, &self,
root: impl Into<Option<&'a StrongWidget>>, root: impl Into<Option<&'a StrongWidget>>,
widgets: &Widgets, widgets: &Widgets,
) -> bool { ) -> bool {
self.root_changed(root) || self.resized || widgets.has_updates() self.needs_full_redraw(root) || widgets.has_updates()
} }
pub fn active_widgets(&self) -> usize { pub fn active_widgets(&self) -> usize {
@@ -329,19 +265,22 @@ impl UiRenderState {
/// redraws a widget that's currently active (drawn) /// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.remove(&id); self.draw_started.remove(&id);
// Whoever read this widget's size may be a different size now, so the // check if parent depends on the desired size of this, if so then redraw it first
// highest reader is what draws. Everything between the two is marked for axis in [Axis::X, Axis::Y] {
// as well: their own boxes have not changed, so the mark is the only if let Some(&(outer, old)) = self.cache.size.axis_dyn(axis).get(&id)
// thing stopping the draw reusing its way past this widget. && let Some(current) = self.active.get(&id)
if let Some(top) = self.mark_readers(id, rsc) { && let Some(pid) = current.parent
self.redraw(top, rsc); {
// Cleared by that draw if it reached here; if it did not, this is self.cache.size.axis_dyn(axis).remove(&id);
// no longer drawn and asking again would not end. let new = self.size_ctx(id, outer, rsc).len_axis(id, axis);
rsc.widgets_mut().needs_redraw.remove(&id); self.cache.size.axis_dyn(axis).insert(id, (outer, new));
return; if new != old {
self.redraw(pid, rsc);
}
}
} }
rsc.widgets_mut().needs_redraw.remove(&id);
if self.draw_started.contains(&id) { if self.draw_started.contains(&id) {
return; return;
@@ -362,23 +301,22 @@ impl UiRenderState {
); );
} }
/// The furthest ancestor that read this widget's size, directly or through pub(super) fn size_ctx<'b>(
/// widgets that did the same, marking everything below it on the way. &'b mut self,
fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<WidgetId> { source: WidgetId,
let mut top = None; outer: UiVec2,
let mut at = id; rsc: &'b mut dyn UiRsc,
while let Some(active) = self.active.get(&at) ) -> SizeCtx<'b> {
&& let Some(parent) = active.parent let ui = rsc.ui_mut();
&& self SizeCtx {
.active source,
.get(&parent) cache: &mut self.cache,
.is_some_and(|p| p.size_deps.contains(&at)) text: &mut ui.text,
{ widgets: &ui.widgets,
rsc.widgets_mut().needs_redraw.insert(at); outer,
top = Some(parent); output_size: self.output_size,
at = parent; id: source,
} }
top
} }
} }
+89
View File
@@ -0,0 +1,89 @@
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)
}
}
+22 -10
View File
@@ -1,21 +1,33 @@
pub const trait LerpUtil: Sized { use std::ops::*;
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) -> Option<Self>; fn lerp_inv(self, from: Self, to: Self) -> Self;
} }
const impl LerpUtil for f32 { pub const trait DivOr {
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, and `None` where `from` and `to` are the same point: /// inverse of lerp
/// every input lerps to it, so there is no one answer to come back to. fn lerp_inv(self, from: Self, to: Self) -> Self {
fn lerp_inv(self, from: Self, to: Self) -> Option<Self> { (self - from).div_or(to - from, from)
match to == from {
true => None,
false => Some((self - from) / (to - from)),
}
} }
} }
+10 -1
View File
@@ -1,4 +1,4 @@
use crate::util::impl_op; use crate::util::{DivOr, impl_op};
use std::{hash::Hash, ops::*}; use std::{hash::Hash, ops::*};
#[repr(C)] #[repr(C)]
@@ -67,6 +67,15 @@ 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;
+19 -32
View File
@@ -1,4 +1,4 @@
use crate::{Axis, Len, Painter, Size}; use crate::{Axis, AxisT, Len, Painter, SizeCtx};
use std::any::Any; use std::any::Any;
mod data; mod data;
@@ -15,45 +15,32 @@ pub use tag::*;
pub use view::*; pub use view::*;
pub use widgets::*; pub use widgets::*;
/// What may be done to a widget's drawing when the box it was given changes
/// on this axis, instead of drawing it again. Asked per axis, because wrapped
/// text reads the width it is offered and not the height.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OnResize {
Scale,
Translate,
#[default]
Redraw,
}
pub trait Widget: Any { pub trait Widget: Any {
/// Draws the widget, and returns what it used of the box it was given. fn draw(&mut self, painter: &mut Painter);
fn draw(&mut self, painter: &mut Painter) -> Size; fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len;
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len;
/// An exact length the widget can give without a painter or its children.
/// Optional, and saves a draw rather than changing one: a hint that
/// disagrees with the eventual draw fails a debug assertion.
fn size_hint(&self, _axis: Axis) -> Option<Len> {
None
} }
fn on_resize(&self, _axis: Axis) -> OnResize { pub trait WidgetAxisFns {
OnResize::default() fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len;
}
impl<W: Widget + ?Sized> WidgetAxisFns for W {
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 () {
/// A gap: nothing drawn, at the default length, so a span gives it a share. fn draw(&mut self, _: &mut Painter) {}
fn draw(&mut self, _: &mut Painter) -> Size { fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
Size::default() Len::ZERO
} }
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
fn size_hint(&self, _axis: Axis) -> Option<Len> { Len::ZERO
Some(Len::default())
}
fn on_resize(&self, _axis: Axis) -> OnResize {
OnResize::Scale
} }
} }
-63
View File
@@ -1,63 +0,0 @@
//! 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());
let aligned = (
wtext("left").size(24).text_align(Align::LEFT),
wtext("centred").size(24).text_align(Align::CENTER),
wtext("right").size(24).text_align(Align::RIGHT),
)
.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 }
}
}
-16
View File
@@ -1,16 +0,0 @@
[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 }
-141
View File
@@ -1,141 +0,0 @@
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);
}
-14
View File
@@ -1,14 +0,0 @@
# 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
-170
View File
@@ -1,170 +0,0 @@
#!/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.
#
# `--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=""
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 ;;
--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] [--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
swaymsg output HEADLESS-1 mode "$mode" >/dev/null
# The extent `replay-touch` positions against, so a script's coordinates
# are the output's own pixels.
out_w=${mode%x*}
out_h=${mode#*x}; out_h=${out_h%@*}
# 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 "$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"
-140
View File
@@ -37,81 +37,6 @@ macro_rules! assert_corners {
} }
pub use crate::assert_corners; 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)] #[derive(Default)]
pub struct HarnessState { pub struct HarnessState {
pub root: Option<StrongWidget>, pub root: Option<StrongWidget>,
@@ -235,23 +160,6 @@ impl Harness {
self.release(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 { fn button(&mut self, button: CursorButton) -> &mut ActivationState {
let buttons = &mut self.cursor.buttons; let buttons = &mut self.cursor.buttons;
match button { match button {
@@ -270,51 +178,3 @@ impl Harness {
self.cursor.end_frame(); 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}");
}
}
+5 -6
View File
@@ -6,17 +6,16 @@ pub struct Image {
} }
impl Widget for Image { impl Widget for Image {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
painter.primitive(&self.handle); painter.primitive(&self.handle);
Size::abs(self.handle.size())
} }
fn size_hint(&self, axis: Axis) -> Option<Len> { fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
Some(Len::abs(self.handle.size().axis(axis))) Len::abs(self.handle.size().x)
} }
fn on_resize(&self, _: Axis) -> OnResize { fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
OnResize::Scale Len::abs(self.handle.size().y)
} }
} }
+8 -5
View File
@@ -5,13 +5,16 @@ pub struct Masked {
} }
impl Widget for Masked { impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
painter.set_mask(painter.region()); painter.set_mask(painter.region());
painter.widget(&self.inner).size() painter.widget(&self.inner);
} }
/// It clips to the box it was given, not to the part its child used. fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
fn on_resize(&self, _: Axis) -> OnResize { ctx.width(&self.inner)
OnResize::Redraw }
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
} }
} }
+21 -8
View File
@@ -6,17 +6,30 @@ pub struct Aligned {
} }
impl Widget for Aligned { impl Widget for Aligned {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
// Drawn where it may be too big, then given its aligned box once its
// size is known.
let size = painter.widget(&self.inner).size();
let region = match self.align.tuple() { let region = match self.align.tuple() {
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }), (Some(x), Some(y)) => painter
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL), .size(&self.inner)
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)), .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, (None, None) => UiRegion::FULL,
}; };
painter.widget_within(&self.inner, region); painter.widget_within(&self.inner, region);
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)
} }
} }
+10 -2
View File
@@ -6,10 +6,18 @@ pub struct LayerOffset {
} }
impl Widget for LayerOffset { impl Widget for LayerOffset {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
for _ in 0..self.offset { for _ in 0..self.offset {
painter.next_layer(); painter.next_layer();
} }
painter.widget(&self.inner).size() painter.widget(&self.inner);
}
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)
} }
} }
+34 -11
View File
@@ -6,20 +6,43 @@ pub struct MaxSize {
pub y: Option<Len>, pub y: Option<Len>,
} }
impl Widget for MaxSize { impl MaxSize {
fn draw(&mut self, painter: &mut Painter) -> Size { fn apply_to_outer(&self, ctx: &mut SizeCtx) {
let child = painter.widget(&self.inner).size(); if let Some(x) = self.x {
let output = painter.output_size(); ctx.outer.x.select_len(x.apply_rest());
Size { }
x: capped(child.x, self.x, output.x), if let Some(y) = self.y {
y: capped(child.y, self.y, output.y), ctx.outer.y.select_len(y.apply_rest());
} }
} }
} }
fn capped(len: Len, max: Option<Len>, output: f32) -> Len { impl Widget for MaxSize {
match max { fn draw(&mut self, painter: &mut Painter) {
Some(max) if len.apply_rest().to_abs(output) > max.apply_rest().to_abs(output) => max, painter.widget(&self.inner);
_ => len, }
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.apply_to_outer(ctx);
let width = ctx.width(&self.inner);
if let Some(x) = self.x {
let width_px = width.apply_rest().to_abs(ctx.output_size().x);
let x_px = x.apply_rest().to_abs(ctx.output_size().x);
if width_px > x_px { x } else { width }
} else {
width
}
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.apply_to_outer(ctx);
let height = ctx.height(&self.inner);
if let Some(y) = self.y {
let height_px = height.apply_rest().to_abs(ctx.output_size().y);
let y_px = y.apply_rest().to_abs(ctx.output_size().y);
if height_px > y_px { y } else { height }
} else {
height
}
} }
} }
+10 -2
View File
@@ -6,8 +6,16 @@ pub struct Offset {
} }
impl Widget for Offset { impl Widget for Offset {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let region = UiRegion::FULL.offset(self.amt); let region = UiRegion::FULL.offset(self.amt);
painter.widget_within(&self.inner, region).size() painter.widget_within(&self.inner, region);
}
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)
} }
} }
+21 -13
View File
@@ -6,20 +6,28 @@ pub struct Pad {
} }
impl Widget for Pad { impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let inner = painter painter.widget_within(&self.inner, self.padding.region());
.widget_within(&self.inner, self.padding.region())
.size();
Size {
x: Len {
abs: inner.x.abs + self.padding.left + self.padding.right,
..inner.x
},
y: Len {
abs: inner.y.abs + self.padding.top + self.padding.bottom,
..inner.y
},
} }
fn desired_width(&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.width(&self.inner);
size.abs += width;
size
}
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
} }
} }
+11 -7
View File
@@ -10,14 +10,11 @@ pub struct Scroll {
} }
impl Widget for Scroll { impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let output_len = painter.output_size().axis(self.axis); let output_len = painter.output_size().axis(self.axis);
let container_len = painter.region().axis(self.axis).len(); let container_len = painter.region().axis(self.axis).len();
// Drawn in the whole container to learn its length, then placed at let content_len = painter
// the scrolled offset. .len_axis(&self.inner, self.axis)
let child = painter.widget(&self.inner).size();
let content_len = child
.axis(self.axis)
.apply_rest() .apply_rest()
.within_len(container_len) .within_len(container_len)
.to_abs(output_len); .to_abs(output_len);
@@ -32,7 +29,14 @@ impl Widget for Scroll {
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0)); let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
painter.widget_within(&self.inner, region); painter.widget_within(&self.inner, region);
child }
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)
} }
} }
+20 -12
View File
@@ -6,21 +6,29 @@ pub struct SetSize {
pub y: Option<Len>, pub y: Option<Len>,
} }
impl Widget for SetSize { impl SetSize {
fn draw(&mut self, painter: &mut Painter) -> Size { fn apply_to_outer(&self, ctx: &mut SizeCtx) {
let child = painter.widget(&self.inner).size(); if let Some(x) = self.x {
Size { ctx.outer.x.select_len(x.apply_rest());
x: self.x.unwrap_or(child.x), }
y: self.y.unwrap_or(child.y), if let Some(y) = self.y {
ctx.outer.y.select_len(y.apply_rest());
}
} }
} }
/// A declared axis is known without looking at the child, which is what impl Widget for SetSize {
/// lets a span lay out around `.height(rest(1))` without drawing it. fn draw(&mut self, painter: &mut Painter) {
fn size_hint(&self, axis: Axis) -> Option<Len> { painter.widget(&self.inner);
match axis {
Axis::X => self.x,
Axis::Y => self.y,
} }
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))
} }
} }
+101 -32
View File
@@ -8,27 +8,13 @@ pub struct Span {
} }
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let axis = self.dir.axis; let total = self.len_sum(&mut painter.size_ctx());
// A length for every child before any is placed: from its own hint
// where it has one, and from drawing it where it does not.
let lens: Vec<Len> = self
.children
.iter()
.map(|child| match painter.size_hint(child, axis) {
Some(len) => len,
None => painter.widget(child).len(axis),
})
.collect();
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
let total = lens.iter().fold(Len::abs(gap), |sum, len| sum + *len);
let mut start = UiScalar::rel_min(); let mut start = UiScalar::rel_min();
let mut ortho = Len::ZERO; for child in &self.children {
for (child, len) in self.children.iter().zip(&lens) {
let mut span = UiSpan::FULL; let mut span = UiSpan::FULL;
span.start = start; span.start = start;
let len = painter.len_axis(child, self.dir.axis);
if len.rest > 0.0 { if len.rest > 0.0 {
let offset = UiScalar::new(total.rel, total.abs); let offset = UiScalar::new(total.rel, total.abs);
let rel_end = UiScalar::rel(len.rest / total.rest); let rel_end = UiScalar::rel(len.rest / total.rest);
@@ -38,25 +24,27 @@ impl Widget for Span {
start.abs += len.abs; start.abs += len.abs;
start.rel += len.rel; start.rel += len.rel;
span.end = start; span.end = start;
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL); let mut child_region = UiRegion::from_axis(self.dir.axis, span, UiSpan::FULL);
if self.dir.sign == Sign::Neg { if self.dir.sign == Sign::Neg {
region.flip(axis); child_region.flip(self.dir.axis);
}
let used = painter.widget_within(child, region).size().axis(!axis);
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
if used.rel > 0.0 || used.rest > 0.0 {
ortho = Len::REST;
} else if ortho.rest == 0.0 {
ortho.abs = ortho.abs.max(used.abs);
} }
painter.widget_within(child, child_region);
start.abs += self.gap; start.abs += self.gap;
} }
}
let along = match total.rest == 0.0 && total.rel == 0.0 { fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
true => total, match self.dir.axis {
false => Len::default(), Axis::X => self.desired_len(ctx),
}; Axis::Y => self.desired_ortho(ctx),
Size::from_axis(axis, along, ortho) }
}
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),
}
} }
} }
@@ -81,6 +69,87 @@ 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> {
+21 -16
View File
@@ -8,25 +8,30 @@ pub struct Stack {
} }
impl Widget for Stack { impl Widget for Stack {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let sizing = match self.size { let mut iter = self.children.iter();
StackSize::Default => None, if let Some(child) = iter.next() {
StackSize::Child(i) => Some(i), painter.child_layer();
}; painter.widget(child);
let mut size = Size::default();
for (i, child) in self.children.iter().enumerate() {
match i {
0 => painter.child_layer(),
_ => painter.next_layer(),
} }
let drawn = painter.widget(child); for child in iter {
// Only the child that sizes the stack is read, so the others painter.next_layer();
// changing size does not redraw it. painter.widget(child);
if sizing == Some(i) {
size = drawn.size();
} }
} }
size
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
match self.size {
StackSize::Default => Len::default(),
StackSize::Child(i) => ctx.width(&self.children[i]),
}
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
match self.size {
StackSize::Default => Len::default(),
StackSize::Child(i) => ctx.height(&self.children[i]),
}
} }
} }
+19 -4
View File
@@ -6,10 +6,25 @@ pub struct WidgetPtr {
} }
impl Widget for WidgetPtr { impl Widget for WidgetPtr {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
match &self.inner { if let Some(id) = &self.inner {
Some(id) => painter.widget(id).size(), painter.widget(id);
None => Size::default(), }
}
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
} }
} }
} }
+5 -7
View File
@@ -28,23 +28,21 @@ impl Rect {
} }
impl Widget for Rect { impl Widget for Rect {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
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::REST
} }
fn size_hint(&self, _: Axis) -> Option<Len> { fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
Some(Len::REST) Len::rest(1)
} }
/// Its box is its primitive's own region, so a new one is written there. fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
fn on_resize(&self, _: Axis) -> OnResize { Len::rest(1)
OnResize::Scale
} }
} }
+13 -16
View File
@@ -55,47 +55,44 @@ impl TextEdit {
} }
impl Widget for TextEdit { impl Widget for TextEdit {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let base = painter.layer; let base = painter.layer;
painter.child_layer(); painter.child_layer();
let (_, size) = self.view.draw(painter); 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 size; return;
}; };
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 rect_size = vec2(rect.width() as f32, rect.height() as f32); let 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),
rect_size size.align(Align::TOP_LEFT).offset(top_left).within(&region),
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
); );
} }
let caret = selection.focus().geometry(layout, CARET_WIDTH); let caret = selection.focus().geometry(layout, CARET_WIDTH);
let caret_size = vec2(caret.width() as f32, caret.height() as f32); let 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),
caret_size size.align(Align::TOP_LEFT).offset(top_left).within(&region),
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
); );
size
} }
fn on_resize(&self, axis: Axis) -> OnResize { fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.view.on_resize(axis) self.view.desired_width(ctx)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.view.desired_height(ctx)
} }
} }
+41 -36
View File
@@ -52,15 +52,15 @@ impl TextView {
.align(self.align) .align(self.align)
} }
fn render(&mut self, painter: &mut Painter) -> &RenderedText { fn render(&mut self, ctx: &mut SizeCtx) -> &RenderedText {
let width = if self.attrs.wrap { let width = if self.attrs.wrap {
Some(painter.px_size().x) Some(ctx.px_size().x)
} else { } else {
None None
}; };
if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed { if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed {
self.width = width; self.width = width;
self.tex = Some(painter.render_text(&mut self.buf, &self.attrs, width)); self.tex = Some(ctx.draw_text(&mut self.buf, &self.attrs, width));
self.attrs.changed = false; self.attrs.changed = false;
self.buf.changed = false; self.buf.changed = false;
} }
@@ -69,40 +69,39 @@ impl TextView {
pub fn tex(&self) -> Option<&RenderedText> { pub fn tex(&self) -> Option<&RenderedText> {
self.tex.as_ref() self.tex.as_ref()
} }
/// Draws the text, and says where the glyphs went and what they use. pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) { 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; let align = self.align;
if self.is_empty() && self.hint.is_some() { if self.is_empty() && self.hint.is_some() {
let region = self.render(painter).size.align(align); let region = self.render(&mut painter.size_ctx()).size.align(align);
let size = match &self.hint { if let Some(hint) = &self.hint {
Some(hint) => painter.widget(hint).size(), painter.widget(hint);
None => Size::ZERO, }
}; return region;
return (region, size);
} }
let tex = self.render(painter); let tex = self.render(&mut painter.size_ctx());
let region = tex.size.align(align); let region = tex.size.align(align);
let size = Size::abs(tex.size);
let within = region.within(&painter.region()); let within = region.within(&painter.region());
painter.glyphs(tex, within); painter.glyphs(tex, within);
(region, size) region
}
/// Wrapping reads the width it is offered, so a wider box reshapes it and
/// a taller one does not. Alignment matters too, and separately: glyphs
/// anchored to the start of an axis stay put when that extent changes,
/// but centred or end-aligned ones move even though the shaping stands.
pub fn on_resize(&self, axis: Axis) -> OnResize {
let reshapes = axis == Axis::X && self.attrs.wrap;
let anchored = match axis {
Axis::X => self.align.x,
Axis::Y => self.align.y,
} == AxisAlign::Neg;
match reshapes || !anchored {
true => OnResize::Redraw,
false => OnResize::Translate,
}
} }
pub fn content(&self) -> String { pub fn content(&self) -> String {
@@ -118,7 +117,7 @@ impl Text {
content: content.into(), content: content.into(),
} }
} }
fn update_buf(&mut self) { fn update_buf(&mut self, _ctx: &mut SizeCtx) {
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());
@@ -127,13 +126,19 @@ impl Text {
} }
impl Widget for Text { impl Widget for Text {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
self.update_buf(); self.update_buf(&mut painter.size_ctx());
self.view.draw(painter).1 self.view.draw(painter);
} }
fn on_resize(&self, axis: Axis) -> OnResize { fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.view.on_resize(axis) 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)
} }
} }
-77
View File
@@ -32,80 +32,3 @@ fn resizing_relays_out_against_the_new_output() {
assert_corners!(h, left, (0, 0), (100, 100)); assert_corners!(h, left, (0, 0), (100, 100));
assert_corners!(h, right, (100, 0), (800, 100)); assert_corners!(h, right, (100, 0), (800, 100));
} }
#[test]
fn an_empty_widget_takes_a_share_of_a_span() {
let mut h = Harness::new((400, 200));
let gap = ().add(&mut h.rsc);
let right = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((gap, right).span(Dir::RIGHT));
assert_corners!(h, gap, (0, 0), (300, 200));
assert_corners!(h, right, (300, 0), (400, 200));
}
#[test]
fn a_child_drawn_twice_moves_once() {
let mut h = Harness::new((400, 200));
// `Aligned` draws its child twice; listing it twice would move it twice.
let inner = rect(Color::BLUE).add(&mut h.rsc);
let centered = inner.center().width(200).add(&mut h.rsc);
let left = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((left, centered).span(Dir::RIGHT));
assert_corners!(h, inner, (100, 0), (300, 200));
h.rsc[left].x = Some(Len::abs(150));
h.frame();
assert_corners!(h, inner, (150, 0), (350, 200));
}
#[test]
fn a_resize_lands_where_a_cold_start_would() {
let build = |h: &mut Harness| {
let para = wtext(
"Wrapping shapes one source into as many lines as its container leaves room \
for, so the height of a paragraph is an answer rather than a setting.",
)
.size(20)
.wrap(true)
.pad(16)
.add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
let root = (para, below).span(Dir::DOWN).pad(12);
h.set_root(root);
(para, below)
};
let mut cold = Harness::new((900, 1200));
let (cold_para, cold_below) = build(&mut cold);
let mut resized = Harness::new((1920, 1200));
let (para, below) = build(&mut resized);
resized.resize((900, 1200));
resized.frame();
assert_eq!(resized.region(&para), cold.region(&cold_para), "paragraph");
assert_eq!(resized.region(&below), cold.region(&cold_below), "below");
}
#[test]
fn a_fixed_box_is_drawn_again_rather_than_stretched() {
let mut h = Harness::new((400, 400));
// The panel fills a stack sized by its sibling, so it is drawn in the
// whole box and then placed in the shorter one. Reusing it in that fixed
// box afterwards would leave it whatever height it happened to have.
let panel = rect(Color::BLUE).add(&mut h.rsc);
let leaf = rect(Color::RED).height(100).add(&mut h.rsc);
let stack = (panel, leaf)
.stack()
.size(StackSize::Child(1))
.add(&mut h.rsc);
h.set_root(stack.align(Align::TOP));
assert_corners!(h, panel, (0, 0), (400, 100));
h.rsc[leaf].y = Some(Len::abs(250));
h.frame();
assert_corners!(h, panel, (0, 0), (400, 250));
}
+1 -26
View File
@@ -2,7 +2,7 @@
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use iris::harness::{Harness, TouchScript}; use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
#[test] #[test]
@@ -58,28 +58,3 @@ fn hover_ends_when_the_cursor_leaves_the_window() {
h.leave(); h.leave();
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1)); 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"]);
}
-27
View File
@@ -1,27 +0,0 @@
//! What a drawing can be taken out of, and what it cannot.
use iris::core::{Remap, UiRegion, UiScalar, UiSpan};
/// A box `size` tall whose top is `rel` of the way down the window.
fn fixed(rel: f32, size: f32) -> UiRegion {
UiRegion::new(
UiSpan::FULL,
UiSpan::new(UiScalar { rel, abs: 0.0 }, UiScalar { rel, abs: size }),
)
}
#[test]
fn a_fixed_box_can_be_carried_but_not_stretched() {
let from = fixed(0.0, 164.0);
assert!(Remap::new(from, UiRegion::FULL).is_none());
assert!(Remap::new(from, fixed(0.5, 164.0)).is_some());
assert!(Remap::new(from, fixed(0.0, 98.0)).is_none());
}
#[test]
fn a_relative_box_can_be_stretched_to_any_other() {
let remap = Remap::new(UiRegion::FULL, fixed(0.0, 98.0)).expect("relative boxes remap");
// A part that filled the window keeps filling what replaced it, which is
// exactly what `outside` could not say for a box of a fixed length.
assert_eq!(remap.apply(UiRegion::FULL), fixed(0.0, 98.0));
}
-252
View File
@@ -1,252 +0,0 @@
//! What a second frame draws again, and what it keeps.
use std::{cell::Cell, rc::Rc};
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
/// A leaf that counts its draws and reports whatever size it is given, so a
/// test can see what the retained path skipped.
struct Counted {
draws: Rc<Cell<usize>>,
size: Size,
dependence: OnResize,
}
impl Widget for Counted {
fn draw(&mut self, _: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
self.size
}
fn on_resize(&self, _: Axis) -> OnResize {
self.dependence
}
}
struct Counts(Rc<Cell<usize>>);
impl Counts {
fn get(&self) -> usize {
self.0.get()
}
}
fn counted(h: &mut Harness, size: Size, dependence: OnResize) -> (WeakWidget<Counted>, Counts) {
let draws = Rc::new(Cell::new(0));
let id = Counted {
draws: draws.clone(),
size,
dependence,
}
.add(&mut h.rsc);
(id, Counts(draws))
}
/// A fixed-width leaf beside one that takes the rest, so changing the first
/// hands the second a different box without the output changing.
fn pair(h: &mut Harness, rest: OnResize) -> (WeakWidget<Counted>, Counts, WidgetId) {
let (first, _) = counted(h, Size::from((100, 200)), OnResize::Translate);
let (second, draws) = counted(h, Size::REST, rest);
h.set_root((first, second).span(Dir::RIGHT));
(first, draws, second.id())
}
#[test]
fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
let mut h = Harness::new((400, 200));
let (first, draws, second) = pair(&mut h, OnResize::Scale);
let settled = draws.get();
assert_corners!(h, second, (100, 0), (400, 200));
h.rsc[first].size = Size::from((150, 200));
h.frame();
assert_eq!(
draws.get(),
settled,
"its box is a field to write, not a reason to draw"
);
assert_corners!(h, second, (150, 0), (400, 200));
}
#[test]
fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
let mut h = Harness::new((400, 200));
let (first, draws, second) = pair(&mut h, OnResize::Redraw);
let settled = draws.get();
h.rsc[first].size = Size::from((150, 200));
h.frame();
// Twice: once for the span to measure it, once for its real box. A child
// that can hint its length is spared the first, and a smaller number here
// means someone has made that cheaper rather than broken it.
assert_eq!(draws.get(), settled + 2);
assert_corners!(h, second, (150, 0), (400, 200));
}
#[test]
fn a_span_child_that_declares_its_length_is_drawn_once() {
let mut h = Harness::new((400, 200));
let (told, told_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
// The span takes one child's length from its hint and has to draw the
// other to find out, so only the second is drawn before it is placed.
let hinted = told.width(100).add(&mut h.rsc);
h.set_root((hinted, asked).span(Dir::RIGHT));
assert_eq!(told_draws.get(), 1);
assert_eq!(
asked_draws.get(),
2,
"drawn to be measured, then again to be placed"
);
}
#[test]
fn a_span_relays_out_when_a_child_it_measured_changes() {
let mut h = Harness::new((400, 200));
let (first, _, second) = pair(&mut h, OnResize::Translate);
h.rsc[first].size = Size::from((250, 200));
h.frame();
assert_corners!(h, first, (0, 0), (250, 200));
assert_corners!(h, second, (250, 0), (400, 200));
}
#[test]
fn a_placed_child_survives_the_next_frame() {
let mut h = Harness::new((400, 200));
// Both children declare a length, so the span places them from their hints
// rather than drawing them to find out.
let top = rect(Color::RED).height(80).add(&mut h.rsc);
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
h.set_root((top, bottom).span(Dir::DOWN));
h.rsc.widgets_mut().get_dyn_mut(top.id());
h.frame();
assert_corners!(h, top, (0, 0), (400, 80));
assert_corners!(h, bottom, (0, 80), (400, 200));
}
/// Lays its child out from the hint alone, never reading what it drew.
struct FromHint {
inner: StrongWidget,
}
impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let mut region = UiRegion::FULL;
region.y.end = region.y.start.offset(len.abs);
painter.widget_within(&self.inner, region);
Size::REST
}
}
#[test]
fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
let mut h = Harness::new((400, 200));
let inner = rect(Color::RED).height(80).add(&mut h.rsc);
let parent = FromHint {
inner: inner.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
h.set_root(parent);
assert_corners!(h, inner, (0, 0), (400, 80));
h.rsc[inner].y = Some(Len::abs(120));
h.frame();
assert_corners!(h, inner, (0, 0), (400, 120));
}
/// Reads the output's size, which nothing but its own draw can put right.
struct ReadsOutput {
draws: Rc<Cell<usize>>,
}
impl Widget for ReadsOutput {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
Size::abs(painter.output_size() / 4.0)
}
}
#[test]
fn a_resize_does_not_redraw_what_the_shader_can_move() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw);
h.set_root(leaf);
let settled = draws.get();
h.resize((800, 100));
assert!(h.needs_redraw());
h.frame();
assert_eq!(
draws.get(),
settled,
"its box is the same fraction of a different output"
);
assert_corners!(h, leaf, (0, 0), (800, 100));
}
#[test]
fn a_resize_redraws_what_read_the_output() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsOutput {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(leaf);
let settled = draws.get();
h.resize((800, 100));
h.frame();
assert_eq!(draws.get(), settled + 1);
}
#[test]
fn narrowing_the_output_reflows_text_and_relays_out_around_it() {
let mut h = Harness::new((600, 400));
let para = wtext(
"Wrapping shapes one source into as many lines as its container leaves \
room for, so the height of a paragraph is an answer rather than a setting.",
)
.size(20)
.wrap(true)
.add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((para, below).span(Dir::DOWN));
let top = h.region(&below).expect("drew nothing").top_left.y;
h.resize((300, 400));
h.frame();
let lower = h.region(&below).expect("drew nothing").top_left.y;
assert!(lower > top, "same words, half the width: {top} -> {lower}");
}
#[test]
fn a_change_two_levels_under_its_reader_still_reaches_it() {
let mut h = Harness::new((400, 400));
// Every wrapper up to the outer pad read the size below it, so the outer
// pad is what draws again -- and the span it hands the box to is the same
// size as before, which is what lets a draw reuse its way past the leaf.
let (leaf, _) = counted(&mut h, Size::abs((100, 100).into()), OnResize::Redraw);
let padded = leaf.pad(10).add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((padded, below).span(Dir::DOWN).pad(12));
assert_corners!(h, below, (12, 132), (388, 388));
h.rsc[leaf].size = Size::abs((100, 200).into());
h.frame();
assert_corners!(h, below, (12, 232), (388, 388));
}