Skip to content

fix(input): stop clicks passing through occluding windows - #74

Merged
XeldarAlz merged 2 commits into
XeldarAlz:masterfrom
BluntEXE:fix/window-occlusion-hittest
Jul 29, 2026
Merged

fix(input): stop clicks passing through occluding windows#74
XeldarAlz merged 2 commits into
XeldarAlz:masterfrom
BluntEXE:fix/window-occlusion-hittest

Conversation

@BluntEXE

Copy link
Copy Markdown
Contributor

Summary

The phone accepted clicks and hovers even when a different Dalamud window sat on top of it. Every row, button, and gesture used raw ImGui rect checks that never asked which window ImGui considers hovered this frame, so clicks passed straight through a plugin window drawn on top.

Root cause

UiInteract.Hover() already existed as the shared hit-test primitive most of the app calls through, but it never checked window ownership. PhoneWindow.Draw() now records ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows) once per frame; Hover() and Click() both gate on that. Click() also re-checks it directly, so a call site with its own hover test still can't fire an action while another window occludes the phone.

The same gap showed up outside UiInteract in a few shared components that predate it or duplicate its logic locally: Marquee's auto-hover variants, DragTracker.Begin (Control Center's swipe, Notification Center's swipe-to-dismiss), HomeInteractionController's tap and magnify hover, and the Games framework's RestartButton/Button. Fixed once in each, which covers every caller.

Per-area

  • UiInteract: window-hover gate, a 3-arg Hover overload for the existing clip parameter, and Click() hardened as a backstop for callers with a raw hover test.
  • PhoneWindow: records window-hover state once per frame before any hit test runs.
  • Home screen: tile taps, magnify hover, page dots/arrows, edit-mode Add/Done, folder contents, widget gallery and size menu.
  • Control Center: tile press/drag-start, swipe-open/dismiss, top-band cursor.
  • Dynamic Island, camera controls, notification center and banners, chat bubbles/transcript/menu, roughly twenty mini-games' board input, and every remaining Marquee/hover call site in the app. Same pattern each time.
  • Games/Flow: kept a plain bounds check for the in-progress drag path. Routing it through UiInteract.Hover let DragScrollHost's scroll-vs-tap heuristic set InputBlocked mid-drag and freeze the pipe being drawn. Only the initiating press is gated now, matching the pattern already used for the home screen's drag continuation and Solitaire's card grab.

Test plan

  • dotnet build clean after each change
  • Verified in-game via full plugin reload across Home, Control Center, Dynamic Island, Camera, Notifications, Message/social apps, and the affected mini-games, with a Dalamud window held on top

The phone accepted clicks and hovers even when a different Dalamud
window sat on top of it. Every row, button, and gesture used raw
ImGui rect checks that never asked which window ImGui considers
hovered this frame, so clicks passed straight through a plugin
window drawn on top.

Root cause: UiInteract.Hover() already existed as the shared
hit-test primitive most of the app calls through, but it never
checked window ownership. PhoneWindow.Draw() now records
ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows) once per
frame; Hover() and Click() both gate on that. Click() also
re-checks it directly, so a call site with its own hover test still
can't fire an action while another window occludes the phone.

The same gap showed up outside UiInteract in a few shared
components that predate it or duplicate its logic locally:
Marquee's auto-hover variants, DragTracker.Begin (Control Center's
swipe, Notification Center's swipe-to-dismiss),
HomeInteractionController's tap and magnify hover, and the Games
framework's RestartButton/Button. Fixed once in each, which covers
every caller.

Per-area:
- UiInteract: window-hover gate, a 3-arg Hover overload for the
  existing clip parameter, and Click() hardened as a backstop for
  callers with a raw hover test.
- PhoneWindow: records window-hover state once per frame before any
  hit test runs.
- Home screen: tile taps, magnify hover, page dots/arrows,
  edit-mode Add/Done, folder contents, widget gallery and size menu.
- Control Center: tile press/drag-start, swipe-open/dismiss,
  top-band cursor.
- Dynamic Island, camera controls, notification center and banners,
  chat bubbles/transcript/menu, roughly twenty mini-games' board
  input, and every remaining Marquee/hover call site in the app.
  Same pattern each time.
- Games/Flow: kept a plain bounds check for the in-progress drag
  path. Routing it through UiInteract.Hover let DragScrollHost's
  scroll-vs-tap heuristic set InputBlocked mid-drag and freeze the
  pipe being drawn. Only the initiating press is gated now, matching
  the pattern already used for the home screen's drag continuation
  and Solitaire's card grab.

Test plan:
- dotnet build clean after each change
- Verified in-game via full plugin reload across Home, Control
  Center, Dynamic Island, Camera, Notifications, Message/social
  apps, and the affected mini-games, with a Dalamud window held on
  top

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@XeldarAlz XeldarAlz left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Built clean on bb1a4de (dotnet build -c Release, 0 errors, 4 pre-existing NU1902 warnings) and 143/143 tests pass. Not verified in-game, so the runtime findings below come from reading the code.

The window-hover gate itself is the right fix. The problem is routing it through UiInteract.Hover, which also drags in InputBlocked and MouseOverOverlay: the overlays that raise those flags for everyone else now block themselves. Four blocking spots inline.

Comment thread src/Aetherphone/Windows/Components/DropdownMenu.cs Outdated
Comment thread src/Aetherphone/Apps/Collections/CollectionsApp.Browse.cs Outdated
Comment thread src/Aetherphone/Apps/Jobs/JobsApp.ColorPicker.cs Outdated
Comment thread src/Aetherphone/Windows/PhoneWindow.cs Outdated
Comment thread src/Aetherphone/Core/Input/DragTracker.cs Outdated
Comment thread src/Aetherphone/Windows/Components/ConfirmOverlay.cs Outdated
Comment thread src/Aetherphone/Apps/Games/Breakout/BreakoutApp.cs Outdated
Comment thread src/Aetherphone/Apps/Games/Flow/FlowApp.cs Outdated
… gate

UiInteract.Hover now gates on InputBlocked and MouseOverOverlay, which
this PR needed for the occlusion fix, but that gate also disabled the
floating UI that is itself the reason a host sets it:

- Add UiInteract.HoverWindowOnly, a window-gate-only hover test for
  content that must stay live despite its own host's BlockThisFrame
  or HoverOverlay reservation this frame. Applied to DropdownMenu,
  ChatMenuController's reaction strip, CollectionsApp's source menu,
  JobsApp's color preset/save buttons and category save button, and
  DragTracker's start-zone check (also fixes the Flow drag freeze,
  a different symptom of the same coupling).
- Add UiInteract.ClickedOutside for the "tap outside to dismiss"
  pattern. The old `!Hover(rect)` form fails toward canceling: with
  another window focused, Hover is false everywhere, so a click in
  that unrelated window read as "outside" and dismissed the overlay.
  ClickedOutside requires the click to land inside this window first.
  Applied to ConfirmOverlay, ShareSheet, ReportOverlay,
  MusicApp.Playlists's card overlay, and PersonPicker.
- PhoneWindow's IsWindowHovered now passes
  AllowWhenBlockedByActiveItem, so focusing an InputText (chat
  composer, search field) no longer kills the whole UiInteract layer
  until focus is lost.
- Sorted using directives in the games touched by this PR's using
  churn (Breakout, Pairs, Reversi, Simon, Solitaire, Tetris,
  Twenty48, WaterSort, Whack, Camera).
- FlowApp.ResolveHover caches GetMousePos() once instead of calling
  it twice per frame, matching Sweeper/Chess/Sudoku/Reversi/Whack.
@XeldarAlz
XeldarAlz merged commit 121e44e into XeldarAlz:master Jul 29, 2026
2 checks passed
XeldarAlz added a commit to BluntEXE/FFXIV-Aetherphone that referenced this pull request Jul 29, 2026
Resolves the conflicts XeldarAlz#73 and XeldarAlz#74 created.

- AppHeader.DrawTitleWithReserve now carries both new optional parameters
  (style from this branch, leftReserve from XeldarAlz#73).
- LinkshellRow keeps this branch's timeReserve subtraction on top of
  master's UiInteract.Hover gate.
- Dailies, Timers and Message drop master's row hover locals: the rows
  moved to Marquee.DrawLeftAuto, which hit-tests internally, so the
  locals were dead (and Message's shadowed an inner declaration).
- Tetris keeps the outerMargin extraction with master's gated hover.
- Music keeps the showAuthor branch with master's gated hover.

Also converts the 15 raw ImGui.IsMouseHoveringRect hit tests this branch
reintroduced to UiInteract.Hover. Master holds the invariant that the
call only appears inside UiInteract; leaving them raw would regress the
occlusion fix for every text row this branch touches.
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