You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(overlay): the toolbar panel is sized to the pill, not to a fixed corner (VRT-jiuo)
The panel carrying the pill was a fixed 240x104 rect pinned to the host's
bottom-right, permanently mounted in BOTH modes, and it consumes presses
across its whole frame — so that corner of the host app could not be
clicked and no frame drag could be started in it. The idle pill used 8%
of it.
The mechanism was settled by measurement last week and is the reason the
only lever is SIZE: macOS does not route mouse events through a window's
transparent parts. A panel whose content view is a bare NSView drawing
nothing at all, with isOpaque = false and a clear background, swallows
the click just the same, and ignoresMouseEvents = true would take the
pill's own clicks with it.
SIZING AND PLACEMENT ARE SPLIT. SwiftUI knows how wide the pill is; the
size is measured from it. What AppKit gets wrong is the ANCHOR — a window
resize preserves the TOP-LEFT and this panel is anchored bottom-RIGHT, so
letting AppKit apply the size slid the control left by the chrome margin.
sizingOptions = [] takes that away and toolbarFrame derives the frame
BACKWARDS from where the pill has to land, so the panel grows leftwards
and shrinks back with the control fixed at host.maxX - 20 — verified from
AppKit's own view frames (buttons at panel-local 20..207), not from the
formula under test.
Two traps, both hit and both worth the comments they now carry: measuring
`fittingSize` on the INSTALLED hosting view returns the frame it was just
stretched to, so it never shrinks (a fresh view is measured instead); and
the pill animates its width over 0.15s, so the panel grows immediately
and shrinks only once that has settled.
What remains dead is the chrome band the drop shadow and count badge draw
into. Probe 11a now pins all three claims with real clicks: a control
click that must land, a click where the old fixed panel used to sit that
must now reach the catcher, and a click in the chrome band that must
still be swallowed. Phases 3a and 9a assert the PILL's corner rather than
the panel frame, since the panel's own edges now move by design; the dead
legacy `panelFrame(for:)` shape and its only (unused) caller are gone.
The real-input legs also now report SKIPPED rather than FAILED when the
app is not frontmost — a posted click that never arrives says nothing
about the overlay either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: PARITY.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -18,6 +18,7 @@ row; each asymmetry is closed by code or has a tracked mitigation.
18
18
| Frame-mode anchoring + hover gating | shared `AnnotationSession` (`selectionAnchorFrame`, the `tool == .point` hover gate, `setTool` clearing `hovered`) rendered by the shared `OverlayView`| same | none in the code — all of it is session-level and platform-free, and one SwiftUI view renders it. ASYMMETRIC VERIFICATION, recorded as a gap rather than closed: `AnnotKitOverlayProbe` Phase 8 drives navigation, the note's `component`, the hover gate and the frame anchor against a REAL accessibility tree, and it is macOS-only (`#if os(macOS)`, AppKit + `AXUIElement`), so the iOS adapter's live behaviour is covered only by unit tests over the pure rules. Mitigated, not fixed, by the fact that everything Phase 8 asserts about anchoring and hover lives in the shared session; what remains unverified on iOS is the ADAPTER's candidate collection. Note the hover gate is also moot on touch-only iOS — hover exists there only with a trackpad or pencil — so the reported symptom cannot arise without a pointer |
19
19
| Marquee drag threshold | cursor slop (a mouse does not move on a deliberate click) | larger touch slop | ASYMMETRIC BY DESIGN, owned by the drag UI, not the adapters: a finger rolls several points on a deliberate tap, so the macOS threshold on iOS would turn taps into marquees. Below the threshold both platforms route the gesture to the point path (`select(atAXPoint:)`), per the caller contract on `select(inAXRect:)`|
20
20
| Escape (back out one level) | `NSEvent.addLocalMonitorForEvents(matching: .keyDown)` owned by `OverlayController`, resolving the shared pure `EscapeRule` (drag → card → mode, pass-through when idle) | none — no Escape key exists on iOS | ASYMMETRIC BY THE HARDWARE, not by the code: a touch device has no Escape key, so there is nothing to bind. The DECISION is platform-free and unit-tested (`EscapeRule`), so an iOS back-out affordance (a swipe, a hardware-keyboard binding on iPad) can adopt it without re-deriving the precedence; only the macOS delivery mechanism is `#if os(macOS)`. The mechanism is a LOCAL KEY MONITOR rather than a SwiftUI modifier (`.onExitCommand`) because a panel-scoped modifier only fires while the overlay panel is KEY, and the panel is made key solely by a card focusing its text field — so in annotate mode with nothing open, the state a user most wants to leave, the HOST window is key and no view in the panel ever sees the keystroke. A local monitor works precisely because AnnotKit is in-process with its host: the Escape headed for the host window passes through it first, and it can swallow what it acted on (`EscapeAction.consumesEvent`), which a global monitor cannot. It is installed in `start()` and removed in BOTH `stop()` and `unmount()` — a monitor outliving the overlay would keep eating the host app's own Escape for the life of the process |
21
+
| Toolbar panel footprint | sized to the pill (`OverlayPlacement.toolbarFrame(hostFrame:visibleFrame:panelSize:)`, measured from a fresh hosting view) | n/a — the iOS overlay is a single pass-through `UIWindow`, and the pill is not carried in a window of its own | ASYMMETRIC BY THE HOSTING MODEL. On macOS the pill lives in its own `NSPanel`, and a panel covers what it covers: macOS does not route mouse events through a window's transparent parts, so the panel's SIZE is the host app's clickable area. iOS has no equivalent problem — `PassThroughWindow` forwards touches outside the overlay's interactive subviews, which is the pass-through macOS does not offer |
21
22
| The first click on the overlay acts |`FirstMouseHostingView` (`acceptsFirstMouse -> true`) on both panels' content views | n/a | ASYMMETRIC BY THE PLATFORM, not by the code: "a mouse-down in a non-key window is discarded unless the view accepts first mouse" is an AppKit window-activation rule with no UIKit equivalent — iOS has no key window a press must first buy, and a touch on the pass-through `UIWindow` is delivered on the first tap. Nothing to mirror; the fix lives entirely in the macOS host |
22
23
| Live selection follows the content on scroll |`translateSelection(by:within:)`, driven by the same measurement `KeyablePanel.scrollWheel` already makes | none | SAME asymmetry and the same reason as the row for captured notes below: the correction is affordable on macOS only because the overlay panel intercepts every wheel event and drives the clip itself. The receiving side is shared and unit-tested (`translateSelection` is pure geometry over session state), so an iOS host that observes its own scrollers can adopt it unchanged |
23
24
| Recallable selection marks (a captured note's geometry comes back on hover) | shared: `AnnotationNote.anchorRect`/`drawnRect` snapshotted in `AnnotationSession.addNote`, `PinAttentionRule` + `RecalledMark` as pure rules, rendered by the shared `AnnotationMarks`| same | none — every part of it is platform-free. The two rects are WINDOW-LOCAL, which is the one place a platform could have crept in: macOS subtracts the overlay panel's `axOrigin`, iOS passes `.zero` because its view-tree frames are already view-local, and `addNote` takes that origin as a parameter rather than knowing which platform it is on. Both fields are UI-only and out of `CodingKeys`, so the exported record is identical on both |
0 commit comments