Compare commits

..
Author SHA1 Message Date
irisandClaude Opus 5 91b71b97dc Drop the bind group ordering comment
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 18:55:45 -04:00
irisandClaude Opus 5 b9c4856e3f Let each primitive record its own draws
`draw` had three branches, one per shader, which is the dynamic dispatch this
was asking for. A primitive now brings a `PrimitiveRender`: it states the
layout its shader reads, uploads whatever it owns, and records its own draws.
`GlyphRender` owns the atlas and binds it once for a list; `ImageRender` owns
the images and binds one per instance; the default owns nothing and draws them
all in one call. The renderer sets the pipeline, the shared group, the list's
data and the vertex buffer, and knows nothing else about what it is drawing.

Measured before committing to it, since dispatch per list is the cost. Wall
time on this machine swings 2x between runs of one binary, so the comparison
is instructions retired, which is stable to 0.1%: at 256 layers drawing 8
rects, 8 glyphs and 2 images each, 7.4074e9 against 7.4145e9, and at 1024
layers 27.467e9 against 27.498e9. Both are 0.1%, which is 6 instructions per
list drawn -- one indirect call. Recording a list into the pass costs wgpu
about 5,400.

`tests/draw_cost.rs` is that measurement, kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 18:30:20 -04:00
irisandClaude Opus 5 79dcc156c9 Give the atlas to the primitive that samples it, not to every shader
`Primitive::SAMPLES` says what a primitive samples and how often it has to be
bound: `Atlas` once for a list, `Image` for one instance alone. Group 2 is
that, whichever it is, so a rect's pipeline has no texture and no sampler in
its layout and the prelude hands out neither.

The two differ only in the view dimension and in what binds them, so they
share `sampled_layout` and `sampled_group`; `GpuPages` owns its bind group
again and rebuilds it when the array grows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 17:43:55 -04:00
irisandClaude Opus 5 c0fcc0345c State every binding size, so nothing is left for wgpu to check per draw
The window uniform and the mask array were still `None`, which is what puts a
binding on wgpu-core's late-sized list: `check_late_buffer_bindings` runs from
`is_ready` on every draw and compares each such binding's bound size against
the naga-derived minimum for the shader global. Stating the size filters the
binding out of that list, and moves the same comparison to bind group and
pipeline creation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 17:26:59 -04:00
irisandClaude Opus 5 444a2cd138 Zip the layer's lists against their pipelines, and stop explaining wgpu wrongly
The update loop zips all three and asserts up front that no list is left
without a pipeline, instead of indexing the pipelines to get that guarantee.

The comment on the data layout claimed a `None` minimum takes its value from
the first pipeline built against the layout. That is not documented and does
not reproduce -- one shared layout with `None` renders the tabs example
correctly today. What is documented is that a stated size is checked when the
bind group and pipeline are created, and `None` is checked on every draw, so
that is what the comment says now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 17:23:35 -04:00
irisandClaude Opus 5 23fb71ee56 Draw a texture handle as the primitive it is
`Painter::primitive` takes `impl PrimitiveLike`: a primitive, or something
that yields one and does whatever else drawing it needs. A `&TextureHandle`
yields a `TexturePrimitive` and retains its share on the way through, so
`texture`, `texture_within` and `texture_at` are gone and an image is drawn
like anything else.

I said last round that the blanket impl would collide with the one for
`&TextureHandle` under coherence. It does not: `Primitive` is ours, so no
crate can add the impl that would overlap, and rustc accepts both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 16:59:08 -04:00
irisandClaude Opus 5 01a9b8633d Register a primitive only when it is drawn, and keep the prelude shared
Nothing seeds the registry any more, so a kind's id is decided by the first
draw and no order within a layer can be relied on even by accident. A ui that
draws no images now pays for no image pipeline, and a layer's list vector only
reaches the highest kind that layer draws.

The atlas and the sampler are still bound for every draw, but are declared by
the two shaders that read them rather than by the prelude, which is now only
what every primitive uses.

`TexturePrimitive` gets a `From<&TextureHandle>`; `Painter::texture_at` stays
because the share of the handle is what keeps the slot from being freed while
it is drawn, which a `Pod` primitive cannot hold.

Checked on the headless rig that the layers carry the ordering rather than the
ids: with a bare text drawn first, so glyph registers before rect, a stacked
label still draws over its background. Also re-ran an image alone in a layer,
now the only primitive a ui registers, and a four-layer atlas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 16:52:32 -04:00
irisandClaude Opus 5 29d390da52 Take a primitive's kind from its type, and keep images out of the rest
A `Primitive` now carries its own WGSL, and `PrimitiveRegistry` keys ids by
`TypeId`, so `Painter::primitive` takes only the value and the `RECT`,
`GLYPH` and `TEXTURE` constants are gone. Registering is what a first draw
does; the built-ins are seeded up front so first-draw order cannot decide
anything about them.

What a primitive samples is no longer something every registration states.
The glyph atlas and the one sampler moved into the shared group, which is
where a mask texture would go too, so a rect's pipeline has no texture in
its layout at all. Only a primitive whose type sets `TEXTURE` gets an image
group, and that is also what records the slot at write time -- so nothing
reads a `u32` back out of the instance payload.

Verified on the headless rig: the tabs example, two images added at runtime,
an image alone in a layer, and text spanning a four-layer atlas after the
array grew twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 16:14:53 -04:00
iris 7b318e3271 Make a texture a registered primitive like any other
`write_texture` differed from `write` by one argument, which is what the
generic parameter was already for, so textures register as a primitive
with a `TexturePrimitive` holding the slot. `write_texture`,
`InstanceKind` and the layer's separate texture list are gone;
`DrawLayers` is back to `write` and `free`, with the kind carried in
`PrimitiveInst` as it carries everything else.

What differs between a texture and a rect is only what it samples, so
that is what registration says: `PrimitiveTexture::Atlas` binds the
shared atlas once for the layer, `PerInstance` binds the texture its own
data names and draws one instance at a time. One loop over a layer's
lists, one match on that.

`Pod` is back to being a supertrait of `Primitive` rather than the bound
itself. The guarantee is that a `PrimitiveKind<P>` is only minted by
`register::<P>` and `write` takes the kind and the value together, so a
primitive always has a list of its own to go in and the write does not
check anything: a list takes its stride from the type it was made for
instead of inferring it from the first write and asserting on the rest.

Also from reviewing this: a layer's lists and their buffers are created
only when that layer draws that primitive, so a primitive nobody uses no
longer costs two buffers in every layer -- which matters more now the set
is open-ended. `ListBuffers::update` takes the two things it uses rather
than the whole pipeline.

Verified again over all five cases: an image alone in a layer, three
images added and one deleted, the masked text-edit tab, the text-layout
tab and the default tab.
2026-09-13 14:46:08 -04:00
iris 89491a5949 Build pipelines before the layers that need them
Reviewing the previous fix, which was itself unreviewed. `update` gave
each list the bind group layout of the pipeline that draws it by zipping
the layer's lists against `self.primitives`, but built those pipelines
afterwards -- so on any pass where one did not exist yet the zip yielded
nothing, and those lists kept no bind group and drew nothing. Measured
on startup: four layers had content while `self.primitives` was still
empty. It only looked right because those layers were marked dirty again
on a later frame and rebuilt then.

`build_pipelines` now runs before the layers, and the pairing is indexed
rather than zipped, so a primitive drawn before it was registered panics
instead of silently leaving its list unbuilt.
2026-09-13 14:32:02 -04:00
iris a08f61a80c Fix what reviewing the primitive rework turned up
Three defects, two of them invisible to every case I had run.

An image alone in a layer failed validation. The texture pipeline never
bound group 1, and every earlier case happened to have a rect in the
same layer, which left one bound from the primitive draw -- so the bug
was hidden by the tests passing.

A `min_binding_size: None` binding takes its minimum from the first
pipeline built against that bind group layout, so one shared group 1
layout held every primitive to the largest. Rect and glyph coexisted
only because glyph is the bigger of the two; the texture slots, at four
bytes, did not. Each primitive now gets its own layout with its entry
size stated, which is also why `PrimitiveRegistry` records the stride.
Because the pipeline layouts now differ per primitive, a pipeline change
drops the bound groups, so group 2 moves after `set_pipeline`.

An empty list still built a bind group over a buffer too small for one
entry, which the stated minimum would now reject. It gets no bind group,
and nothing draws it.

Also from the read-through: `UiRenderNode` kept a `Device` beside the
one `update` is handed, `PrimitiveRegistry::default` registered inside
an `assert_eq!`, and `mask_idx` was an unqualified integer varying where
`idx` beside it was `flat`.
2026-09-13 14:27:09 -04:00
iris 4d9839f380 Draw each primitive with its own pipeline, registered rather than declared
The `primitives!` macro, `PrimitiveData`, `PrimitiveVec`,
`PrimitiveBuffers`, the `Primitive` trait and the shader's dispatch
switch are gone. A primitive is now a registration: its WGSL and,
implicitly, the size of the entry that WGSL reads. Everything else --
its instance list, its free list, its buffers, its bind group and its
pipeline -- follows from that, so adding one is a `register` call and a
shader file, with nothing per-type to remember and no cross-type
dispatch to extend.

Nothing dispatches dynamically. Push, free, renumber, upload and draw
are identical for every primitive; what differs is the entry size and
the pipeline, which are data. So `InstanceList` carries a runtime
stride and its instances' data as bytes, and one concrete type serves
every primitive and the textures. Measured against a typed list it
costs 0.2ns per write, where a trait object costs 1.6ns.

Because each type has its own list, an instance's index is also its
data index: `@builtin(instance_index)` replaces the `idx` field, the
`binding` field goes with the switch, and `PrimitiveInstance` drops from
28 bytes to 20. `PrimitiveVec`'s free list merges into the instance
list's, so an instance and its data are freed by one `swap_remove`
rather than two arenas kept in step.

`shader.wgsl` becomes `shader/prelude.wgsl` plus one file per primitive.
The prelude carries the window, masks, sampled texture, vertex shader
and `masked()`, and is compiled ahead of each primitive's own source --
which is also what a caller's own primitive would be. Masks move back
into group 0 beside the window uniform, since every pipeline shares one
layout.

Within a layer, types now draw in registration order: a rect under a
glyph under a texture. Order within a layer was never meaningful --
freeing an instance swaps another into its place -- so this replaces an
accident with a defined order, and backgrounds land under their content.

Verified with a headless run per case: text over its own rect and an
image over its own rect in one layer, a masked stack clipping, the
text-layout tab, and adding three images and deleting one.
2026-09-13 14:07:37 -04:00
iris b3d3da5dab Draw textures separately, and keep them out of Primitives
Interleaving textures with primitives was solving a problem that does not
exist: within a layer, order is already undefined because freeing an
instance swap-removes it, and layering is what layers are for. So the run
batching is gone.

A layer is now `LayerDraws`: a `Primitives` and a texture `InstanceList`
side by side, with `updated` covering both. `Primitives` holds only
primitives again -- its instance list plus the group-1 data those
instances read -- and `InstanceList` is the shared push/free/apply_free
the two lists have in common rather than a second copy of it.
`PrimitiveHandle` names which list with `InstanceKind`, and
`PrimitiveChange` carries the same, since the two index independently.
The handle no longer carries a group-1 index at all: a primitive
instance already records where its entry is.

The renderer gives each layer a second instance buffer and draws its
textures one at a time after the instanced draw, each binding its own
group 2.

Review fixes alongside: `GlyphAtlas::allocate` returns the `PageUpload`
it reserved instead of a bare tuple; a page or image region uploads
through a new `write_region`, which passes the row stride to
`write_texture` rather than copying the rectangle out first.

Verified by replaying taps into the `tabs` example: three images added
and one deleted leaves two drawn with two live texture slots, and the
masked text-edit tab still clips, with the images freed on tab switch.
2026-09-13 13:32:05 -04:00
iris f5864da3c4 Keep the glyph uvs as vectors on both sides
Review response: `GlyphInfo` goes back to two `vec2<f32>`, which is what
the uvs are, and `GlyphPrimitive` takes `#[repr(C, align(8))]` to match.
That leaves four padding bytes, which the `primitives!` macro's
`unsafe impl Pod` accepts. Measured at 32 bytes, align 8, the same as
WGSL's layout for the struct.
2026-09-13 12:55:56 -04:00
iris 0106257be0 Separate atlas pages from textures, and draw both through one instance list
Rework of the review on #11. Pages and standalone images were one
`Textures` manager separated by a `TextureKind` tag, and images were a
second instance list beside `Primitives::instances`. The tag forced
`image_index()`/`layer()` to panic on the wrong kind of handle, and the
second list forced an `is_image` branch through `free`, `region_mut`,
`apply_free` and `PrimitiveChange`.

Pages are now their own thing. `GlyphAtlas` owns its page images
outright and hands the renderer dirty rectangles; `GpuPages` owns the
array texture they upload to. `Textures` is standalone images only, so
`TextureHandle` has one kind, `slot()` cannot be wrong, and nothing
needs a free list that skips pages. `GlyphAtlas::insert` no longer takes
a `Textures`, which drops that parameter from `TextData::render` and
`SizeCtx` too.

Images go back through the one instance list. A texture instance is an
ordinary `PrimitiveInstance` whose `idx` names a texture rather than a
group-1 entry, which `PrimitiveHandle::data_idx: Option` records.
`RenderLayer::plan_draws` batches the layer's instances into runs
sharing a bind group, so a ui with no images still plans a single draw,
and an image draws in instance order rather than on top of its layer.

Group 2 is now one `texture_2d_array` and a sampler, bound per run: the
atlas for rects and glyphs, or one image viewed as an array of one. That
removes the second texture binding and the 1x1 null view that had to
fill it. Masks move to group 3, so resizing that buffer no longer stales
every texture bind group, and `GpuTextures` no longer reports whether
the caller must rebuild one.

`GlyphPrimitive` drops its manual pad: the WGSL struct now declares the
uvs as scalars, which matches the Rust layout exactly. `#[repr(C,
align(8))]` would have left real padding bytes, which `bytemuck::Pod`
forbids.

Verified with a headless run of the `tabs` example and of a scratch
example mixing images, rects, glyphs and a mask in one layer; 52 glyphs
at size 300 grew the atlas array from 1 to 4 layers with every earlier
page still sampling correctly.
2026-09-13 12:47:41 -04:00
iris bafaa1db6d Draw the glyph atlas as an array texture and images with their own bind groups
The renderer bound every texture through one
`binding_array<texture_2d<f32>>` indexed per primitive. That needs
`VK_EXT_descriptor_indexing`, which a real share of Android GPUs do not
have, so the shape did not run there at all.

Split the two things being bound, since they want opposite treatment:

- Glyph atlas pages become layers of one `texture_2d_array`. A glyph
  primitive carries a layer rather than a view/sampler index pair, and a
  layer index is an ordinary sampling operand -- no extension. Growing the
  atlas recreates the array with headroom and copies the old layers across
  GPU-side.
- 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` at all: the bind group has already picked the texture.

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

Verified on this machine's real GPU (Venus onto an RX 7900 XT, confirmed
by the loaded ICD rather than assumed): the `tabs` example renders
byte-identical screenshots before and after, both for a text-and-rect tab
and for one holding a standalone image.
2026-09-13 03:58:12 -04:00
29 changed files with 409 additions and 1014 deletions

No files matched your search

Generated
+229 -203
View File
@@ -234,18 +234,18 @@ dependencies = [
[[package]]
name = "bit-set"
version = "0.10.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.9.1"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bit_field"
@@ -274,6 +274,12 @@ dependencies = [
"core2",
]
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "block2"
version = "0.5.1"
@@ -283,15 +289,6 @@ dependencies = [
"objc2 0.5.2",
]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2 0.6.4",
]
[[package]]
name = "built"
version = "0.8.0"
@@ -394,9 +391,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.2"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clipboard-win"
@@ -409,9 +406,9 @@ dependencies = [
[[package]]
name = "codespan-reporting"
version = "0.13.1"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
dependencies = [
"serde",
"termcolor",
@@ -453,6 +450,16 @@ dependencies = [
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -466,8 +473,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-graphics-types",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
"foreign-types",
"libc",
]
@@ -479,7 +486,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation 0.9.4",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.10.0",
"core-foundation 0.10.1",
"libc",
]
@@ -721,6 +739,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "font-types"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5"
dependencies = [
"bytemuck",
]
[[package]]
name = "font-types"
version = "0.12.4"
@@ -744,7 +771,7 @@ dependencies = [
"objc2-core-text",
"objc2-foundation 0.3.2",
"parlance",
"read-fonts",
"read-fonts 0.41.0",
"roxmltree",
"smallvec",
"windows",
@@ -779,30 +806,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "fxhash"
version = "0.2.1"
@@ -857,9 +860,9 @@ dependencies = [
[[package]]
name = "glow"
version = "0.17.0"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5"
checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08"
dependencies = [
"js-sys",
"slotmap",
@@ -890,6 +893,26 @@ dependencies = [
"windows",
]
[[package]]
name = "gpu-descriptor"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
dependencies = [
"bitflags 2.10.0",
"gpu-descriptor-types",
"hashbrown 0.15.5",
]
[[package]]
name = "gpu-descriptor-types"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91"
dependencies = [
"bitflags 2.10.0",
]
[[package]]
name = "half"
version = "2.7.1"
@@ -910,7 +933,7 @@ checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9"
dependencies = [
"bitflags 2.10.0",
"bytemuck",
"read-fonts",
"read-fonts 0.41.0",
"smallvec",
]
@@ -949,6 +972,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hexf-parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "icu_collections"
version = "2.3.0"
@@ -1079,9 +1108,9 @@ checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad"
[[package]]
name = "image"
version = "0.25.10"
version = "0.25.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
dependencies = [
"bytemuck",
"byteorder-lite",
@@ -1097,8 +1126,8 @@ dependencies = [
"rayon",
"rgb",
"tiff",
"zune-core",
"zune-jpeg",
"zune-core 0.5.0",
"zune-jpeg 0.5.8",
]
[[package]]
@@ -1218,12 +1247,11 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.105"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
@@ -1252,9 +1280,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]]
name = "libc"
version = "0.2.189"
version = "0.2.179"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
[[package]]
name = "libfuzzer-sys"
@@ -1347,6 +1375,15 @@ dependencies = [
"imgref",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]]
name = "maybe-rayon"
version = "0.1.1"
@@ -1372,6 +1409,21 @@ dependencies = [
"libc",
]
[[package]]
name = "metal"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15"
dependencies = [
"bitflags 2.10.0",
"block",
"core-graphics-types 0.2.0",
"foreign-types",
"log",
"objc",
"paste",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -1384,9 +1436,9 @@ dependencies = [
[[package]]
name = "moxcms"
version = "0.8.1"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
dependencies = [
"num-traits",
"pxfm",
@@ -1394,9 +1446,9 @@ dependencies = [
[[package]]
name = "naga"
version = "30.0.1"
version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130"
checksum = "618f667225063219ddfc61251087db8a9aec3c3f0950c916b614e403486f1135"
dependencies = [
"arrayvec",
"bit-set",
@@ -1405,11 +1457,11 @@ dependencies = [
"cfg_aliases",
"codespan-reporting",
"half",
"hashbrown 0.17.1",
"hashbrown 0.16.1",
"hexf-parse",
"indexmap",
"libm",
"log",
"naga-types",
"num-traits",
"once_cell",
"rustc-hash",
@@ -1418,18 +1470,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "naga-types"
version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49"
dependencies = [
"hashbrown 0.17.1",
"indexmap",
"rustc-hash",
"thiserror 2.0.17",
]
[[package]]
name = "ndk"
version = "0.9.0"
@@ -1554,6 +1594,15 @@ dependencies = [
"syn 2.0.113",
]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
]
[[package]]
name = "objc-sys"
version = "0.3.5"
@@ -1586,13 +1635,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [
"bitflags 2.10.0",
"block2 0.5.1",
"block2",
"libc",
"objc2 0.5.2",
"objc2-core-data",
"objc2-core-image",
"objc2-foundation 0.2.2",
"objc2-quartz-core 0.2.2",
"objc2-quartz-core",
]
[[package]]
@@ -1614,7 +1663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [
"bitflags 2.10.0",
"block2 0.5.1",
"block2",
"objc2 0.5.2",
"objc2-core-location",
"objc2-foundation 0.2.2",
@@ -1626,7 +1675,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [
"block2 0.5.1",
"block2",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
@@ -1638,7 +1687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.10.0",
"block2 0.5.1",
"block2",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
@@ -1673,10 +1722,10 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [
"block2 0.5.1",
"block2",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal 0.2.2",
"objc2-metal",
]
[[package]]
@@ -1685,7 +1734,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
dependencies = [
"block2 0.5.1",
"block2",
"objc2 0.5.2",
"objc2-contacts",
"objc2-foundation 0.2.2",
@@ -1714,7 +1763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
"bitflags 2.10.0",
"block2 0.5.1",
"block2",
"dispatch",
"libc",
"objc2 0.5.2",
@@ -1748,7 +1797,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [
"block2 0.5.1",
"block2",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
@@ -1761,23 +1810,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
"bitflags 2.10.0",
"block2 0.5.1",
"block2",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc2-metal"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
dependencies = [
"bitflags 2.10.0",
"block2 0.6.2",
"objc2 0.6.4",
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-quartz-core"
version = "0.2.2"
@@ -1785,24 +1822,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags 2.10.0",
"block2 0.5.1",
"block2",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal 0.2.2",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
dependencies = [
"bitflags 2.10.0",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.2",
"objc2-metal 0.3.2",
"objc2-metal",
]
[[package]]
@@ -1822,7 +1845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [
"bitflags 2.10.0",
"block2 0.5.1",
"block2",
"objc2 0.5.2",
"objc2-cloud-kit",
"objc2-core-data",
@@ -1830,7 +1853,7 @@ dependencies = [
"objc2-core-location",
"objc2-foundation 0.2.2",
"objc2-link-presentation",
"objc2-quartz-core 0.2.2",
"objc2-quartz-core",
"objc2-symbols",
"objc2-uniform-type-identifiers",
"objc2-user-notifications",
@@ -1842,7 +1865,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [
"block2 0.5.1",
"block2",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
@@ -1854,7 +1877,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [
"bitflags 2.10.0",
"block2 0.5.1",
"block2",
"objc2 0.5.2",
"objc2-core-location",
"objc2-foundation 0.2.2",
@@ -1948,7 +1971,7 @@ dependencies = [
"linebender_resource_handle",
"parlance",
"parley_data",
"skrifa",
"skrifa 0.44.0",
]
[[package]]
@@ -2252,9 +2275,9 @@ dependencies = [
[[package]]
name = "ravif"
version = "0.13.0"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285"
dependencies = [
"avif-serialize",
"imgref",
@@ -2271,18 +2294,6 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "raw-window-metal"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135"
dependencies = [
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"objc2-quartz-core 0.3.2",
]
[[package]]
name = "rayon"
version = "1.11.0"
@@ -2303,6 +2314,16 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "read-fonts"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eaa2941a4c05443ee3a7b26ab076a553c343ad5995230cc2b1d3e993bdc6345"
dependencies = [
"bytemuck",
"font-types 0.10.1",
]
[[package]]
name = "read-fonts"
version = "0.41.0"
@@ -2310,7 +2331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709"
dependencies = [
"bytemuck",
"font-types",
"font-types 0.12.4",
"once_cell",
]
@@ -2485,6 +2506,16 @@ dependencies = [
"quote",
]
[[package]]
name = "skrifa"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9eb0b904a04d09bd68c65d946617b8ff733009999050f3b851c32fb3cfb60e"
dependencies = [
"bytemuck",
"read-fonts 0.36.0",
]
[[package]]
name = "skrifa"
version = "0.44.0"
@@ -2492,7 +2523,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68"
dependencies = [
"bytemuck",
"read-fonts",
"read-fonts 0.41.0",
]
[[package]]
@@ -2552,9 +2583,9 @@ dependencies = [
[[package]]
name = "spirv"
version = "0.4.0+sdk-1.4.341.0"
version = "0.3.0+sdk-1.3.268.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f"
checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
dependencies = [
"bitflags 2.10.0",
]
@@ -2583,7 +2614,7 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e"
dependencies = [
"skrifa",
"skrifa 0.39.0",
"yazi",
"zeno",
]
@@ -2672,16 +2703,16 @@ dependencies = [
[[package]]
name = "tiff"
version = "0.11.3"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
dependencies = [
"fax",
"flate2",
"half",
"quick-error",
"weezl",
"zune-jpeg",
"zune-jpeg 0.4.21",
]
[[package]]
@@ -2854,9 +2885,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.128"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
@@ -2867,19 +2898,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.78"
version = "0.4.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
dependencies = [
"cfg-if",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.128"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -2887,22 +2921,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.128"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 3.0.5",
"syn 2.0.113",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.128"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
@@ -3018,9 +3052,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.105"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -3044,9 +3078,9 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "wgpu"
version = "30.0.1"
version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e"
checksum = "f9cb534d5ffd109c7d1135f34cdae29e60eab94855a625dcfe1705f8bc7ad79f"
dependencies = [
"arrayvec",
"bitflags 2.10.0",
@@ -3054,7 +3088,7 @@ dependencies = [
"cfg-if",
"cfg_aliases",
"document-features",
"hashbrown 0.17.1",
"hashbrown 0.16.1",
"js-sys",
"log",
"naga",
@@ -3074,9 +3108,9 @@ dependencies = [
[[package]]
name = "wgpu-core"
version = "30.0.1"
version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14"
checksum = "8bb4c8b5db5f00e56f1f08869d870a0dff7c8bc7ebc01091fec140b0cf0211a9"
dependencies = [
"arrayvec",
"bit-set",
@@ -3085,11 +3119,10 @@ dependencies = [
"bytemuck",
"cfg_aliases",
"document-features",
"hashbrown 0.17.1",
"hashbrown 0.16.1",
"indexmap",
"log",
"naga",
"naga-types",
"once_cell",
"parking_lot",
"portable-atomic",
@@ -3102,70 +3135,66 @@ dependencies = [
"wgpu-core-deps-emscripten",
"wgpu-core-deps-windows-linux-android",
"wgpu-hal",
"wgpu-naga-bridge",
"wgpu-types",
]
[[package]]
name = "wgpu-core-deps-apple"
version = "30.0.1"
version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68"
checksum = "87b7b696b918f337c486bf93142454080a32a37832ba8a31e4f48221890047da"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-core-deps-emscripten"
version = "30.0.1"
version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86"
checksum = "34b251c331f84feac147de3c4aa3aa45112622a95dd7ee1b74384fa0458dbd79"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-core-deps-windows-linux-android"
version = "30.0.1"
version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f"
checksum = "68ca976e72b2c9964eb243e281f6ce7f14a514e409920920dcda12ae40febaae"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-hal"
version = "30.0.1"
version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58"
checksum = "293080d77fdd14d6b08a67c5487dfddbf874534bb7921526db56a7b75d7e3bef"
dependencies = [
"android_system_properties",
"arrayvec",
"ash",
"bit-set",
"bitflags 2.10.0",
"block2 0.6.2",
"block",
"bytemuck",
"cfg-if",
"cfg_aliases",
"core-graphics-types 0.2.0",
"glow",
"glutin_wgl_sys",
"gpu-allocator",
"hashbrown 0.17.1",
"gpu-descriptor",
"hashbrown 0.16.1",
"js-sys",
"khronos-egl",
"libc",
"libloading",
"log",
"metal",
"naga",
"naga-types",
"ndk-sys",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.2",
"objc2-metal 0.3.2",
"objc2-quartz-core 0.3.2",
"objc",
"once_cell",
"ordered-float",
"parking_lot",
@@ -3174,44 +3203,26 @@ dependencies = [
"profiling",
"range-alloc",
"raw-window-handle",
"raw-window-metal",
"renderdoc-sys",
"smallvec",
"static_assertions",
"thiserror 2.0.17",
"wasm-bindgen",
"wayland-sys",
"web-sys",
"wgpu-naga-bridge",
"wgpu-types",
"windows",
"windows-core",
"windows-result",
]
[[package]]
name = "wgpu-naga-bridge"
version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081"
dependencies = [
"naga",
"wgpu-types",
]
[[package]]
name = "wgpu-types"
version = "30.0.1"
version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286"
checksum = "e18308757e594ed2cd27dddbb16a139c42a683819d32a2e0b1b0167552f5840c"
dependencies = [
"bitflags 2.10.0",
"bytemuck",
"js-sys",
"log",
"naga-types",
"raw-window-handle",
"static_assertions",
"web-sys",
]
@@ -3575,12 +3586,12 @@ dependencies = [
"android-activity",
"atomic-waker",
"bitflags 2.10.0",
"block2 0.5.1",
"block2",
"bytemuck",
"calloop",
"cfg_aliases",
"concurrent-queue",
"core-foundation",
"core-foundation 0.9.4",
"core-graphics",
"cursor-icon",
"dpi",
@@ -3847,6 +3858,12 @@ dependencies = [
"syn 3.0.5",
]
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-core"
version = "0.5.0"
@@ -3862,11 +3879,20 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "zune-jpeg"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
dependencies = [
"zune-core 0.4.12",
]
[[package]]
name = "zune-jpeg"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5"
dependencies = [
"zune-core",
"zune-core 0.5.0",
]
+2 -2
View File
@@ -29,9 +29,9 @@ edition = "2024"
[workspace.dependencies]
pollster = "0.4.0"
winit = "0.30.12"
wgpu = "30.0.1"
wgpu = "28.0.0"
bytemuck = "1.23.1"
image = "0.25.10"
image = "0.25.6"
parley = "0.11.1"
swash = "0.2.10"
fxhash = "0.2.1"
+1 -6
View File
@@ -79,7 +79,6 @@ 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>>>,
}
@@ -108,7 +107,6 @@ impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> {
fn default() -> Self {
Self {
active: Default::default(),
global: Default::default(),
map: Default::default(),
}
}
@@ -140,13 +138,11 @@ 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) -> bool + 'a {
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) + '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,
@@ -156,7 +152,6 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
)
}
}
consumed
}
}
}
-9
View File
@@ -9,19 +9,10 @@ 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 {
+1 -2
View File
@@ -21,13 +21,12 @@ 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)
}
+1 -1
View File
@@ -421,7 +421,7 @@ impl Display for UiRegion {
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug)]
pub struct PixelRegion {
pub top_left: Vec2,
pub bot_right: Vec2,
+3 -3
View File
@@ -186,8 +186,8 @@ impl UiRenderNode {
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 mut groups = vec![&self.shared_layout, &data_layout];
groups.extend(render.layout());
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some(source.label),
bind_group_layouts: &groups,
@@ -219,7 +219,7 @@ impl UiRenderNode {
vertex: VertexState {
module: &module,
entry_point: Some("vs_main"),
buffers: &[Some(PrimitiveInstance::desc())],
buffers: &[PrimitiveInstance::desc()],
compilation_options: Default::default(),
},
fragment: Some(FragmentState {
-4
View File
@@ -34,10 +34,6 @@ 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
+5 -1
View File
@@ -10,7 +10,11 @@ struct State {
}
impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
rect(Color::RED).set_root(rsc, &mut ui_state);
Self { ui_state }
}
+5 -1
View File
@@ -15,7 +15,11 @@ pub struct Client {
}
impl DefaultAppState for Client {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let rrect = rect(Color::WHITE).radius(20);
let pad_test = (
rrect.color(Color::BLUE),
+5 -1
View File
@@ -11,7 +11,11 @@ struct State {
}
impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> 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;
+5 -1
View File
@@ -36,7 +36,11 @@ impl Test {
}
impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let test = Test::new(rsc);
test.on(CursorSense::click(), move |_, rsc| {
+49 -62
View File
@@ -1,6 +1,10 @@
use crate::prelude::*;
use arboard::Clipboard;
use std::{marker::PhantomData, sync::Arc, time::Instant};
use std::{
marker::{PhantomData, Sized},
sync::Arc,
time::Instant,
};
use winit::{
event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
@@ -25,35 +29,7 @@ pub use sense::*;
pub use state::*;
pub use task::*;
/// 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 type Proxy<Event> = EventLoopProxy<Event>;
pub struct DefaultUiState {
pub root: Option<StrongWidget>,
@@ -94,8 +70,9 @@ pub trait HasDefaultUiState: Sized + 'static {
}
pub trait DefaultAppState: HasDefaultUiState {
type Event: Send = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self>) -> Self;
type Event = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>)
-> Self;
#[allow(unused_variables)]
fn event(
&mut self,
@@ -128,14 +105,18 @@ pub struct DefaultRsc<State: 'static> {
}
impl<State> DefaultRsc<State> {
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(),
}
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 create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
@@ -200,32 +181,43 @@ 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 = DefaultEvent<State>;
type Event = State::Event;
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 = DefaultRsc::init(Arc::new(Proxy(proxy.clone())));
let state = State::new(default_state, &mut rsc, Proxy(proxy));
let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone());
let state = State::new(default_state, &mut rsc, proxy);
let render = UiRenderState::new();
Self { rsc, state, render }
Self {
rsc,
state,
render,
task_recv,
}
}
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
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();
self.state.event(event, &mut self.rsc, &mut self.render);
}
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let Self { rsc, render, state } = self;
let Self {
rsc,
render,
state,
task_recv,
} = self;
for update in task_recv.try_iter() {
update(state, rsc);
}
let ui_state = state.default_state_mut();
let input_changed = ui_state.input.event(&event);
@@ -235,7 +227,8 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.focus = None;
}
if input_changed {
render.run_sensors(rsc, state, cursor_state);
let window_size = ui_state.window_size();
render.run_sensors(rsc, state, cursor_state, window_size);
}
let ui_state = state.default_state_mut();
if old != ui_state.focus
@@ -304,8 +297,11 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
_ => (),
}
state.window_event(event, rsc, render);
self.request_redraw_if_needed();
self.state.default_state_mut().input.end_frame();
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();
}
fn exit(&mut self) {
@@ -313,15 +309,6 @@ 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;
+4 -20
View File
@@ -22,20 +22,7 @@ impl UiRenderer {
}
pub fn draw(&mut self) {
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 output = self.surface.get_current_texture().unwrap();
let view = output
.texture
.create_view(&TextureViewDescriptor::default());
@@ -59,7 +46,7 @@ impl UiRenderer {
self.queue.submit(std::iter::once(encoder.finish()));
self.window.pre_present_notify();
self.queue.present(output);
output.present();
}
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
@@ -78,10 +65,9 @@ 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,
display: Some(Box::new(window.clone())),
..InstanceDescriptor::new_without_display_handle()
..Default::default()
});
let surface = instance
@@ -93,7 +79,6 @@ 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!");
@@ -120,7 +105,6 @@ 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,
+35 -103
View File
@@ -4,14 +4,14 @@ use std::{
rc::Rc,
};
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Clone, Copy, PartialEq)]
pub enum CursorButton {
Left,
Right,
Middle,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(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 Global = Hovered;
type State = SensorState;
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,24 +37,6 @@ 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 {
@@ -70,12 +52,6 @@ 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)]
@@ -120,12 +96,6 @@ 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;
@@ -153,6 +123,11 @@ 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
@@ -172,6 +147,7 @@ pub trait SensorUi {
rsc: &mut Rsc,
state: &mut Rsc::State,
cursor: CursorState,
window_size: Vec2,
);
}
@@ -181,98 +157,54 @@ 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 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);
let mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
for layer in self.layers.indices().rev() {
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) {
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 {
continue;
}
hovered.now.push(id);
let hover = match hovered.was.contains(&id) {
true => ActivationState::On,
false => ActivationState::Start,
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,
};
// 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;
rsc.run_event::<CursorSense>(*id, data, state);
}
// Applied after the layer, never during it: senses on one layer do
// not block each other.
if consumed {
if sensed {
break;
}
}
// 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;
rsc.events_mut().get_type::<CursorSense>().active = active;
}
}
/// 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(),
+34 -16
View File
@@ -1,5 +1,11 @@
use iris_core::HasState;
use std::{pin::Pin, sync::Arc};
use std::{
pin::Pin,
sync::{
Arc,
mpsc::{Receiver as SyncReceiver, Sender as SyncSender, channel as sync_channel},
},
};
use tokio::{
runtime::Runtime,
sync::mpsc::{
@@ -7,52 +13,64 @@ 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>,
queue: Arc<dyn TaskQueue<Rsc>>,
window: Arc<Window>,
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
}
pub struct TaskCtx<Rsc: HasState> {
queue: Arc<dyn TaskQueue<Rsc>>,
send: TaskMsgSender<Rsc>,
}
impl<Rsc: HasState> TaskCtx<Rsc> {
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
self.queue.send(Box::new(f));
let _ = self.send.send(Box::new(f));
}
}
impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
fn new(send: TaskMsgSender<Rsc>) -> Self {
Self { send }
}
}
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
impl<Rsc: HasState> Tasks<Rsc> {
pub fn init(queue: Arc<dyn TaskQueue<Rsc>>) -> Self {
pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) {
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, queue }
(
Self {
start,
msg_send: msgs,
window,
},
msgs_recv,
)
}
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
where
F::CallOnceFuture: Send,
{
let queue = self.queue.clone();
let send = self.msg_send.clone();
let window = self.window.clone();
let _ = self.start.send(Box::pin(async move {
task(TaskCtx { queue }).await;
task(TaskCtx::new(send)).await;
window.request_redraw();
}));
}
}
-180
View File
@@ -1,180 +0,0 @@
//! 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;
#[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);
}
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();
}
}
-1
View File
@@ -7,7 +7,6 @@
pub mod default;
pub mod event;
pub mod harness;
pub mod widget;
pub use iris_core as core;
+2 -2
View File
@@ -4,7 +4,7 @@ mod max_size;
mod offset;
mod pad;
mod scroll;
mod set_size;
mod sized;
mod span;
mod stack;
@@ -14,6 +14,6 @@ pub use max_size::*;
pub use offset::*;
pub use pad::*;
pub use scroll::*;
pub use set_size::*;
pub use sized::*;
pub use span::*;
pub use stack::*;
@@ -1,12 +1,12 @@
use crate::prelude::*;
pub struct SetSize {
pub struct Sized {
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
}
impl SetSize {
impl Sized {
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
if let Some(x) = self.x {
ctx.outer.x.select_len(x.apply_rest());
@@ -17,7 +17,7 @@ impl SetSize {
}
}
impl Widget for SetSize {
impl Widget for Sized {
fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.inner);
}
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*;
use std::marker::Unsize;
use std::marker::{Sized, Unsize};
pub struct WidgetPtr {
pub inner: Option<StrongWidget>,
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*;
use std::marker::PhantomData;
use std::marker::{PhantomData, Sized};
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
pub content: String,
+6 -6
View File
@@ -31,9 +31,9 @@ widget_trait! {
}
}
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, SetSize> {
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, Sized> {
let size = size.into();
move |state| SetSize {
move |state| Sized {
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, SetSize> {
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
let len = len.into();
move |state| SetSize {
move |state| Sized {
inner: self.add_strong(state),
x: Some(len),
y: None,
}
}
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
let len = len.into();
move |state| SetSize {
move |state| Sized {
inner: self.add_strong(state),
x: None,
y: Some(len),
+17 -17
View File
@@ -4,19 +4,20 @@
//!
//! 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:
//! **Read the instruction count, not the clock.** Wall time here swings by 2x
//! between runs of one binary on this machine -- more under `cargo test` than
//! run directly -- while instructions retired are stable to 0.1%:
//!
//! 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.
//! Measured that way on 2026-09-13, drawing each primitive through its own
//! `PrimitiveRender` rather than a match in the renderer costs **6
//! instructions per list drawn**, which is 0.1% of a frame at both 256 and
//! 1024 layers. Recording one list into the pass costs wgpu ~5,400.
//!
//! 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.
//! The instance is leaked on purpose. Dropping the last one makes the Vulkan
//! loader unload Mesa's ICD, which faults when a thread that touched Vulkan
//! exits -- and libtest runs every test on a spawned thread.
use std::time::Instant;
@@ -29,20 +30,20 @@ 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.
/// Reported as the best of this many batches. The mean moves by 15% between
/// runs on this machine, which is 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());
// Probed rather than assumed: this machine's Vulkan device comes and goes,
// and GL is what is left when it is gone.
let all = Instance::new(&InstanceDescriptor::default());
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
Err(_) => Instance::new(&InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
..Default::default()
}),
};
// Leaked rather than dropped: see the note at the top of the file.
@@ -57,7 +58,6 @@ fn config(format: TextureFormat) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: SIZE,
height: SIZE,
present_mode: PresentMode::Fifo,
-34
View File
@@ -1,34 +0,0 @@
//! 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));
}
-60
View File
@@ -1,60 +0,0 @@
//! Which widget an input reaches.
use std::{cell::RefCell, rc::Rc};
use iris::harness::Harness;
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));
}
-225
View File
@@ -1,225 +0,0 @@
//! 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"
);
}
-26
View File
@@ -1,26 +0,0 @@
//! 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));
}
-23
View File
@@ -1,23 +0,0 @@
//! 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);
}