Compare commits

...
Author SHA1 Message Date
iris-ai db1751fdfd Retire Remap: a translation shifts, and only a stretch needs a fraction
`Remap` existed to invert a composition, and a translation never needed
one: shifting a box shifts everything composed into it by the same
amount, because `lerp(s + d, e + d, t) == lerp(s, e, t) + d` on both
channels. That holds whether or not the box has a relative extent, so
the carry branch was answering a question it did not have to ask.

So the decision is made once, before the walk, and neither relocation
method branches. A translation is already one slot write. A change of
length calls `UiRegion::stretch`, which re-expresses each part at its
own fraction of the new box and needs `stretchable` -- a fixed length
holds its parts as offsets from its start and keeps no fraction to
stretch by.

`Remap`, `UiScalar::outside`, `UiSpan::outside` and `LerpUtil::lerp_inv`
are all gone with it. Nothing inverts a lerp any more: the one division
is done against a denominator `stretchable` already established is not
zero.

What it gives up is the per-axis carry, so a box that changed length on
one axis and not the other is redrawn where it used to be remapped.
Counted: six of `tabs`'s fourteen relocations and five of `text`'s
sixteen, and one extra redraw per frame on `replace_cost`'s 200 rows --
354,310,889 instructions against 354,272,387, which is noise.

Checked: fmt, clippy and 42 tests. `tabs` (with the image replay),
`view`, `minimal` and `text` all still render byte-identical to
`upstream/main`.
2026-09-14 11:07:32 -04:00
iris-ai 78a53b6bf6 Keep the re-place load as a rig, so the next attempt is compared not argued 2026-09-14 10:49:49 -04:00
iris-ai d8c497fcd7 Measure what the chain walk costs, and find that depth is the cost
`tests/chain_cost.rs` times the pass on the GPU with timestamp queries,
which this adapter supports, rather than by the clock. 200,000 two-pixel
instances at 1024x1024, so vertex work dominates, best of eight batches:

  depth  1     77.9 us    +0.0%
  depth  2     78.1 us    +0.2%
  depth  4     79.0 us    +1.3%
  depth  8     81.8 us    +5.0%
  depth 16    111.1 us   +42.6%
  depth 32    159.6 us  +104.9%
  depth 64    250.3 us  +221.3%

Free to about depth 8 and then roughly 3 us per level. Each step is a
storage load whose address is the previous load's result, so it is the
chaining that costs rather than the arithmetic at each level -- which
means the number would look the same for a slot carrying a whole region
instead of a delta.

That matters because every active widget owns a slot, so a primitive
resolves through its full depth in the widget tree, and LAYOUT.md notes
real trees have exceeded 16. At the couple of hundred primitives an
example draws it is nothing; a transcript's glyphs are tens of thousands
of primitives, which is the regime measured here.

No behaviour change. Recorded rather than acted on: keeping the chain
shallow means not giving every widget a slot, which is a design decision
of LAYOUT.md §2 and §6 and the owner's to make.
2026-09-14 03:31:33 -04:00
iris-ai c8ec0866d4 Exercise the subtree remap, which no test reached
`mov` ran ten times across the suite and never once recursed: `Rect`,
`Image` and `()` are the only widgets claiming `OnResize::Scale` and all
three are childless, so the walk that remaps a subtree's children -- the
thing `Remap` exists for -- had no coverage at all.

`Stretchy` is a test widget that claims `Scale` and holds a child, which
is the shape no shipped container has. Removing the recursion leaves its
child behind at the old box and the test says so.
2026-09-14 03:25:35 -04:00
iris-ai 8223a55cfb Move a subtree by writing one slot
`try_reuse`'s pure-translation case now writes the widget's move slot
instead of remapping every primitive in its subtree. Counted on a span
of 20 rows, each five primitives deep, when the row above them changes
height:

  before   100 primitive region writes
  now        0, and 20 slot writes -- one per row the span re-placed

`window_region` walks the same chain on the CPU, so hit testing and
anyone asking in window pixels see a widget where the shader draws it.
`Moves::resolve` stops at `CHAIN_LIMIT` like the shader, and asserts in
debug that it got to the end rather than running out.

Two things fall out of it. Rewriting a region is the one thing a slot
cannot express, so `mov` zeroes the slots of everything it rewrites: a
region is what its slot was a delta from. And `try_reuse` loses its
`old == region` shortcut, which was wrong once a slot exists -- a widget
offered exactly the box it drew against has to have its delta cleared,
not skipped.

`Moves` lives on `UiRenderState` rather than `UiData`, because the draw
is what produces it and `window_region` should not need the ui's
resources to answer where something is. The renderer already takes both.

A slot is retired in `remove_rec`, after the descendants whose slots
name it as their parent. Either order is correct here -- nothing can
claim a freed index while a subtree is coming down, since `on_undraw`
cannot reach the slots -- but this way `remove`'s `undraw` flag only
notifies rather than also deciding slot lifetime, and the retirement
sits beside the recursion it follows.

Checked: fmt, clippy and 41 tests. `tabs` (with the image replay),
`view`, `minimal` and `text` all still render byte-identical, and the
live sway resize round trip -- which re-places most of the tree at the
same size, so it is the slot path throughout -- matches a cold start at
each size.
2026-09-14 03:15:17 -04:00
iris-ai f9ef7514e7 Resolve a primitive's position through a chain of move slots
The plumbing for O(1) subtree movement (LAYOUT.md §2), with every slot
still at zero, so this changes no pixels and the next commit can change
behaviour against a known-good picture.

Every active widget owns a slot in `UiData::moves`: a translation in
physical pixels and the slot it is relative to. A primitive instance and
a mask each name one, and `prelude.wgsl` walks the chain and adds the
accumulated delta. A mask resolves its own chain rather than the drawn
primitive's, so a stationary viewport can clip content that moves inside
it. `CHAIN_LIMIT` is stated on both sides; it bounds a malformed cycle
rather than any real tree.

A slot outlives any one `ActiveData`, because a redraw replaces that
while the widget's children go on pointing at the slot, so it lives in
`UiRenderState::moves` keyed by widget and is retired when the widget
stops being drawn. `MoveIdx` is its own type rather than another
`Id<u32>`: it sits beside `MaskIdx` in an instance and the two must not
be swappable.

`Vec2` is now `repr(align(8))`, which is WGSL's alignment for a
`vec2<f32>`, so a GPU struct holding one is laid out the way its shader
reads it without saying so itself -- `GlyphPrimitive` no longer states
its own alignment, and `MoveOffset` never has to. Both keep a manual
`unsafe impl Pod`, since the trailing padding that alignment introduces
is what `derive(Pod)` refuses. `WindowUniform` holds the `Vec2` its
shader has always called `dim` rather than two loose floats, which was
the last place the two sides described the same bytes differently.

Checked: fmt, clippy and 40 tests. `tabs` (with the image replay),
`view` and `minimal` render byte-identical to `upstream/main`, and
`text` is unchanged.
2026-09-14 03:05:14 -04:00
iris-ai ca2b4b2173 Bring the headless rig into the repository (#17)
Reviewed-on: iris/iris#17
Reviewed-by: iris <2+iris@noreply.localhost>
Co-authored-by: iris-ai <4+iris-ai@noreply.localhost>
2026-09-14 02:50:09 -04:00
iris-ai f9423855e1 Size a widget while drawing it, not in a pass of its own (#16)
Reviewed-on: iris/iris#16
Reviewed-by: iris <2+iris@noreply.localhost>
Co-authored-by: iris-ai <4+iris-ai@noreply.localhost>
2026-09-14 02:48:02 -04:00
iris-ai 43ce8c7d02 Route pointer input per kind, so a scroll falls through a hovered button (#12)
Reviewed-on: iris/iris#12
Reviewed-by: iris <2+iris@noreply.localhost>
Co-authored-by: AIris <4+iris-ai@noreply.localhost>
2026-09-13 22:05:02 -04:00
iris-aiandiris c8ac669f95 Run a ui without a window, and test one (#15)
Small, and disjoint from #12 — this touches `task.rs`, `harness.rs` and `render_state.rs`, none of which #12 goes near.

`Tasks` held an `Arc<Window>` only to call `request_redraw` when a task finished, which made the task queue, and so `DefaultRsc`, impossible to build without a window. It now takes an `Arc<dyn WakeTaskQueue>`, and `Window` implements it.

Waking also moves from *the task ended* to *an update was sent*, which is when there is actually something for the host to apply. A task that keeps running after sending one no longer holds it until it finishes, and a task that sends none no longer asks for a frame nothing needs.

`iris::harness` is what that buys. `UiRenderState` already does layout, hit testing and primitive building with no surface, so a test can build a tree, run frames, move a pointer and read back where widgets landed. `tests/harness.rs` covers span layout, resize relayout, press routing, hover start and end, wheel scrolling with its clamp, and a task update reaching the tree. None of them could be written before, since the only way into layout was a window.

It does not draw. A claim about pixels still needs a real surface — I checked this one against the rig rather than asserting it: `examples/task` under headless sway, centre pixel `ff0000` before the click and `0000ff` after, so the windowed path still applies task updates under the new wake.

The only core change is `UiRenderState::output_size()`, so that a host reading back the size it set does not have to keep a second copy.

---------

Co-authored-by: iris <2+iris@noreply.localhost>
Reviewed-on: iris/iris#15
Reviewed-by: iris <2+iris@noreply.localhost>
Co-authored-by: AIris <4+iris-ai@noreply.localhost>
2026-09-13 21:53:54 -04:00
iris-aiandiris 32b10383d8 Rename the Sized widget to SetSize (#14)
`Sized` shadowed the marker trait, so a `?Sized` bound in any crate that imports the prelude failed to resolve -- a compile error in someone else's code that nothing here would have caught. It was already biting inside iris: `default/mod.rs`, `widget/ptr.rs` and `widget/text/build.rs` all imported `std::marker::Sized` explicitly to get out from under it, which they no longer need.

`SetSize` rather than `FixedSize` because the size it sets need not be fixed -- `width(rest(2))` (a flex weight) and `width(rel(0.5))` (half the parent) build the same widget, and both are more common than `sized((100, 100))`. It also pairs with the `MaxSize` beside it in that module: one sets a length, the other caps it. The builders are unchanged.

`tests/prelude_bounds.rs` is a compile-level guard -- it fails to build if the prelude shadows `Sized` again, which I checked by reverting `src/` under it:

```
error[E0404]: expected trait, found struct `Sized`
 --> tests/prelude_bounds.rs:8:22
  |
8 | fn takes_unsized<T: ?Sized>(_: &T) {}
  |                      ^^^^^ not a trait
```

The pad tab of the tabs example -- the one built out of `sized` and the flexible widths -- renders pixel-identical to before the rename.

---------

Co-authored-by: iris <2+iris@noreply.localhost>
Reviewed-on: iris/iris#14
Co-authored-by: AIris <4+iris-ai@noreply.localhost>
2026-09-13 20:16:17 -04:00
iris-aiandiris 00d2230b84 Build on wgpu 30 (#13)
Two majors, and the renderer is under everything else left to extract -- so it goes before the slices that would otherwise be written against wgpu 28 and then again against 30. `image` 0.25.6 -> 0.25.10 rides along. `winit` stays on 0.30.12, since 0.31 is only a prerelease and nothing here needs it; `parley` 0.11.1 is current.

What the API asked for, beyond the version:

- **An instance takes the display it will present on**, and GLES on Wayland needs it, so the window the surface is made from is handed over with it. That one matters for Android rather than for this machine.
- **`get_current_texture` returns a status rather than a `Result`**, which replaced an `unwrap` that would have panicked on a resize or an occluded window: reconfigure when the surface is outdated, lost or suboptimal, and skip the frame when there is nothing to draw into.
- **Presenting moved to the queue**, still after `pre_present_notify`.
- **Bind group and vertex buffer layouts are sparse**, so each slot states `Some(layout)`.

Verified the same way as #11: the tabs example with two runtime-added images, an image alone in a layer, and glyphs from a four-page atlas all render identically. `tests/draw_cost.rs` gives 33.6/167/587/2855 us per frame at 8/64/256/1024 layers, against 33.3/161/588/2903 on wgpu 28 -- no change.

---------

Co-authored-by: iris <2+iris@noreply.localhost>
Reviewed-on: iris/iris#13
Reviewed-by: iris <2+iris@noreply.localhost>
Co-authored-by: AIris <4+iris-ai@noreply.localhost>
2026-09-13 19:07:47 -04:00
iris-aiandiris b234497d21 Draw the glyph atlas as an array texture and images with their own bind groups + primitive rendering overhaul
Replaces the bindless `binding_array<texture_2d<f32>>` the renderer bound every texture through. That array needs `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack, so the old shape did not run there at all.

The two things being bound want opposite treatment, so they are now split:

- **Glyph atlas pages become layers of one `texture_2d_array`.** A glyph primitive carries a `layer` instead of a view/sampler index pair. A layer index is an ordinary sampling operand, so this needs nothing beyond plain Vulkan 1.0 / GLES. Growing the atlas recreates the array with headroom and `copy_texture_to_texture`s the old layers across, no readback.
- **A standalone image gets its own texture and its own bind group,** and draws in its own call. It no longer needs a per-instance entry in `PrimitiveData`: the bind group has already picked the texture.

`Primitives` keeps images in a list of their own as a result, with `PrimitiveChange::is_image` naming which list a renumbering belongs to -- the two have independent index spaces, so `(layer, inst_idx)` alone would collide between them.

Two notes on judgement calls, since this slice was rebuilt on top of `main` rather than transplanted:

- The source version renamed `GlyphEntry::is_colored` to `is_color` and added a second `IS_COLOR` flag constant beside the existing `GlyphEntry::IS_COLORED`. Both dropped: #10's naming and its `flags()` are kept, and UVs stay `Vec2` rather than going back to `[f32; 2]`.
- `ImageGpu` no longer holds the `Texture` behind its view, which removes an `#[allow(dead_code)]`. A `TextureView` keeps its own reference to the texture, checked by rendering rather than assumed -- see below.

### Verification

```
cargo fmt --all --check
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked
```

All clean; the 4 text-edit tests pass. The only clippy output is the pre-existing future-incompatibility notice about `naga`/`wgpu`/`winit`.

Because this is a rendering change, it was also run for real rather than only compiled. The `tabs` example was rendered on this machine's GPU -- Venus onto an RX 7900 XT, confirmed from the loaded ICD (`libvulkan_virtio.so` on `/dev/dri/renderD128`) rather than assumed, since a failed Vulkan init here silently falls back to llvmpipe and would make the screenshots meaningless.

Screenshots before and after the change are **byte-identical** (same md5) in two scenes: the default tab, which exercises text (the atlas path) and rects, and the image tab with a standalone image pushed at startup, which exercises the per-image bind group. The image-tab scene needed a temporary local edit to the example to push the image without a click; that edit is not part of this branch. The same comparison, re-run after dropping the `Texture` field, is still byte-identical -- which is the check that the view alone keeps it alive.

---------

Co-authored-by: iris <2+iris@noreply.localhost>
Reviewed-on: iris/iris#11
Reviewed-by: iris <2+iris@noreply.localhost>
Co-authored-by: AIris <4+iris-ai@noreply.localhost>
2026-09-13 18:56:59 -04:00
75 changed files with 4406 additions and 1925 deletions

No files matched your search

Generated
+228 -245
View File
File diff suppressed because it is too large. Load diff
+5 -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"] }
[workspace]
members = ["core", "macro"]
members = ["core", "macro", "rig-input"]
[workspace.package]
version = "0.1.0"
@@ -29,9 +29,9 @@ edition = "2024"
[workspace.dependencies]
pollster = "0.4.0"
winit = "0.30.12"
wgpu = "28.0.0"
wgpu = "30.0.1"
bytemuck = "1.23.1"
image = "0.25.6"
image = "0.25.10"
parley = "0.11.1"
swash = "0.2.10"
fxhash = "0.2.1"
@@ -40,3 +40,5 @@ arboard = "3.6.1"
iris-core = { path = "core" }
iris-macro = { path = "macro" }
tokio = "1.49.0"
wayland-client = "0.31.15"
wayland-protocols-wlr = { version = "0.3.12", features = ["client"] }
+6 -1
View File
@@ -79,6 +79,7 @@ type EventData<Rsc, E> = (E, Rc<dyn for<'a> EventFn<Rsc, <E as Event>::Data<'a>>
pub struct TypeEventManager<Rsc: HasEvents, E: Event> {
// TODO: reduce visiblity!!
pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>,
pub global: E::Global,
map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>,
}
@@ -107,6 +108,7 @@ impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> {
fn default() -> Self {
Self {
active: Default::default(),
global: Default::default(),
map: Default::default(),
}
}
@@ -138,11 +140,13 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
pub fn run_fn<'a>(
&mut self,
id: impl IdLike,
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) + 'a {
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) -> bool + 'a {
let fs = self.map.get(&id.id()).cloned().unwrap_or_default();
move |ctx, rsc| {
let mut consumed = false;
for (e, f) in fs {
if let Some(data) = e.should_run(&ctx.data) {
consumed |= e.consumes(&data);
f(
EventCtx {
state: ctx.state,
@@ -152,6 +156,7 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
)
}
}
consumed
}
}
}
+9
View File
@@ -9,10 +9,19 @@ pub use rsc::*;
pub trait Event: Sized + 'static + Clone {
type Data<'a>: Clone = ();
type State: Default = ();
/// State the whole event type keeps, rather than one copy per widget.
type Global: Default = ();
#[allow(unused_variables)]
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
Some(data.clone())
}
/// Whether having run on this data uses up whatever triggered it, so
/// nothing further should see it.
#[allow(unused_variables)]
fn consumes(&self, data: &Self::Data<'_>) -> bool {
false
}
}
pub trait EventLike {
+2 -1
View File
@@ -21,12 +21,13 @@ pub trait HasEvents: Sized + UiRsc + HasState {
}
pub trait RunEvents: HasEvents {
/// Whether anything that ran used up what triggered it.
fn run_event<E: EventLike>(
&mut self,
id: impl IdLike,
data: <E::Event as Event>::Data<'_>,
state: &mut Self::State,
) {
) -> bool {
let f = self.events_mut().get_type::<E>().run_fn(id);
f(EventCtx { state, data }, self)
}
+35 -27
View File
@@ -56,13 +56,6 @@ 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 {
match axis {
Axis::X => &mut self.x,
@@ -209,10 +202,15 @@ impl UiScalar {
}
}
pub const fn outside(&self, span: &UiSpan) -> Self {
let rel = self.rel.lerp_inv(span.start.rel, span.end.rel);
let abs = self.abs - rel.lerp(span.start.abs, span.end.abs);
Self { rel, abs }
/// `within` undone against `from` and redone against `to`, for one axis.
/// `from`'s relative extent is the denominator, so it must not be zero.
fn stretch(&self, from: &UiSpan, to: &UiSpan) -> Self {
let frac = (self.rel - from.start.rel) / (from.end.rel - from.start.rel);
Self {
rel: frac.lerp(to.start.rel, to.end.rel),
abs: self.abs - frac.lerp(from.start.abs, from.end.abs)
+ frac.lerp(to.start.abs, to.end.abs),
}
}
pub fn within_len(&self, len: UiScalar) -> Self {
@@ -283,13 +281,6 @@ impl UiSpan {
}
}
pub const fn outside(&self, parent: &Self) -> Self {
Self {
start: self.start.outside(parent),
end: self.end.outside(parent),
}
}
pub const fn len(&self) -> UiScalar {
self.end - self.start
}
@@ -324,14 +315,7 @@ impl UiRegion {
y: self.y.within(&parent.y),
}
}
pub const fn outside(&self, parent: &Self) -> Self {
Self {
x: self.x.outside(&parent.x),
y: self.y.outside(&parent.y),
}
}
pub const fn axis(&mut self, axis: Axis) -> &UiSpan {
pub const fn axis(&self, axis: Axis) -> &UiSpan {
match axis {
Axis::X => &self.x,
Axis::Y => &self.y,
@@ -407,6 +391,30 @@ impl UiRegion {
},
}
}
/// Whether a stretch out of this box can be expressed. Each part inside a
/// box is held as a fraction of it, and a fixed length has no fraction to
/// hold one by -- every part of it is just an offset from its start.
pub fn stretchable(&self) -> bool {
self.x.start.rel != self.x.end.rel && self.y.start.rel != self.y.end.rel
}
/// Re-expresses a region inside `from` as the same fractions of `to`.
/// `from` must be `stretchable`; a translation is `shift` instead, which
/// needs no fractions and works out of any box.
pub fn stretch(&self, from: &UiRegion, to: &UiRegion) -> UiRegion {
debug_assert!(from.stretchable(), "a fixed length has no fraction");
UiRegion {
x: UiSpan {
start: self.x.start.stretch(&from.x, &to.x),
end: self.x.end.stretch(&from.x, &to.x),
},
y: UiSpan {
start: self.y.start.stretch(&from.y, &to.y),
end: self.y.end.stretch(&from.y, &to.y),
},
}
}
}
impl Display for UiRegion {
@@ -421,7 +429,7 @@ impl Display for UiRegion {
}
}
#[derive(Debug)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PixelRegion {
pub top_left: Vec2,
pub bot_right: Vec2,
+3 -3
View File
@@ -1,7 +1,7 @@
use std::ops::{Index, IndexMut};
use crate::{
render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives},
render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
util::to_mut,
};
@@ -39,7 +39,7 @@ struct Child {
tail: usize,
}
pub type PrimitiveLayers = Layers<Primitives>;
pub type DrawLayers = Layers<LayerDraws>;
impl<T: Default> Layers<T> {
pub fn new() -> Layers<T> {
@@ -119,7 +119,7 @@ impl<T: Default> Layers<T> {
}
}
impl PrimitiveLayers {
impl DrawLayers {
pub fn write<P: Primitive>(
&mut self,
layer: LayerId,
+13 -22
View File
@@ -1,6 +1,5 @@
use crate::{
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor,
util::Vec2,
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
};
use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
@@ -159,7 +158,7 @@ impl TextBuffer {
}
impl TextData {
pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec<PlacedGlyph> {
pub fn place(&mut self, buffer: &TextBuffer) -> Vec<PlacedGlyph> {
let mut placed = Vec::new();
for line in buffer.layout.lines() {
for item in line.items() {
@@ -185,17 +184,14 @@ impl TextData {
subpixel,
coords: coords_hash,
};
let Some(entry) = self.glyph_entry(
GlyphRaster {
key,
font: font_ref,
font_size,
coords,
subpixel,
glyph_id: glyph.id,
},
textures,
) else {
let Some(entry) = self.glyph_entry(GlyphRaster {
key,
font: font_ref,
font_size,
coords,
subpixel,
glyph_id: glyph.id,
}) else {
continue;
};
placed.push(PlacedGlyph {
@@ -211,11 +207,7 @@ impl TextData {
placed
}
fn glyph_entry(
&mut self,
glyph: GlyphRaster<'_>,
textures: &mut Textures,
) -> Option<GlyphEntry> {
fn glyph_entry(&mut self, glyph: GlyphRaster<'_>) -> Option<GlyphEntry> {
if let Some(entry) = self.atlas.get(&glyph.key) {
return entry;
}
@@ -237,7 +229,7 @@ impl TextData {
.render(&mut scaler, glyph.glyph_id as u16);
if let Some(image) = image {
self.atlas.insert(glyph.key, &image, textures)
self.atlas.insert(glyph.key, &image)
} else {
self.atlas.insert_empty(glyph.key);
None
@@ -278,10 +270,9 @@ impl TextData {
buffer: &mut TextBuffer,
attrs: &TextAttrs,
width: Option<f32>,
textures: &mut Textures,
) -> RenderedText {
buffer.shape(self, attrs, width);
let glyphs = self.place(buffer, textures);
let glyphs = self.place(buffer);
RenderedText {
glyphs,
size: buffer.size(),
+18 -19
View File
@@ -1,7 +1,4 @@
use crate::{
render::TexturePrimitive,
util::{RefCounter, Vec2},
};
use crate::util::{RefCounter, Vec2};
use image::{DynamicImage, GenericImageView};
use std::{
ops::Index,
@@ -10,7 +7,7 @@ use std::{
#[derive(Debug, Clone)]
pub struct TextureHandle {
inner: TexturePrimitive,
slot: u32,
size: Vec2,
counter: RefCounter,
send: Sender<u32>,
@@ -31,6 +28,8 @@ pub enum TextureUpdate<'a> {
Set(u32, &'a DynamicImage),
Patch(u32, PatchRect, &'a DynamicImage),
Free(u32),
/// Added and freed before the renderer drained either update. It still has
/// to push a slot to stay lined up with `images`; `Free` then empties it.
PushFree,
SetFree,
}
@@ -64,14 +63,8 @@ impl Textures {
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
let image = image.into();
let size = image.dimensions().into();
let view_idx = self.push(image);
// 0 == default in renderer; TODO: actually create samplers here
let sampler_idx = 0;
TextureHandle {
inner: TexturePrimitive {
view_idx,
sampler_idx,
},
slot: self.push(image),
size,
counter: RefCounter::new(),
send: self.send.clone(),
@@ -92,15 +85,20 @@ impl Textures {
}
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
self.images[handle.inner.view_idx as usize]
self.images[handle.slot as usize]
.as_mut()
.expect("texture was freed while still held")
}
/// Queue an upload of just `rect`, after writing it with `image_mut`.
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
self.updates
.push(Update::Patch(handle.inner.view_idx, rect));
self.updates.push(Update::Patch(handle.slot, rect));
}
/// How many textures are live, which is what a ui can ask; the renderer's
/// copies follow from the updates it drains.
pub fn count(&self) -> usize {
self.images.iter().flatten().count()
}
pub fn free(&mut self) {
@@ -131,8 +129,9 @@ impl Textures {
}
impl TextureHandle {
pub fn primitive(&self) -> TexturePrimitive {
self.inner
/// Index into `Textures`, and into the renderer's parallel slots.
pub fn slot(&self) -> u32 {
self.slot
}
pub fn size(&self) -> Vec2 {
self.size
@@ -142,7 +141,7 @@ impl TextureHandle {
impl Drop for TextureHandle {
fn drop(&mut self) {
if self.counter.drop() {
let _ = self.send.send(self.inner.view_idx);
let _ = self.send.send(self.slot);
}
}
}
@@ -151,7 +150,7 @@ impl Index<&TextureHandle> for Textures {
type Output = DynamicImage;
fn index(&self, index: &TextureHandle) -> &Self::Output {
self.images[index.inner.view_idx as usize].as_ref().unwrap()
self.images[index.slot as usize].as_ref().unwrap()
}
}
+49 -36
View File
@@ -1,11 +1,12 @@
use crate::{
PatchRect, TextureHandle, Textures,
PatchRect,
util::{HashMap, Vec2},
};
use image::RgbaImage;
use swash::scale::image::{Content, Image};
const PAGE: u32 = 1024;
/// Side of one page, and so of every layer of `render::page`'s array texture.
pub(crate) const PAGE: u32 = 1024;
/// Transparent margin kept around every glyph, so that sampling one cannot
/// pick up its neighbour along a shared edge.
@@ -35,8 +36,8 @@ pub struct GlyphEntry {
pub width: u32,
pub height: u32,
pub is_colored: bool,
pub view_idx: u32,
pub sampler_idx: u32,
/// Which atlas array layer this glyph is on.
pub layer: u32,
}
impl GlyphEntry {
@@ -48,18 +49,26 @@ impl GlyphEntry {
}
struct Page {
handle: TextureHandle,
image: RgbaImage,
x: u32,
y: u32,
shelf_height: u32,
}
/// A rectangle of one page the renderer has not uploaded yet.
#[derive(Clone, Copy)]
pub struct PageUpload {
pub layer: u32,
pub rect: PatchRect,
}
#[derive(Default)]
pub struct GlyphAtlas {
pages: Vec<Page>,
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
/// too, so it is not re-rasterised on every layout.
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
uploads: Vec<PageUpload>,
}
impl GlyphAtlas {
@@ -67,12 +76,7 @@ impl GlyphAtlas {
self.entries.get(key).copied()
}
pub fn insert(
&mut self,
key: GlyphKey,
image: &Image,
textures: &mut Textures,
) -> Option<GlyphEntry> {
pub fn insert(&mut self, key: GlyphKey, image: &Image) -> Option<GlyphEntry> {
let w = image.placement.width;
let h = image.placement.height;
if w == 0 || h == 0 {
@@ -94,23 +98,11 @@ impl GlyphAtlas {
return None;
}
let (page_idx, x, y) = self.allocate(w, h, textures);
let page = &self.pages[page_idx];
let upload = self.allocate(w, h);
let PatchRect { x, y, .. } = upload.rect;
write_glyph(&mut self.pages[upload.layer as usize].image, image, x, y);
self.uploads.push(upload);
let img = textures.image_mut(&page.handle);
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
write_glyph(rgba, image, x, y);
let handle = page.handle.clone();
let rect = PatchRect {
x,
y,
width: w,
height: h,
};
textures.patch(&handle, rect);
let page = &self.pages[page_idx];
let scale = 1.0 / PAGE as f32;
let entry = GlyphEntry {
uv_min: Vec2::new(x as f32 * scale, y as f32 * scale),
@@ -120,39 +112,60 @@ impl GlyphAtlas {
width: w,
height: h,
is_colored: matches!(image.content, Content::Color),
view_idx: page.handle.primitive().view_idx,
sampler_idx: page.handle.primitive().sampler_idx,
layer: upload.layer,
};
self.entries.insert(key, Some(entry));
Some(entry)
}
fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) {
/// Reserves room for a `w` by `h` glyph, adding a page if none has it.
fn allocate(&mut self, w: u32, h: u32) -> PageUpload {
let rect = |x, y| PatchRect {
x,
y,
width: w,
height: h,
};
if let Some((i, (x, y))) = self
.pages
.iter_mut()
.enumerate()
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
{
return (i, x, y);
return PageUpload {
layer: i as u32,
rect: rect(x, y),
};
}
let handle = textures.add(RgbaImage::new(PAGE, PAGE));
self.pages.push(Page {
handle,
image: RgbaImage::new(PAGE, PAGE),
x: PAD + w + PAD,
y: PAD,
shelf_height: h + PAD,
});
(self.pages.len() - 1, PAD, PAD)
PageUpload {
layer: self.pages.len() as u32 - 1,
rect: rect(PAD, PAD),
}
}
/// Drains what has been written since the last call, for the renderer to
/// upload. A new page needs nothing more: wgpu leaves the rest of a fresh
/// layer transparent, which is what an atlas wants.
pub fn uploads(&mut self) -> impl Iterator<Item = (PageUpload, &RgbaImage)> {
let pages = &self.pages;
self.uploads
.drain(..)
.map(|upload| (upload, &pages[upload.layer as usize].image))
}
pub fn insert_empty(&mut self, key: GlyphKey) {
self.entries.insert(key, None);
}
pub fn page_count(&self) -> usize {
self.pages.len()
pub fn page_count(&self) -> u32 {
self.pages.len() as u32
}
pub fn glyph_count(&self) -> usize {
+45 -7
View File
@@ -1,31 +1,28 @@
use crate::{UiRegion, util::Id};
use crate::{UiRegion, util::Id, util::Vec2};
use wgpu::*;
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
pub struct WindowUniform {
pub width: f32,
pub height: f32,
pub dim: Vec2,
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct PrimitiveInstance {
pub region: UiRegion,
pub binding: u32,
pub idx: u32,
pub mask_idx: MaskIdx,
pub move_idx: MoveIdx,
}
impl PrimitiveInstance {
const ATTRIBS: [VertexAttribute; 7] = vertex_attr_array![
const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![
0 => Float32x2,
1 => Float32x2,
2 => Float32x2,
3 => Float32x2,
4 => Uint32,
5 => Uint32,
6 => Uint32,
];
pub fn desc() -> VertexBufferLayout<'static> {
@@ -47,4 +44,45 @@ impl MaskIdx {
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Mask {
pub region: UiRegion,
pub move_idx: MoveIdx,
}
/// Its own type rather than another `Id<u32>`, because it sits beside
/// `MaskIdx` in an instance and the two must not be swappable.
#[repr(transparent)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MoveIdx(u32);
impl MoveIdx {
pub const NONE: Self = Self(u32::MAX);
pub(crate) fn slot(idx: usize) -> Self {
Self(idx as u32)
}
pub(crate) fn idx(self) -> usize {
self.0 as usize
}
}
/// One link of the chain a primitive's position is resolved through: a
/// translation in physical pixels, and the slot it is relative to. Moving a
/// subtree writes its own slot and nothing else.
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct MoveOffset {
pub delta: Vec2,
pub parent: MoveIdx,
}
unsafe impl bytemuck::Pod for MoveOffset {}
unsafe impl bytemuck::Zeroable for MoveOffset {}
impl MoveOffset {
pub fn root(parent: MoveIdx) -> Self {
Self {
delta: Vec2::ZERO,
parent,
}
}
}
+233 -197
View File
@@ -1,8 +1,6 @@
use std::num::NonZero;
use crate::{
UiData, UiRenderState,
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
render::{data::PrimitiveInstance, util::ArrBuf},
util::{HashMap, Vec2},
};
use data::WindowUniform;
@@ -13,50 +11,73 @@ use wgpu::{
mod atlas;
mod data;
mod page;
mod primitive;
mod texture;
mod util;
pub use atlas::*;
pub use data::{Mask, MaskIdx};
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset};
pub use primitive::*;
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
pub struct UiRenderNode {
uniform_group: BindGroup,
primitive_layout: BindGroupLayout,
rsc_layout: BindGroupLayout,
rsc_group: BindGroup,
shared_layout: BindGroupLayout,
shared_group: BindGroup,
format: TextureFormat,
pipeline: RenderPipeline,
/// One per registered primitive, in id order.
primitives: Vec<PrimitivePipeline>,
layers: HashMap<usize, RenderLayer>,
active: Vec<usize>,
window_buffer: Buffer,
textures: GpuTextures,
masks: ArrBuf<Mask>,
moves: ArrBuf<MoveOffset>,
}
struct RenderLayer {
/// One per registered primitive, `None` where this layer draws none.
primitives: Vec<Option<ListBuffers>>,
}
/// What draws one registered primitive.
struct PrimitivePipeline {
data_layout: BindGroupLayout,
pipeline: RenderPipeline,
render: Box<dyn PrimitiveRender>,
}
/// One list's vertex buffer and the data its shader reads.
struct ListBuffers {
instance: ArrBuf<PrimitiveInstance>,
primitives: PrimitiveBuffers,
primitive_group: BindGroup,
data: ArrBuf<u8>,
group: Option<BindGroup>,
/// What the primitive asked to keep per instance, if anything.
bindings: Vec<u32>,
}
impl UiRenderNode {
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.uniform_group, &[]);
pass.set_bind_group(2, &self.rsc_group, &[]);
pass.set_bind_group(0, &self.shared_group, &[]);
for i in &self.active {
let layer = &self.layers[i];
if layer.instance.len() == 0 {
continue;
for (id, list) in layer.primitives.iter().enumerate() {
let Some(list) = list else { continue };
let Some(group) = &list.group else { continue };
let primitive = &self.primitives[id];
pass.set_pipeline(&primitive.pipeline);
pass.set_bind_group(1, group, &[]);
pass.set_vertex_buffer(0, list.instance.buffer.slice(..));
primitive.render.draw(
pass,
ListDraw {
instances: list.instance.len() as u32,
bindings: &list.bindings,
},
);
}
pass.set_bind_group(1, &layer.primitive_group, &[]);
pass.set_vertex_buffer(0, layer.instance.buffer.slice(..));
pass.draw(0..4, 0..layer.instance.len() as u32);
}
}
@@ -67,81 +88,75 @@ impl UiRenderNode {
ui: &mut UiData,
ui_render: &mut UiRenderState,
) {
// Before the layers: each list is given its pipeline's data layout.
self.build_pipelines(device, queue, &ui.primitives);
self.active.clear();
for (i, primitives) in ui_render.layers.iter_mut() {
for (i, draws) in ui_render.layers.iter_mut() {
self.active.push(i);
for change in primitives.apply_free() {
for change in draws.apply_free() {
if let Some(inst) = ui_render.active.get_mut(&change.id) {
for h in &mut inst.primitives {
if h.layer == i && h.inst_idx == change.old {
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
h.inst_idx = change.new;
break;
}
}
}
}
let rlayer = self.layers.entry(i).or_insert_with(|| {
let primitives = PrimitiveBuffers::new(device);
let primitive_group =
Self::primitive_group(device, &self.primitive_layout, primitives.buffers());
RenderLayer {
instance: ArrBuf::new(
device,
BufferUsages::VERTEX | BufferUsages::COPY_DST,
"instance",
),
primitives,
primitive_group,
let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new);
if draws.updated {
let lists = draws.primitives();
// The zip would otherwise skip a list with no pipeline.
assert!(lists.len() <= self.primitives.len());
rlayer.primitives.resize_with(lists.len(), || None);
for ((buffers, list), primitive) in rlayer
.primitives
.iter_mut()
.zip(lists)
.zip(&self.primitives)
{
let Some(list) = list else {
continue;
};
buffers
.get_or_insert_with(|| ListBuffers::new(device))
.update(device, queue, primitive, list);
}
});
if primitives.updated {
rlayer
.instance
.update(device, queue, primitives.instances());
rlayer.primitives.update(device, queue, primitives.data());
rlayer.primitive_group = Self::primitive_group(
device,
&self.primitive_layout,
rlayer.primitives.buffers(),
);
primitives.updated = false;
draws.updated = false;
}
}
let mut changed = false;
changed |= self.textures.update(&mut ui.textures);
for primitive in &mut self.primitives {
primitive.render.update(ui);
}
let mut regroup = false;
if ui.masks.changed {
ui.masks.changed = false;
self.masks.update(device, queue, &ui.masks[..]);
changed = true;
regroup |= self.masks.update(device, queue, &ui.masks[..]);
}
if changed {
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures, &self.masks);
if ui_render.moves.changed {
ui_render.moves.changed = false;
regroup |= self.moves.update(device, queue, ui_render.moves.entries());
}
if regroup {
self.shared_group = Self::shared_group(
device,
&self.shared_layout,
&self.window_buffer,
&self.masks,
&self.moves,
);
}
}
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
let size = size.into();
let slice = &[WindowUniform {
width: size.x,
height: size.y,
}];
let slice = &[WindowUniform { dim: size }];
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
}
pub fn new(
device: &Device,
queue: &Queue,
config: &SurfaceConfiguration,
limits: UiLimits,
) -> Self {
let shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("UI Shape Shader"),
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
});
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
let window_uniform = WindowUniform {
width: config.width as f32,
height: config.height as f32,
dim: Vec2::new(config.width as f32, config.height as f32),
};
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("window"),
@@ -149,67 +164,80 @@ impl UiRenderNode {
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
});
let uniform_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
label: Some("window"),
});
let uniform_group = Self::bind_group_0(device, &uniform_layout, &window_buffer);
let primitive_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &core::array::from_fn::<_, { PrimitiveBuffers::LEN }, _>(|i| {
BindGroupLayoutEntry {
binding: i as u32,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
}),
label: Some("primitive"),
});
let tex_manager = GpuTextures::new(device, queue);
let shared_layout = Self::shared_layout(device);
let masks = ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui masks",
);
let moves = ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui move offsets",
);
let shared_group =
Self::shared_group(device, &shared_layout, &window_buffer, &masks, &moves);
let rsc_layout = Self::rsc_layout(device, &limits);
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks);
Self {
shared_layout,
shared_group,
format: config.format,
primitives: Vec::new(),
window_buffer,
layers: HashMap::default(),
active: Vec::new(),
masks,
moves,
}
}
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("UI Shape Pipeline Layout"),
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout],
immediate_size: 0,
/// Compiles a pipeline for every primitive registered since the last call.
/// Sources only ever arrive at the end, so an id keeps its pipeline.
fn build_pipelines(&mut self, device: &Device, queue: &Queue, registry: &PrimitiveRegistry) {
for source in &registry.sources()[self.primitives.len()..] {
let render = (source.render)(device, queue);
let data_layout = Self::data_layout(device, source.stride);
let mut groups = vec![Some(&self.shared_layout), Some(&data_layout)];
groups.extend(render.layout().map(Some));
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some(source.label),
bind_group_layouts: &groups,
immediate_size: 0,
});
let pipeline = Self::pipeline(device, &layout, self.format, source.wgsl, source.label);
self.primitives.push(PrimitivePipeline {
data_layout,
pipeline,
render,
});
}
}
fn pipeline(
device: &Device,
layout: &PipelineLayout,
format: TextureFormat,
wgsl: &str,
label: &str,
) -> RenderPipeline {
let module = device.create_shader_module(ShaderModuleDescriptor {
label: Some(label),
source: ShaderSource::Wgsl(format!("{PRELUDE}\n{wgsl}").into()),
});
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("UI Shape Pipeline"),
layout: Some(&pipeline_layout),
device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some(label),
layout: Some(layout),
vertex: VertexState {
module: &shader,
module: &module,
entry_point: Some("vs_main"),
buffers: &[PrimitiveInstance::desc()],
buffers: &[Some(PrimitiveInstance::desc())],
compilation_options: Default::default(),
},
fragment: Some(FragmentState {
module: &shader,
module: &module,
entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState {
format: config.format,
format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
@@ -232,136 +260,144 @@ impl UiRenderNode {
},
multiview_mask: None,
cache: None,
});
Self {
uniform_group,
primitive_layout,
rsc_layout,
rsc_group,
pipeline,
window_buffer,
layers: HashMap::default(),
active: Vec::new(),
textures: tex_manager,
masks,
}
}
fn bind_group_0(
device: &Device,
layout: &BindGroupLayout,
window_buffer: &Buffer,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[BindGroupEntry {
binding: 0,
resource: window_buffer.as_entire_binding(),
}],
label: Some("ui window"),
})
}
fn primitive_group(
device: &Device,
layout: &BindGroupLayout,
buffers: [(u32, &Buffer); PrimitiveBuffers::LEN],
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &buffers.map(|(binding, buf)| BindGroupEntry {
binding,
resource: buf.as_entire_binding(),
}),
label: Some("ui primitives"),
})
}
fn rsc_layout(device: &Device, limits: &UiLimits) -> BindGroupLayout {
/// What every draw in the ui is given: the window, the masks and the
/// move chain every position is resolved through.
fn shared_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: false },
view_dimension: TextureViewDimension::D2,
multisampled: false,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: BufferSize::new(size_of::<WindowUniform>() as u64),
},
count: Some(NonZero::new(limits.max_textures).unwrap()),
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: Some(NonZero::new(limits.max_samplers).unwrap()),
},
BindGroupLayoutEntry {
binding: 2,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
min_binding_size: BufferSize::new(size_of::<Mask>() as u64),
},
count: None,
},
BindGroupLayoutEntry {
binding: 2,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: BufferSize::new(size_of::<MoveOffset>() as u64),
},
count: None,
},
],
label: Some("ui rsc"),
label: Some("ui shared"),
})
}
fn rsc_group(
fn shared_group(
device: &Device,
layout: &BindGroupLayout,
tex_manager: &GpuTextures,
window: &Buffer,
masks: &ArrBuf<Mask>,
moves: &ArrBuf<MoveOffset>,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureViewArray(&tex_manager.views()),
resource: window.as_entire_binding(),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::SamplerArray(&tex_manager.samplers()),
resource: masks.buffer.as_entire_binding(),
},
BindGroupEntry {
binding: 2,
resource: masks.buffer.as_entire_binding(),
resource: moves.buffer.as_entire_binding(),
},
],
label: Some("ui rsc"),
label: Some("ui shared"),
})
}
pub fn view_count(&self) -> usize {
self.textures.view_count()
/// Layout for a list of one primitive's data. Every size in the ui is
/// stated, so "is the buffer big enough for one entry?" is answered when
/// the bind group is made; a `None` size is wgpu's to check on every draw.
fn data_layout(device: &Device, stride: u64) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: BufferSize::new(stride),
},
count: None,
}],
label: Some("ui primitive data"),
})
}
}
pub struct UiLimits {
max_textures: u32,
max_samplers: u32,
}
impl Default for UiLimits {
fn default() -> Self {
impl RenderLayer {
fn new() -> Self {
Self {
max_textures: 100000,
max_samplers: 1000,
primitives: Vec::new(),
}
}
}
impl UiLimits {
pub fn max_binding_array_elements_per_shader_stage(&self) -> u32 {
self.max_textures + self.max_samplers
impl ListBuffers {
fn new(device: &Device) -> Self {
Self {
instance: ArrBuf::new(
device,
BufferUsages::VERTEX | BufferUsages::COPY_DST,
"instance",
),
data: ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"primitive data",
),
group: None,
bindings: Vec::new(),
}
}
pub fn max_binding_array_sampler_elements_per_shader_stage(&self) -> u32 {
self.max_samplers
fn update(
&mut self,
device: &Device,
queue: &Queue,
primitive: &PrimitivePipeline,
list: &InstanceList,
) {
self.bindings.clear();
primitive.render.instance_bindings(list, &mut self.bindings);
self.instance.update(device, queue, list.instances());
let resized = self.data.update(device, queue, list.data());
if list.instances().is_empty() {
self.group = None;
} else if resized || self.group.is_none() {
self.group = Some(device.create_bind_group(&BindGroupDescriptor {
layout: &primitive.data_layout,
entries: &[BindGroupEntry {
binding: 0,
resource: self.data.buffer.as_entire_binding(),
}],
label: Some("ui primitive data"),
}));
}
}
}
+156
View File
@@ -0,0 +1,156 @@
use wgpu::*;
use crate::{GlyphAtlas, UiData};
use super::{
atlas::PAGE,
primitive::{ListDraw, PrimitiveRender},
texture::{default_sampler, sampled_group, sampled_layout, write_region},
};
/// Draws glyphs from the atlas, which it owns: one array texture bound once
/// for a whole list, since every glyph in it reads the same pages.
pub struct GlyphRender {
pages: GpuPages,
layout: BindGroupLayout,
sampler: Sampler,
}
impl GlyphRender {
pub fn new(device: &Device, queue: &Queue) -> Self {
let layout = sampled_layout(device, TextureViewDimension::D2Array, "ui atlas");
let sampler = default_sampler(device);
Self {
pages: GpuPages::new(device, queue, &layout, &sampler),
layout,
sampler,
}
}
}
impl PrimitiveRender for GlyphRender {
fn layout(&self) -> Option<&BindGroupLayout> {
Some(&self.layout)
}
fn update(&mut self, ui: &mut UiData) {
self.pages
.update(&mut ui.text.atlas, &self.layout, &self.sampler);
}
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
pass.set_bind_group(2, self.pages.group(), &[]);
pass.draw(0..4, 0..list.instances);
}
}
/// The glyph atlas on the GPU: one array texture whose layers are the pages
/// `GlyphAtlas` packs.
///
/// One array rather than a texture per page because a layer index is ordinary
/// Vulkan 1.0 / GLES sampling, where a `binding_array` would need
/// `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack.
pub struct GpuPages {
device: Device,
queue: Queue,
texture: Texture,
group: BindGroup,
}
impl GpuPages {
pub fn new(
device: &Device,
queue: &Queue,
layout: &BindGroupLayout,
sampler: &Sampler,
) -> Self {
let texture = create_array(device, 1);
Self {
device: device.clone(),
queue: queue.clone(),
group: atlas_group(device, layout, &texture, sampler),
texture,
}
}
pub fn update(&mut self, atlas: &mut GlyphAtlas, layout: &BindGroupLayout, sampler: &Sampler) {
if atlas.page_count() > self.texture.depth_or_array_layers() {
self.grow(atlas.page_count(), layout, sampler);
}
for (upload, page) in atlas.uploads() {
let dst = TexelCopyTextureInfo {
texture: &self.texture,
mip_level: 0,
origin: Origin3d {
x: upload.rect.x,
y: upload.rect.y,
z: upload.layer,
},
aspect: TextureAspect::All,
};
write_region(&self.queue, dst, page, upload.rect);
}
}
pub fn group(&self) -> &BindGroup {
&self.group
}
/// Doubles until `needed` fits and copies the old layers across GPU side.
/// The new texture stales the group, so that is rebuilt here.
fn grow(&mut self, needed: u32, layout: &BindGroupLayout, sampler: &Sampler) {
let old = self.texture.depth_or_array_layers();
let mut layers = old;
while layers < needed {
layers *= 2;
}
let texture = create_array(&self.device, layers);
let mut encoder = self
.device
.create_command_encoder(&CommandEncoderDescriptor {
label: Some("atlas grow"),
});
encoder.copy_texture_to_texture(
self.texture.as_image_copy(),
texture.as_image_copy(),
Extent3d {
width: PAGE,
height: PAGE,
depth_or_array_layers: old,
},
);
self.queue.submit(std::iter::once(encoder.finish()));
self.group = atlas_group(&self.device, layout, &texture, sampler);
self.texture = texture;
}
}
fn atlas_group(
device: &Device,
layout: &BindGroupLayout,
texture: &Texture,
sampler: &Sampler,
) -> BindGroup {
let view = texture.create_view(&TextureViewDescriptor {
dimension: Some(TextureViewDimension::D2Array),
..Default::default()
});
sampled_group(device, layout, &view, sampler, "ui atlas")
}
fn create_array(device: &Device, layers: u32) -> Texture {
device.create_texture(&TextureDescriptor {
label: Some("glyph atlas"),
size: Extent3d {
width: PAGE,
height: PAGE,
depth_or_array_layers: layers,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC,
view_formats: &[],
})
}
+309 -203
View File
@@ -1,181 +1,321 @@
use std::ops::{Deref, DerefMut};
use std::{any::TypeId, marker::PhantomData};
use crate::{
Color, UiRegion, WidgetId,
Color, TextureHandle, UiData, UiRegion, WidgetId,
render::{
ArrBuf,
data::{MaskIdx, PrimitiveInstance},
data::{MaskIdx, MoveIdx, PrimitiveInstance},
page::GlyphRender,
texture::ImageRender,
},
util::Vec2,
util::{HashMap, Vec2},
};
use bytemuck::Pod;
use wgpu::*;
use wgpu::{BindGroupLayout, Device, Queue, RenderPass};
pub struct Primitives {
instances: Vec<PrimitiveInstance>,
assoc: Vec<WidgetId>,
data: PrimitiveData,
free: Vec<usize>,
pub updated: bool,
/// One instance of a primitive, laid out as the struct its shader reads.
///
/// The type carries its own shader, so drawing one is all the wiring it needs:
/// its list, free list, buffers and pipeline follow from being registered.
pub trait Primitive: Pod + 'static {
/// Compiled after `prelude.wgsl`, which states what it declares and what
/// it is given.
const WGSL: &'static str;
/// Made once, the first time the renderer sees this primitive. It owns
/// whatever the shader samples and records the primitive's own draws; the
/// default owns nothing and draws every instance in one call.
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender>
where
Self: Sized,
{
let _ = (device, queue);
Box::new(Instanced)
}
}
impl Default for Primitives {
fn default() -> Self {
/// The renderer's half of a primitive: what it samples, what it uploads, and
/// what draws it records.
///
/// Everything a draw shares -- the pipeline, the window and masks, the list's
/// own data and instance buffer -- is set before this is called. What is left
/// is what only this primitive knows: its group 2, and how many draws its
/// instances are.
pub trait PrimitiveRender {
/// The layout its shader reads at group 2. `None` for a primitive whose
/// shader samples nothing, whose pipeline then has no group 2 at all.
fn layout(&self) -> Option<&BindGroupLayout> {
None
}
/// Uploads whatever this primitive owns, once a frame, before any draw.
fn update(&mut self, ui: &mut UiData) {
let _ = ui;
}
/// Keeps what the primitive needs per instance at draw time, read from
/// the list's own data. A primitive that binds nothing per instance --
/// most of them -- leaves this empty and draws in one call.
fn instance_bindings(&self, list: &InstanceList, out: &mut Vec<u32>) {
let _ = (list, out);
}
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>);
}
/// What a `PrimitiveRender` draws: this list's instances, and whatever
/// `instance_bindings` kept for them.
pub struct ListDraw<'a> {
pub instances: u32,
pub bindings: &'a [u32],
}
/// The default: nothing sampled, every instance in one call.
pub struct Instanced;
impl PrimitiveRender for Instanced {
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
pass.draw(0..4, 0..list.instances);
}
}
/// Which registered primitive an instance is.
pub struct PrimitiveKind<P> {
id: u32,
_p: PhantomData<fn(P)>,
}
impl<P> PrimitiveKind<P> {
fn new(id: u32) -> Self {
Self {
instances: Default::default(),
assoc: Default::default(),
data: Default::default(),
free: Vec::new(),
updated: true,
id,
_p: PhantomData,
}
}
}
pub trait Primitive: Pod {
const BINDING: u32;
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self>;
impl<P> Clone for PrimitiveKind<P> {
fn clone(&self) -> Self {
*self
}
}
macro_rules! primitives {
($($name:ident: $ty:ty => $binding:expr,)*) => {
#[derive(Default)]
pub struct PrimitiveData {
$(pub(crate) $name: PrimitiveVec<$ty>,)*
}
impl<P> Copy for PrimitiveKind<P> {}
pub struct PrimitiveBuffers {
$($name: ArrBuf<$ty>,)*
}
impl PrimitiveBuffers {
pub fn update(&mut self, device: &Device, queue: &Queue, data: &PrimitiveData) {
$(self.$name.update(device, queue, &data.$name);)*
}
}
impl PrimitiveBuffers {
pub const LEN: usize = primitives!(@count $($name)*);
pub fn buffers(&self) -> [(u32, &Buffer); Self::LEN] {
[
$((<$ty>::BINDING, &self.$name.buffer),)*
]
}
pub fn new(device: &Device) -> Self {
Self {
$($name: ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
stringify!($name),
),)*
}
}
}
impl PrimitiveData {
pub fn clear(&mut self) {
$(self.$name.clear();)*
}
pub fn free(&mut self, binding: u32, idx: usize) {
match binding {
$(<$ty>::BINDING => self.$name.free(idx),)*
_ => unreachable!()
}
}
}
$(
unsafe impl bytemuck::Pod for $ty {}
unsafe impl bytemuck::Zeroable for $ty {}
impl Primitive for $ty {
const BINDING: u32 = $binding;
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self> {
&mut data.$name
}
}
)*
};
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t)+) };
(@count $t:tt) => { 1 };
/// Every primitive a ui can draw, in the order they were first drawn.
#[derive(Default)]
pub struct PrimitiveRegistry {
kinds: Vec<PrimitiveSource>,
ids: HashMap<TypeId, u32>,
}
pub struct PrimitiveInst<P> {
pub id: WidgetId,
pub primitive: P,
pub region: UiRegion,
pub mask_idx: MaskIdx,
pub struct PrimitiveSource {
pub wgsl: &'static str,
pub label: &'static str,
/// Size of one instance's entry, stated as the data binding's minimum.
pub stride: u64,
pub render: fn(&Device, &Queue) -> Box<dyn PrimitiveRender>,
}
impl Primitives {
pub fn write<P: Primitive>(
&mut self,
layer: usize,
PrimitiveInst {
id,
primitive,
region,
mask_idx,
}: PrimitiveInst<P>,
) -> PrimitiveHandle {
self.updated = true;
let vec = P::vec(&mut self.data);
let i = vec.add(primitive);
let inst = PrimitiveInstance {
region,
idx: i as u32,
mask_idx,
binding: P::BINDING,
};
let inst_i = if let Some(i) = self.free.pop() {
impl PrimitiveRegistry {
/// Registers `P` if this is the first time it has been drawn.
pub fn kind<P: Primitive>(&mut self) -> PrimitiveKind<P> {
let Self { kinds, ids } = self;
let id = *ids.entry(TypeId::of::<P>()).or_insert_with(|| {
kinds.push(PrimitiveSource {
wgsl: P::WGSL,
label: std::any::type_name::<P>(),
stride: size_of::<P>() as u64,
render: P::render,
});
kinds.len() as u32 - 1
});
PrimitiveKind::new(id)
}
pub fn sources(&self) -> &[PrimitiveSource] {
&self.kinds
}
}
/// One registered primitive's instances in one layer. Everything per-instance
/// rides here, so it stays in step through a `swap_remove`.
pub struct InstanceList {
instances: Vec<PrimitiveInstance>,
/// The widget each instance belongs to, for renumbering its handles.
assoc: Vec<WidgetId>,
free: Vec<usize>,
/// `stride` bytes of the primitive's own data per instance.
data: Vec<u8>,
/// From the type the list was made for, so a write is never checked.
stride: usize,
}
impl InstanceList {
fn new<P: Primitive>() -> Self {
Self {
instances: Vec::new(),
assoc: Vec::new(),
free: Vec::new(),
data: Vec::new(),
stride: size_of::<P>(),
}
}
pub fn instances(&self) -> &[PrimitiveInstance] {
&self.instances
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn stride(&self) -> usize {
self.stride
}
fn push(&mut self, id: WidgetId, inst: PrimitiveInstance, data: &[u8]) -> usize {
if let Some(i) = self.free.pop() {
self.instances[i] = inst;
self.assoc[i] = id;
self.data[i * self.stride..][..self.stride].copy_from_slice(data);
i
} else {
let i = self.instances.len();
self.instances.push(inst);
self.assoc.push(id);
self.data.extend_from_slice(data);
i
};
PrimitiveHandle::new::<P>(layer, inst_i, i)
}
}
/// returns (old index, new index)
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
fn free(&mut self, i: usize) -> MaskIdx {
self.free.push(i);
self.instances[i].mask_idx
}
fn apply_free(&mut self, kind: u32) -> impl Iterator<Item = PrimitiveChange> {
self.free.sort_by(|a, b| b.cmp(a));
self.free.drain(..).filter_map(|i| {
self.instances.swap_remove(i);
self.assoc.swap_remove(i);
if i == self.instances.len() {
let instances = &mut self.instances;
let assoc = &mut self.assoc;
let data = &mut self.data;
let stride = self.stride;
self.free.drain(..).filter_map(move |i| {
instances.swap_remove(i);
assoc.swap_remove(i);
let last = instances.len();
data.copy_within(last * stride..(last + 1) * stride, i * stride);
data.truncate(last * stride);
if i == last {
return None;
}
let id = self.assoc[i];
let old = self.instances.len();
Some(PrimitiveChange { id, old, new: i })
let id = assoc[i];
Some(PrimitiveChange {
id,
kind,
old: last,
new: i,
})
})
}
}
/// Everything one layer draws, one list per registered primitive.
pub struct LayerDraws {
/// `None` until this layer draws that primitive, because only the write
/// knows the type the list is for.
primitives: Vec<Option<InstanceList>>,
pub updated: bool,
}
impl Default for LayerDraws {
fn default() -> Self {
Self {
primitives: Vec::new(),
updated: true,
}
}
}
impl LayerDraws {
pub fn write<P: Primitive>(
&mut self,
layer: usize,
PrimitiveInst {
kind,
id,
primitive,
region,
mask_idx,
move_idx,
}: PrimitiveInst<P>,
) -> PrimitiveHandle {
self.updated = true;
// Grown on first use rather than sized from the registry, which a
// layer cannot see.
if self.primitives.len() <= kind.id as usize {
self.primitives.resize_with(kind.id as usize + 1, || None);
}
let inst_idx = self.primitives[kind.id as usize]
.get_or_insert_with(InstanceList::new::<P>)
.push(
id,
PrimitiveInstance {
region,
mask_idx,
move_idx,
},
bytemuck::bytes_of(&primitive),
);
PrimitiveHandle {
layer,
kind: kind.id,
inst_idx,
}
}
pub fn primitives(&self) -> &[Option<InstanceList>] {
&self.primitives
}
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
self.primitives
.iter_mut()
.enumerate()
.filter_map(|(kind, list)| Some((kind as u32, list.as_mut()?)))
.flat_map(|(kind, list)| list.apply_free(kind))
}
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self.updated = true;
self.data.free(h.binding, h.data_idx);
self.free.push(h.inst_idx);
self.instances[h.inst_idx].mask_idx
}
pub fn data(&self) -> &PrimitiveData {
&self.data
}
pub fn instances(&self) -> &Vec<PrimitiveInstance> {
&self.instances
self.list(h).free(h.inst_idx)
}
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
self.updated = true;
&mut self.instances[h.inst_idx].region
&mut self.list(h).instances[h.inst_idx].region
}
/// A handle is only ever made by `write`, which is what created the list.
fn list(&mut self, h: &PrimitiveHandle) -> &mut InstanceList {
self.primitives[h.kind as usize]
.as_mut()
.expect("handle names a primitive this layer never drew")
}
}
pub struct PrimitiveInst<P> {
pub kind: PrimitiveKind<P>,
pub id: WidgetId,
pub primitive: P,
pub region: UiRegion,
pub mask_idx: MaskIdx,
pub move_idx: MoveIdx,
}
pub struct PrimitiveChange {
pub id: WidgetId,
/// Which registered primitive's list moved, since they index separately.
pub kind: u32,
pub old: usize,
pub new: usize,
}
@@ -183,30 +323,12 @@ pub struct PrimitiveChange {
#[derive(Debug)]
pub struct PrimitiveHandle {
pub layer: usize,
pub kind: u32,
pub inst_idx: usize,
pub data_idx: usize,
pub binding: u32,
}
impl PrimitiveHandle {
fn new<P: Primitive>(layer: usize, inst_idx: usize, data_idx: usize) -> Self {
Self {
layer,
inst_idx,
data_idx,
binding: P::BINDING,
}
}
}
primitives!(
rects: RectPrimitive => 0,
textures: TexturePrimitive => 1,
glyphs: GlyphPrimitive => 2,
);
#[repr(C)]
#[derive(Copy, Clone)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RectPrimitive {
pub color: Color<u8>,
pub radius: f32,
@@ -214,6 +336,10 @@ pub struct RectPrimitive {
pub inner_radius: f32,
}
impl Primitive for RectPrimitive {
const WGSL: &'static str = include_str!("shader/rect.wgsl");
}
impl RectPrimitive {
pub fn color(color: Color<u8>) -> Self {
Self {
@@ -225,71 +351,51 @@ impl RectPrimitive {
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct TexturePrimitive {
pub view_idx: u32,
pub sampler_idx: u32,
}
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct GlyphPrimitive {
pub uv_min: Vec2,
pub uv_max: Vec2,
pub view_idx: u32,
pub sampler_idx: u32,
/// Which atlas array layer this glyph is on.
pub layer: u32,
pub color: Color<u8>,
pub flags: u32,
}
pub struct PrimitiveVec<T> {
vec: Vec<T>,
free: Vec<usize>,
// Manual rather than derived: `Vec2`'s alignment leaves four bytes of padding
// here, which is how WGSL lays the struct out.
unsafe impl bytemuck::Pod for GlyphPrimitive {}
unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
impl Primitive for GlyphPrimitive {
const WGSL: &'static str = include_str!("shader/glyph.wgsl");
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender> {
Box::new(GlyphRender::new(device, queue))
}
}
impl<T> PrimitiveVec<T> {
pub fn new() -> Self {
/// One drawn image. Its shader reads nothing per instance; the slot names the
/// texture to bind for it.
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct TexturePrimitive {
pub slot: u32,
}
impl Primitive for TexturePrimitive {
const WGSL: &'static str = include_str!("shader/texture.wgsl");
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender> {
Box::new(ImageRender::new(device, queue))
}
}
impl From<&TextureHandle> for TexturePrimitive {
fn from(handle: &TextureHandle) -> Self {
Self {
vec: Vec::new(),
free: Vec::new(),
slot: handle.slot(),
}
}
pub fn add(&mut self, t: T) -> usize {
if let Some(i) = self.free.pop() {
self.vec[i] = t;
i
} else {
let i = self.vec.len();
self.vec.push(t);
i
}
}
pub fn free(&mut self, i: usize) {
self.free.push(i);
}
pub fn clear(&mut self) {
self.free.clear();
self.vec.clear();
}
}
impl<T> Default for PrimitiveVec<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> Deref for PrimitiveVec<T> {
type Target = Vec<T>;
fn deref(&self) -> &Self::Target {
&self.vec
}
}
impl<T> DerefMut for PrimitiveVec<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.vec
}
}
-204
View File
@@ -1,204 +0,0 @@
const RECT: u32 = 0u;
const TEXTURE: u32 = 1u;
const GLYPH: u32 = 2u;
@group(0) @binding(0)
var<uniform> window: WindowUniform;
@group(1) @binding(RECT)
var<storage> rects: array<Rect>;
@group(1) @binding(TEXTURE)
var<storage> textures: array<TextureInfo>;
@group(1) @binding(GLYPH)
var<storage> glyphs: array<GlyphInfo>;
struct Rect {
color: u32,
radius: f32,
thickness: f32,
inner_radius: f32,
}
struct TextureInfo {
view_idx: u32,
sampler_idx: u32,
}
struct GlyphInfo {
uv_min: vec2<f32>,
uv_max: vec2<f32>,
view_idx: u32,
sampler_idx: u32,
color: u32,
flags: u32,
}
struct Mask {
x: UiSpan,
y: UiSpan,
}
struct UiSpan {
start: UiScalar,
end: UiScalar,
}
struct UiScalar {
rel: f32,
abs: f32,
}
struct UiVec2 {
rel: vec2<f32>,
abs: vec2<f32>,
}
@group(2) @binding(0)
var views: binding_array<texture_2d<f32>>;
@group(2) @binding(1)
var samplers: binding_array<sampler>;
@group(2) @binding(2)
var<storage> masks: array<Mask>;
struct WindowUniform {
dim: vec2<f32>,
};
struct InstanceInput {
@location(0) x_start: vec2<f32>,
@location(1) x_end: vec2<f32>,
@location(2) y_start: vec2<f32>,
@location(3) y_end: vec2<f32>,
@location(4) binding: u32,
@location(5) idx: u32,
@location(6) mask_idx: u32,
}
struct VertexOutput {
@location(0) top_left: vec2<f32>,
@location(1) bot_right: vec2<f32>,
@location(2) uv: vec2<f32>,
@location(3) binding: u32,
@location(4) idx: u32,
@location(5) mask_idx: u32,
@builtin(position) clip_position: vec4<f32>,
};
struct Region {
pos: vec2<f32>,
uv: vec2<f32>,
top_left: vec2<f32>,
bot_right: vec2<f32>,
}
@vertex
fn vs_main(
@builtin(vertex_index) vi: u32,
in: InstanceInput,
) -> VertexOutput {
var out: VertexOutput;
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
let size = bot_right - top_left;
let uv = vec2<f32>(
f32(vi % 2u),
f32(vi / 2u)
);
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
out.uv = uv;
out.binding = in.binding;
out.idx = in.idx;
out.top_left = top_left;
out.bot_right = bot_right;
out.mask_idx = in.mask_idx;
return out;
}
@fragment
fn fs_main(
in: VertexOutput
) -> @location(0) vec4<f32> {
let pos = in.clip_position.xy;
let region = Region(pos, in.uv, in.top_left, in.bot_right);
let i = in.idx;
var color: vec4<f32>;
switch in.binding {
case RECT: {
color = draw_rounded_rect(region, rects[i]);
}
case TEXTURE: {
color = draw_texture(region, textures[i]);
}
case GLYPH: {
color = draw_glyph(region, glyphs[i]);
}
default: {
color = vec4(1.0, 0.0, 1.0, 1.0);
}
}
if in.mask_idx != 4294967295u {
let mask = masks[in.mask_idx];
let tl = UiVec2(vec2(mask.x.start.rel, mask.y.start.rel), vec2(mask.x.start.abs, mask.y.start.abs));
let br = UiVec2(vec2(mask.x.end.rel, mask.y.end.rel), vec2(mask.x.end.abs, mask.y.end.abs));
let top_left = floor(tl.rel * window.dim) + floor(tl.abs);
let bot_right = floor(br.rel * window.dim) + floor(br.abs);
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
color *= 0.0;
}
}
return color;
}
// TODO: this seems really inefficient (per frag indexing)?
fn draw_texture(region: Region, info: TextureInfo) -> vec4<f32> {
return textureSample(views[info.view_idx], samplers[info.sampler_idx], region.uv);
}
fn draw_glyph(region: Region, g: GlyphInfo) -> vec4<f32> {
let uv = mix(g.uv_min, g.uv_max, region.uv);
let texel = textureSample(views[g.view_idx], samplers[g.sampler_idx], uv);
if (g.flags & 1u) != 0u {
return texel;
}
var color = unpack4x8unorm(g.color);
color.a *= texel.a;
return color;
}
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
var color = unpack4x8unorm(rect.color);
let edge = 0.5;
let size = region.bot_right - region.top_left;
let corner = size / 2.0;
let center = region.top_left + corner;
let dist = distance_from_rect(region.pos, center, corner, rect.radius);
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
if rect.thickness > 0.0 {
let dist2 = distance_from_rect(region.pos, center, corner - rect.thickness, rect.inner_radius);
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
}
return color;
}
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
// vec from center to pixel
let p = pixel_pos - rect_center;
// vec from inner rect corner to pixel
let q = abs(p) - (rect_corner - radius);
return length(max(q, vec2(0.0))) - radius;
}
+33
View File
@@ -0,0 +1,33 @@
// Matches `GlyphEntry::IS_COLORED`.
const COLORED: u32 = 1u;
// The glyph atlas, whose array layers are its pages.
@group(2) @binding(0)
var atlas: texture_2d_array<f32>;
@group(2) @binding(1)
var samp: sampler;
struct GlyphInfo {
uv_min: vec2<f32>,
uv_max: vec2<f32>,
// Which layer of the atlas array this glyph's page is.
layer: u32,
color: u32,
flags: u32,
}
@group(1) @binding(0)
var<storage> glyphs: array<GlyphInfo>;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let g = glyphs[in.idx];
let uv = mix(g.uv_min, g.uv_max, in.uv);
let texel = textureSample(atlas, samp, uv, i32(g.layer));
if (g.flags & COLORED) != 0u {
return masked(in, texel);
}
var color = unpack4x8unorm(g.color);
color.a *= texel.a;
return masked(in, color);
}
+129
View File
@@ -0,0 +1,129 @@
// Prepended to every primitive's shader, which declares its own instance data
// as `var<storage> <name>: array<T>` at group 1 binding 0, and an `fs_main`
// shading one instance of it. What it samples, if anything, is bound at group
// 2: the texture at binding 0 and the sampler at binding 1.
@group(0) @binding(0)
var<uniform> window: WindowUniform;
@group(0) @binding(1)
var<storage> masks: array<Mask>;
@group(0) @binding(2)
var<storage> move_offsets: array<MoveOffset>;
struct WindowUniform {
dim: vec2<f32>,
};
struct Mask {
x: UiSpan,
y: UiSpan,
move_idx: u32,
}
struct MoveOffset {
delta: vec2<f32>,
parent: u32,
}
const MOVE_NONE: u32 = 4294967295u;
// Keep in step with `iris_core::CHAIN_LIMIT`. It bounds a malformed cycle
// rather than any real tree, and the CPU walk uses the same number so both
// resolve a deep one the same way.
const CHAIN_LIMIT: u32 = 64u;
fn resolve_move(idx: u32) -> vec2<f32> {
var total = vec2<f32>(0.0, 0.0);
var at = idx;
for (var step = 0u; step < CHAIN_LIMIT; step++) {
if at == MOVE_NONE {
break;
}
let entry = move_offsets[at];
total += entry.delta;
at = entry.parent;
}
return total;
}
struct UiSpan {
start: UiScalar,
end: UiScalar,
}
struct UiScalar {
rel: f32,
abs: f32,
}
struct InstanceInput {
@location(0) x_start: vec2<f32>,
@location(1) x_end: vec2<f32>,
@location(2) y_start: vec2<f32>,
@location(3) y_end: vec2<f32>,
@location(4) mask_idx: u32,
@location(5) move_idx: u32,
}
struct VertexOutput {
@location(0) top_left: vec2<f32>,
@location(1) bot_right: vec2<f32>,
@location(2) uv: vec2<f32>,
@location(3) @interpolate(flat) mask_idx: u32,
@location(4) @interpolate(flat) idx: u32,
@builtin(position) clip_position: vec4<f32>,
};
@vertex
fn vs_main(
@builtin(vertex_index) vi: u32,
@builtin(instance_index) ii: u32,
in: InstanceInput,
) -> VertexOutput {
var out: VertexOutput;
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
let moved = resolve_move(in.move_idx);
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs + moved);
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs + moved);
let size = bot_right - top_left;
let uv = vec2<f32>(
f32(vi % 2u),
f32(vi / 2u)
);
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
out.uv = uv;
out.top_left = top_left;
out.bot_right = bot_right;
out.mask_idx = in.mask_idx;
out.idx = ii;
return out;
}
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
if in.mask_idx == 4294967295u {
return color;
}
let mask = masks[in.mask_idx];
let tl = vec2(mask.x.start.rel, mask.y.start.rel);
let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs);
let br = vec2(mask.x.end.rel, mask.y.end.rel);
let br_abs = vec2(mask.x.end.abs, mask.y.end.abs);
// Its own chain, not the drawn primitive's, so a stationary viewport
// clips content that moves inside it.
let moved = resolve_move(mask.move_idx);
let top_left = floor(tl * window.dim) + floor(tl_abs + moved);
let bot_right = floor(br * window.dim) + floor(br_abs + moved);
let pos = in.clip_position.xy;
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
return color * 0.0;
}
return color;
}
+39
View File
@@ -0,0 +1,39 @@
struct Rect {
color: u32,
radius: f32,
thickness: f32,
inner_radius: f32,
}
@group(1) @binding(0)
var<storage> rects: array<Rect>;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let rect = rects[in.idx];
var color = unpack4x8unorm(rect.color);
let edge = 0.5;
let size = in.bot_right - in.top_left;
let corner = size / 2.0;
let center = in.top_left + corner;
let pos = in.clip_position.xy;
let dist = distance_from_rect(pos, center, corner, rect.radius);
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
if rect.thickness > 0.0 {
let dist2 = distance_from_rect(pos, center, corner - rect.thickness, rect.inner_radius);
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
}
return masked(in, color);
}
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
// vec from center to pixel
let p = pixel_pos - rect_center;
// vec from inner rect corner to pixel
let q = abs(p) - (rect_corner - radius);
return length(max(q, vec2(0.0))) - radius;
}
+10
View File
@@ -0,0 +1,10 @@
// The image this instance draws, bound for it alone.
@group(2) @binding(0)
var image: texture_2d<f32>;
@group(2) @binding(1)
var samp: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return masked(in, textureSample(image, samp, in.uv));
}
+194 -142
View File
@@ -1,119 +1,119 @@
use image::{DynamicImage, EncodableLayout, GenericImageView};
use image::{DynamicImage, EncodableLayout, GenericImageView, RgbaImage};
use wgpu::{util::DeviceExt, *};
use crate::{PatchRect, TextureUpdate, Textures};
use crate::{
PatchRect, TextureUpdate, Textures, UiData,
render::{
TexturePrimitive,
primitive::{ListDraw, PrimitiveRender},
},
};
/// Draws standalone images, which it owns. Each is its own texture, so each
/// instance binds its own and is a draw of its own.
pub struct ImageRender {
textures: GpuTextures,
layout: BindGroupLayout,
sampler: Sampler,
}
impl ImageRender {
pub fn new(device: &Device, queue: &Queue) -> Self {
Self {
textures: GpuTextures::new(device, queue),
layout: sampled_layout(device, TextureViewDimension::D2, "ui image"),
sampler: default_sampler(device),
}
}
}
impl PrimitiveRender for ImageRender {
fn layout(&self) -> Option<&BindGroupLayout> {
Some(&self.layout)
}
fn update(&mut self, ui: &mut UiData) {
self.textures
.update(&mut ui.textures, &self.layout, &self.sampler);
}
fn instance_bindings(&self, list: &super::InstanceList, out: &mut Vec<u32>) {
let slots = list
.data()
.chunks_exact(list.stride())
.map(|data| bytemuck::pod_read_unaligned::<TexturePrimitive>(data).slot);
out.extend(slots);
}
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
for (i, &slot) in list.bindings.iter().enumerate() {
let Some(image) = self.textures.group(slot) else {
continue;
};
pass.set_bind_group(2, image, &[]);
pass.draw(0..4, i as u32..i as u32 + 1);
}
}
}
/// The standalone images a ui draws, each its own texture and bind group --
/// unlike the glyph atlas in `super::page`, which is one array they share.
pub struct GpuTextures {
device: Device,
queue: Queue,
/// Parallel to `views`; patches require textures rather than views.
textures: Vec<Option<Texture>>,
views: Vec<TextureView>,
view_count: usize,
samplers: Vec<Sampler>,
null_view: TextureView,
no_views: Vec<TextureView>,
slots: Vec<Option<ImageGpu>>,
}
struct ImageGpu {
/// Kept for `patch`, which needs the texture rather than the view.
texture: Texture,
group: BindGroup,
}
impl GpuTextures {
pub fn update(&mut self, textures: &mut Textures) -> bool {
let mut bindings_changed = false;
pub fn new(device: &Device, queue: &Queue) -> Self {
Self {
device: device.clone(),
queue: queue.clone(),
slots: Vec::new(),
}
}
pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) {
for update in textures.updates() {
bindings_changed |= match update {
match update {
TextureUpdate::Push(image) => {
self.push(image);
true
let image = self.create(image, layout, sampler);
self.slots.push(Some(image));
}
TextureUpdate::Set(i, image) => {
self.set(i, image);
true
let image = self.create(image, layout, sampler);
self.slots[i as usize] = Some(image);
}
TextureUpdate::Patch(i, rect, image) => {
self.patch(i, rect, image);
false
}
TextureUpdate::SetFree => {
self.view_count += 1;
true
}
TextureUpdate::Free(i) => {
self.free(i);
true
}
TextureUpdate::PushFree => {
self.push_free();
true
}
};
TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image),
TextureUpdate::PushFree => self.slots.push(None),
TextureUpdate::SetFree => {}
TextureUpdate::Free(i) => self.slots[i as usize] = None,
}
}
bindings_changed
}
fn set(&mut self, i: u32, image: &DynamicImage) {
self.view_count += 1;
let (texture, view) = self.create(image);
self.textures[i as usize] = Some(texture);
self.views[i as usize] = view;
}
fn free(&mut self, i: u32) {
self.view_count -= 1;
self.textures[i as usize] = None;
self.views[i as usize] = self.null_view.clone();
}
fn push(&mut self, image: &DynamicImage) {
self.view_count += 1;
let (texture, view) = self.create(image);
self.textures.push(Some(texture));
self.views.push(view);
}
fn push_free(&mut self) {
self.view_count += 1;
self.textures.push(None);
self.views.push(self.null_view.clone());
}
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
let Some(texture) = &self.textures[i as usize] else {
return;
};
if rect.width == 0 || rect.height == 0 {
return;
}
// `write_texture` requires tightly packed rows, unlike the atlas image.
let sub = image
.view(rect.x, rect.y, rect.width, rect.height)
.to_image();
self.queue.write_texture(
TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: Origin3d {
x: rect.x,
y: rect.y,
z: 0,
},
aspect: TextureAspect::All,
},
sub.as_bytes(),
TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(rect.width * 4),
rows_per_image: Some(rect.height),
},
Extent3d {
width: rect.width,
height: rect.height,
depth_or_array_layers: 1,
},
);
pub fn group(&self, slot: u32) -> Option<&BindGroup> {
self.slots.get(slot as usize)?.as_ref().map(|i| &i.group)
}
fn create(&self, image: &DynamicImage) -> (Texture, TextureView) {
let image = image.to_rgba8();
let (width, height) = image.dimensions();
fn create(
&self,
image: &DynamicImage,
layout: &BindGroupLayout,
sampler: &Sampler,
) -> ImageGpu {
let rgba = image.to_rgba8();
let (width, height) = rgba.dimensions();
let texture = self.device.create_texture_with_data(
&self.queue,
&TextureDescriptor {
label: None,
label: Some("image"),
size: Extent3d {
width,
height,
@@ -127,63 +127,115 @@ impl GpuTextures {
view_formats: &[],
},
wgt::TextureDataOrder::MipMajor,
image.as_bytes(),
rgba.as_bytes(),
);
let view = texture.create_view(&TextureViewDescriptor::default());
(texture, view)
let group = sampled_group(&self.device, layout, &view, sampler, "ui image");
ImageGpu { texture, group }
}
pub fn new(device: &Device, queue: &Queue) -> Self {
let null_view = null_texture_view(device);
Self {
device: device.clone(),
queue: queue.clone(),
textures: Vec::new(),
views: Vec::new(),
samplers: vec![default_sampler(device)],
no_views: vec![null_view.clone()],
null_view,
view_count: 0,
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
let Some(Some(slot)) = self.slots.get(i as usize) else {
return;
};
let dst = TexelCopyTextureInfo {
texture: &slot.texture,
mip_level: 0,
origin: Origin3d {
x: rect.x,
y: rect.y,
z: 0,
},
aspect: TextureAspect::All,
};
match image.as_rgba8() {
Some(rgba) => write_region(&self.queue, dst, rgba, rect),
// The texture is rgba8, so any other layout has to be converted --
// and converting the rectangle is cheaper than the whole image.
None => {
let sub = image
.view(rect.x, rect.y, rect.width, rect.height)
.to_image();
write_region(&self.queue, dst, &sub, PatchRect { x: 0, y: 0, ..rect });
}
}
}
pub fn views(&self) -> Vec<&TextureView> {
if self.views.is_empty() {
&self.no_views
} else {
&self.views
}
.iter()
.by_ref()
.collect()
}
pub fn samplers(&self) -> Vec<&Sampler> {
self.samplers.iter().by_ref().collect()
}
pub fn view_count(&self) -> usize {
self.view_count
}
}
pub fn null_texture_view(device: &Device) -> TextureView {
device
.create_texture(&TextureDescriptor {
label: Some("null"),
size: Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) {
if rect.width == 0 || rect.height == 0 {
return;
}
let stride = src.width() * 4;
queue.write_texture(
dst,
src.as_bytes(),
TexelCopyBufferLayout {
offset: (rect.y * stride + rect.x * 4) as u64,
bytes_per_row: Some(stride),
rows_per_image: Some(rect.height),
},
Extent3d {
width: rect.width,
height: rect.height,
depth_or_array_layers: 1,
},
);
}
/// What a primitive that samples binds: a texture, and the sampler that reads
/// it.
pub fn sampled_group(
device: &Device,
layout: &BindGroupLayout,
view: &TextureView,
sampler: &Sampler,
label: &'static str,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(view),
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING,
view_formats: &[],
})
.create_view(&TextureViewDescriptor::default())
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(sampler),
},
],
label: Some(label),
})
}
/// The layout for one of those. The dimension differs -- the atlas is an
/// array of pages and an image is not -- and nothing else does.
pub fn sampled_layout(
device: &Device,
dimension: TextureViewDimension,
label: &'static str,
) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: false },
view_dimension: dimension,
multisampled: false,
},
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: None,
},
],
label: Some(label),
})
}
pub fn default_sampler(device: &Device) -> Sampler {
+10 -6
View File
@@ -21,17 +21,25 @@ impl<T: Pod> ArrBuf<T> {
_pd: PhantomData,
}
}
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) {
if self.len != data.len() {
/// Returns whether the `Buffer` was recreated, which stales any cached
/// `BindGroup` holding it.
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool {
let resized = self.len != data.len();
if resized {
self.len = data.len();
self.buffer =
Self::init_buf(device, std::mem::size_of_val(data), self.usage, self.label);
}
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
resized
}
pub fn len(&self) -> usize {
self.len
}
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
let mut size = size as u64;
if usage.contains(BufferUsages::STORAGE) {
// A binding cannot be empty or under the layout's minimum.
size = size.max(std::mem::size_of::<T>() as u64);
}
device.create_buffer(&BufferDescriptor {
@@ -41,8 +49,4 @@ impl<T: Pod> ArrBuf<T> {
usage,
})
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.len
}
}
+9 -1
View File
@@ -1,14 +1,22 @@
use crate::{LayerId, MaskIdx, PrimitiveHandle, TextureHandle, UiRegion, WidgetId};
use crate::{LayerId, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId};
/// important non rendering data for retained drawing
#[derive(Debug)]
pub struct ActiveData {
pub id: WidgetId,
pub region: UiRegion,
/// What the widget said it used of `region`, the last time it drew.
pub size: Size,
pub parent: Option<WidgetId>,
pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>,
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,
/// The move slot its primitives are positioned through.
pub move_idx: MoveIdx,
pub mask: MaskIdx,
pub layer: LayerId,
}
-18
View File
@@ -1,18 +0,0 @@
use crate::{BothAxis, Len, UiVec2, WidgetId, util::HashMap};
#[derive(Default)]
pub struct Cache {
pub size: BothAxis<HashMap<WidgetId, (UiVec2, Len)>>,
}
impl Cache {
pub fn remove(&mut self, id: WidgetId) {
self.size.x.remove(&id);
self.size.y.remove(&id);
}
pub fn clear(&mut self) {
self.size.x.clear();
self.size.y.clear();
}
}
+72 -5
View File
@@ -1,24 +1,91 @@
use crate::{Mask, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena};
use crate::{
Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId,
Widgets,
util::{Arena, Id, TrackedArena, Vec2},
};
/// How far the shader will walk a move chain. It bounds a malformed cycle
/// rather than any real tree; `Moves::resolve` uses the same number so the
/// two agree on what a deep tree resolves to.
pub const CHAIN_LIMIT: u32 = 64;
mod active;
mod cache;
mod painter;
mod render_state;
mod size;
pub use active::*;
pub use painter::Painter;
pub use painter::{Painter, PrimitiveLike};
pub use render_state::*;
pub use size::*;
#[derive(Default)]
pub struct UiData {
pub widgets: Widgets,
/// Every primitive this ui can draw.
pub primitives: PrimitiveRegistry,
pub textures: Textures,
pub text: TextData,
pub masks: TrackedArena<Mask, u32>,
}
/// Where each widget's drawing sits relative to its parent's slot, so moving
/// a subtree writes one entry rather than every descendant's primitives.
#[derive(Default)]
pub struct Moves {
arena: Arena<MoveOffset, u32>,
pub changed: bool,
}
impl Moves {
pub fn push(&mut self, parent: MoveIdx) -> MoveIdx {
self.changed = true;
MoveIdx::slot(self.arena.push(MoveOffset::root(parent)).idx())
}
pub fn remove(&mut self, idx: MoveIdx) {
self.changed = true;
self.arena.remove(Id::preset(idx.idx() as u32));
}
/// Sets a slot's translation, in physical pixels, relative to its parent.
pub fn set(&mut self, idx: MoveIdx, delta: Vec2) {
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
if entry.delta != delta {
entry.delta = delta;
self.changed = true;
}
}
/// The translation a primitive in `idx` has accumulated, which is the
/// same walk the vertex shader does.
pub fn resolve(&self, idx: MoveIdx) -> Vec2 {
let mut total = Vec2::ZERO;
let mut at = idx;
for _ in 0..CHAIN_LIMIT {
if at == MoveIdx::NONE {
return total;
}
let entry = self.arena[at.idx()];
total += entry.delta;
at = entry.parent;
}
debug_assert!(
at == MoveIdx::NONE,
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
and the shader stops at the same depth"
);
total
}
pub fn entries(&self) -> &[MoveOffset] {
&self.arena
}
pub fn clear(&mut self) {
self.changed = true;
self.arena = Arena::default();
}
}
pub trait UiRsc {
fn ui(&self) -> &UiData;
fn ui_mut(&mut self) -> &mut UiData;
+123 -44
View File
@@ -1,7 +1,10 @@
use crate::{
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
PrimitiveKind, TexturePrimitive,
},
util::Vec2,
};
@@ -15,21 +18,38 @@ pub struct Painter<'a> {
pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>,
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,
/// The move slot this widget's primitives are positioned through.
pub(super) move_idx: MoveIdx,
pub layer: usize,
pub(super) id: WidgetId,
}
impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<P>();
self.write(kind, primitive, region);
}
/// Takes the kind, for a caller writing many of one primitive.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
let h = self.state.layers.write(
self.layer,
PrimitiveInst {
kind,
id: self.id,
primitive,
region,
mask_idx: self.mask,
move_idx: self.move_idx,
},
);
self.push_primitive(h);
}
fn push_primitive(&mut self, h: PrimitiveHandle) {
if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask);
@@ -38,33 +58,50 @@ impl<'a> Painter<'a> {
}
/// Writes a primitive to be rendered
pub fn primitive<P: Primitive>(&mut self, primitive: P) {
pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, self.region)
}
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, region.within(&self.region));
}
pub fn set_mask(&mut self, region: UiRegion) {
assert!(self.mask == MaskIdx::NONE);
self.mask = self.rsc.ui_mut().masks.push(Mask { region });
self.mask = self.rsc.ui_mut().masks.push(Mask {
region,
move_idx: self.move_idx,
});
}
/// Draws a widget within this widget's region.
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.widget_at(id, self.region);
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
self.widget_at(id, self.region)
}
/// Draws a widget somewhere within this one.
/// Useful for drawing child widgets in select areas.
pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
self.widget_at(id, region.within(&self.region));
/// Draws a widget somewhere within this one. Drawing one a second time
/// gives it a new box, keeping the drawing it already has where it can.
pub fn widget_within<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
let region = region.within(&self.region);
self.widget_at(id, region)
}
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
self.children.push(id.id());
self.state.draw_inner(
fn widget_at<'s, W: ?Sized>(
&'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());
}
let size = self.state.draw_inner(
self.layer,
id.id(),
region,
@@ -73,21 +110,25 @@ impl<'a> Painter<'a> {
None,
self.rsc,
);
DrawResult {
child: id,
painter: self,
size,
}
}
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
self.textures.push(handle.clone());
self.primitive_at(handle.primitive(), region.within(&self.region));
/// 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)
}
pub fn texture(&mut self, handle: &TextureHandle) {
self.textures.push(handle.clone());
self.primitive(handle.primitive());
}
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
self.textures.push(handle.clone());
self.primitive_at(handle.primitive(), region);
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(
@@ -97,10 +138,12 @@ impl<'a> Painter<'a> {
width: Option<f32>,
) -> RenderedText {
let ui = self.rsc.ui_mut();
ui.text.render(buffer, attrs, width, &mut ui.textures)
ui.text.render(buffer, attrs, width)
}
// TODO: merge the text methods into the primitive ones.
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() {
let mut region = origin;
region.x.end = region.x.start;
@@ -108,12 +151,12 @@ impl<'a> Painter<'a> {
let mut region = region.offset(UiVec2::abs(glyph.offset));
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
self.primitive_at(
self.write(
kind,
GlyphPrimitive {
uv_min: glyph.entry.uv_min,
uv_max: glyph.entry.uv_max,
view_idx: glyph.entry.view_idx,
sampler_idx: glyph.entry.sampler_idx,
layer: glyph.entry.layer,
color: text.color,
flags: glyph.entry.flags(),
},
@@ -126,22 +169,17 @@ impl<'a> Painter<'a> {
self.region
}
pub fn size<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>) -> Size {
self.size_ctx().size(id)
}
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 {
/// The output's size in pixels. A widget that reads it draws again when
/// 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
}
/// 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 {
self.reads_output = true;
self.region.size().to_abs(self.state.output_size)
}
@@ -164,8 +202,49 @@ impl<'a> Painter<'a> {
pub fn id(&self) -> &WidgetId {
&self.id
}
}
pub fn size_ctx(&mut self) -> SizeCtx<'_> {
self.state.size_ctx(self.id, self.region.size(), self.rsc)
/// 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
/// 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)
}
}
/// What `Painter::primitive` takes: a primitive, or something that yields one
/// and does whatever else drawing it needs.
pub trait PrimitiveLike {
type Primitive: Primitive;
fn into_primitive(self, painter: &mut Painter) -> Self::Primitive;
}
impl<P: Primitive> PrimitiveLike for P {
type Primitive = P;
fn into_primitive(self, _: &mut Painter) -> P {
self
}
}
impl PrimitiveLike for &TextureHandle {
type Primitive = TexturePrimitive;
/// Retains a share of the handle, so the slot the primitive names cannot
/// be freed and reused while it is still drawn.
fn into_primitive(self, painter: &mut Painter) -> TexturePrimitive {
painter.textures.push(self.clone());
self.into()
}
}
+186 -73
View File
@@ -1,19 +1,21 @@
use crate::{
ActiveData, Axis, IdLike, MaskIdx, Painter, PixelRegion, PrimitiveLayers, SizeCtx,
StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
ui::cache::Cache,
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion,
Size, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
util::{HashMap, HashSet, Vec2, forget_ref},
};
pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>,
pub layers: PrimitiveLayers,
pub layers: DrawLayers,
pub(super) output_size: Vec2,
pub cache: Cache,
old_root: Option<WidgetId>,
resized: bool,
draw_started: HashSet<WidgetId>,
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
/// replaces that while its children go on pointing at the slot.
slots: HashMap<WidgetId, MoveIdx>,
pub moves: Moves,
}
impl UiRenderState {
@@ -21,11 +23,12 @@ impl UiRenderState {
Self {
active: Default::default(),
layers: Default::default(),
cache: Default::default(),
output_size: Vec2::ZERO,
old_root: None,
resized: false,
draw_started: Default::default(),
slots: Default::default(),
moves: Default::default(),
}
}
@@ -34,6 +37,10 @@ impl UiRenderState {
self.resized = true;
}
pub fn output_size(&self) -> Vec2 {
self.output_size
}
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
// safety mechanism for memory leaks; might wanna return a result instead so user can
// decide whether to panic or not
@@ -52,11 +59,21 @@ impl UiRenderState {
);
}
let root = root.into();
if self.needs_full_redraw(root) {
if self.root_changed(root) {
self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id());
self.resized = false;
} else if rsc.widgets().has_updates() {
} 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;
if rsc.widgets().has_updates() {
self.redraw_updates(rsc);
}
}
@@ -80,19 +97,11 @@ impl UiRenderState {
mask: MaskIdx,
old_children: Option<Vec<WidgetId>>,
rsc: &mut dyn UiRsc,
) {
) -> Size {
let mut old_children = old_children.unwrap_or_default();
if let Some(active) = self.active.get_mut(&id)
&& !rsc.widgets().needs_redraw.contains(&id)
{
// 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 self.active.contains_key(&id) {
if let Some(size) = self.try_reuse(id, region, rsc) {
return size;
}
// if not, then maintain resize and track old children to remove unneeded
let active = self.remove(id, false, rsc).unwrap();
@@ -100,6 +109,9 @@ impl UiRenderState {
}
// draw widget
let move_idx = self.move_slot(id, parent);
self.moves.set(move_idx, Vec2::ZERO);
rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.insert(id);
let mut painter = Painter {
@@ -111,11 +123,14 @@ impl UiRenderState {
textures: Vec::new(),
primitives: Vec::new(),
children: Vec::new(),
size_deps: Vec::new(),
reads_output: false,
move_idx,
rsc,
};
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
widget.draw(&mut painter);
let size = widget.draw(&mut painter);
drop(widget);
let Painter {
@@ -126,18 +141,31 @@ impl UiRenderState {
textures,
primitives,
children,
size_deps,
reads_output,
move_idx,
layer,
id,
} = 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
let active = ActiveData {
id,
region,
size,
parent,
textures,
primitives,
children,
size_deps,
reads_output,
move_idx,
mask,
layer,
};
@@ -151,19 +179,101 @@ impl UiRenderState {
rsc.on_draw(&active);
self.active.insert(id, active);
size
}
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
/// The slot a widget's drawing is positioned through, made on its first
/// draw and kept until it stops being drawn.
fn move_slot(&mut self, id: WidgetId, parent: Option<WidgetId>) -> MoveIdx {
if let Some(&idx) = self.slots.get(&id) {
return idx;
}
let parent = parent
.and_then(|p| self.slots.get(&p).copied())
.unwrap_or(MoveIdx::NONE);
let idx = self.moves.push(parent);
self.slots.insert(id, idx);
idx
}
/// The drawing a widget already has, kept for a new box if the box has not
/// 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, slot) = (active.size, active.region, active.move_idx);
// TODO: epsilon?
if old.size() == region.size() {
// The right shape and only somewhere else, which is one slot to
// write however much is under it. Both boxes are in the
// coordinates its parent drew, so the chain above applies alike.
let moved =
region.to_px(self.output_size).top_left - old.to_px(self.output_size).top_left;
self.moves.set(slot, moved);
return Some(size);
}
if !self.reusable(id, region, rsc) || !old.stretchable() {
return None;
}
// Its drawing stands, re-expressed as the same fractions of the box.
self.stretch(id, 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))
})
}
/// Rewrites a subtree's regions as the same fractions of a new box, for a
/// change of length that a slot cannot express. Every region it rewrites
/// is a region some slot was a delta from, so those go back to zero.
fn stretch(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
let active = self.active.get_mut(&id).unwrap();
for h in &active.primitives {
let region = self.layers[h.layer].region_mut(h);
*region = region.outside(&from).within(&to);
*region = region.stretch(&from, &to);
}
active.region = active.region.outside(&from).within(&to);
active.region = active.region.stretch(&from, &to);
self.moves.set(active.move_idx, Vec2::ZERO);
// SAFETY: children cannot be recursive
let children = unsafe { forget_ref(&active.children) };
for child in children {
self.mov(*child, from, to);
self.stretch(*child, from, to);
}
}
@@ -187,13 +297,16 @@ impl UiRenderState {
}
fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
self.cache.remove(id);
let inst = self.remove(id, true, rsc);
if let Some(inst) = &inst {
for c in &inst.children {
self.remove_rec(*c, rsc);
}
}
// After the descendants, whose slots name this one as their parent.
if let Some(idx) = self.slots.remove(&id) {
self.moves.remove(idx);
}
inst
}
@@ -201,7 +314,8 @@ impl UiRenderState {
for (_, active) in self.active.drain() {
rsc.on_undraw(&active);
}
self.cache.clear();
self.slots.clear();
self.moves.clear();
self.layers.clear();
rsc.widgets_mut().needs_redraw.clear();
rsc.free();
@@ -218,17 +332,12 @@ impl UiRenderState {
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>(
&self,
root: impl Into<Option<&'a StrongWidget>>,
widgets: &Widgets,
) -> bool {
self.needs_full_redraw(root) || widgets.has_updates()
self.root_changed(root) || self.resized || widgets.has_updates()
}
pub fn active_widgets(&self) -> usize {
@@ -243,40 +352,44 @@ impl UiRenderState {
}
pub fn debug_layers(&self) {
for ((idx, depth), primitives) in self.layers.iter_depth() {
for ((idx, depth), draws) in self.layers.iter_depth() {
let indent = " ".repeat(depth * 2);
let len = primitives.instances().len();
print!("{indent}{idx}: {len} primitives");
if len >= 1 {
print!(" ({})", primitives.instances()[0].binding);
}
println!();
let counts: Vec<String> = draws
.primitives()
.iter()
.map(|l| l.as_ref().map_or(0, |l| l.instances().len()).to_string())
.collect();
println!("{indent}{idx}: [{}]", counts.join(", "));
}
}
/// Where a widget is on screen: the box it drew against plus whatever the
/// chain above has moved since, which is the walk the vertex shader does.
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let region = self.active.get(&id.id())?.region;
Some(region.to_px(self.output_size))
let active = self.active.get(&id.id())?;
let moved = self.moves.resolve(active.move_idx);
let region = active.region.to_px(self.output_size);
Some(PixelRegion {
top_left: region.top_left + moved,
bot_right: region.bot_right + moved,
})
}
/// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.remove(&id);
// check if parent depends on the desired size of this, if so then redraw it first
for axis in [Axis::X, Axis::Y] {
if let Some(&(outer, old)) = self.cache.size.axis_dyn(axis).get(&id)
&& let Some(current) = self.active.get(&id)
&& let Some(pid) = current.parent
{
self.cache.size.axis_dyn(axis).remove(&id);
let new = self.size_ctx(id, outer, rsc).len_axis(id, axis);
self.cache.size.axis_dyn(axis).insert(id, (outer, new));
if new != old {
self.redraw(pid, rsc);
}
}
// 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
// as well: their own boxes have not changed, so the mark is the only
// thing stopping the draw reusing its way past this widget.
if let Some(top) = self.mark_readers(id, 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;
}
rsc.widgets_mut().needs_redraw.remove(&id);
if self.draw_started.contains(&id) {
return;
@@ -297,23 +410,23 @@ impl UiRenderState {
);
}
pub(super) fn size_ctx<'b>(
&'b mut self,
source: WidgetId,
outer: UiVec2,
rsc: &'b mut dyn UiRsc,
) -> SizeCtx<'b> {
let ui = rsc.ui_mut();
SizeCtx {
source,
cache: &mut self.cache,
text: &mut ui.text,
textures: &mut ui.textures,
widgets: &ui.widgets,
outer,
output_size: self.output_size,
id: source,
/// The furthest ancestor that read this widget's size, directly or through
/// widgets that did the same, marking everything below it on the way.
fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<WidgetId> {
let mut top = None;
let mut at = id;
while let Some(active) = self.active.get(&at)
&& let Some(parent) = active.parent
&& self
.active
.get(&parent)
.is_some_and(|p| p.size_deps.contains(&at))
{
rsc.widgets_mut().needs_redraw.insert(at);
top = Some(parent);
at = parent;
}
top
}
}
-91
View File
@@ -1,91 +0,0 @@
use crate::{
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures,
UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
};
pub struct SizeCtx<'a> {
pub text: &'a mut TextData,
pub textures: &'a mut Textures,
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,
textures: self.textures,
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, self.textures)
}
pub fn label(&self, id: WidgetId) -> &String {
self.widgets.label(id)
}
}
+4
View File
@@ -34,6 +34,10 @@ impl<T, I: IdNum> Arena<T, I> {
self.tracker.free(id);
self.data[i]
}
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
&mut self.data[id.idx()]
}
}
impl<T, I: IdNum> Default for Arena<T, I> {
+1 -22
View File
@@ -1,34 +1,13 @@
use std::ops::*;
pub const trait LerpUtil {
fn lerp(self, from: Self, to: Self) -> Self;
fn lerp_inv(self, from: Self, to: Self) -> Self;
}
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
{
const impl LerpUtil for f32 {
/// linear interpolation
/// from * (1.0 - self) + to * self
fn lerp(self, from: Self, to: Self) -> Self {
from + (to - from) * self
}
/// inverse of lerp
fn lerp_inv(self, from: Self, to: Self) -> Self {
(self - from).div_or(to - from, from)
}
}
macro_rules! impl_op {
+6 -11
View File
@@ -1,7 +1,11 @@
use crate::util::{DivOr, impl_op};
use crate::util::impl_op;
use std::{hash::Hash, ops::*};
#[repr(C)]
/// `align(8)` because that is WGSL's alignment for a `vec2<f32>`, so any GPU
/// struct holding one is laid out the way its shader reads it without having
/// to say so itself. Those structs still need a manual `unsafe impl Pod`,
/// since the trailing padding this introduces is what `derive(Pod)` refuses.
#[repr(C, align(8))]
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vec2 {
pub x: f32,
@@ -67,15 +71,6 @@ impl_op!(Vec2 Sub sub; x y);
impl_op!(Vec2 Mul mul; 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 {
type Output = Self;
+32 -19
View File
@@ -1,4 +1,4 @@
use crate::{Axis, AxisT, Len, Painter, SizeCtx};
use crate::{Axis, Len, Painter, Size};
use std::any::Any;
mod data;
@@ -15,32 +15,45 @@ pub use tag::*;
pub use view::*;
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 {
fn draw(&mut self, painter: &mut Painter);
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len;
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len;
}
/// Draws the widget, and returns what it used of the box it was given.
fn draw(&mut self, painter: &mut Painter) -> Size;
pub trait WidgetAxisFns {
fn desired_len<A: AxisT>(&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
}
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),
}
fn on_resize(&self, _axis: Axis) -> OnResize {
OnResize::default()
}
}
impl Widget for () {
fn draw(&mut self, _: &mut Painter) {}
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
Len::ZERO
/// A gap: nothing drawn, at the default length, so a span gives it a share.
fn draw(&mut self, _: &mut Painter) -> Size {
Size::default()
}
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
Len::ZERO
fn size_hint(&self, _axis: Axis) -> Option<Len> {
Some(Len::default())
}
fn on_resize(&self, _axis: Axis) -> OnResize {
OnResize::Scale
}
}
+1 -5
View File
@@ -10,11 +10,7 @@ struct State {
}
impl DefaultAppState for State {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
rect(Color::RED).set_root(rsc, &mut ui_state);
Self { ui_state }
}
+3 -7
View File
@@ -15,11 +15,7 @@ pub struct Client {
}
impl DefaultAppState for Client {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
let rrect = rect(Color::WHITE).radius(20);
let pad_test = (
rrect.color(Color::BLUE),
@@ -212,10 +208,10 @@ impl DefaultAppState for Client {
render: &mut UiRenderState,
) {
let new = format!(
"widgets: {}\nactive: {}\nviews: {}",
"widgets: {}\nactive: {}\ntextures: {}",
rsc.widgets().len(),
render.active_widgets(),
self.ui_state.renderer.ui.view_count(),
rsc.ui().textures.count(),
);
if new != *rsc.widgets()[self.info].content {
*rsc.widgets_mut()[self.info].content = new;
+1 -5
View File
@@ -11,11 +11,7 @@ struct State {
}
impl DefaultAppState for State {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
let rect = rect(Color::RED).add(rsc);
rect.task_on(CursorSense::click(), async move |mut ctx| {
tokio::time::sleep(Duration::from_secs(1)).await;
+63
View File
@@ -0,0 +1,63 @@
//! 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 }
}
}
+1 -5
View File
@@ -36,11 +36,7 @@ impl Test {
}
impl DefaultAppState for State {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
let test = Test::new(rsc);
test.on(CursorSense::click(), move |_, rsc| {
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "rig-input"
version.workspace = true
edition.workspace = true
# Replays `.touch` recordings through Wayland's virtual-pointer protocol;
# headless sway has no input devices for coordinate-driving tools to move.
[[bin]]
name = "replay-touch"
path = "src/main.rs"
[dependencies]
# Share the harness parser so both ways of replaying read a file the same.
iris = { path = ".." }
wayland-client = { workspace = true }
wayland-protocols-wlr = { workspace = true }
+141
View File
@@ -0,0 +1,141 @@
use iris::harness::{TouchAction, TouchScript};
use std::time::Duration;
use wayland_client::protocol::wl_pointer::ButtonState;
use wayland_client::protocol::{wl_registry, wl_seat};
use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1,
zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1,
};
const BTN_LEFT: u32 = 0x110;
const SETTLE: Duration = Duration::from_millis(200);
#[derive(Default)]
struct Globals {
seat: Option<wl_seat::WlSeat>,
manager: Option<ZwlrVirtualPointerManagerV1>,
}
impl Dispatch<wl_registry::WlRegistry, ()> for Globals {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
let wl_registry::Event::Global {
name,
interface,
version,
} = event
else {
return;
};
match interface.as_str() {
"wl_seat" => {
state.seat = Some(registry.bind(name, version.min(7), qh, ()));
}
"zwlr_virtual_pointer_manager_v1" => {
state.manager = Some(registry.bind(name, version.min(2), qh, ()));
}
_ => {}
}
}
}
delegate_noop!(Globals: ignore wl_seat::WlSeat);
delegate_noop!(Globals: ZwlrVirtualPointerManagerV1);
delegate_noop!(Globals: ZwlrVirtualPointerV1);
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let [width, height, path] = args.as_slice() else {
eprintln!("usage: replay-touch WIDTH HEIGHT FILE");
std::process::exit(2);
};
let (width, height) = (parse(width, "WIDTH"), parse(height, "HEIGHT"));
let text = std::fs::read_to_string(path)
.unwrap_or_else(|e| fail(&format!("could not read {path}: {e}")));
let script = TouchScript::parse(&text).unwrap_or_else(|e| fail(&e));
let conn = Connection::connect_to_env().unwrap_or_else(|e| {
fail(&format!(
"no wayland display ({e}); is WAYLAND_DISPLAY set?"
))
});
let mut queue = conn.new_event_queue();
let qh = queue.handle();
let display = conn.display();
display.get_registry(&qh, ());
let mut globals = Globals::default();
queue
.roundtrip(&mut globals)
.unwrap_or_else(|e| fail(&format!("wayland roundtrip failed: {e}")));
let manager = globals.manager.as_ref().unwrap_or_else(|| {
fail(
"this compositor does not offer zwlr_virtual_pointer_manager_v1, so a pointer cannot \
be synthesised; sway and every wlroots compositor do",
)
});
let pointer = manager.create_virtual_pointer(globals.seat.as_ref(), &qh, ());
// Put the pointer where the gesture starts and let the compositor
// settle before anything is pressed. Without this the press is
// dropped: sway has just learned about this pointer, and a button
// sent in the same breath as the motion that first puts it over a
// window arrives before there is a focused surface to send it to --
// winit sees `CursorEntered`, the moves and the *release*, never the
// press, so the gesture reads as a hover and nothing scrolls. Found
// by printing winit's own events; the settle is what fixed it.
if let Some(first) = script.samples.first() {
pointer.motion_absolute(0, first.pos.x as u32, first.pos.y as u32, width, height);
pointer.frame();
conn.flush()
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
std::thread::sleep(SETTLE);
}
let mut previous = 0;
for sample in &script.samples {
std::thread::sleep(Duration::from_millis(sample.t_ms - previous));
previous = sample.t_ms;
let t = sample.t_ms as u32;
pointer.motion_absolute(t, sample.pos.x as u32, sample.pos.y as u32, width, height);
pointer.frame();
// The button goes in a frame of its own, *after* the motion has
// been committed. Sent in the same frame as the motion that
// first puts the pointer over the window, sway drops it: the
// client sees `CursorEntered` and the moves but never a
// `MouseInput { state: Pressed }`, so the whole gesture reads as
// a hover and nothing scrolls. Found exactly that way, by
// printing winit's events.
let state = match sample.action {
TouchAction::Down => Some(ButtonState::Pressed),
TouchAction::Up => Some(ButtonState::Released),
TouchAction::Move => None,
};
if let Some(state) = state {
pointer.button(t, BTN_LEFT, state);
pointer.frame();
}
conn.flush()
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
}
pointer.destroy();
conn.flush().ok();
}
fn parse(text: &str, what: &str) -> u32 {
text.parse()
.unwrap_or_else(|_| fail(&format!("{what} is not a whole number: {text:?}")))
}
fn fail(message: &str) -> ! {
eprintln!("replay-touch: {message}");
std::process::exit(1);
}
+14
View File
@@ -0,0 +1,14 @@
# The compositor `scripts/run-headless.sh` starts, so that an example has a
# surface where there is no display. Nothing here is meant to be looked at
# directly; `grim` is.
#
# No Xwayland: winit talks Wayland natively, so an X server is a second thing
# to go wrong for no gain.
xwayland disable
# The default output, overridden per run by `--mode`. Larger than the window
# an example opens, so nothing is scaled or clipped.
output HEADLESS-1 mode 1920x1200@60Hz
default_border none
focus_follows_mouse no
+170
View File
@@ -0,0 +1,170 @@
#!/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"
+62 -49
View File
@@ -1,10 +1,6 @@
use crate::prelude::*;
use arboard::Clipboard;
use std::{
marker::{PhantomData, Sized},
sync::Arc,
time::Instant,
};
use std::{marker::PhantomData, sync::Arc, time::Instant};
use winit::{
event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
@@ -29,7 +25,35 @@ pub use sense::*;
pub use state::*;
pub use task::*;
pub type Proxy<Event> = EventLoopProxy<Event>;
/// Sends an application's own events to its event loop. It wraps the proxy
/// rather than being one because task updates travel the same way: what an
/// application sends is its `Event`, not the loop's whole message type.
pub struct Proxy<State: DefaultAppState>(EventLoopProxy<DefaultEvent<State>>);
impl<State: DefaultAppState> Clone for Proxy<State> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<State: DefaultAppState> Proxy<State> {
pub fn send_event(&self, event: State::Event) {
let _ = self.0.send_event(DefaultEvent::User(event));
}
}
/// What the event loop carries: the application's own events, and the
/// updates tasks send back to the ui thread.
pub enum DefaultEvent<State: DefaultAppState> {
User(State::Event),
Update(Box<dyn TaskUpdate<DefaultRsc<State>>>),
}
impl<State: DefaultAppState> TaskQueue<DefaultRsc<State>> for Proxy<State> {
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<State>>>) {
let _ = self.0.send_event(DefaultEvent::Update(update));
}
}
pub struct DefaultUiState {
pub root: Option<StrongWidget>,
@@ -70,9 +94,8 @@ pub trait HasDefaultUiState: Sized + 'static {
}
pub trait DefaultAppState: HasDefaultUiState {
type Event = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>)
-> Self;
type Event: Send = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self>) -> Self;
#[allow(unused_variables)]
fn event(
&mut self,
@@ -105,18 +128,14 @@ pub struct DefaultRsc<State: 'static> {
}
impl<State> DefaultRsc<State> {
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, recv) = Tasks::init(window);
(
Self {
ui: Default::default(),
events: Default::default(),
tasks,
state: Default::default(),
_state: Default::default(),
},
recv,
)
pub fn init(queue: Arc<dyn TaskQueue<Self>>) -> Self {
Self {
ui: Default::default(),
events: Default::default(),
tasks: Tasks::init(queue),
state: Default::default(),
_state: Default::default(),
}
}
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
@@ -181,43 +200,32 @@ pub struct DefaultApp<State: DefaultAppState> {
rsc: DefaultRsc<State>,
render: UiRenderState,
state: State,
task_recv: TaskMsgReceiver<DefaultRsc<State>>,
}
impl<State: DefaultAppState> AppState for DefaultApp<State> {
type Event = State::Event;
type Event = DefaultEvent<State>;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
let window = event_loop
.create_window(State::window_attributes())
.unwrap();
let default_state = DefaultUiState::new(window);
let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone());
let state = State::new(default_state, &mut rsc, proxy);
let mut rsc = DefaultRsc::init(Arc::new(Proxy(proxy.clone())));
let state = State::new(default_state, &mut rsc, Proxy(proxy));
let render = UiRenderState::new();
Self {
rsc,
state,
render,
task_recv,
}
Self { rsc, state, render }
}
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
self.state.event(event, &mut self.rsc, &mut self.render);
match event {
DefaultEvent::User(event) => self.state.event(event, &mut self.rsc, &mut self.render),
DefaultEvent::Update(update) => update(&mut self.state, &mut self.rsc),
}
self.request_redraw_if_needed();
}
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let Self {
rsc,
render,
state,
task_recv,
} = self;
for update in task_recv.try_iter() {
update(state, rsc);
}
let Self { rsc, render, state } = self;
let ui_state = state.default_state_mut();
let input_changed = ui_state.input.event(&event);
@@ -227,8 +235,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.focus = None;
}
if input_changed {
let window_size = ui_state.window_size();
render.run_sensors(rsc, state, cursor_state, window_size);
render.run_sensors(rsc, state, cursor_state);
}
let ui_state = state.default_state_mut();
if old != ui_state.focus
@@ -297,11 +304,8 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
_ => (),
}
state.window_event(event, rsc, render);
let ui_state = self.state.default_state_mut();
if render.needs_redraw(&ui_state.root, rsc.widgets()) {
ui_state.renderer.window().request_redraw();
}
ui_state.input.end_frame();
self.request_redraw_if_needed();
self.state.default_state_mut().input.end_frame();
}
fn exit(&mut self) {
@@ -309,6 +313,15 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
}
}
impl<State: DefaultAppState> DefaultApp<State> {
fn request_redraw_if_needed(&mut self) {
let ui_state = self.state.default_state_mut();
if self.render.needs_redraw(&ui_state.root, self.rsc.widgets()) {
ui_state.renderer.window().request_redraw();
}
}
}
pub trait RscIdx<Rsc> {
type Output;
fn get(self, rsc: &Rsc) -> &Self::Output;
+22 -15
View File
@@ -1,4 +1,4 @@
use iris_core::{UiData, UiLimits, UiRenderNode, UiRenderState};
use iris_core::{UiData, UiRenderNode, UiRenderState};
use pollster::FutureExt;
use std::sync::Arc;
use wgpu::*;
@@ -22,7 +22,20 @@ impl UiRenderer {
}
pub fn draw(&mut self) {
let output = self.surface.get_current_texture().unwrap();
let output = match self.surface.get_current_texture() {
CurrentSurfaceTexture::Success(texture) => texture,
CurrentSurfaceTexture::Suboptimal(texture) => {
self.surface.configure(&self.device, &self.config);
texture
}
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
self.surface.configure(&self.device, &self.config);
return;
}
CurrentSurfaceTexture::Timeout
| CurrentSurfaceTexture::Occluded
| CurrentSurfaceTexture::Validation => return,
};
let view = output
.texture
.create_view(&TextureViewDescriptor::default());
@@ -46,7 +59,7 @@ impl UiRenderer {
self.queue.submit(std::iter::once(encoder.finish()));
self.window.pre_present_notify();
output.present();
self.queue.present(output);
}
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
@@ -65,9 +78,10 @@ impl UiRenderer {
pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size();
let instance = Instance::new(&InstanceDescriptor {
let instance = Instance::new(InstanceDescriptor {
backends: Backends::PRIMARY,
..Default::default()
display: Some(Box::new(window.clone())),
..InstanceDescriptor::new_without_display_handle()
});
let surface = instance
@@ -79,22 +93,14 @@ impl UiRenderer {
power_preference: PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
apply_limit_buckets: false,
})
.block_on()
.expect("Could not get adapter!");
let ui_limits = UiLimits::default();
let (device, queue) = adapter
.request_device(&DeviceDescriptor {
required_features: Features::TEXTURE_BINDING_ARRAY
| Features::PARTIALLY_BOUND_BINDING_ARRAY
| Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
required_limits: Limits {
max_binding_array_elements_per_shader_stage: ui_limits
.max_binding_array_elements_per_shader_stage(),
max_binding_array_sampler_elements_per_shader_stage: ui_limits
.max_binding_array_sampler_elements_per_shader_stage(),
max_buffer_size: 1 << 30,
..Default::default()
},
@@ -114,6 +120,7 @@ impl UiRenderer {
let config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
color_space: SurfaceColorSpace::Auto,
width: size.width,
height: size.height,
present_mode: PresentMode::AutoVsync,
@@ -126,7 +133,7 @@ impl UiRenderer {
let encoder = Self::create_encoder(&device);
let ui = UiRenderNode::new(&device, &queue, &config, ui_limits);
let ui = UiRenderNode::new(&device, &config);
Self {
surface,
+103 -35
View File
@@ -4,14 +4,14 @@ use std::{
rc::Rc,
};
#[derive(Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CursorButton {
Left,
Right,
Middle,
}
#[derive(Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CursorSense {
PressStart(CursorButton),
Pressing(CursorButton),
@@ -27,7 +27,7 @@ pub struct CursorSenses(Vec<CursorSense>);
impl Event for CursorSenses {
type Data<'a> = CursorData<'a>;
type State = SensorState;
type Global = Hovered;
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
if let Some(sense) = should_run(self, &data.cursor, data.hover) {
let mut data = data.clone();
@@ -37,6 +37,24 @@ impl Event for CursorSenses {
None
}
}
/// A press or a scroll is used up by whatever answered it, so it stops
/// there. Hovering is not: a cursor resting somewhere goes on resting.
fn consumes(&self, data: &Self::Data<'_>) -> bool {
!data.sense.position_only()
}
}
/// Who the cursor was inside, before and after an input. The difference is
/// whose hover has ended -- including a widget a higher layer has covered,
/// which the walk stops before reaching.
///
/// Two buffers that swap rather than one rebuilt, so an input allocates
/// nothing once they have grown.
#[derive(Default)]
pub struct Hovered {
was: Vec<WidgetId>,
now: Vec<WidgetId>,
}
impl CursorSense {
@@ -52,6 +70,12 @@ impl CursorSense {
pub fn is_dragging(&self) -> bool {
matches!(self, CursorSense::Pressing(CursorButton::Left))
}
/// False if the sense is a button or a scroll, true if it is only about
/// where the cursor is.
fn position_only(&self) -> bool {
matches!(self, Self::HoverStart | Self::Hovering | Self::HoverEnd)
}
}
#[derive(Default, Clone)]
@@ -96,6 +120,12 @@ impl CursorButtons {
}
impl CursorState {
/// True if the cursor is only reporting where it is: no button and no
/// scroll this frame.
pub fn position_only(&self) -> bool {
self.scroll_delta == Vec2::ZERO && self.buttons.iter().all(|(_, state)| state.is_off())
}
pub fn end_frame(&mut self) {
self.buttons.end_frame();
self.scroll_delta = Vec2::ZERO;
@@ -123,11 +153,6 @@ pub struct Sensor<Ctx: HasEvents, Data> {
pub type SenseShape = UiRegion;
#[derive(Default, Debug)]
pub struct SensorState {
pub hover: ActivationState,
}
#[derive(Clone)]
pub struct CursorData<'a> {
/// where this widget was hit
@@ -147,7 +172,6 @@ pub trait SensorUi {
rsc: &mut Rsc,
state: &mut Rsc::State,
cursor: CursorState,
window_size: Vec2,
);
}
@@ -157,54 +181,98 @@ impl SensorUi for UiRenderState {
rsc: &mut Rsc,
state: &mut Rsc::State,
cursor: CursorState,
window_size: Vec2,
) {
// in order to remove this take, need to store active list in UiRenderState somehow
// this would probably be done through a generic parameter that adds yet another rsc /
// state like thing, but local to render state, and is passed to UiRsc events so you can
// update it there?
let mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
let active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
let mut hovered = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().global);
hovered.now.clear();
let position_only = cursor.position_only();
let region_of = |id| self.window_region(&id);
for layer in self.layers.indices().rev() {
let mut sensed = false;
for (id, sensor) in active.get_mut(&layer).into_flat_iter() {
let shape = self.active.get(id).unwrap().region;
let region = shape.to_px(window_size);
let in_shape = cursor.exists && region.contains(cursor.pos);
sensor.hover.update(in_shape);
if sensor.hover == ActivationState::Off {
let mut consumed = false;
for id in active.get(&layer).into_flat_iter().map(|(id, _)| *id) {
let Some(region) = region_of(id) else {
continue;
};
if !cursor.exists || !region.contains(cursor.pos) {
continue;
}
sensed = true;
let cursor = cursor.clone();
let data = CursorData {
pos: cursor.pos - region.top_left,
size: region.bot_right - region.top_left,
scroll_delta: cursor.scroll_delta,
hover: sensor.hover,
cursor,
// this does not have any meaning;
// might wanna set up Event to have a prepare stage
sense: CursorSense::Hovering,
render: self,
hovered.now.push(id);
let hover = match hovered.was.contains(&id) {
true => ActivationState::On,
false => ActivationState::Start,
};
rsc.run_event::<CursorSense>(*id, data, state);
// A press or a scroll stops where something answered it, so a
// button over a list does not swallow the list's scrolling.
consumed |= deliver(self, rsc, state, id, hover, &cursor, region);
// A cursor doing neither stops at whatever it is over, so
// hovering does not reach through.
consumed |= position_only;
}
if sensed {
// Applied after the layer, never during it: senses on one layer do
// not block each other.
if consumed {
break;
}
}
rsc.events_mut().get_type::<CursorSense>().active = active;
// Whatever the cursor was inside and is not now, whether it left or a
// layer above took the input before the walk reached it. A widget that
// stopped being drawn has no region to report and is simply dropped.
for &id in &hovered.was {
if !hovered.now.contains(&id)
&& let Some(region) = region_of(id)
{
deliver(self, rsc, state, id, ActivationState::End, &cursor, region);
}
}
std::mem::swap(&mut hovered.was, &mut hovered.now);
let senses = rsc.events_mut().get_type::<CursorSense>();
senses.active = active;
senses.global = hovered;
}
}
/// Runs one widget's cursor senses, and says whether they used up the input.
fn deliver<Rsc: HasEvents>(
render: &UiRenderState,
rsc: &mut Rsc,
state: &mut Rsc::State,
id: WidgetId,
hover: ActivationState,
cursor: &CursorState,
region: PixelRegion,
) -> bool {
let data = CursorData {
pos: cursor.pos - region.top_left,
size: region.bot_right - region.top_left,
scroll_delta: cursor.scroll_delta,
hover,
cursor: cursor.clone(),
// this does not have any meaning;
// might wanna set up Event to have a prepare stage
sense: CursorSense::Hovering,
render,
};
rsc.run_event::<CursorSense>(id, data, state)
}
pub fn should_run(
senses: &CursorSenses,
cursor: &CursorState,
hover: ActivationState,
) -> Option<CursorSense> {
for sense in senses.iter() {
// A widget the cursor is no longer inside senses only its position:
// the press that ended its hover landed on something else.
if !hover.is_on() && !sense.position_only() {
continue;
}
if match sense {
CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(),
CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(),
+16 -34
View File
@@ -1,11 +1,5 @@
use iris_core::HasState;
use std::{
pin::Pin,
sync::{
Arc,
mpsc::{Receiver as SyncReceiver, Sender as SyncSender, channel as sync_channel},
},
};
use std::{pin::Pin, sync::Arc};
use tokio::{
runtime::Runtime,
sync::mpsc::{
@@ -13,64 +7,52 @@ use tokio::{
unbounded_channel as async_channel,
},
};
use winit::window::Window;
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
pub trait TaskUpdate<Rsc: HasState>: FnOnce(&mut Rsc::State, &mut Rsc) + Send {}
impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {}
/// Hands an update from a task to the thread that owns the ui. Delivery and
/// waking are one act: a host posts the update as a message its loop already
/// carries, so nothing has to wake the loop separately, or claim a redraw to
/// be looked at.
pub trait TaskQueue<Rsc: HasState>: Send + Sync + 'static {
fn send(&self, update: Box<dyn TaskUpdate<Rsc>>);
}
pub struct Tasks<Rsc: HasState> {
start: AsyncSender<BoxTask>,
window: Arc<Window>,
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
queue: Arc<dyn TaskQueue<Rsc>>,
}
pub struct TaskCtx<Rsc: HasState> {
send: TaskMsgSender<Rsc>,
queue: Arc<dyn TaskQueue<Rsc>>,
}
impl<Rsc: HasState> TaskCtx<Rsc> {
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
let _ = self.send.send(Box::new(f));
}
}
impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
fn new(send: TaskMsgSender<Rsc>) -> Self {
Self { send }
self.queue.send(Box::new(f));
}
}
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
impl<Rsc: HasState> Tasks<Rsc> {
pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) {
pub fn init(queue: Arc<dyn TaskQueue<Rsc>>) -> Self {
let (start, start_recv) = async_channel();
let (msgs, msgs_recv) = sync_channel();
std::thread::spawn(|| {
let rt = Runtime::new().unwrap();
rt.block_on(listen(start_recv))
});
(
Self {
start,
msg_send: msgs,
window,
},
msgs_recv,
)
Self { start, queue }
}
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
where
F::CallOnceFuture: Send,
{
let send = self.msg_send.clone();
let window = self.window.clone();
let queue = self.queue.clone();
let _ = self.start.send(Box::pin(async move {
task(TaskCtx::new(send)).await;
window.request_redraw();
task(TaskCtx { queue }).await;
}));
}
}
+320
View File
@@ -0,0 +1,320 @@
//! A ui with no window: build a tree, run frames, move a pointer, and read
//! back where widgets landed.
//!
//! It does not draw. A claim about pixels still needs a real surface.
use crate::prelude::*;
use std::{
sync::{
Arc,
mpsc::{Receiver, SyncSender, sync_channel},
},
time::Duration,
};
/// There is no loop here to post to, so updates queue until the test asks
/// for them.
struct Queue(SyncSender<Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>>);
impl TaskQueue<DefaultRsc<HarnessState>> for Queue {
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>) {
let _ = self.0.send(update);
}
}
/// `assert_eq!` for where a frame put a widget, written as its two corners.
#[macro_export]
macro_rules! assert_corners {
($harness:expr, $id:expr, ($x0:expr, $y0:expr), ($x1:expr, $y1:expr)) => {
assert_eq!(
$harness.region(&$id).expect("widget drew nothing"),
$crate::core::PixelRegion {
top_left: $crate::core::util::Vec2::new($x0 as f32, $y0 as f32),
bot_right: $crate::core::util::Vec2::new($x1 as f32, $y1 as f32),
}
);
};
}
pub use crate::assert_corners;
/// One replayed pointer sample, cut down to what a window delivers: where the
/// pointer is, and whether the button changed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TouchAction {
Down,
Move,
Up,
}
impl TouchAction {
fn parse(word: &str) -> Option<Self> {
match word {
"down" => Some(Self::Down),
"move" => Some(Self::Move),
"up" => Some(Self::Up),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct TouchSample {
pub t_ms: u64,
pub action: TouchAction,
pub pos: Vec2,
}
/// A recorded gesture, in the output's own pixels: `<ms> down|move|up <x> <y>`
/// a line, `#` to end of line ignored.
///
/// One parser for both ways of replaying a recording -- into a harness, and
/// into a real window -- so the two cannot read the same file differently.
pub struct TouchScript {
pub samples: Vec<TouchSample>,
}
impl TouchScript {
pub fn parse(text: &str) -> Result<Self, String> {
let mut samples: Vec<TouchSample> = Vec::new();
for (i, line) in text.lines().enumerate() {
let line = line.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let at = |what: &str| format!("touch script line {}: {what}: {line:?}", i + 1);
let mut words = line.split_whitespace();
let (Some(t), Some(action), Some(x), Some(y), None) = (
words.next(),
words.next(),
words.next(),
words.next(),
words.next(),
) else {
return Err(at("expected `t_ms action x y`"));
};
let t_ms: u64 = t.parse().map_err(|_| at("t_ms is not a whole number"))?;
let action =
TouchAction::parse(action).ok_or_else(|| at("action is not down/move/up"))?;
let x: f32 = x.parse().map_err(|_| at("x is not a number"))?;
let y: f32 = y.parse().map_err(|_| at("y is not a number"))?;
if let Some(last) = samples.last()
&& t_ms < last.t_ms
{
return Err(at("samples must be in time order"));
}
samples.push(TouchSample {
t_ms,
action,
pos: Vec2::new(x, y),
});
}
Ok(Self { samples })
}
}
#[derive(Default)]
pub struct HarnessState {
pub root: Option<StrongWidget>,
}
impl HasRoot for HarnessState {
fn set_root(&mut self, root: StrongWidget) {
self.root = Some(root);
}
}
pub struct Harness {
pub rsc: DefaultRsc<HarnessState>,
pub render: UiRenderState,
pub state: HarnessState,
updates: Receiver<Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>>,
cursor: CursorState,
}
impl Harness {
/// `size` is the output in physical pixels.
pub fn new(size: impl Into<Vec2>) -> Self {
// A `TaskQueue` must be `Sync`, which `mpsc::Sender` is not; the
// bound that comes with `SyncSender` is far past anything a test
// leaves unread.
let (send, updates) = sync_channel(1024);
let rsc = DefaultRsc::init(Arc::new(Queue(send)));
let mut render = UiRenderState::new();
render.resize(size);
Self {
rsc,
render,
state: HarnessState::default(),
updates,
cursor: CursorState::default(),
}
}
pub fn size(&self) -> Vec2 {
self.render.output_size()
}
pub fn resize(&mut self, size: impl Into<Vec2>) {
self.render.resize(size);
}
/// Sets the root and lays it out, so a pointer event has something to hit.
pub fn set_root<T>(&mut self, widget: impl WidgetLike<DefaultRsc<HarnessState>, T>) {
widget.set_root(&mut self.rsc, &mut self.state);
self.frame();
}
pub fn needs_redraw(&self) -> bool {
self.render
.needs_redraw(&self.state.root, self.rsc.widgets())
}
pub fn apply_updates(&mut self) -> usize {
let mut applied = 0;
while let Ok(update) = self.updates.try_recv() {
update(&mut self.state, &mut self.rsc);
applied += 1;
}
applied
}
/// Waits for a task's first update, then applies everything waiting.
/// False if none arrived in time.
#[must_use]
pub fn await_update(&mut self, timeout: Duration) -> bool {
let Ok(update) = self.updates.recv_timeout(timeout) else {
return false;
};
update(&mut self.state, &mut self.rsc);
self.apply_updates();
true
}
/// Lays the tree out and builds its primitives.
pub fn frame(&mut self) {
self.apply_updates();
self.render.update(&self.state.root, &mut self.rsc);
}
/// Where the last frame put a widget, or `None` if it drew nothing.
pub fn region(&self, id: &impl IdLike) -> Option<PixelRegion> {
self.render.window_region(id)
}
pub fn move_to(&mut self, pos: impl Into<Vec2>) {
self.cursor.pos = pos.into();
self.cursor.exists = true;
self.sense();
}
pub fn leave(&mut self) {
self.cursor.exists = false;
self.sense();
}
pub fn press(&mut self, button: CursorButton) {
self.button(button).update(true);
self.sense();
}
pub fn release(&mut self, button: CursorButton) {
self.button(button).update(false);
self.sense();
}
/// A wheel carries no position, so this goes wherever the cursor was last
/// moved to -- nowhere, until it has been moved.
pub fn scroll(&mut self, delta: impl Into<Vec2>) {
self.cursor.scroll_delta = delta.into();
self.sense();
}
pub fn click(&mut self, pos: impl Into<Vec2>) {
self.move_to(pos);
self.press(CursorButton::Left);
self.release(CursorButton::Left);
}
/// Drives a recorded gesture through the harness.
pub fn replay(&mut self, script: &TouchScript) {
for sample in &script.samples {
match sample.action {
TouchAction::Down => {
self.move_to(sample.pos);
self.press(CursorButton::Left);
}
TouchAction::Move => self.move_to(sample.pos),
TouchAction::Up => {
self.move_to(sample.pos);
self.release(CursorButton::Left);
}
}
}
}
fn button(&mut self, button: CursorButton) -> &mut ActivationState {
let buttons = &mut self.cursor.buttons;
match button {
CursorButton::Left => &mut buttons.left,
CursorButton::Middle => &mut buttons.middle,
CursorButton::Right => &mut buttons.right,
}
}
/// Dispatches against the layout of the last frame, which is what a
/// window delivers input against too.
fn sense(&mut self) {
let cursor = self.cursor.clone();
self.render
.run_sensors(&mut self.rsc, &mut self.state, cursor);
self.cursor.end_frame();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(text: &str) -> Result<Vec<(u64, TouchAction, f32, f32)>, String> {
Ok(TouchScript::parse(text)?
.samples
.iter()
.map(|s| (s.t_ms, s.action, s.pos.x, s.pos.y))
.collect())
}
#[test]
fn a_recording_is_time_action_and_a_point() {
assert_eq!(
parse("0 down 10 20\n16 move 10.5 24\n32 up 10.5 24").unwrap(),
[
(0, TouchAction::Down, 10.0, 20.0),
(16, TouchAction::Move, 10.5, 24.0),
(32, TouchAction::Up, 10.5, 24.0),
]
);
}
#[test]
fn blank_lines_and_comments_are_not_samples() {
assert_eq!(
parse("# a flick\n\n 0 down 1 2 # the finger lands\n\n").unwrap(),
[(0, TouchAction::Down, 1.0, 2.0)]
);
}
#[test]
fn a_recording_that_goes_backwards_is_rejected() {
// Replay waits out the gap between samples, so time running backwards
// would silently become no wait at all.
let err = parse("16 down 1 2\n0 up 1 2").unwrap_err();
assert!(err.contains("time order"), "{err}");
}
#[test]
fn a_line_that_is_not_a_sample_says_which_line() {
let err = parse("0 down 1 2\n16 wiggle 1 2").unwrap_err();
assert!(err.contains("line 2"), "{err}");
assert!(err.contains("down/move/up"), "{err}");
}
}
+1
View File
@@ -7,6 +7,7 @@
pub mod default;
pub mod event;
pub mod harness;
pub mod widget;
pub use iris_core as core;
+7 -6
View File
@@ -6,16 +6,17 @@ pub struct Image {
}
impl Widget for Image {
fn draw(&mut self, painter: &mut Painter) {
painter.texture(&self.handle);
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.primitive(&self.handle);
Size::abs(self.handle.size())
}
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
Len::abs(self.handle.size().x)
fn size_hint(&self, axis: Axis) -> Option<Len> {
Some(Len::abs(self.handle.size().axis(axis)))
}
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
Len::abs(self.handle.size().y)
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
}
+5 -8
View File
@@ -5,16 +5,13 @@ pub struct Masked {
}
impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region());
painter.widget(&self.inner);
painter.widget(&self.inner).size()
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.width(&self.inner)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
/// It clips to the box it was given, not to the part its child used.
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Redraw
}
}
+8 -21
View File
@@ -6,30 +6,17 @@ pub struct Aligned {
}
impl Widget for Aligned {
fn draw(&mut self, painter: &mut Painter) {
fn draw(&mut self, painter: &mut Painter) -> Size {
// 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() {
(Some(x), Some(y)) => painter
.size(&self.inner)
.to_uivec2()
.align(RegionAlign { x, y }),
(Some(x), None) => {
let x = painter.size_ctx().width(&self.inner).apply_rest().align(x);
UiRegion::new(x, UiSpan::FULL)
}
(None, Some(y)) => {
let y = painter.size_ctx().height(&self.inner).apply_rest().align(y);
UiRegion::new(UiSpan::FULL, y)
}
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL),
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
(None, None) => UiRegion::FULL,
};
painter.widget_within(&self.inner, region);
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.width(&self.inner)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
size
}
}
+2 -10
View File
@@ -6,18 +6,10 @@ pub struct LayerOffset {
}
impl Widget for LayerOffset {
fn draw(&mut self, painter: &mut Painter) {
fn draw(&mut self, painter: &mut Painter) -> Size {
for _ in 0..self.offset {
painter.next_layer();
}
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)
painter.widget(&self.inner).size()
}
}
+13 -36
View File
@@ -6,43 +6,20 @@ pub struct MaxSize {
pub y: Option<Len>,
}
impl MaxSize {
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
if let Some(x) = self.x {
ctx.outer.x.select_len(x.apply_rest());
}
if let Some(y) = self.y {
ctx.outer.y.select_len(y.apply_rest());
}
}
}
impl Widget for MaxSize {
fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.inner);
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.apply_to_outer(ctx);
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
fn draw(&mut self, painter: &mut Painter) -> Size {
let child = painter.widget(&self.inner).size();
let output = painter.output_size();
Size {
x: capped(child.x, self.x, output.x),
y: capped(child.y, self.y, output.y),
}
}
}
fn capped(len: Len, max: Option<Len>, output: f32) -> Len {
match max {
Some(max) if len.apply_rest().to_abs(output) > max.apply_rest().to_abs(output) => max,
_ => len,
}
}
+2 -2
View File
@@ -4,7 +4,7 @@ mod max_size;
mod offset;
mod pad;
mod scroll;
mod sized;
mod set_size;
mod span;
mod stack;
@@ -14,6 +14,6 @@ pub use max_size::*;
pub use offset::*;
pub use pad::*;
pub use scroll::*;
pub use sized::*;
pub use set_size::*;
pub use span::*;
pub use stack::*;
+2 -10
View File
@@ -6,16 +6,8 @@ pub struct Offset {
}
impl Widget for Offset {
fn draw(&mut self, painter: &mut Painter) {
fn draw(&mut self, painter: &mut Painter) -> Size {
let region = UiRegion::FULL.offset(self.amt);
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)
painter.widget_within(&self.inner, region).size()
}
}
+14 -22
View File
@@ -6,28 +6,20 @@ pub struct Pad {
}
impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) {
painter.widget_within(&self.inner, self.padding.region());
}
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
fn draw(&mut self, painter: &mut Painter) -> Size {
let inner = painter
.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
},
}
}
}
+7 -11
View File
@@ -10,11 +10,14 @@ pub struct Scroll {
}
impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) {
fn draw(&mut self, painter: &mut Painter) -> Size {
let output_len = painter.output_size().axis(self.axis);
let container_len = painter.region().axis(self.axis).len();
let content_len = painter
.len_axis(&self.inner, self.axis)
// Drawn in the whole container to learn its length, then placed at
// the scrolled offset.
let child = painter.widget(&self.inner).size();
let content_len = child
.axis(self.axis)
.apply_rest()
.within_len(container_len)
.to_abs(output_len);
@@ -29,14 +32,7 @@ impl Widget for Scroll {
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);
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)
child
}
}
+26
View File
@@ -0,0 +1,26 @@
use crate::prelude::*;
pub struct SetSize {
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
}
impl Widget for SetSize {
fn draw(&mut self, painter: &mut Painter) -> Size {
let child = painter.widget(&self.inner).size();
Size {
x: self.x.unwrap_or(child.x),
y: self.y.unwrap_or(child.y),
}
}
/// A declared axis is known without looking at the child, which is what
/// lets a span lay out around `.height(rest(1))` without drawing it.
fn size_hint(&self, axis: Axis) -> Option<Len> {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
}
-34
View File
@@ -1,34 +0,0 @@
use crate::prelude::*;
pub struct Sized {
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
}
impl Sized {
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
if let Some(x) = self.x {
ctx.outer.x.select_len(x.apply_rest());
}
if let Some(y) = self.y {
ctx.outer.y.select_len(y.apply_rest());
}
}
}
impl Widget for Sized {
fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.inner);
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.apply_to_outer(ctx);
self.x.unwrap_or_else(|| ctx.width(&self.inner))
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.apply_to_outer(ctx);
self.y.unwrap_or_else(|| ctx.height(&self.inner))
}
}
+32 -101
View File
@@ -8,13 +8,27 @@ pub struct Span {
}
impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) {
let total = self.len_sum(&mut painter.size_ctx());
fn draw(&mut self, painter: &mut Painter) -> Size {
let axis = self.dir.axis;
// 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();
for child in &self.children {
let mut ortho = Len::ZERO;
for (child, len) in self.children.iter().zip(&lens) {
let mut span = UiSpan::FULL;
span.start = start;
let len = painter.len_axis(child, self.dir.axis);
if len.rest > 0.0 {
let offset = UiScalar::new(total.rel, total.abs);
let rel_end = UiScalar::rel(len.rest / total.rest);
@@ -24,27 +38,25 @@ impl Widget for Span {
start.abs += len.abs;
start.rel += len.rel;
span.end = start;
let mut child_region = UiRegion::from_axis(self.dir.axis, span, UiSpan::FULL);
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
if self.dir.sign == Sign::Neg {
child_region.flip(self.dir.axis);
region.flip(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;
}
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
match self.dir.axis {
Axis::X => self.desired_len(ctx),
Axis::Y => self.desired_ortho(ctx),
}
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
match self.dir.axis {
Axis::X => self.desired_ortho(ctx),
Axis::Y => self.desired_len(ctx),
}
let along = match total.rest == 0.0 && total.rel == 0.0 {
true => total,
false => Len::default(),
};
Size::from_axis(axis, along, ortho)
}
}
@@ -69,87 +81,6 @@ impl Span {
pub fn pop(&mut self) -> Option<StrongWidget> {
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> {
+18 -23
View File
@@ -8,30 +8,25 @@ pub struct Stack {
}
impl Widget for Stack {
fn draw(&mut self, painter: &mut Painter) {
let mut iter = self.children.iter();
if let Some(child) = iter.next() {
painter.child_layer();
painter.widget(child);
}
for child in iter {
painter.next_layer();
painter.widget(child);
}
}
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]),
fn draw(&mut self, painter: &mut Painter) -> Size {
let sizing = match self.size {
StackSize::Default => None,
StackSize::Child(i) => Some(i),
};
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);
// Only the child that sizes the stack is read, so the others
// changing size does not redraw it.
if sizing == Some(i) {
size = drawn.size();
}
}
size
}
}
+5 -20
View File
@@ -1,30 +1,15 @@
use crate::prelude::*;
use std::marker::{Sized, Unsize};
use std::marker::Unsize;
pub struct WidgetPtr {
pub inner: Option<StrongWidget>,
}
impl Widget for WidgetPtr {
fn draw(&mut self, painter: &mut Painter) {
if let Some(id) = &self.inner {
painter.widget(id);
}
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
if let Some(id) = &self.inner {
ctx.width(id)
} else {
Len::ZERO
}
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
if let Some(id) = &self.inner {
ctx.height(id)
} else {
Len::ZERO
fn draw(&mut self, painter: &mut Painter) -> Size {
match &self.inner {
Some(id) => painter.widget(id).size(),
None => Size::default(),
}
}
}
+7 -5
View File
@@ -28,21 +28,23 @@ impl Rect {
}
impl Widget for Rect {
fn draw(&mut self, painter: &mut Painter) {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.primitive(RectPrimitive {
color: self.color,
radius: self.radius,
thickness: self.thickness,
inner_radius: self.inner_radius,
});
Size::REST
}
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
Len::rest(1)
fn size_hint(&self, _: Axis) -> Option<Len> {
Some(Len::REST)
}
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
Len::rest(1)
/// Its box is its primitive's own region, so a new one is written there.
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*;
use std::marker::{PhantomData, Sized};
use std::marker::PhantomData;
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
pub content: String,
+16 -13
View File
@@ -55,44 +55,47 @@ impl TextEdit {
}
impl Widget for TextEdit {
fn draw(&mut self, painter: &mut Painter) {
fn draw(&mut self, painter: &mut Painter) -> Size {
let base = painter.layer;
painter.child_layer();
self.view.draw(painter);
let (_, size) = self.view.draw(painter);
painter.layer = base;
let region = self.region();
let Some(selection) = self.selection else {
return;
return size;
};
let layout = self.view.buf.layout();
// parley reports selection as boxes in layout space, so bidi and
// wrapped lines come out right without this code knowing about either.
for (rect, _) in selection.geometry(layout) {
let size = vec2(rect.width() as f32, rect.height() as f32);
let rect_size = vec2(rect.width() as f32, rect.height() as f32);
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
painter.primitive_within(
RectPrimitive::color(Color::SKY),
size.align(Align::TOP_LEFT).offset(top_left).within(&region),
rect_size
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
);
}
let caret = selection.focus().geometry(layout, CARET_WIDTH);
let size = vec2(caret.width() as f32, caret.height() as f32);
let caret_size = vec2(caret.width() as f32, caret.height() as f32);
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
painter.primitive_within(
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(top_left).within(&region),
caret_size
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
);
size
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.view.desired_width(ctx)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.view.desired_height(ctx)
fn on_resize(&self, axis: Axis) -> OnResize {
self.view.on_resize(axis)
}
}
+36 -41
View File
@@ -52,15 +52,15 @@ impl TextView {
.align(self.align)
}
fn render(&mut self, ctx: &mut SizeCtx) -> &RenderedText {
fn render(&mut self, painter: &mut Painter) -> &RenderedText {
let width = if self.attrs.wrap {
Some(ctx.px_size().x)
Some(painter.px_size().x)
} else {
None
};
if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed {
self.width = width;
self.tex = Some(ctx.draw_text(&mut self.buf, &self.attrs, width));
self.tex = Some(painter.render_text(&mut self.buf, &self.attrs, width));
self.attrs.changed = false;
self.buf.changed = false;
}
@@ -69,39 +69,40 @@ impl TextView {
pub fn tex(&self) -> Option<&RenderedText> {
self.tex.as_ref()
}
pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
if self.is_empty()
&& let Some(hint) = &self.hint
{
ctx.width(hint)
} else {
Len::abs(self.render(ctx).size.x)
}
}
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
if self.is_empty()
&& let Some(hint) = &self.hint
{
ctx.height(hint)
} else {
Len::abs(self.render(ctx).size.y)
}
}
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
/// Draws the text, and says where the glyphs went and what they use.
pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) {
let align = self.align;
if self.is_empty() && self.hint.is_some() {
let region = self.render(&mut painter.size_ctx()).size.align(align);
if let Some(hint) = &self.hint {
painter.widget(hint);
}
return region;
let region = self.render(painter).size.align(align);
let size = match &self.hint {
Some(hint) => painter.widget(hint).size(),
None => Size::ZERO,
};
return (region, size);
}
let tex = self.render(&mut painter.size_ctx());
let tex = self.render(painter);
let region = tex.size.align(align);
let size = Size::abs(tex.size);
let within = region.within(&painter.region());
painter.glyphs(tex, within);
region
(region, size)
}
/// 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 {
@@ -117,7 +118,7 @@ impl Text {
content: content.into(),
}
}
fn update_buf(&mut self, _ctx: &mut SizeCtx) {
fn update_buf(&mut self) {
if self.content.changed {
self.content.changed = false;
self.view.buf.set_text(self.content.as_str());
@@ -126,19 +127,13 @@ impl Text {
}
impl Widget for Text {
fn draw(&mut self, painter: &mut Painter) {
self.update_buf(&mut painter.size_ctx());
self.view.draw(painter);
fn draw(&mut self, painter: &mut Painter) -> Size {
self.update_buf();
self.view.draw(painter).1
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.update_buf(ctx);
self.view.desired_width(ctx)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.update_buf(ctx);
self.view.desired_height(ctx)
fn on_resize(&self, axis: Axis) -> OnResize {
self.view.on_resize(axis)
}
}
+6 -6
View File
@@ -31,9 +31,9 @@ widget_trait! {
}
}
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, Sized> {
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, SetSize> {
let size = size.into();
move |state| Sized {
move |state| SetSize {
inner: self.add_strong(state),
x: Some(size.x),
y: Some(size.y),
@@ -58,18 +58,18 @@ widget_trait! {
}
}
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
let len = len.into();
move |state| Sized {
move |state| SetSize {
inner: self.add_strong(state),
x: Some(len),
y: None,
}
}
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
let len = len.into();
move |state| Sized {
move |state| SetSize {
inner: self.add_strong(state),
x: None,
y: Some(len),
+224
View File
@@ -0,0 +1,224 @@
//! What the vertex shader's move-chain walk costs, against how deep the chain
//! is. Every active widget owns a slot, so the depth a primitive resolves
//! through is its depth in the widget tree.
//!
//! cargo test --release --test chain_cost -- --ignored --nocapture
//!
//! Timed on the GPU with timestamp queries rather than by the clock: wall time
//! here varied by 2x between runs of one unchanged binary. The pass is
//! submitted and waited on, so this is the GPU's cost and not the recording
//! loop's -- which is what `draw_cost.rs` measures instead.
//!
//! The instances are two pixels wide so that vertex work dominates; a chain
//! walk that does not show up against small quads will not show up against
//! anything.
//!
//! The instance is leaked deliberately, for the reason `draw_cost.rs` gives.
use iris::prelude::*;
use iris_core::{
MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode, UiRenderState,
UiScalar, UiSpan,
};
use wgpu::{Color as GpuColor, *};
const SIZE: u32 = 1024;
const INSTANCES: usize = 200_000;
const FRAMES: u32 = 20;
/// Reported as the best of this many batches, since the mean moves by more
/// than the thing being measured.
const BATCHES: u32 = 8;
fn gpu() -> Option<(Device, Queue, f32)> {
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
}),
};
let instance: &'static Instance = Box::leak(Box::new(instance));
let adapter =
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
println!("no timestamp queries on {:?}", adapter.get_info().name);
return None;
}
println!("adapter: {:?}", adapter.get_info().name);
let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor {
required_features: Features::TIMESTAMP_QUERY,
..Default::default()
}))
.ok()?;
let period = queue.get_timestamp_period();
Some((device, queue, period))
}
fn config(format: TextureFormat) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: SIZE,
height: SIZE,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: vec![],
}
}
/// A chain `depth` slots long, and instances that all resolve through its end.
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
let kind = ui.primitives.kind::<RectPrimitive>();
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
let mut slot = MoveIdx::NONE;
for _ in 0..depth {
slot = render.moves.push(slot);
}
let px = |v: f32| UiScalar { rel: 0.0, abs: v };
for i in 0..INSTANCES {
let x = (i % (SIZE as usize / 2)) as f32 * 2.0;
let y = (i / (SIZE as usize / 2)) as f32;
render.layers.write(
0,
PrimitiveInst {
kind,
id,
primitive: RectPrimitive::color(UiColor::WHITE),
region: UiRegion::new(
UiSpan::new(px(x), px(x + 2.0)),
UiSpan::new(px(y), px(y + 1.0)),
),
mask_idx: MaskIdx::NONE,
move_idx: slot,
},
);
}
}
/// Nanoseconds the pass took on the GPU, best of `BATCHES`.
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
let format = TextureFormat::Bgra8Unorm;
let mut node = UiRenderNode::new(device, &config(format));
let mut ui = UiData::default();
let mut render = UiRenderState::new();
fill(&mut ui, &mut render, depth);
node.update(device, queue, &mut ui, &mut render);
let target = device.create_texture(&TextureDescriptor {
label: Some("chain cost"),
size: Extent3d {
width: SIZE,
height: SIZE,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format,
usage: TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = target.create_view(&TextureViewDescriptor::default());
let queries = device.create_query_set(&QuerySetDescriptor {
label: Some("chain cost"),
ty: QueryType::Timestamp,
count: 2,
});
let resolved = device.create_buffer(&BufferDescriptor {
label: Some("resolved"),
size: 16,
usage: BufferUsages::QUERY_RESOLVE | BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let readback = device.create_buffer(&BufferDescriptor {
label: Some("readback"),
size: 16,
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let frame = || {
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
{
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
label: None,
color_attachments: &[Some(RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(GpuColor::BLACK),
store: StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: Some(RenderPassTimestampWrites {
query_set: &queries,
beginning_of_pass_write_index: Some(0),
end_of_pass_write_index: Some(1),
}),
occlusion_query_set: None,
multiview_mask: None,
});
node.draw(pass);
}
encoder.resolve_query_set(&queries, 0..2, &resolved, 0);
encoder.copy_buffer_to_buffer(&resolved, 0, &readback, 0, 16);
queue.submit(Some(encoder.finish()));
let slice = readback.slice(..);
slice.map_async(MapMode::Read, |_| {});
let _ = device.poll(PollType::Wait {
submission_index: None,
timeout: None,
});
let ns = {
let view = slice.get_mapped_range().expect("timestamps did not map");
let stamps: [u64; 2] = [
u64::from_le_bytes(view[..8].try_into().unwrap()),
u64::from_le_bytes(view[8..16].try_into().unwrap()),
];
(stamps[1].saturating_sub(stamps[0])) as f64 * period as f64
};
readback.unmap();
ns
};
frame();
let mut best = f64::MAX;
for _ in 0..BATCHES {
let mut total = 0.0;
for _ in 0..FRAMES {
total += frame();
}
best = best.min(total / FRAMES as f64);
}
best
}
#[test]
#[ignore = "measurement, not a check"]
fn chain_cost_by_depth() {
let Some((device, queue, period)) = gpu() else {
println!("no gpu with timestamps; nothing measured");
return;
};
println!("{INSTANCES} instances, {SIZE}x{SIZE}, best of {BATCHES} batches");
let mut base = None;
for depth in [1, 2, 4, 8, 16, 32, 64] {
let ns = pass_cost(&device, &queue, period, depth);
let base = *base.get_or_insert(ns);
println!(
"depth {depth:>3}: {:>9.1} us {:+6.1}% against depth 1",
ns / 1000.0,
(ns - base) / base * 100.0
);
}
}
+208
View File
@@ -0,0 +1,208 @@
//! What one frame of `UiRenderNode::draw` costs on the CPU, against the number
//! of layers it walks. Recording only: the pass is built and dropped without
//! being submitted, so this is the loop's cost and not the GPU's.
//!
//! cargo test --release --test draw_cost -- --ignored --nocapture
//!
//! Wall time is the wrong number to read for anything under a few percent --
//! it varied by 2x between runs of one unchanged binary where instructions
//! retired varied by 0.1%. Count those instead:
//!
//! perf stat -e instructions:u target/release/.../draw_cost-* --ignored
//!
//! That is how `PrimitiveRender` was measured against a match in the renderer:
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording
//! one.
//!
//! The instance is leaked deliberately. A Vulkan loader may unload the driver
//! when the last one drops, which can fault as a thread that used it exits --
//! and every test runs on a spawned thread.
use std::time::Instant;
use iris::prelude::*;
use iris_core::{
GlyphPrimitive, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, TextureHandle,
TexturePrimitive, UiData, UiRegion, UiRenderNode, UiRenderState,
};
use wgpu::{Color as GpuColor, *};
const SIZE: u32 = 1024;
const FRAMES: u32 = 200;
/// Reported as the best of this many batches, since the mean moves by more
/// than the thing being measured.
const BATCHES: u32 = 8;
fn gpu() -> Option<(Device, Queue)> {
// Probed rather than assumed: there may be no Vulkan adapter, and GL is
// what is left when there is not.
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
}),
};
// Leaked rather than dropped: see the note at the top of the file.
let instance: &'static Instance = Box::leak(Box::new(instance));
let adapter =
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
println!("adapter: {:?}", adapter.get_info());
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
}
fn config(format: TextureFormat) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: SIZE,
height: SIZE,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: vec![],
}
}
/// Every layer draws all three primitives, so the renderer takes a different
/// path for each list it walks -- which is the case a single-primitive layer
/// would never exercise. Images are bound per instance, so there are few.
fn fill(
ui: &mut UiData,
render: &mut UiRenderState,
layers: usize,
per_layer: usize,
) -> Vec<TextureHandle> {
let rect = ui.primitives.kind::<RectPrimitive>();
let glyph = ui.primitives.kind::<GlyphPrimitive>();
let texture = ui.primitives.kind::<TexturePrimitive>();
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
let handles: Vec<_> = (0..4)
.map(|_| ui.textures.add(image::RgbaImage::new(4, 4)))
.collect();
let mut layer = 0;
for _ in 0..layers {
for _ in 0..per_layer {
render.layers.write(
layer,
PrimitiveInst {
kind: rect,
id,
primitive: RectPrimitive::color(UiColor::WHITE),
region: UiRegion::FULL,
mask_idx: MaskIdx::NONE,
move_idx: MoveIdx::NONE,
},
);
render.layers.write(
layer,
PrimitiveInst {
kind: glyph,
id,
primitive: GlyphPrimitive {
uv_min: vec2(0.0, 0.0),
uv_max: vec2(1.0, 1.0),
layer: 0,
color: UiColor::WHITE,
flags: 0,
},
region: UiRegion::FULL,
mask_idx: MaskIdx::NONE,
move_idx: MoveIdx::NONE,
},
);
}
for h in &handles[..2] {
render.layers.write(
layer,
PrimitiveInst {
kind: texture,
id,
primitive: TexturePrimitive::from(h),
region: UiRegion::FULL,
mask_idx: MaskIdx::NONE,
move_idx: MoveIdx::NONE,
},
);
}
layer = render.layers.next(layer);
}
handles
}
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 {
let format = TextureFormat::Bgra8Unorm;
let mut node = UiRenderNode::new(device, &config(format));
let mut ui = UiData::default();
let mut render = UiRenderState::new();
let _handles = fill(&mut ui, &mut render, layers, per_layer);
node.update(device, queue, &mut ui, &mut render);
let target = device.create_texture(&TextureDescriptor {
label: Some("draw cost"),
size: Extent3d {
width: SIZE,
height: SIZE,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format,
usage: TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = target.create_view(&TextureViewDescriptor::default());
let record = |frames: u32| {
let start = Instant::now();
for _ in 0..frames {
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
{
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
color_attachments: &[Some(RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(GpuColor::BLACK),
store: StoreOp::Store,
},
depth_slice: None,
})],
..Default::default()
});
node.draw(pass);
}
drop(encoder.finish());
}
start.elapsed().as_secs_f64() / frames as f64
};
record(FRAMES / 4);
(0..BATCHES)
.map(|_| record(FRAMES))
.fold(f64::MAX, f64::min)
}
#[test]
#[ignore = "measurement, not a check"]
fn draw_cost_by_layer_count() {
let Some((device, queue)) = gpu() else {
panic!("no wgpu device; see the this-machine-graphics notes");
};
println!(
"layers, each 8 rects + 8 glyphs + 2 images: us/frame (us per layer), best of {BATCHES}"
);
let base = frame_cost(&device, &queue, 1, 8) * 1e6;
for layers in [8, 64, 256, 1024] {
let per_frame = frame_cost(&device, &queue, layers, 8) * 1e6;
// Net of the empty pass, which is the same in any version of this.
println!(
"{layers:>5}: {per_frame:8.1} us ({:.3} us)",
(per_frame - base).max(0.0) / layers as f64
);
}
}
+128
View File
@@ -0,0 +1,128 @@
//! Where a frame puts things, with no window to put them in.
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
/// A fixed 100 wide, and the rest of the 400 to its neighbour.
fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) {
let left = rect(Color::RED).width(100).add(&mut h.rsc);
let right = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
(left.id(), right.id())
}
#[test]
fn a_span_gives_each_child_the_width_it_asked_for() {
let mut h = Harness::new((400, 200));
let (left, right) = two_rects(&mut h);
assert_corners!(h, left, (0, 0), (100, 200));
assert_corners!(h, right, (100, 0), (400, 200));
}
#[test]
fn resizing_relays_out_against_the_new_output() {
let mut h = Harness::new((400, 200));
let (left, right) = two_rects(&mut h);
h.resize((800, 100));
assert!(h.needs_redraw());
h.frame();
assert_corners!(h, left, (0, 0), (100, 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));
}
#[test]
fn a_moved_subtree_takes_its_children_with_it() {
let mut h = Harness::new((400, 400));
let first = rect(Color::RED).height(40).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = inner.pad(10).height(40).add(&mut h.rsc);
h.set_root((first, row).span(Dir::DOWN));
assert_corners!(h, inner, (10, 50), (390, 70));
h.rsc[first].y = Some(Len::abs(80));
h.frame();
// The row is the same shape somewhere else, so one slot moved it and
// `inner`'s own region was never rewritten.
assert_corners!(h, inner, (10, 90), (390, 110));
}
+85
View File
@@ -0,0 +1,85 @@
//! Which widget an input reaches.
use std::{cell::RefCell, rc::Rc};
use iris::harness::{Harness, TouchScript};
use iris::prelude::*;
#[test]
fn a_press_reaches_only_the_widget_under_the_cursor() {
let mut h = Harness::new((400, 200));
let clicks = Rc::new(RefCell::new(Vec::new()));
let (on_left, on_right) = (clicks.clone(), clicks.clone());
let left = rect(Color::RED)
.width(100)
.on(CursorSense::click(), move |_, _| {
on_left.borrow_mut().push("left")
})
.add(&mut h.rsc);
let right = rect(Color::BLUE)
.on(CursorSense::click(), move |_, _| {
on_right.borrow_mut().push("right")
})
.add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
h.click((50, 100));
assert_eq!(*clicks.borrow(), ["left"]);
h.click((300, 100));
assert_eq!(*clicks.borrow(), ["left", "right"]);
}
#[test]
fn hover_ends_when_the_cursor_leaves_the_window() {
let mut h = Harness::new((400, 200));
let hovered = Rc::new(RefCell::new(0));
let ended = Rc::new(RefCell::new(0));
let (h_count, e_count) = (hovered.clone(), ended.clone());
let widget = rect(Color::RED)
.on(CursorSense::HoverStart, move |_, _| {
*h_count.borrow_mut() += 1
})
.on(CursorSense::HoverEnd, move |_, _| {
*e_count.borrow_mut() += 1
})
.add(&mut h.rsc);
h.set_root(widget);
h.move_to((200, 100));
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
// A second sample inside the same widget is not a second hover.
h.move_to((210, 100));
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
h.leave();
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1));
}
#[test]
fn a_recorded_gesture_presses_where_it_says() {
let mut h = Harness::new((400, 200));
let clicks = Rc::new(RefCell::new(Vec::new()));
let (on_left, on_right) = (clicks.clone(), clicks.clone());
let left = rect(Color::RED)
.width(100)
.on(CursorSense::click(), move |_, _| {
on_left.borrow_mut().push("left")
})
.add(&mut h.rsc);
let right = rect(Color::BLUE)
.on(CursorSense::click(), move |_, _| {
on_right.borrow_mut().push("right")
})
.add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
let script = TouchScript::parse("0 down 300 100\n80 up 300 100").unwrap();
h.replay(&script);
assert_eq!(*clicks.borrow(), ["right"]);
}
+225
View File
@@ -0,0 +1,225 @@
//! Input across layers: what stops at a layer, what passes through it, and
//! where hovering stops.
use std::{cell::RefCell, rc::Rc};
use iris::harness::Harness;
use iris::prelude::*;
const WINDOW: f32 = 100.0;
/// Every sense that has fired on one widget since it was last read.
#[derive(Default, Clone)]
struct Fired(Rc<RefCell<Vec<CursorSense>>>);
impl Fired {
fn take(&self) -> Vec<CursorSense> {
std::mem::take(&mut self.0.borrow_mut())
}
}
/// A widget filling whatever it is given, recording the senses it is sent.
fn listener(h: &mut Harness, senses: impl Into<CursorSenses>) -> (WeakWidget<Rect>, Fired) {
let fired = Fired::default();
let record = fired.clone();
let id = rect(Color::WHITE)
.on(senses.into(), move |ctx, _| {
record.0.borrow_mut().push(ctx.data.sense)
})
.add(&mut h.rsc);
(id, fired)
}
/// A widget with no senses of its own, to leave a gap beside one that has.
fn blank(h: &mut Harness) -> WeakWidget<Rect> {
rect(Color::WHITE).add(&mut h.rsc)
}
fn harness() -> Harness {
Harness::new((WINDOW, WINDOW))
}
#[test]
fn hover_stops_at_the_topmost_widget() {
let mut h = harness();
let (bottom, bottom_hover) = listener(&mut h, CursorSense::HoverStart);
let (middle, middle_hover) = listener(&mut h, CursorSense::HoverStart);
let (top, top_hover) = listener(&mut h, CursorSense::HoverStart);
h.set_root((bottom, middle, top).stack());
h.move_to((50, 50));
assert_eq!(top_hover.take(), [CursorSense::HoverStart]);
assert_eq!(
middle_hover.take(),
[],
"hover is not shared with a layer below"
);
assert_eq!(bottom_hover.take(), []);
}
#[test]
fn a_scroll_passes_through_every_widget_that_does_not_want_it() {
let mut h = harness();
let (list, scrolled) = listener(&mut h, CursorSense::Scroll);
let (button, clicked) = listener(&mut h, CursorSense::click());
let (overlay, overlay_clicked) = listener(&mut h, CursorSense::click());
h.set_root((list, button, overlay).stack());
h.move_to((50, 50));
h.scroll((0, 10));
assert_eq!(
scrolled.take(),
[CursorSense::Scroll],
"two layers of click-only widgets do not stop a scroll"
);
assert_eq!(clicked.take(), []);
assert_eq!(overlay_clicked.take(), []);
}
#[test]
fn hovering_a_button_above_does_not_stop_a_later_scroll() {
let mut h = harness();
let (list, scrolled) = listener(&mut h, CursorSense::Scroll);
let (button, _clicked) = listener(&mut h, CursorSense::click());
h.set_root((list, button).stack());
// The hover arrives in its own frame, as a window delivers it.
h.move_to((50, 50));
assert_eq!(scrolled.take(), []);
h.scroll((0, 10));
assert_eq!(
scrolled.take(),
[CursorSense::Scroll],
"a hover already resting on the button must not consume the wheel"
);
}
#[test]
fn only_the_topmost_listener_takes_a_press() {
let mut h = harness();
let (below, below_clicked) = listener(&mut h, CursorSense::click());
let (above, above_clicked) = listener(&mut h, CursorSense::click());
h.set_root((below, above).stack());
h.click((50, 50));
assert_eq!(above_clicked.take(), [CursorSense::click()]);
assert_eq!(below_clicked.take(), [], "one press goes to one widget");
}
#[test]
fn a_press_beside_the_button_reaches_the_layer_below() {
let mut h = harness();
let (list, list_clicked) = listener(&mut h, CursorSense::click());
// The row above the list covers it, but only its left half is the button.
let (button, button_clicked) = listener(&mut h, CursorSense::click());
let row = (button, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((list, row).stack());
h.click((20, 50));
assert_eq!(button_clicked.take(), [CursorSense::click()]);
assert_eq!(list_clicked.take(), []);
h.click((80, 50));
assert_eq!(button_clicked.take(), [], "the cursor is not on the button");
assert_eq!(
list_clicked.take(),
[CursorSense::click()],
"a press beside the button belongs to what is under it"
);
}
#[test]
fn leaving_a_widget_still_ends_its_hover() {
let mut h = harness();
let (widget, hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
h.set_root(widget);
h.move_to((50, 50));
assert_eq!(hover.take(), [CursorSense::HoverStart]);
h.leave();
assert_eq!(hover.take(), [CursorSense::HoverEnd]);
}
#[test]
fn leaving_a_widget_does_not_block_the_layer_below() {
let mut h = harness();
let (below, below_hover) = listener(&mut h, CursorSense::HoverStart);
// Only the left half of the layer above is a widget, so the cursor can
// leave it without leaving the one underneath.
let (above, above_hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
let row = (above, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((below, row).stack());
h.move_to((20, 50));
assert_eq!(above_hover.take(), [CursorSense::HoverStart]);
assert_eq!(below_hover.take(), [], "the layer above is over it");
h.move_to((80, 50));
assert_eq!(above_hover.take(), [CursorSense::HoverEnd]);
assert_eq!(
below_hover.take(),
[CursorSense::HoverStart],
"ending a hover above must not stop the hover below"
);
}
#[test]
fn covering_a_widget_ends_its_hover() {
let mut h = harness();
let (below, below_hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
let (above, above_hover) = listener(&mut h, CursorSense::HoverStart);
let row = (above, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((below, row).stack());
h.move_to((80, 50));
assert_eq!(below_hover.take(), [CursorSense::HoverStart]);
h.move_to((20, 50));
assert_eq!(above_hover.take(), [CursorSense::HoverStart]);
assert_eq!(
below_hover.take(),
[CursorSense::HoverEnd],
"a widget covered by one that took the input is no longer hovered"
);
h.move_to((80, 50));
assert_eq!(
below_hover.take(),
[CursorSense::HoverStart],
"uncovering it hovers it again"
);
}
#[test]
fn hover_starts_and_ends_once_each() {
let mut h = harness();
// Only the left half is the widget, so the cursor can leave it without
// leaving the window.
let (widget, hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
let row = (widget, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(row);
h.move_to((20, 50));
assert_eq!(hover.take(), [CursorSense::HoverStart]);
h.move_to((30, 50));
assert_eq!(hover.take(), [], "staying inside is not a second start");
h.move_to((80, 50));
assert_eq!(hover.take(), [CursorSense::HoverEnd]);
h.move_to((90, 50));
assert_eq!(hover.take(), [], "an ended hover does not end again");
h.move_to((20, 50));
assert_eq!(
hover.take(),
[CursorSense::HoverStart],
"re-entering starts it"
);
}
+43
View File
@@ -0,0 +1,43 @@
//! What re-placing a subtree costs per frame, as a load for a counter rather
//! than a check. A span of 200 fixed-height rows, five primitives each, with
//! the row above them changing height every frame, so every row below is
//! offered a box the same shape somewhere else.
//!
//! cargo test --release --test replace_cost -- --ignored
//! perf stat -e instructions:u target/release/.../replace_cost-* --ignored
//!
//! Wall time is the wrong number here; see `draw_cost.rs`. Measured on
//! 2026-09-14 at 1.98M instructions per frame, against 2.38M for rewriting
//! each row's regions instead and 7.13M for redrawing them.
use iris::harness::Harness;
use iris::prelude::*;
const ROWS: usize = 200;
const FRAMES: usize = 200;
#[test]
#[ignore = "measurement, not a check"]
fn replacing_rows_every_frame() {
let mut h = Harness::new((1920, 1200));
let first = rect(Color::RED).height(40).add(&mut h.rsc);
let mut span = Span::empty(Dir::DOWN);
span.push(first.add_strong(&mut h.rsc));
for i in 0..ROWS {
let row = (
rect(Color::BLUE.darker(i as f32 / (ROWS * 2) as f32)),
rect(Color::GREEN).pad(2),
wtext("row").size(16).pad(2),
)
.span(Dir::RIGHT)
.pad(4)
.height(40)
.add(&mut h.rsc);
span.push(row.add_strong(&mut h.rsc));
}
h.set_root(span);
for i in 0..FRAMES {
h.rsc[first].y = Some(Len::abs(40.0 + (i % 2) as f32));
h.frame();
}
}
+298
View File
@@ -0,0 +1,298 @@
//! 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));
}
/// Claims its drawing may be stretched, and has a child so that the stretch
/// has to reach one. No shipped container claims `Scale` -- `Rect`, `Image`
/// and `()` are all childless -- so nothing else walks a subtree to remap it.
struct Stretchy {
inner: StrongWidget,
draws: Rc<Cell<usize>>,
}
impl Widget for Stretchy {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.widget(&self.inner).size()
}
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
}
#[test]
fn stretching_a_subtree_remaps_the_children_in_it() {
let mut h = Harness::new((400, 400));
let first = rect(Color::RED).height(40).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
let outer = Stretchy {
inner: inner.add_strong(&mut h.rsc),
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root((first, outer).span(Dir::DOWN));
let settled = draws.get();
assert_corners!(h, inner, (0, 40), (400, 400));
h.rsc[first].y = Some(Len::abs(80));
h.frame();
assert_eq!(
draws.get(),
settled,
"its drawing is stretched, not redrawn"
);
assert_corners!(h, outer, (0, 80), (400, 400));
assert_corners!(h, inner, (0, 80), (400, 400));
}
+26
View File
@@ -0,0 +1,26 @@
//! Scrolling moves content and stops at its ends.
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
#[test]
fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
let mut h = Harness::new((400, 200));
// Twice the window's height, so there is 200 to scroll.
let top = rect(Color::RED).height(200).add(&mut h.rsc);
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
h.set_root((top, bottom).span(Dir::DOWN).scrollable());
h.move_to((200, 100));
// `Scroll` starts snapped to the end.
assert_corners!(h, top, (0, -200), (400, 0));
// The handler scales a wheel line by 50.
h.scroll((0, 1));
h.frame();
assert_corners!(h, top, (0, -150), (400, 50));
h.scroll((0, 10));
h.frame();
assert_corners!(h, top, (0, 0), (400, 200));
}
+46
View File
@@ -0,0 +1,46 @@
//! What a drawing can be taken out of, and what it cannot.
use iris::core::{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_length_cannot_be_stretched_out_of() {
assert!(!fixed(0.0, 164.0).stretchable());
assert!(!fixed(0.5, 164.0).stretchable());
assert!(UiRegion::FULL.stretchable());
}
#[test]
fn a_stretch_keeps_each_part_at_its_fraction() {
let to = fixed(0.0, 98.0);
// A part filling the window fills what replaced it.
assert_eq!(UiRegion::FULL.stretch(&UiRegion::FULL, &to), to);
// And the middle half of it stays the middle half.
let half = UiRegion::new(
UiSpan::FULL,
UiSpan::new(UiScalar::rel(0.25), UiScalar::rel(0.75)),
);
assert_eq!(
half.stretch(&UiRegion::FULL, &to),
UiRegion::new(
UiSpan::FULL,
UiSpan::new(
UiScalar {
rel: 0.0,
abs: 24.5
},
UiScalar {
rel: 0.0,
abs: 73.5
}
)
)
);
}
+23
View File
@@ -0,0 +1,23 @@
//! What a background task can change, and how it gets back to the ui.
use std::time::Duration;
use iris::harness::Harness;
use iris::prelude::*;
#[test]
fn a_task_update_reaches_the_tree() {
let mut h = Harness::new((400, 200));
let widget = rect(Color::RED).add(&mut h.rsc);
h.set_root(widget.task_on(CursorSense::click(), async move |mut ctx| {
ctx.update(move |_, rsc| widget(rsc).color = Color::BLUE);
}));
h.click((200, 100));
assert!(
h.await_update(Duration::from_secs(5)),
"the task sent no update"
);
assert_eq!(h.rsc[widget].color, Color::BLUE);
}