Compare commits

..
1 Commits
Author SHA1 Message Date
iris f192f75b25 Size a widget while drawing it, not in a pass of its own
`desired_width`/`desired_height`, `WidgetAxisFns`, `SizeCtx` and the size
cache are gone. A widget states what it used with `Painter::set_size`
while it draws, and `Painter::widget` hands back a `DrawResult` whose
`size()` both reads the child and records that this widget's size depends
on it. Reading nothing keeps the parent independent of what the child came
to.

`Span` is what the change is for. It takes each child's `size_hint` where
there is one, draws only the children that cannot answer, allocates the
flexible space, then places everything -- which deletes `desired_ortho`,
whose own comment said it "literally copies draw ... which makes this slow
and not cool".

Invalidation follows the dependency edges the draw recorded: a widget that
needs redrawing hands off to the highest ancestor that read its size,
instead of re-running a measurement to find out whether anything changed.

`Widget::size_dependence(axis)` says how much of its box a widget's
drawing depends on -- none of it, its own extent, or the whole box -- so
the retained path can keep a drawing and write a new box into it. Asked
per axis, because wrapped text depends on the width it is offered and not
on the height. `Internal` does not yet buy more than `External`: keeping a
drawing when only the room around it changed is a translation, which waits
for the move chain.

`tests/retained.rs` covers the second frame rather than the first, which
is where the bugs were: a placed child that was not recorded as one got
pruned as departed on the next draw.

`examples/text.rs` is new, since wrapping was the one thing here with no
way to see it on its own.
2026-09-13 22:51:05 -04:00
33 changed files with 302 additions and 1015 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!(
-2
View File
@@ -13,8 +13,6 @@ pub struct ActiveData {
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>, pub size_deps: Vec<WidgetId>,
/// 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,
} }
+56 -35
View File
@@ -20,7 +20,7 @@ pub struct Painter<'a> {
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>, pub(super) size_deps: Vec<WidgetId>,
pub(super) reads_output: bool, pub(super) size: Option<Size>,
pub layer: usize, pub layer: usize,
pub(super) id: WidgetId, pub(super) id: WidgetId,
} }
@@ -70,31 +70,39 @@ impl<'a> Painter<'a> {
self.mask = self.rsc.ui_mut().masks.push(Mask { region }); self.mask = self.rsc.ui_mut().masks.push(Mask { region });
} }
/// States what this widget uses of the box it was handed, once per draw.
/// This is the size a parent reads.
pub fn set_size(&mut self, size: impl Into<Size>) {
debug_assert!(
self.size.is_none(),
"{} set its size twice in one draw",
self.label(),
);
self.size = Some(size.into());
}
/// 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>) -> DrawResult<'_, 'a> {
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>(
&'s mut self, &mut self,
id: &'s StrongWidget<W>, id: &StrongWidget<W>,
region: UiRegion, region: UiRegion,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'_, 'a> {
let region = region.within(&self.region); let region = region.within(&self.region);
self.widget_at(id, region) self.widget_at(id, region)
} }
fn widget_at<'s, W: ?Sized>( fn widget_at<W: ?Sized>(
&'s mut self, &mut self,
id: &'s StrongWidget<W>, id: &StrongWidget<W>,
region: UiRegion, region: UiRegion,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'_, 'a> {
// A child listed twice would be moved twice.
if !self.children.contains(&id.id()) {
self.children.push(id.id()); self.children.push(id.id());
}
let size = self.state.draw_inner( let size = self.state.draw_inner(
self.layer, self.layer,
id.id(), id.id(),
@@ -105,24 +113,43 @@ impl<'a> Painter<'a> {
self.rsc, self.rsc,
); );
DrawResult { DrawResult {
child: id, child: id.id(),
painter: self, painter: self,
size, size,
} }
} }
/// What a child says its length is without being drawn, if it can say. /// What a child says its length is without being drawn, if it can say.
/// Asking counts as reading its size. pub fn size_hint<W: ?Sized>(&self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> { self.rsc.widgets().get_dyn(id.id())?.size_hint(axis)
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>) { /// Gives a child its final box once this widget knows what that is,
if !self.size_deps.contains(&child.id()) { /// keeping the drawing it already has where it can.
self.size_deps.push(child.id()); pub fn place<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) -> Size {
let region = region.within(&self.region);
let id = id.id();
// Choosing a child's box depends on its size, from a draw or a hint.
// Claiming it is separate: one left out of `children` is removed.
if !self.size_deps.contains(&id) {
self.size_deps.push(id);
} }
if !self.children.contains(&id) {
self.children.push(id);
}
if self.state.active.contains_key(&id) {
return self.state.place(id, region, self.rsc);
}
// Not drawn yet: its length came from a hint, so this is its one draw.
self.state.draw_inner(
self.layer,
id,
region,
Some(self.id),
self.mask,
None,
self.rsc,
)
} }
pub fn render_text( pub fn render_text(
@@ -163,17 +190,11 @@ impl<'a> Painter<'a> {
self.region self.region
} }
/// The output's size in pixels. A widget that reads it draws again when pub fn output_size(&self) -> Vec2 {
/// the output changes, since nothing else can put that right.
pub fn output_size(&mut self) -> Vec2 {
self.reads_output = true;
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)
} }
@@ -201,15 +222,15 @@ impl<'a> Painter<'a> {
/// A child that has just been drawn. Reading its size records that this /// A child that has just been drawn. Reading its size records that this
/// widget's own size depends on it; dropping it without reading draws the /// widget's own size depends on it; dropping it without reading draws the
/// child and leaves the parent independent of what it came to. /// child and leaves the parent independent of what it came to.
pub struct DrawResult<'p, 'a, W: ?Sized> { pub struct DrawResult<'p, 'a> {
painter: &'p mut Painter<'a>, painter: &'p mut Painter<'a>,
child: &'p StrongWidget<W>, child: WidgetId,
size: Size, size: Size,
} }
impl<W: ?Sized> DrawResult<'_, '_, W> { impl DrawResult<'_, '_> {
pub fn size(self) -> Size { pub fn size(self) -> Size {
self.painter.depend_on_size(self.child); self.painter.size_deps.push(self.child);
self.size self.size
} }
+68 -53
View File
@@ -1,5 +1,5 @@
use crate::{ use crate::{
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, OnResize, Painter, PixelRegion, Remap, Size, ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, Size, SizeDependence,
StrongWidget, UiRegion, UiRsc, WidgetId, Widgets, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
util::{HashMap, HashSet, Vec2, forget_ref}, util::{HashMap, HashSet, Vec2, forget_ref},
}; };
@@ -53,21 +53,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);
} }
} }
@@ -103,7 +93,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 {
@@ -116,12 +105,12 @@ impl UiRenderState {
primitives: Vec::new(), primitives: Vec::new(),
children: Vec::new(), children: Vec::new(),
size_deps: Vec::new(), size_deps: Vec::new(),
reads_output: false, size: None,
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 {
@@ -133,11 +122,17 @@ impl UiRenderState {
primitives, primitives,
children, children,
size_deps, size_deps,
reads_output, size,
layer, layer,
id, id,
} = painter; } = painter;
let size = size.unwrap_or_else(|| {
panic!(
"'{}' ({id:?}) drew without a size; every widget calls Painter::set_size",
rsc.widgets().label(id)
)
});
debug_assert!( debug_assert!(
Self::hints_agree(id, size, rsc), Self::hints_agree(id, size, rsc),
"'{}' ({id:?}) drew a size its size_hint disagrees with", "'{}' ({id:?}) drew a size its size_hint disagrees with",
@@ -154,7 +149,6 @@ impl UiRenderState {
primitives, primitives,
children, children,
size_deps, size_deps,
reads_output,
mask, mask,
layer, layer,
}; };
@@ -171,6 +165,26 @@ impl UiRenderState {
size size
} }
/// Gives an already drawn widget a new box, keeping its drawing if it can
/// and drawing it again if it cannot.
pub(super) fn place(&mut self, id: WidgetId, region: UiRegion, rsc: &mut dyn UiRsc) -> Size {
if let Some(size) = self.try_reuse(id, region, rsc) {
return size;
}
let Some(active) = self.remove(id, false, rsc) else {
return Size::ZERO;
};
self.draw_inner(
active.layer,
id,
region,
active.parent,
active.mask,
Some(active.children),
rsc,
)
}
/// The drawing a widget already has, kept for a new box if the box has not /// The drawing a widget already has, kept for a new box if the box has not
/// changed in a way it depends on. /// changed in a way it depends on.
fn try_reuse(&mut self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> Option<Size> { fn try_reuse(&mut self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> Option<Size> {
@@ -178,41 +192,43 @@ impl UiRenderState {
return None; return None;
} }
let active = self.active.get(&id)?; let active = self.active.get(&id)?;
let (size, old) = (active.size, active.region); let (size, was) = (active.size, active.region);
if old == region { if was == region {
return Some(size); return Some(size);
} }
// TODO: epsilon? // TODO: epsilon?
if old.size() != region.size() && !self.reusable(id, region, rsc) { if was.size() == region.size() {
return None; self.mov(id, was, region);
return Some(size);
} }
// Its drawing stands, if the new box can be reached from the old one. if self.reusable(id, region, rsc) {
self.mov(id, &Remap::new(old, region)?); // Its drawing stands; the box is written into the primitives.
Some(size) self.mov(id, was, region);
return Some(size);
}
None
} }
/// Whether the widget can keep the drawing it has and be given `region` /// 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 /// instead, asked one axis at a time: a change on an axis it does not
/// depend on costs nothing, whatever it depends on elsewhere. /// depend on costs nothing, whatever it depends on elsewhere.
fn reusable(&self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> bool { fn reusable(&self, id: WidgetId, mut region: UiRegion, rsc: &dyn UiRsc) -> bool {
let Some(active) = self.active.get(&id) else { let Some(active) = self.active.get(&id) else {
return false; return false;
}; };
let Some(widget) = rsc.widgets().get_dyn(id) else { let Some(widget) = rsc.widgets().get_dyn(id) else {
return false; return false;
}; };
let mut was = active.region;
[Axis::X, Axis::Y].into_iter().all(|axis| { [Axis::X, Axis::Y].into_iter().all(|axis| {
let offered = region.axis(axis).len(); let offered = region.axis_mut(axis).len();
let had = active.region.axis(axis).len(); let had = was.axis_mut(axis).len();
match widget.on_resize(axis) { match widget.size_dependence(axis) {
OnResize::Scale => true, SizeDependence::None => true,
// `Translate` is not acted on yet, and cannot be until a // `Internal` could also keep its drawing when only the room
// drawing can sit somewhere other than its box. `region` is // around it changed, but that is a translation rather than a
// both the box a widget was given and the box its primitives // remap, so it waits for the move chain.
// are in, and `mov` remaps from it -- so carrying a drawing at SizeDependence::Internal | SizeDependence::External => offered == had,
// 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,
} }
}) })
} }
@@ -228,17 +244,17 @@ impl UiRenderState {
}) })
} }
fn mov(&mut self, id: WidgetId, remap: &Remap) { fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
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);
} }
} }
@@ -291,12 +307,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 +350,14 @@ 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 // Whoever read this widget's size may be a different size now, so the
// highest reader is what draws. Everything between the two is marked // highest reader is what draws; it reaches this one on the way down.
// as well: their own boxes have not changed, so the mark is the only if let Some(top) = self.highest_reader(id) {
// thing stopping the draw reusing its way past this widget.
if let Some(top) = self.mark_readers(id, rsc) {
self.redraw(top, rsc); self.redraw(top, rsc);
// Cleared by that draw if it reached here; if it did not, this is
// no longer drawn and asking again would not end.
rsc.widgets_mut().needs_redraw.remove(&id);
return; return;
} }
rsc.widgets_mut().needs_redraw.remove(&id);
if self.draw_started.contains(&id) { if self.draw_started.contains(&id) {
return; return;
@@ -363,8 +379,8 @@ impl UiRenderState {
} }
/// The furthest ancestor that read this widget's size, directly or through /// The furthest ancestor that read this widget's size, directly or through
/// widgets that did the same, marking everything below it on the way. /// widgets that did the same.
fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<WidgetId> { fn highest_reader(&self, id: WidgetId) -> Option<WidgetId> {
let mut top = None; let mut top = None;
let mut at = id; let mut at = id;
while let Some(active) = self.active.get(&at) while let Some(active) = self.active.get(&at)
@@ -374,7 +390,6 @@ impl UiRenderState {
.get(&parent) .get(&parent)
.is_some_and(|p| p.size_deps.contains(&at)) .is_some_and(|p| p.size_deps.contains(&at))
{ {
rsc.widgets_mut().needs_redraw.insert(at);
top = Some(parent); top = Some(parent);
at = parent; at = parent;
} }
+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;
+21 -17
View File
@@ -15,20 +15,25 @@ 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 /// How much of the box a widget was handed its drawing depends on, and so
/// on this axis, instead of drawing it again. Asked per axis, because wrapped /// what has to change before it must be drawn again. Asked per axis, because
/// text reads the width it is offered and not the height. /// wrapped text depends on the width it is offered and not on the height.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OnResize { pub enum SizeDependence {
Scale, /// None of it: the box only says where the primitives go, so a new one is
Translate, /// written into them instead of drawn.
None,
/// Its own extent, whatever box that sits in. Reusable in any box that
/// leaves that extent unchanged, including a larger one it does not fill.
#[default] #[default]
Redraw, Internal,
/// The extent of the box itself, used or not.
External,
} }
pub trait Widget: Any { pub trait Widget: Any {
/// Draws the widget, and returns what it used of the box it was given. /// Draws the widget, and states what it used with `Painter::set_size`.
fn draw(&mut self, painter: &mut Painter) -> Size; fn draw(&mut self, painter: &mut Painter);
/// An exact length the widget can give without a painter or its children. /// An exact length the widget can give without a painter or its children.
/// Optional, and saves a draw rather than changing one: a hint that /// Optional, and saves a draw rather than changing one: a hint that
@@ -37,23 +42,22 @@ pub trait Widget: Any {
None None
} }
fn on_resize(&self, _axis: Axis) -> OnResize { fn size_dependence(&self, _axis: Axis) -> SizeDependence {
OnResize::default() SizeDependence::Internal
} }
} }
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, painter: &mut Painter) {
fn draw(&mut self, _: &mut Painter) -> Size { painter.set_size(Size::ZERO);
Size::default()
} }
fn size_hint(&self, _axis: Axis) -> Option<Len> { fn size_hint(&self, _axis: Axis) -> Option<Len> {
Some(Len::default()) Some(Len::ZERO)
} }
fn on_resize(&self, _axis: Axis) -> OnResize { fn size_dependence(&self, _axis: Axis) -> SizeDependence {
OnResize::Scale SizeDependence::None
} }
} }
-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}");
}
}
+4 -4
View File
@@ -6,17 +6,17 @@ 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()) painter.set_size(Size::abs(self.handle.size()));
} }
fn size_hint(&self, axis: Axis) -> Option<Len> { fn size_hint(&self, axis: Axis) -> Option<Len> {
Some(Len::abs(self.handle.size().axis(axis))) Some(Len::abs(self.handle.size().axis(axis)))
} }
fn on_resize(&self, _: Axis) -> OnResize { fn size_dependence(&self, _: Axis) -> SizeDependence {
OnResize::Scale SizeDependence::None
} }
} }
+5 -4
View File
@@ -5,13 +5,14 @@ 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() let size = painter.widget(&self.inner).size();
painter.set_size(size);
} }
/// It clips to the box it was given, not to the part its child used. /// It clips to the box it was given, not to the part its child used.
fn on_resize(&self, _: Axis) -> OnResize { fn size_dependence(&self, _: Axis) -> SizeDependence {
OnResize::Redraw SizeDependence::External
} }
} }
+3 -3
View File
@@ -6,7 +6,7 @@ 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 // Drawn where it may be too big, then given its aligned box once its
// size is known. // size is known.
let size = painter.widget(&self.inner).size(); let size = painter.widget(&self.inner).size();
@@ -16,7 +16,7 @@ impl Widget for Aligned {
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)), (None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
(None, None) => UiRegion::FULL, (None, None) => UiRegion::FULL,
}; };
painter.widget_within(&self.inner, region); painter.place(&self.inner, region);
size painter.set_size(size);
} }
} }
+3 -2
View File
@@ -6,10 +6,11 @@ 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() let size = painter.widget(&self.inner).size();
painter.set_size(size);
} }
} }
+5 -3
View File
@@ -6,14 +6,16 @@ pub struct MaxSize {
pub y: Option<Len>, pub y: Option<Len>,
} }
impl MaxSize {}
impl Widget for MaxSize { impl Widget for MaxSize {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let child = painter.widget(&self.inner).size(); let child = painter.widget(&self.inner).size();
let output = painter.output_size(); let output = painter.output_size();
Size { painter.set_size(Size {
x: capped(child.x, self.x, output.x), x: capped(child.x, self.x, output.x),
y: capped(child.y, self.y, output.y), y: capped(child.y, self.y, output.y),
} });
} }
} }
+3 -2
View File
@@ -6,8 +6,9 @@ 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() let size = painter.widget_within(&self.inner, region).size();
painter.set_size(size);
} }
} }
+3 -3
View File
@@ -6,11 +6,11 @@ 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 let inner = painter
.widget_within(&self.inner, self.padding.region()) .widget_within(&self.inner, self.padding.region())
.size(); .size();
Size { painter.set_size(Size {
x: Len { x: Len {
abs: inner.x.abs + self.padding.left + self.padding.right, abs: inner.x.abs + self.padding.left + self.padding.right,
..inner.x ..inner.x
@@ -19,7 +19,7 @@ impl Widget for Pad {
abs: inner.y.abs + self.padding.top + self.padding.bottom, abs: inner.y.abs + self.padding.top + self.padding.bottom,
..inner.y ..inner.y
}, },
} });
} }
} }
+3 -3
View File
@@ -10,7 +10,7 @@ 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 // Drawn in the whole container to learn its length, then placed at
@@ -31,8 +31,8 @@ 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.place(&self.inner, region);
child painter.set_size(child);
} }
} }
+5 -3
View File
@@ -6,13 +6,15 @@ pub struct SetSize {
pub y: Option<Len>, pub y: Option<Len>,
} }
impl SetSize {}
impl Widget for SetSize { impl Widget for SetSize {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let child = painter.widget(&self.inner).size(); let child = painter.widget(&self.inner).size();
Size { painter.set_size(Size {
x: self.x.unwrap_or(child.x), x: self.x.unwrap_or(child.x),
y: self.y.unwrap_or(child.y), y: self.y.unwrap_or(child.y),
} });
} }
/// A declared axis is known without looking at the child, which is what /// A declared axis is known without looking at the child, which is what
+3 -3
View File
@@ -8,7 +8,7 @@ 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 axis = self.dir.axis;
// A length for every child before any is placed: from its own hint // 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. // where it has one, and from drawing it where it does not.
@@ -42,7 +42,7 @@ impl Widget for Span {
if self.dir.sign == Sign::Neg { if self.dir.sign == Sign::Neg {
region.flip(axis); region.flip(axis);
} }
let used = painter.widget_within(child, region).size().axis(!axis); let used = painter.place(child, region).axis(!axis);
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels // TODO: rel shouldn't do this, but no easy way before actually calculating pixels
if used.rel > 0.0 || used.rest > 0.0 { if used.rel > 0.0 || used.rest > 0.0 {
ortho = Len::REST; ortho = Len::REST;
@@ -56,7 +56,7 @@ impl Widget for Span {
true => total, true => total,
false => Len::default(), false => Len::default(),
}; };
Size::from_axis(axis, along, ortho) painter.set_size(Size::from_axis(axis, along, ortho));
} }
} }
+2 -2
View File
@@ -8,7 +8,7 @@ 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 sizing = match self.size {
StackSize::Default => None, StackSize::Default => None,
StackSize::Child(i) => Some(i), StackSize::Child(i) => Some(i),
@@ -26,7 +26,7 @@ impl Widget for Stack {
size = drawn.size(); size = drawn.size();
} }
} }
size painter.set_size(size);
} }
} }
+5 -4
View File
@@ -6,11 +6,12 @@ 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 { let size = match &self.inner {
Some(id) => painter.widget(id).size(), Some(id) => painter.widget(id).size(),
None => Size::default(), None => Size::ZERO,
} };
painter.set_size(size);
} }
} }
+4 -4
View File
@@ -28,14 +28,14 @@ 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 painter.set_size(Size::REST);
} }
fn size_hint(&self, _: Axis) -> Option<Len> { fn size_hint(&self, _: Axis) -> Option<Len> {
@@ -43,8 +43,8 @@ impl Widget for Rect {
} }
/// Its box is its primitive's own region, so a new one is written there. /// Its box is its primitive's own region, so a new one is written there.
fn on_resize(&self, _: Axis) -> OnResize { fn size_dependence(&self, _: Axis) -> SizeDependence {
OnResize::Scale SizeDependence::None
} }
} }
+9 -15
View File
@@ -55,47 +55,41 @@ 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); let (_, size) = self.view.draw(painter);
painter.set_size(size);
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 size_dependence(&self, axis: Axis) -> SizeDependence {
self.view.on_resize(axis) self.view.size_dependence(axis)
} }
} }
+11 -17
View File
@@ -89,19 +89,12 @@ impl TextView {
(region, size) (region, size)
} }
/// Wrapping reads the width it is offered, so a wider box reshapes it and /// Wrapping reads the width it is offered, so a wider box reshapes it; a
/// a taller one does not. Alignment matters too, and separately: glyphs /// taller one never does.
/// anchored to the start of an axis stay put when that extent changes, pub fn size_dependence(&self, axis: Axis) -> SizeDependence {
/// but centred or end-aligned ones move even though the shaping stands. match axis == Axis::X && self.attrs.wrap {
pub fn on_resize(&self, axis: Axis) -> OnResize { true => SizeDependence::External,
let reshapes = axis == Axis::X && self.attrs.wrap; false => SizeDependence::Internal,
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,
} }
} }
@@ -127,13 +120,14 @@ 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();
self.view.draw(painter).1 let (_, size) = self.view.draw(painter);
painter.set_size(size);
} }
fn on_resize(&self, axis: Axis) -> OnResize { fn size_dependence(&self, axis: Axis) -> SizeDependence {
self.view.on_resize(axis) self.view.size_dependence(axis)
} }
} }
-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));
}
+16 -131
View File
@@ -10,16 +10,16 @@ use iris::prelude::*;
struct Counted { struct Counted {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
size: Size, size: Size,
dependence: OnResize, dependence: SizeDependence,
} }
impl Widget for Counted { impl Widget for Counted {
fn draw(&mut self, _: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
self.size painter.set_size(self.size);
} }
fn on_resize(&self, _: Axis) -> OnResize { fn size_dependence(&self, _: Axis) -> SizeDependence {
self.dependence self.dependence
} }
} }
@@ -32,7 +32,11 @@ impl Counts {
} }
} }
fn counted(h: &mut Harness, size: Size, dependence: OnResize) -> (WeakWidget<Counted>, Counts) { fn counted(
h: &mut Harness,
size: Size,
dependence: SizeDependence,
) -> (WeakWidget<Counted>, Counts) {
let draws = Rc::new(Cell::new(0)); let draws = Rc::new(Cell::new(0));
let id = Counted { let id = Counted {
draws: draws.clone(), draws: draws.clone(),
@@ -45,8 +49,8 @@ fn counted(h: &mut Harness, size: Size, dependence: OnResize) -> (WeakWidget<Cou
/// A fixed-width leaf beside one that takes the rest, so changing the first /// A fixed-width leaf beside one that takes the rest, so changing the first
/// hands the second a different box without the output changing. /// hands the second a different box without the output changing.
fn pair(h: &mut Harness, rest: OnResize) -> (WeakWidget<Counted>, Counts, WidgetId) { fn pair(h: &mut Harness, rest: SizeDependence) -> (WeakWidget<Counted>, Counts, WidgetId) {
let (first, _) = counted(h, Size::from((100, 200)), OnResize::Translate); let (first, _) = counted(h, Size::from((100, 200)), SizeDependence::Internal);
let (second, draws) = counted(h, Size::REST, rest); let (second, draws) = counted(h, Size::REST, rest);
h.set_root((first, second).span(Dir::RIGHT)); h.set_root((first, second).span(Dir::RIGHT));
(first, draws, second.id()) (first, draws, second.id())
@@ -55,7 +59,7 @@ fn pair(h: &mut Harness, rest: OnResize) -> (WeakWidget<Counted>, Counts, Widget
#[test] #[test]
fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() { fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let (first, draws, second) = pair(&mut h, OnResize::Scale); let (first, draws, second) = pair(&mut h, SizeDependence::None);
let settled = draws.get(); let settled = draws.get();
assert_corners!(h, second, (100, 0), (400, 200)); assert_corners!(h, second, (100, 0), (400, 200));
@@ -73,7 +77,7 @@ fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
#[test] #[test]
fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() { fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let (first, draws, second) = pair(&mut h, OnResize::Redraw); let (first, draws, second) = pair(&mut h, SizeDependence::External);
let settled = draws.get(); let settled = draws.get();
h.rsc[first].size = Size::from((150, 200)); h.rsc[first].size = Size::from((150, 200));
@@ -89,8 +93,8 @@ fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
#[test] #[test]
fn a_span_child_that_declares_its_length_is_drawn_once() { fn a_span_child_that_declares_its_length_is_drawn_once() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let (told, told_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate); let (told, told_draws) = counted(&mut h, Size::from((100, 200)), SizeDependence::Internal);
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate); let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), SizeDependence::Internal);
// The span takes one child's length from its hint and has to draw the // The span takes one child's length from its hint and has to draw the
// other to find out, so only the second is drawn before it is placed. // other to find out, so only the second is drawn before it is placed.
let hinted = told.width(100).add(&mut h.rsc); let hinted = told.width(100).add(&mut h.rsc);
@@ -107,7 +111,7 @@ fn a_span_child_that_declares_its_length_is_drawn_once() {
#[test] #[test]
fn a_span_relays_out_when_a_child_it_measured_changes() { fn a_span_relays_out_when_a_child_it_measured_changes() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let (first, _, second) = pair(&mut h, OnResize::Translate); let (first, _, second) = pair(&mut h, SizeDependence::Internal);
h.rsc[first].size = Size::from((250, 200)); h.rsc[first].size = Size::from((250, 200));
h.frame(); h.frame();
@@ -131,122 +135,3 @@ fn a_placed_child_survives_the_next_frame() {
assert_corners!(h, top, (0, 0), (400, 80)); assert_corners!(h, top, (0, 0), (400, 80));
assert_corners!(h, bottom, (0, 80), (400, 200)); 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));
}