fix(input): stop clicks passing through occluding windows - #74
Merged
XeldarAlz merged 2 commits intoJul 29, 2026
Conversation
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
reviewed
Jul 28, 2026
XeldarAlz
left a comment
Owner
There was a problem hiding this comment.
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.
… 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
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.
This was referenced Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 recordsImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows)once per frame;Hover()andClick()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
UiInteractin 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'sRestartButton/Button. Fixed once in each, which covers every caller.Per-area
UiInteract: window-hover gate, a 3-argHoveroverload for the existingclipparameter, andClick()hardened as a backstop for callers with a raw hover test.PhoneWindow: records window-hover state once per frame before any hit test runs.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 throughUiInteract.HoverletDragScrollHost's scroll-vs-tap heuristic setInputBlockedmid-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 buildclean after each change