feat(notch): trackpad gestures, Space-swipe collapse, and motion/appearance controls - #314
feat(notch): trackpad gestures, Space-swipe collapse, and motion/appearance controls#314Mrjamedd wants to merge 18 commits into
Conversation
…s (PR wxtsky#314 watch) New upstream PR wxtsky#314 (wxtsky/CodeIsland, open Aug 14): configurable trackpad gesture support for the notch panel — swipe up/down to open/close, left/right to cycle filter modes, hover-open delay slider, haptic feedback. Not yet merged; added as T-084 (Backlog, medium priority, M effort, gate on merge). All other watched PRs (wxtsky#285, wxtsky#295, wxtsky#305, wxtsky#310, wxtsky#311) still open. No new commits on upstream/main since v1.0.31 (Jul 23). vibe-island quiet since Jul 17. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxYTPGyRVNKvyjfKbBivfh
|
Ran the full Result: 919 tests, 2 skipped, 2 failures — both in this PR's own test file, and they are a genuine contradiction rather than an environment problem. Two of your tests disagree about the same input: // testPhysicalDirectionsMapToNaturalNotchActions — matches the implementation
XCTAssertEqual(action(x: -30), .navigatePrevious)
// testGestureEmitsOnlyOnceUntilEnded — expects the opposite for the same delta
XCTAssertEqual(interpreter.consume(sample(x: -30)), .navigateNext)Both helpers feed - XCTAssertEqual(interpreter.consume(sample(x: -30)), .navigateNext)
+ XCTAssertEqual(interpreter.consume(sample(x: -30)), .navigatePrevious)
XCTAssertNil(interpreter.consume(sample(x: -30)))
XCTAssertNil(interpreter.consume(sample(ended: true)))
- XCTAssertEqual(interpreter.consume(sample(x: -30, began: true)), .navigateNext)
+ XCTAssertEqual(interpreter.consume(sample(x: -30, began: true)), .navigatePrevious)Everything else passes, including all of Rebase note: No rush — leaving it as a draft since it's yours to finish. The gesture work reads well; the hitbox tests in particular are nice. |
…speed Collapse the island when the user swipes to another desktop -------------------------------------------------------- The existing NSWorkspace.activeSpaceDidChangeNotification observer only fires once macOS has finished the transition, so an expanded island rode through the whole Spaces animation and snapped shut at the end. The responder-chain NSTouch path added earlier only fires while the panel is the key window, which is never the case when the user is working in another app. Neither NSEvent global monitors nor NSEvent.trackSwipeEvent can close that gap. Global monitors deliver scroll events but strip the NSTouch payload (measured: 156 scroll events, zero touches, no gesture events). trackSwipeEvent only operates on .scrollWheel events with precise deltas — the two-finger page-swipe gesture — so a three/four-finger Spaces swipe never reaches it. MultitouchSupport does deliver normalized finger positions to a background app with no permission prompt. It is resolved via dlopen rather than linked, and MTTouch's 96-byte stride is validated before any field is read, so a future macOS degrades to isAvailable == false and the existing activeSpaceDidChange fallback rather than crashing. Detection is scoped tightly: armed only while a collapsible surface is on screen, and the threshold crossing collapses only when the pointer is over the island's rendered content, reusing the same NotchGestureHitbox and visible-content geometry the in-view gesture monitor already computes. Without that gate a global gesture would collapse the island on every desktop switch anywhere on screen. The required finger count is read from the system trackpad preference, so it matches whichever gesture the user actually has bound. Match the contrast edge to the mascot ------------------------------------- Opt-in, off by default, so the white edge remains the shipped look and turning it off restores it exactly. Each mascot's signature color is lifted 62% toward white before it reaches the stroke, keeping a hairline highlight that carries a hue rather than a colored outline; geometry and opacities are untouched. Signature colors moved out of a private table in MascotsPage into MascotPalette so the settings swatches and the edge tint cannot drift, keyed to match MascotView's routing including aliases. Open/close speed control ------------------------ A 0.5x–2.0x slider scaling the spring response for the island's open and close animations. It scales response rather than swapping in a fixed duration, so the motion stays interruptible at every setting, and the responses are read live so the slider takes effect without a relaunch. It sits outside the hover-to-open branch because it governs the close animation too, which runs regardless of what opened the island. Shipped defaults ---------------- openOnHover now ships off: brushing the notch on the way to the menu bar should not expand the island, leaving click and swipe as the deliberate ways in. showContrastEdge stays on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
810711a flipped the horizontal mapping to `accumulatedX > 0 ? .navigateNext : .navigatePrevious` and updated testPhysicalDirectionsMapToNaturalNotchActions to match, but testGestureEmitsOnlyOnceUntilEnded kept the pre-flip expectation. The two then asserted opposite results for the same input, since `action(x:)` is just `consume(sample(x:))`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3088104 to
d0d0d7a
Compare
Adds trackpad gesture control to the island, collapses it when the user swipes to another desktop, and adds two appearance/motion settings. Rebased on
mainat v1.0.32;swift testis green (957 tests, 0 failures).Gestures on the island
Scroll gestures over the island, interpreted by a small pure state machine (
NotchGestureInterpreter) with a 24pt activation threshold and a 1.2× axis-dominance rule so a diagonal drift doesn't fire the wrong action:Horizontal direction is invertible via a setting. Gesture delivery uses both a local and a global monitor, because a nonactivating panel doesn't receive scroll events when macOS routes them to the app underneath the physical notch.
Collapse when switching desktops
An expanded island used to ride through the entire Spaces animation and snap shut at the end, because
NSWorkspace.activeSpaceDidChangeNotificationonly fires once macOS has finished the transition.Detecting the swipe as it happens turned out to need a mechanism worth spelling out, since two obvious candidates don't work:
NSEventglobal monitors deliver scroll events to a background app but strip theNSTouchpayload — measured 156 scroll events with zero touches and no gesture events at all.NSEvent.trackSwipeEventonly operates on.scrollWheelevents with precise deltas, i.e. the two-finger page-swipe gesture. A three/four-finger Spaces swipe never arrives as a scroll event, so there is nogestureAmountto read.touchesMoved(with:)path only fires while the panel is the key window — never the case when the user is working in another app.This uses MultitouchSupport, the private framework, which does deliver normalized finger positions to a background app with no permission prompt. Flagging that explicitly for review, since it's a private-API dependency:
dlopen/dlsymrather than linkedMTTouch's 96-byte stride is validated before any field is readisAvailablegoes false and the existingactiveSpaceDidChangeobserver remains as the fallback — no crash, just the old late-collapse behaviourScoping keeps it from firing on unrelated gestures:
NotchGestureHitboxand visible-content geometry the in-view gesture monitor already computes. Without this, a global gesture would collapse the island on every desktop switch anywhere on screen.TrackpadThreeFingerHorizSwipeGesture/TrackpadFourFingerHorizSwipeGesture), so it matches whichever gesture the user actually has bound, and stays silent if they've turned it offVerified against real hardware: 2006 contact frames, 4 simultaneous fingers, 4/4 swipes detected, one crossing per gesture.
Contrast edge tinted by mascot
Opt-in, off by default, so the white edge stays the shipped look and turning it off restores it exactly. Each mascot's signature color is lifted 62% toward white before it reaches the stroke, keeping a hairline highlight that carries a hue rather than a colored outline. Geometry and opacities are untouched — toggling changes hue and nothing else. The edge follows whichever mascot the bar is showing and crossfades over 0.35s when that changes.
Signature colors moved out of a private table in
MascotsPageintoMascotPalette, so the settings swatches and the edge tint can't drift, keyed to matchMascotView's routing including aliases (cursor-cli,qoderwork,google-antigravity,omp).Open / close speed
A 0.5×–2.0× slider scaling the spring response for the island's open and close animations. It scales the response rather than swapping in a fixed duration, so the motion stays interruptible at every setting, and responses are read live so the slider applies without a relaunch. It sits outside the hover-to-open branch deliberately: it governs the close animation too, which runs regardless of what opened the island.
Default change
openOnHovernow ships off. Brushing the notch on the way to the menu bar shouldn't expand the island; click and swipe remain the deliberate ways in.showContrastEdgestays on. Flagging this as the one behaviour change for existing users who never touched the setting.Tests
swift test— 957 tests, 0 failures. New coverage for the gesture interpreter, action policy, hitbox, Spaces-swipe detector (direction, single-fire-per-gesture, jitter rejection, finger-count filtering, rebaselining after a lift), mascot palette (fallbacks, aliases, blend bounds, plus guards that every mascot stays pale enough to read as a highlight and saturated ones keep a distinguishable hue), and the animation-speed math.One drive-by fix:
810711aflipped the horizontal direction mapping and updated one of its two tests.testGestureEmitsOnlyOnceUntilEndedkept the pre-flip expectation, so the suite had two tests asserting opposite results for identical input. Corrected ind0d0d7a.Notes for review
MultitouchDevice.swiftbehind an availability check, and the feature degrades to the previous behaviour if it ever stops resolving — but it is private API, and it would block Mac App Store distribution if that's ever a goal.MascotPaletteneeded agrokentry after rebasing onto the new Grok mascot; it uses white to matchGrokView. Worth a glance that the color is right.