Skip to content

Mouse input interactions (click & hover) - #42

Closed
nixonyh wants to merge 9 commits into
mainfrom
nixon/input
Closed

Mouse input interactions (click & hover)#42
nixonyh wants to merge 9 commits into
mainfrom
nixon/input

Conversation

@nixonyh

@nixonyh nixonyh commented Jun 11, 2026

Copy link
Copy Markdown
Member
Screen.Recording.2026-06-10.at.10.59.13.PM.mov

@nixonyh
nixonyh requested a review from ian-hon June 11, 2026 13:01
@nixonyh
nixonyh force-pushed the nixon/input branch 4 times, most recently from a0be473 to fd2b3a0 Compare June 11, 2026 15:59
@nixonyh nixonyh changed the title Input interactions Mouse input interactions (click & hover) Jun 11, 2026
Comment thread crates/fynix_input/src/hit_test.rs Outdated
@voxell-tech voxell-tech deleted a comment from coderabbitai Bot Jun 12, 2026
@voxell-tech voxell-tech deleted a comment from coderabbitai Bot Jun 12, 2026
@nixonyh

nixonyh commented Jun 12, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f66cb1a2-c2b2-4fef-8856-3063f5921cbb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a complete pointer input handling system for fynix, encompassing raw input models, spatial hit-testing, gesture recognition, and semantic interactions. Core fynix gains dispatch_bubbling to propagate events up the element tree, while the new fynix_input crate converts platform input into UI interactions. The example app demonstrates interactive pointer-driven UI with reactive state updates.

Changes

Pointer Input System

Layer / File(s) Summary
Workspace and crate setup
Cargo.toml, crates/fynix_input/Cargo.toml, crates/fynix_input/src/lib.rs
Added fynix_input and spatree as workspace dependencies, created fynix_input crate with module organization and is_hit_target predicate to identify UI elements eligible for pointer hit-testing.
Core fynix interaction bubbling and traversal
crates/fynix/src/lib.rs, crates/fynix/src/interaction.rs, crates/fynix/src/element/storage.rs, crates/fynix/src/ctx.rs
Added Fynix::dispatch_bubbling to walk parent chain finding handlers, Interactions::contains to query handler presence, and Elements::visit_paint_order to traverse subtrees in paint order. Updated test suite to use path! macro alias and added reactive_restores_style_set_before_it test verifying style persistence across reactive rebuilds.
Raw input event model
crates/fynix_input/src/raw_input.rs
Defined unified raw input types: PointerId (with MOUSE constant), PointerButton (Primary/Secondary/Middle/Other), RawInput container, and RawInputKind variants for pointer movement, press, and release events with coordinates.
Spatial hit-testing for elements
crates/fynix_input/src/hit_test.rs
Implemented Hit and HitTest using Spatree spatial indexing to query topmost elements under pointer coordinates, supporting paint-order-aware element selection and containment checks for bubbling gates.
Semantic pointer interaction events
crates/fynix_input/src/interaction.rs
Defined Click (pointer/button/local coords), PointerEnter (pointer id), and PointerLeave (pointer id) as semantic UI interaction event types.
Gesture recognition state machine
crates/fynix_input/src/recognizer.rs
Implemented PointerRecognizer to track active presses per (pointer, button) and emit Click when release matches press element within click-slop, emit PointerEnter/PointerLeave on hover target changes; includes comprehensive gesture boundary tests.
Example app framework integration
examples/vello_winit_examples/src/lib.rs, examples/vello_winit_examples/Cargo.toml
Extended VelloWinitApp with PointerRecognizer and cursor tracking, added FynixDemo::apply_events hook to process UI events before reactive updates, converted winit CursorMoved/MouseInput into fynix_input RawInput and ran through recognizer against current hit-test.
Interactive hello world demo
examples/vello_winit_examples/examples/hello_world.rs
Updated HelloWorld to track click count and hover state with reactive rebuilds, implemented apply_events to drain interaction events, replaced static buttons with single interactive "Increment" button wired to Click/PointerEnter/PointerLeave handlers.

Sequence Diagram(s)

sequenceDiagram
  participant WinitEvent as Winit Event
  participant VelloApp as VelloWinitApp
  participant Recognizer as PointerRecognizer
  participant Fynix as Fynix
  participant Demo as FynixDemo
  WinitEvent->>VelloApp: CursorMoved / MouseInput
  VelloApp->>VelloApp: update cursor position
  VelloApp->>VelloApp: create RawInput
  VelloApp->>VelloApp: build HitTest from layout
  VelloApp->>Recognizer: handle(RawInput)
  Recognizer->>Fynix: dispatch_bubbling(Click/PointerEnter/Leave)
  Fynix->>Demo: handler executes, queues events
  VelloApp->>Demo: apply_events(events)
  Demo->>Demo: drain and process UI events
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • voxell-tech/fynix#40: The new reactive_restores_style_set_before_it test in crates/fynix/src/ctx.rs exercises the reactive-scope machinery introduced by that PR to verify style persistence across reactive subtree rebuilds.

Suggested reviewers

  • ian-hon

🐰 Pointers fly through the trees,
Click and hover make it dance,
Spatial index brings delight,
Gestures bloom in fynix light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is off-topic: it contains only a link to an external asset (a video/image) with no textual explanation of the changes, objectives, or rationale related to the pull request. Provide a meaningful textual description explaining the purpose of the changes, the new features added (click and hover interactions), and how they integrate with the existing codebase.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Mouse input interactions (click & hover)' directly and clearly summarizes the main change: adding mouse/pointer input interaction support with click and hover functionality.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
examples/vello_winit_examples/examples/hello_world.rs (1)

145-145: ⚡ Quick win

Remove stale commented-out code at Line 145.

This dead commented line adds noise in the changed path and should be removed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/vello_winit_examples/examples/hello_world.rs` at line 145, Remove
the stale commented-out call to ctx.set(path!(<Label>::font_size), 42.0) —
delete that commented line (the dead code around the ctx.set(...) invocation) so
the diff no longer contains noisy commented code; simply remove the line and run
a quick build/format to ensure no trailing whitespace remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/fynix_input/src/hit_test.rs`:
- Around line 65-97: The build() routine currently only records Entry bounds
when include(id) is true, but contains() (used by dispatch_bubbling) needs
bounds for all visited ancestors; change the visit_paint_order callback so you
always compute origin/size and always push an Entry (id, x0,y0,x1,y1) into
entries for every visited node, while only calling
tree.push_rect(Rect::new(...)) when include(id) is true -- i.e., keep the
Spatree (tree.push_rect / tree.build) filtered for query(), but populate entries
for every node so contains() and dispatch_bubbling can check ancestor bounds
correctly (refer to build(), Spatree, tree.push_rect, entries.push, contains(),
and dispatch_bubbling).

In `@crates/fynix_input/src/recognizer.rs`:
- Around line 32-34: The recognizer currently stores a single hovered:
Option<ElementId> shared across all pointers causing incorrect
PointerLeave/PointerEnter events when multiple pointers move; change the hover
state to be tracked per pointer (e.g., replace hovered with a HashMap<PointerId,
ElementId> or similar) and update the PointerMoved handling to look up and
update the entry for the specific pointer id instead of the global hovered,
emitting PointerEnter/PointerLeave with the correct pointer id; alternatively,
if hover is intended for mouse only, explicitly guard in the PointerMoved
handler (and code paths around PointerLeave/PointerEnter) to ignore non-mouse
PointerId values so only mouse pointer updates affect hover state (apply same
change to the related code around lines referenced by the comment).
- Around line 116-123: The PointerLeave dispatch currently uses a permissive
predicate (|_| true) which lets bubbling reach ancestors that still contain the
pointer; update the predicate passed to fynix.dispatch_bubbling::<PointerLeave>
so it stops bubbling at any ancestor that is still covered by the current
hit-test. Concretely, in the block that checks self.hovered and calls
fynix.dispatch_bubbling::<PointerLeave>(&left, PointerLeave { pointer }, |_|
true), replace the |_| true predicate with one that returns false for entities
contained by the current hit-test (e.g., |ent|
!self.current_hit_test.covers(ent) or equivalent method your hit-test exposes),
so PointerLeave is not delivered to ancestors that still contain the pointer.

In `@examples/vello_winit_examples/src/lib.rs`:
- Around line 371-381: Add a WindowEvent::CursorLeft handler in the event match
(near the WindowEvent::CursorMoved / MouseInput arms) that clears hover by
feeding the recognizer a synthesized off-window pointer move: update self.cursor
to an offscreen coordinate and call self.feed_input with
RawInputKind::PointerMoved (PointerId::MOUSE, x/y off-window) so hit_test.query
returns None and the recognizer emits PointerLeave; alternatively call into the
recognizer’s explicit “pointer left” path if available.
- Around line 62-69: The cursor field is currently a plain (f64, f64)
initialized to (0.0,0.0) which causes handle_mouse_input() to perform
hit-testing on PointerDown/PointerUp before any real CursorMoved has been
received; change the struct field cursor to Option<(f64, f64)> and update
initialization to None, update any assignments in the CursorMoved handler to set
Some((x,y)), and modify handle_mouse_input() to ignore MouseInput
(PointerDown/PointerUp) when cursor is None (only perform hit-test and dispatch
when cursor is Some), keeping references to PointerDown/PointerUp and
CursorMoved to locate the changes.
- Around line 241-247: The match that converts winit MouseButton to vello
PointerButton (the match on variable button) currently sends MouseButton::Back
and MouseButton::Forward through the wildcard arm which returns
PointerButton::Other(u16::MAX), causing distinct physical buttons to be
collapsed; add explicit match arms for MouseButton::Back and
MouseButton::Forward and map them to distinct PointerButton::Other(...) values
(or other distinct PointerButton variants) instead of the wildcard, leaving the
wildcard arm only for truly unknown buttons so PointerRecognizer can correctly
pair press/release by button.

---

Nitpick comments:
In `@examples/vello_winit_examples/examples/hello_world.rs`:
- Line 145: Remove the stale commented-out call to
ctx.set(path!(<Label>::font_size), 42.0) — delete that commented line (the dead
code around the ctx.set(...) invocation) so the diff no longer contains noisy
commented code; simply remove the line and run a quick build/format to ensure no
trailing whitespace remains.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 80fa4b00-aee7-44a6-b3de-154a3ceac1bf

📥 Commits

Reviewing files that changed from the base of the PR and between 61d0199 and fd2b3a0.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • Cargo.toml
  • crates/fynix/src/ctx.rs
  • crates/fynix/src/element/storage.rs
  • crates/fynix/src/interaction.rs
  • crates/fynix/src/lib.rs
  • crates/fynix_input/Cargo.toml
  • crates/fynix_input/src/hit_test.rs
  • crates/fynix_input/src/interaction.rs
  • crates/fynix_input/src/lib.rs
  • crates/fynix_input/src/raw_input.rs
  • crates/fynix_input/src/recognizer.rs
  • examples/vello_winit_examples/Cargo.toml
  • examples/vello_winit_examples/examples/hello_world.rs
  • examples/vello_winit_examples/src/lib.rs

Comment thread crates/fynix_input/src/hit_test.rs Outdated
Comment on lines +65 to +97
pub fn build(
elements: &Elements,
root: &ElementId,
include: impl Fn(&ElementId) -> bool,
) -> Self {
let mut tree = Spatree::new();
let mut entries = Vec::new();

elements.visit_paint_order(root, |id, meta| {
if !include(id) {
return;
}

let origin = meta.node.world_translation;
let size = meta.node.size;

let x0 = origin.x as f64;
let y0 = origin.y as f64;
let x1 = x0 + size.width as f64;
let y1 = y0 + size.height as f64;
tree.push_rect(Rect::new(x0, y0, x1, y1));
entries.push(Entry {
id: *id,
x0,
y0,
x1,
y1,
});
});

tree.build(|rect| rect.center());

Self { tree, entries }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep ancestor bounds outside the hit-target index.

build() only records entries for include(id) hits, but contains() is later used as the should_bubble gate for every ancestor during dispatch_bubbling. That means any layout-only wrapper between the hit node and a higher interactive ancestor becomes an unintended hard stop, because contains(wrapper, x, y) returns false and bubbling aborts before the real handler. Store bounds for all visited nodes for contains(), and keep the spatree filtered only for query().

Also applies to: 122-125

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fynix_input/src/hit_test.rs` around lines 65 - 97, The build() routine
currently only records Entry bounds when include(id) is true, but contains()
(used by dispatch_bubbling) needs bounds for all visited ancestors; change the
visit_paint_order callback so you always compute origin/size and always push an
Entry (id, x0,y0,x1,y1) into entries for every visited node, while only calling
tree.push_rect(Rect::new(...)) when include(id) is true -- i.e., keep the
Spatree (tree.push_rect / tree.build) filtered for query(), but populate entries
for every node so contains() and dispatch_bubbling can check ancestor bounds
correctly (refer to build(), Spatree, tree.push_rect, entries.push, contains(),
and dispatch_bubbling).

Comment on lines +32 to +34
/// The topmost hit-tested element the mouse pointer is currently
/// over, tracked to emit enter/leave when it changes.
hovered: Option<ElementId>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Track hover state per pointer, not globally.

hovered stores one shared element for the entire recognizer, but PointerMoved is keyed by pointer. Once two pointers move interleaved, the later move can emit PointerLeave for the earlier pointer's target with the wrong pointer id and overwrite its hover state. Keep hover state per PointerId, or explicitly ignore non-mouse pointers here if hover is intended to be mouse-only.

Also applies to: 110-133

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fynix_input/src/recognizer.rs` around lines 32 - 34, The recognizer
currently stores a single hovered: Option<ElementId> shared across all pointers
causing incorrect PointerLeave/PointerEnter events when multiple pointers move;
change the hover state to be tracked per pointer (e.g., replace hovered with a
HashMap<PointerId, ElementId> or similar) and update the PointerMoved handling
to look up and update the entry for the specific pointer id instead of the
global hovered, emitting PointerEnter/PointerLeave with the correct pointer id;
alternatively, if hover is intended for mouse only, explicitly guard in the
PointerMoved handler (and code paths around PointerLeave/PointerEnter) to ignore
non-mouse PointerId values so only mouse pointer updates affect hover state
(apply same change to the related code around lines referenced by the comment).

Comment on lines +116 to +123
if let Some(left) = self.hovered {
// The pointer has moved off `left`, so the
// hit-test no longer covers it: always deliver.
fynix.dispatch_bubbling::<PointerLeave>(
&left,
PointerLeave { pointer },
|_| true,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't bubble PointerLeave through ancestors that still contain the pointer.

With |_| true, moving from a child to a sibling or ancestor can still deliver PointerLeave to a parent that the pointer never actually left. That happens whenever the old target has no handler and dispatch_bubbling walks up to the nearest ancestor handler. Based on PR context, dispatch_bubbling searches ancestors for the nearest handler, so this gate needs to stop at ancestors still covered by the current hit-test point. Based on learnings: none. Based on PR context: dispatch_bubbling::<I> walks up parents to the nearest handler, gated by a caller-supplied predicate.

Suggested fix.
                 if let Some(left) = self.hovered {
                     // The pointer has moved off `left`, so the
                     // hit-test no longer covers it: always deliver.
                     fynix.dispatch_bubbling::<PointerLeave>(
                         &left,
                         PointerLeave { pointer },
-                        |_| true,
+                        |id| !hit_test.contains(id, x, y),
                     );
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(left) = self.hovered {
// The pointer has moved off `left`, so the
// hit-test no longer covers it: always deliver.
fynix.dispatch_bubbling::<PointerLeave>(
&left,
PointerLeave { pointer },
|_| true,
);
if let Some(left) = self.hovered {
// The pointer has moved off `left`, so the
// hit-test no longer covers it: always deliver.
fynix.dispatch_bubbling::<PointerLeave>(
&left,
PointerLeave { pointer },
|id| !hit_test.contains(id, x, y),
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fynix_input/src/recognizer.rs` around lines 116 - 123, The
PointerLeave dispatch currently uses a permissive predicate (|_| true) which
lets bubbling reach ancestors that still contain the pointer; update the
predicate passed to fynix.dispatch_bubbling::<PointerLeave> so it stops bubbling
at any ancestor that is still covered by the current hit-test. Concretely, in
the block that checks self.hovered and calls
fynix.dispatch_bubbling::<PointerLeave>(&left, PointerLeave { pointer }, |_|
true), replace the |_| true predicate with one that returns false for entities
contained by the current hit-test (e.g., |ent|
!self.current_hit_test.covers(ent) or equivalent method your hit-test exposes),
so PointerLeave is not delivered to ancestors that still contain the pointer.

Comment on lines +62 to +69
/// Start of the app, used to stamp raw input with a monotonic
/// millisecond timestamp.
start: Instant,
recognizer: PointerRecognizer,
/// Last known cursor position, in physical pixels. winit's
/// `MouseInput` carries no position, so it is paired with the
/// most recent `CursorMoved`.
cursor: (f64, f64),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="examples/vello_winit_examples/src/lib.rs"

echo "== File =="
ls -l "$FILE"

echo
echo "== Lines 40-130 =="
sed -n '40,130p' "$FILE" | cat -n

echo
echo "== Lines 200-320 =="
sed -n '200,320p' "$FILE" | cat -n

echo
echo "== Lines 340-420 =="
sed -n '340,420p' "$FILE" | cat -n

echo
echo "== Search for CursorLeft, cursor initialization, and handle_mouse_input =="
rg -n "CursorLeft|PointerLeave|handle_mouse_input|cursor:\s*\(|cursor\s*=" "$FILE" || true

Repository: voxell-tech/fynix

Length of output: 10290


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="examples/vello_winit_examples/src/lib.rs"

echo "== Cursor assignment sites =="
rg -n "self\.cursor\s*=" "$FILE"

echo
echo "== PointerMoved feed_input call sites =="
rg -n "PointerMoved|RawInputKind::PointerMoved" "$FILE"

echo
echo "== Any initialization hit-test/input before first CursorMoved? =="
rg -n "feed_input\(|recognizer\.handle\(" "$FILE"

Repository: voxell-tech/fynix

Length of output: 547


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "hello_world\.rs|hello_world" -S examples/vello_winit_examples/src || true
ls -R examples/vello_winit_examples/src

echo "== Inspect hello_world.rs if present =="
if [ -f examples/vello_winit_examples/src/hello_world.rs ]; then
  sed -n '1,220p' examples/vello_winit_examples/src/hello_world.rs | cat -n
elif [ -f examples/vello_winit_examples/src/examples/hello_world.rs ]; then
  sed -n '1,220p' examples/vello_winit_examples/src/examples/hello_world.rs | cat -n
else
  echo "hello_world.rs not found at expected locations."
fi

echo
echo "== Search for PointerLeave / hover draining =="
rg -n "PointerLeave|leave|hover|drain" examples/vello_winit_examples/src || true

echo
echo "== Check Cargo.toml for winit version =="
rg -n "winit\s*=" -S Cargo.toml examples/**/Cargo.toml || true
rg -n "name\s*=\s*\"vello_winit_examples\"|vello_winit_examples" -S examples/**/Cargo.toml Cargo.toml || true

echo
echo "== Search for MouseButton::Back / Forward usage in repo =="
rg -n "MouseButton::(Back|Forward)" examples/vello_winit_examples/src examples -S || true

Repository: voxell-tech/fynix

Length of output: 443


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate recognizer implementation and input kinds.
rg -n "struct PointerRecognizer|PointerRecognizer" -S . | head -n 50

echo
echo "== Find RawInputKind variants involving leave =="
rg -n "enum RawInputKind|RawInputKind::Pointer|PointerLeave|CursorLeft" -S . || true

echo
echo "== Search for PointerLeave emission =="
rg -n "PointerLeave" -S . || true

echo
echo "== Search for how PointerMoved/Down/Up are handled in recognizer =="
rg -n "handle\\(&mut .*PointerRecognizer|fn handle\\(|recognizer\\.handle|PointerMoved|PointerDown|PointerUp" -S . | head -n 80

Repository: voxell-tech/fynix

Length of output: 5688


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show RawInputKind and recognizer hit-test usage for PointerDown/Up.
sed -n '1,120p' crates/fynix_input/src/raw_input.rs | cat -n

echo
echo "== recognizer.rs: pointer moved/leave handling around line ~90-140 =="
sed -n '1,170p' crates/fynix_input/src/recognizer.rs | cat -n

echo
echo "== recognizer.rs: handle implementation around line ~200-280 =="
sed -n '170,280p' crates/fynix_input/src/recognizer.rs | cat -n

echo
echo "== Interaction emission for PointerLeave around line ~90-140 (if present) =="
rg -n "PointerLeave" crates/fynix_input/src/recognizer.rs

Repository: voxell-tech/fynix

Length of output: 12987


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find HitTest::query definition.
rg -n "struct HitTest|impl HitTest|fn query\\(" crates/fynix_input -S

# Show the query implementation.
FILE="$(rg -l "fn query\\(" crates/fynix_input -S | head -n 1)"
echo "== HitTest query file =="
echo "$FILE"
sed -n '1,220p' "$FILE" | cat -n

Repository: voxell-tech/fynix

Length of output: 5292


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="examples/vello_winit_examples/examples/hello_world.rs"

echo "== File =="
ls -l "$FILE"
echo
echo "== Lines 150-280 =="
sed -n '150,280p' "$FILE" | cat -n

echo
echo "== Search for 'drain' / hover bookkeeping =="
rg -n "drain|hover|PointerEnter|PointerLeave|events\.push|events\.clear" "$FILE" || true

Repository: voxell-tech/fynix

Length of output: 6172


🌐 Web query:

winit 0.30 MouseButton enum variants Left Right Middle Back Forward Other(u16)

💡 Result:

In winit 0.30, the MouseButton enum does not include an Other(u16) variant. Instead, it explicitly defines 32 distinct mouse button variants [1][2]. The variants are: Left, Right, Middle, Back, Forward, Button6, Button7, Button8, Button9, Button10, Button11, Button12, Button13, Button14, Button15, Button16, Button17, Button18, Button19, Button20, Button21, Button22, Button23, Button24, Button25, Button26, Button27, Button28, Button29, Button30, Button31, and Button32 [1][2]. These are defined in the winit-core crate's event module [1][2]. While some older documentation or secondary sources may show an Other(u16) variant, the actual implementation in version 0.30 utilizes these numbered variants to support a broader range of hardware buttons [1][2].

Citations:


Avoid hit-testing MouseInput with a default (0.0, 0.0) cursor position.

cursor is initialized to (0.0, 0.0) and handle_mouse_input() always uses it for PointerDown/PointerUp, even if no CursorMoved has occurred yet (or if the cached position is stale), so the first press/release can be hit-tested at the top-left and dispatch an incorrect/phantom click. Store the cursor as Option<(f64, f64)> and ignore MouseInput until a real position has been received.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/vello_winit_examples/src/lib.rs` around lines 62 - 69, The cursor
field is currently a plain (f64, f64) initialized to (0.0,0.0) which causes
handle_mouse_input() to perform hit-testing on PointerDown/PointerUp before any
real CursorMoved has been received; change the struct field cursor to
Option<(f64, f64)> and update initialization to None, update any assignments in
the CursorMoved handler to set Some((x,y)), and modify handle_mouse_input() to
ignore MouseInput (PointerDown/PointerUp) when cursor is None (only perform
hit-test and dispatch when cursor is Some), keeping references to
PointerDown/PointerUp and CursorMoved to locate the changes.

Comment on lines +241 to +247
let button = match button {
MouseButton::Left => PointerButton::Primary,
MouseButton::Right => PointerButton::Secondary,
MouseButton::Middle => PointerButton::Middle,
MouseButton::Other(n) => PointerButton::Other(n),
_ => PointerButton::Other(u16::MAX),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="examples/vello_winit_examples/src/lib.rs"

echo "== File existence =="
ls -la "$FILE" || true

echo
echo "== Context around line 241 =="
nl -ba "$FILE" | sed -n '220,280p'

echo
echo "== Find PointerButton mapping/usages =="
rg -n "PointerButton" "$FILE"

echo
echo "== Find click/press/release handling around mapping =="
rg -n "MouseButton|press|release|Input|pointer.*button|click" "$FILE"

Repository: voxell-tech/fynix

Length of output: 266


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="examples/vello_winit_examples/src/lib.rs"

echo "== Context around lines 220-280 =="
awk 'NR>=220 && NR<=280 {printf "%5d  %s\n", NR, $0}' "$FILE"

echo
echo "== PointerButton mapping/usages (in this file) =="
rg -n "PointerButton" "$FILE"

echo
echo "== MouseButton and press/release matching (in this file) =="
rg -n "MouseButton|press|release|pointer.*button|Input|click" "$FILE"

Repository: voxell-tech/fynix

Length of output: 4019


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="examples/vello_winit_examples/src/lib.rs"

echo "== PointerRecognizer usage (in this file) =="
rg -n "PointerRecognizer|recognizer|feed_input|PointerDown|PointerUp|RawInputKind::Pointer" "$FILE"

echo
echo "== Context around feed_input/recognizer dispatch =="
awk 'NR>=260 && NR<=420 {printf "%5d  %s\n", NR, $0}' "$FILE"

Repository: voxell-tech/fynix

Length of output: 5735


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find PointerButton / PointerRecognizer definitions in the repo
rg -n "struct PointerRecognizer|enum PointerButton|PointerRecognizer" . -S

# 2) Find where winit version is pinned
fd -a "Cargo.toml" -S . | head -n 50
# Try common locations first
rg -n "winit\s*=" -S Cargo.toml **/Cargo.toml 2>/dev/null || true
rg -n "\"winit\"" -S Cargo.toml **/Cargo.toml 2>/dev/null || true

# 3) Look for MouseButton mapping elsewhere (might exist in other example files)
rg -n "MouseButton::(Back|Forward)|MouseButton::Left|MouseButton::Right|MouseButton::Middle|MouseButton::Other" examples -S

Repository: voxell-tech/fynix

Length of output: 1239


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List Cargo.toml files and show the one(s) likely defining winit
echo "== Cargo.toml files (top 20) =="
fd -e toml Cargo.toml -p . 2>/dev/null | head -n 20 || true
# Some fd versions use different flags; fallback:
fd -e toml Cargo.toml . | head -n 20 || true

echo
echo "== Search for winit version pins in workspace Cargo.toml(s) =="
rg -n "winit\s*=" -S . --glob "Cargo.toml" || true
rg -n "winit" -S Cargo.toml **/Cargo.toml 2>/dev/null || true

echo
echo "== Inspect PointerButton enum =="
FILE="crates/fynix_input/src/raw_input.rs"
rg -n "pub enum PointerButton" "$FILE"
awk 'NR>=1 && NR<=140 {printf "%5d  %s\n", NR, $0}' "$FILE" | sed -n '1,140p'

echo
echo "== Inspect PointerRecognizer button handling =="
FILE="crates/fynix_input/src/recognizer.rs"
rg -n "PointerButton|button|Mouse|PointerDown|PointerUp" "$FILE"

Repository: voxell-tech/fynix

Length of output: 3732


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="crates/fynix_input/src/recognizer.rs"
echo "== Pointer press tracking structures =="
rg -n "in_flight|presses|held|take_press|struct Press|Vec|HashMap|HashSet" "$FILE"

echo
echo "== take_press and matching logic (around take_press) =="
rg -n "fn take_press" "$FILE"
START=$(rg -n "fn take_press" "$FILE" | head -n 1 | cut -d: -f1)
awk -v s="$START" 'NR>=s-40 && NR<=s+80 {printf "%5d  %s\n", NR, $0}' "$FILE"

echo
echo "== Any normalization/collapsing of buttons in recognizer =="
rg -n "PointerButton::Other|Other\\(|match .*PointerButton|u16::MAX" "$FILE"

Repository: voxell-tech/fynix

Length of output: 5461


🌐 Web query:

winit 0.30 enum MouseButton variants Back Forward Other(u16)

💡 Result:

In winit 0.30, the MouseButton enum does not contain an Other(u16) variant [1][2][3]. Instead, it defines explicit variants for buttons up to 32 [1][2]. The MouseButton enum variants are [1][2]: - Left (assigned 0) - Right (assigned 1) - Middle (assigned 2) - Back (assigned 3) - Forward (assigned 4) - Button6 through Button32 (assigned 5 through 31, respectively) The Back and Forward variants are intended to represent the common side buttons on a mouse [1][2][3]. While some older or external documentation might reflect an Other(u16) variant (often found in different libraries or older versions), the current winit 0.30 implementation uses a fixed set of variants to represent mouse buttons [1][2]. Supporting specific buttons beyond these is not handled via an Other variant but is constrained to this explicit range [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== winit version from Cargo.lock =="
if [ -f Cargo.lock ]; then
  rg -n 'name = "winit"|version = ' Cargo.lock | head -n 50
  rg -n 'name = "winit"' -n Cargo.lock -n && true
else
  echo "No Cargo.lock found"
fi

echo
echo "== Search for MouseButton variants in local cargo registry (if present) =="
REG="${CARGO_HOME:-$HOME/.cargo}/registry/src"
if [ -d "$REG" ]; then
  rg -n "enum MouseButton|pub enum MouseButton" "$REG" -S --glob "*event.rs" | head -n 50
  rg -n "MouseButton::Other\\b|Other\\(u16\\)" "$REG" -S --glob "*event.rs" | head -n 50
  rg -n "MouseButton::Back\\b|Back\\b" "$REG" -S --glob "*event.rs" | head -n 50
else
  echo "No cargo registry cache at: $REG"
fi

echo
echo "== Search intent/docs for PointerButton::Other in repo =="
rg -n "PointerButton::Other|enum PointerButton|Other\\(u16\\)|u16::MAX|Back|Forward|Button[0-9]+" crates/fynix_input examples/vello_winit_examples/src/lib.rs -S

Repository: voxell-tech/fynix

Length of output: 1740


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="crates/fynix_input/src/recognizer.rs"
awk 'NR>=1 && NR<=200 {printf "%5d  %s\n", NR, $0}' "$FILE" | sed -n '1,200p'

Repository: voxell-tech/fynix

Length of output: 7610


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check winit dependency versions per package
echo "== winit deps =="
rg -n "winit\s*=\s*\"|winit\s*=" -S Cargo.toml crates/**/Cargo.toml examples/**/Cargo.toml

echo
echo "== Any other MouseButton mappings elsewhere =="
rg -n "MouseButton::(Back|Forward|Button|Other\()|MouseButton::Other" -S examples crates || true

echo
echo "== Show current mapping snippet for context =="
awk 'NR>=230 && NR<=260 {printf "%5d  %s\n", NR, $0}' examples/vello_winit_examples/src/lib.rs

Repository: voxell-tech/fynix

Length of output: 1562


🌐 Web query:

winit 0.30 MouseButton Other(u16) Back Forward Button32

💡 Result:

In winit version 0.30, the MouseButton enum is defined as follows: pub enum MouseButton { Left, Right, Middle, Back, Forward, Other(u16), } The Back and Forward variants were introduced to provide a cross-platform way to handle common side buttons found on many mice [1]. The Other(u16) variant is used for mouse buttons that do not map to the standard Left, Right, Middle, Back, or Forward variants [2][3]. Regarding "Button32": In winit 0.30, there is no explicit variant named Button32 within the MouseButton enum [2][4][3]. While newer developments and discussions in the winit repository have explored revising the MouseButton type to better support high-count button mice—including proposed support for buttons up to Button32—these changes are not part of the standard MouseButton enum definition in the 0.30 stable release [5][6]. In version 0.30, any button beyond the standard set is typically handled via the Other(u16) variant, where the u16 value represents the raw button code provided by the underlying operating system [2][3].

Citations:


🌐 Web query:

winit 0.30 MouseButton enum definition "Other(u16)" "Back" "Forward"

💡 Result:

In winit 0.30, the MouseButton enum is used to represent buttons on a mouse controller [1][2]. Its definition is as follows: pub enum MouseButton { Left, Right, Middle, Back, Forward, Other(u16), } Explanation of variants: - Left, Right, Middle: Represent the standard primary, secondary, and tertiary mouse buttons [1][3]. - Back, Forward: Represent the common side buttons often found on modern mice [1][4]. These were added to support cross-platform mouse input, though platform-specific limitations may exist (e.g., support on macOS may depend on hardware and driver implementation) [4][5][6]. - Other(u16): This variant is used for any mouse buttons that do not correspond to the named variants [1][5]. The u16 value allows for the representation of additional buttons, accommodating different platform conventions for button numbering [1][7]. Historical context: The Back and Forward variants were introduced to standardize mouse input across major platforms, including Wayland, X11, Windows, macOS, and Web [4]. Previously, these buttons were often inconsistently mapped or handled via the Other variant [4][7]. The Other(u16) variant itself has evolved; for instance, some earlier versions used a u8 for this field, but it was updated to u16 to better accommodate the wider range of button IDs supported by various operating systems [3][4][7].

Citations:


Preserve distinct non-primary mouse buttons.

MouseButton::Back and MouseButton::Forward fall into the wildcard arm and are both mapped to PointerButton::Other(u16::MAX), so PointerRecognizer can pair a down event with the wrong physical button on release (press.button == button). Match Back/Forward explicitly and map them to distinct PointerButton::Other(..) codes (or add dedicated PointerButton variants).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/vello_winit_examples/src/lib.rs` around lines 241 - 247, The match
that converts winit MouseButton to vello PointerButton (the match on variable
button) currently sends MouseButton::Back and MouseButton::Forward through the
wildcard arm which returns PointerButton::Other(u16::MAX), causing distinct
physical buttons to be collapsed; add explicit match arms for MouseButton::Back
and MouseButton::Forward and map them to distinct PointerButton::Other(...)
values (or other distinct PointerButton variants) instead of the wildcard,
leaving the wildcard arm only for truly unknown buttons so PointerRecognizer can
correctly pair press/release by button.

Comment on lines +371 to +381
WindowEvent::CursorMoved { position, .. } => {
self.cursor = (position.x, position.y);
self.feed_input(RawInputKind::PointerMoved {
pointer: PointerId::MOUSE,
x: position.x,
y: position.y,
});
}
WindowEvent::MouseInput { state, button, .. } => {
self.handle_mouse_input(state, button);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the referenced section
sed -n '330,430p' examples/vello_winit_examples/src/lib.rs | nl -ba | sed -n '1,140p'

# 2) Search for cursor/leave handling in this file
rg -n --no-heading "CursorLeft|PointerLeave|RawInputKind::.*Leave|hover|leave" examples/vello_winit_examples/src/lib.rs

# 3) Search repo-wide for how PointerLeave / cursor-left is handled or defined
rg -n --no-heading "CursorLeft|PointerLeave|RawInputKind::.*Leave" -S .

# 4) Find where hover state is stored/cleared and how it’s triggered
rg -n --no-heading "hover" examples/vello_winit_examples/src/lib.rs
rg -n --no-heading "cursor.*left|leave.*cursor|Pointer.*Leave" -S .

Repository: voxell-tech/fynix

Length of output: 104


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the referenced section with line numbers
sed -n '330,430p' examples/vello_winit_examples/src/lib.rs | cat -n

# 2) Search for cursor/leave handling in this file
rg -n --no-heading "CursorLeft|PointerLeave|RawInputKind::.*Leave|hover|leave" examples/vello_winit_examples/src/lib.rs || true

# 3) Search repo-wide for how PointerLeave / cursor-left is handled or defined
rg -n --no-heading "CursorLeft|PointerLeave|RawInputKind::.*Leave" -S . || true

# 4) Find where hover state is stored/cleared and how it’s triggered
rg -n --no-heading "hover|cursor.*left|leave.*cursor|Pointer.*Leave" -S . || true

Repository: voxell-tech/fynix

Length of output: 6116


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for CursorLeft handling in the example file
rg -n "CursorLeft" examples/vello_winit_examples/src/lib.rs || true

# Show all RawInputKind variants used by the example
rg -n "RawInputKind::" examples/vello_winit_examples/src/lib.rs

# Locate and show feed_input implementation in this crate example
rg -n "fn feed_input|feed_input\(" examples/vello_winit_examples/src/lib.rs

# Inspect recognizer hover/leave logic
sed -n '1,220p' crates/fynix_input/src/recognizer.rs | cat -n
echo "----"
sed -n '220,380p' crates/fynix_input/src/recognizer.rs | cat -n

# Definitions of PointerLeave / related interactions
sed -n '1,120p' crates/fynix_input/src/interaction.rs | cat -n

# Show how recognizer triggers PointerLeave on hover changes
rg -n "hovered: Option|PointerLeave|PointerEnter|target" crates/fynix_input/src/recognizer.rs

Repository: voxell-tech/fynix

Length of output: 15709


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Check example event match for CursorLeft
rg -n "CursorLeft" examples/vello_winit_examples/src/lib.rs || true

# Check the whole repo for CursorLeft / pointer-left mapping
rg -n "CursorLeft" . || true
rg -n "PointerLeft|RawInputKind::.*Left|PointerLeave" crates/fynix_input . || true

# Inspect raw_input kind variants
rg -n "enum RawInputKind" -S crates/fynix_input/src || true
rg -n "RawInputKind::" crates/fynix_input/src/raw_input.rs crates/fynix_input/src -S || true

# Inspect feed_input call site and surrounding code in the example for cursor->raw mapping
sed -n '200,330p' examples/vello_winit_examples/src/lib.rs | cat -n

Repository: voxell-tech/fynix

Length of output: 7855


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect RawInputKind definition
sed -n '1,120p' crates/fynix_input/src/raw_input.rs | cat -n

# Search for CursorLeft usage in the repo (more targeted)
rg -n "CursorLeft" -S . || true

# Confirm whether any window event maps to PointerMoved with "off-window" coords
rg -n "PointerMoved\\s*\\{[^}]*x|PointerMoved\\s*\\{" examples/vello_winit_examples/src/lib.rs -n || true

# Show full match arms around window_event for CursorLeft-related events
sed -n '320,420p' examples/vello_winit_examples/src/lib.rs | cat -n

Repository: voxell-tech/fynix

Length of output: 4822


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "WindowEvent::CursorMoved" examples/vello_winit_examples/src/lib.rs
rg -n "WindowEvent::CursorLeft" examples/vello_winit_examples/src/lib.rs || true
rg -n "match event \\{" -n examples/vello_winit_examples/src/lib.rs

Repository: voxell-tech/fynix

Length of output: 150


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "impl PointerRecognizer|pub fn|hovered" crates/fynix_input/src/recognizer.rs

Repository: voxell-tech/fynix

Length of output: 322


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact window_event match arms with line numbers
sed -n '330,410p' examples/vello_winit_examples/src/lib.rs | cat -n

# Repo-wide check for CursorLeft handling
rg -n "CursorLeft" . || true

Repository: voxell-tech/fynix

Length of output: 2085


Fix stuck hover when the cursor leaves the window.

PointerLeave in fynix_input is emitted only when the recognizer processes RawInputKind::PointerMoved and the new hit-test target differs from the stored hovered element. In examples/vello_winit_examples/src/lib.rs, hover updates come only from WindowEvent::CursorMoved, and there is no WindowEvent::CursorLeft handling, so when the cursor exits without more CursorMoved events the hovered element never receives PointerLeave.

Add a WindowEvent::CursorLeft arm (around lines 371-381) that clears the recognizer’s hover by dispatching PointerLeave immediately, e.g. by feeding a synthesized PointerMoved with off-window coordinates so hit_test.query returns None (or by extending the recognizer with an explicit “pointer left/offscreen” path).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/vello_winit_examples/src/lib.rs` around lines 371 - 381, Add a
WindowEvent::CursorLeft handler in the event match (near the
WindowEvent::CursorMoved / MouseInput arms) that clears hover by feeding the
recognizer a synthesized off-window pointer move: update self.cursor to an
offscreen coordinate and call self.feed_input with RawInputKind::PointerMoved
(PointerId::MOUSE, x/y off-window) so hit_test.query returns None and the
recognizer emits PointerLeave; alternatively call into the recognizer’s explicit
“pointer left” path if available.

@nixonyh
nixonyh force-pushed the nixon/input branch 3 times, most recently from 5cae53f to 6426057 Compare June 15, 2026 01:34
nixonyh added 9 commits June 15, 2026 20:34
Introduce a `fynix_input` crate for the stateful input layer, keeping
fynix-core free of spatial-indexing concerns.

Core (fynix):
- `Elements::visit_paint_order` exposes a paint-order traversal so an
  external index can read each element's absolute rect.
- `Fynix::dispatch_bubbling::<I>` walks up parents to the nearest
  handler, gated by a caller-supplied `should_bubble` predicate (the
  element tree alone cannot express the stop condition).
- `Interactions::contains::<I>` probes for a handler without running
  it.
- `FynixCtx::compose`/`compose_with` now return an `ElementHandle`, so
  `.on` chains directly onto composed elements.

fynix_input:
- `RawInput` (backend-facing) + a unified pointer model.
- `HitTest` builds a spatree over only the elements an `include`
  predicate accepts (`is_hit_target`), so non-interactive elements
  stay out of the index.
- `PointerRecognizer` turns raw pointer events into `Click`,
  `PointerEnter`, and `PointerLeave`, dispatched via bubbling with a
  hit-test gate.

spatree now re-exports kurbo; workspace points rectree/spatree at the
local checkout.

hello_world: clickable counter and a hover label driven by the new
input layer.
Replace the RawInput-based recognizer with a generic
PointerRecognizer<Id, Btn> driven by semantic methods
(pointer_down/up/moved/cancel) instead of a raw-input envelope.

Backends classify their native button into a ButtonRole via the
ClassifyButton trait, so the recognizer picks Click vs SecondaryClick
without the native type leaking into interaction handlers. Interaction
payloads drop their pointer/button fields and stay concrete, keeping
UI code backend-agnostic.

Move the recognizer into pointer.rs alongside ButtonRole and
ClassifyButton, leaving room for future key/touch modules. The demo
gains a thin WinitInput shim that wraps winit events (mouse + touch)
onto the recognizer, the shape a future fynix_winit crate would take.
@nixonyh

nixonyh commented Jun 18, 2026

Copy link
Copy Markdown
Member Author

Superseded by #56

@nixonyh nixonyh closed this Jun 18, 2026
@nixonyh
nixonyh deleted the nixon/input branch June 18, 2026 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants