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

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

`tests/pointer_routing.rs` drops its own `Rsc`, event manager and layer
scaffolding for `iris::harness`, which is what it was standing in for.
Presses now arrive as a move and then a press, and a scroll after the
hover that precedes it, because that is what the harness delivers and
what a window does.
2026-09-13 21:58:39 -04:00
iris 36fec09d11 Track who is hovered, apart from what consumes
Hover was per-sensor state that only changed when the walk reached that
sensor, so ending it depended on the walk, which consumption cuts short.
`CursorSenses` now keeps the set of widgets the cursor was inside, in a
new `Event::Global` slot for state a whole event type owns rather than
each widget -- which is also where the input restructure keeps its pointer
capture.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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