diff options
| -rw-r--r-- | docs/macos.md | 41 | ||||
| -rw-r--r-- | src/CHANGELOG.md | 18 | ||||
| -rw-r--r-- | src/macos/Sources/PardesView.swift | 52 | ||||
| -rw-r--r-- | test/macos-snapshots/trackpad.golden | 2 | ||||
| -rw-r--r-- | test/macos-snapshots/trackpad.snap | 20 | ||||
| -rw-r--r-- | test/macos_e2e.swift | 8 |
6 files changed, 87 insertions, 54 deletions
diff --git a/docs/macos.md b/docs/macos.md index 0ce29bf9..ae990944 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -177,14 +177,14 @@ marks the view dirty. acme wants three mouse buttons — 1 selects, 2 executes, 3 looks — and the machine this runs on has a glass rectangle. So the rectangle is taught to speak -the vocabulary, and the mapping is the one macOS itself already suggests: +the vocabulary, cheapest gesture to commonest verb: | gesture | button | verb | | --- | --- | --- | | one finger | 1 | select | -| two fingers | 3 | Look | +| two fingers | 2 | Exec | | three fingers | 2 | Exec | -| a deep press | 2 | Exec | +| a deep press | 3 | Look | | two fingers twisted | — | `n` / `N` | **The finger count decides, not the button stream.** This is the part that only @@ -192,8 +192,9 @@ real hardware could teach, and it is worth spelling out because the obvious implementation is wrong. macOS's secondary click is "click or tap with **two or more** fingers", so with that setting on — the default — a *three*-finger click is delivered as `rightMouseDown` exactly like a two-finger one. A view that -trusts the stream cannot tell them apart and quietly does Look for both. The -trace that caught it, from a real trackpad: +trusts the stream cannot tell them apart and quietly does Look for both, which +is the one thing a multi-finger click here must NOT do. The trace that caught +it, from a real trackpad: ``` pardes: rightMouseDown: resting=2 @@ -205,10 +206,18 @@ button from the fingers first and falls back to the stream only when there are no fingers to count — which is exactly the real-mouse case, where right is Look and the middle button is Exec. +Two and three fingers landing on the same verb is therefore not a wasted +gesture, it is the hardware refusing to distinguish them under the default +setting. Look is the deep press instead: on a trackpad it is the one gesture +the system does not overload, and it wants "Force Click and haptic feedback" +on in System Settings — which nothing in this process can read, so a Mac with +that off (or a trackpad with no force sensor) reaches Look through a real +mouse's right button and the `Look` builtin, keyboard Enter included. + The count comes from `event.touches(matching: .touching, in: nil)`, with `nil` rather than the view because that argument filters on touch/view association and an association that fails does not raise, it returns zero fingers — a -two-finger Look silently degrading into a select. Belt and braces: the view also +two-finger Exec silently degrading into a select. Belt and braces: the view also keeps a running `restingFingers` from the four `touchesXxx` callbacks, because the touch set hanging off a *mouse* event is an accident of how the click was produced and can come back empty. The mouse event's own set wins when it has @@ -220,21 +229,21 @@ core tracks a drag keyed by button, and answering a press of 3 with a release of A deep press arrives as `pressureChange` reaching stage 2, and only the transition counts — AppKit repeats stage 2 for as long as the finger stays down. -By then a press has already gone out, so it is *released* before the middle one -is sent. That ordering is not tidiness: a middle press arriving while the core -holds a left select-drag is acme's 1-2 chord, which is **Cut**. The release +By then a press has already gone out, so it is *released* before the right one +is sent. That ordering is not tidiness: a right press arriving while the core +holds a left select-drag is acme's 1-3 chord, which is **Paste**. The release costs a cursor move at the click point, which is what clicking there would have -done anyway. +done anyway; a multi-finger press released this way fires the Exec its fingers +already asked for. Which press gets upgraded is deliberately not restricted to the left one, and that too came from the trace: on a Force Touch trackpad the deep press usually rides a click that already went out on the *right* stream, so gating on a latched left button meant the conversion never fired at all — the log showed `pressure: stage=2 latched=nil` and nothing else. Any in-flight click upgrades; -already-Exec is the only case with nothing to do. The view also needs +already-Look is the only case with nothing to do. The view also needs `NSPressureConfiguration(pressureBehavior: .primaryDeepClick)` or stage 2 is the -system's business and never arrives — and the user needs "Force Click and -haptic feedback" on in System Settings, which nothing in this process can read. +system's business and never arrives. Twisting two fingers is a dial, and a dial over a list of look-able places is `n`. A notch moves the SELECTION one place along and opens nothing; Enter opens @@ -388,11 +397,11 @@ layer here; everything pardes binds lives on the other four. All three verbs are cheap to mistake for broken, because acme's verbs are about the *word under the pointer* and most words resolve to nothing: -- **Look** (two fingers) on a filename opens it; on a word that names no file +- **Look** (a deep press) on a filename opens it; on a word that names no file and matches nothing else on screen, it searches, finds where it already is, and the screen does not move. The pulse still fires — the gesture worked. -- **Exec** (three fingers) on a builtin name runs it. On ordinary prose it types - that word at a shell, which needs a terminal pane to type into. +- **Exec** (two or three fingers) on a builtin name runs it. On ordinary prose + it types that word at a shell, which needs a terminal pane to type into. - **`n`/`N`** (twist) moves the SELECTION to the next look-able place and opens nothing; Enter opens what it landed on. It walks a ring across panes — the ones a Look came from first, then the output buffers none has — so a twist diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 714497b6..39c2057d 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -2,6 +2,24 @@ ## 0.0.2 +- The macOS trackpad's two verbs trade places: a two-finger click is Exec and + a deep press is Look. Exec is what a hand spends a session doing, so it takes + the gesture that costs nothing, and the verb that goes somewhere is worth one + deliberate push past the click. Three fingers stay Exec, and that is the + hardware rather than a gesture left unused: macOS's secondary click is "click + or tap with two or more fingers", so a three-finger click arrives as + `rightMouseDown` exactly like a two-finger one, and the resting count is the + only thing that keeps either of them off Look. `pressureChange` still + releases the click in flight before the new button goes out, which now steps + around acme's 1-3 chord (Paste) where it used to step around 1-2 (Cut). Look + by finger count is gone, so a Mac with "Force Click and haptic feedback" off + — or a trackpad with no force sensor — reaches it through a real mouse's + right button, the `Look` builtin, or Enter on the word. `trackpad.snap` + asserts all of it without a trackpad, because `NSTouch` and the pressure + stages have no public constructors and `Trackpad` is therefore pure policy a + script can name finger counts to; the swap left every golden screen + byte-identical under the other gesture, which is what shows the two paths + were exchanged and nothing else moved. - A filtered terminal costs what an unfiltered one does. `Filter`'s second stage asked `RGB.contrast` for every cell it painted, and that call ends in `std.math.pow` six times over — a libm round trip per cell, per frame, to diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift index 3f5f1346..f9afa7b1 100644 --- a/src/macos/Sources/PardesView.swift +++ b/src/macos/Sources/PardesView.swift @@ -45,25 +45,27 @@ protocol PardesViewDelegate: AnyObject { /// constructed, so this is the part of the gesture a test can reach. /// /// acme's three buttons are the whole vocabulary — 1 selects, 2 executes, -/// 3 looks — and a trackpad has one surface. Two fingers is the gesture macOS -/// itself spells "secondary", so it is Look; three is the one left over, so it -/// is Exec, the heavier verb, which is also what a deep press means. +/// 3 looks — and a trackpad has one surface. More than one finger is Exec, +/// because running something is what a hand does over and over and it gets +/// the gesture that costs nothing; Look is the deep press, one deliberate +/// push past the click for the verb that goes somewhere. /// -/// The finger count has to win over the stream, and that is not a preference. -/// macOS's secondary click is "click or tap with TWO OR MORE fingers": with it -/// on, a three-finger click is delivered as rightMouseDown exactly like a -/// two-finger one, and a view that trusts the stream cannot tell them apart — -/// three fingers silently did Look. Measured on real hardware, which is the -/// only way this was ever going to be found: `rightMouseDown: resting=3`. +/// Two and three fingers are the same verb here, and that is the hardware +/// talking rather than a shrug. macOS's secondary click is "click or tap with +/// TWO OR MORE fingers": with it on — the default — a three-finger click is +/// delivered as rightMouseDown exactly like a two-finger one, and a view that +/// trusts the stream calls both of them Look. Measured on real hardware, which +/// is the only way this was ever going to be found: +/// `rightMouseDown: resting=3`. So the count is what keeps a multi-finger +/// click off Look, and the stream cannot be trusted to do it. /// -/// So the stream is only the fallback, for when there are no fingers to count: +/// The stream is only the fallback, for when there are no fingers to count: /// a real mouse's right button is Look and its middle button is Exec, and both /// arrive with an empty touch set. enum Trackpad { static func button(stream: pardes_mouse_button_e, fingers: Int) -> pardes_mouse_button_e { switch fingers { - case 2: return PARDES_MOUSE_RIGHT - case 3...: return PARDES_MOUSE_MIDDLE + case 2...: return PARDES_MOUSE_MIDDLE // One finger, or none to count: the stream is the answer. A trackpad // single click comes in on the left stream and stays left; a real // mouse's right and middle buttons keep their acme meanings. @@ -72,9 +74,9 @@ enum Trackpad { } /// A force click is a deliberate second gesture on top of an ordinary one, - /// so it gets the verb that does something rather than the one that - /// navigates. - static let forceClickButton: pardes_mouse_button_e = PARDES_MOUSE_MIDDLE + /// so it gets the verb that goes somewhere rather than the one that runs + /// something: press harder and you Look. + static let forceClickButton: pardes_mouse_button_e = PARDES_MOUSE_RIGHT } // Matches bg_default/fg_default in src/gui/gui.zig and DEFAULT_FG/DEFAULT_BG in @@ -643,7 +645,7 @@ final class PardesView: NSView { // hardware it does not always: AppKit routes NSTouch through the four // touchesXxx callbacks, and the touch set hanging off a *mouse* event can // come back empty depending on how the click was produced. Empty reads as - // one finger, which is a two-finger Look silently degrading into a select + // one finger, which is a two-finger Exec silently degrading into a select // — the exact failure this was supposed to avoid. So the count is // maintained here and the mouse event's own set is preferred only when it // has something in it. @@ -674,7 +676,7 @@ final class PardesView: NSView { registerForDraggedTypes([.fileURL]) // Indirect touches are the trackpad's. Without this the touch set is // always empty and every click looks like one finger, which is exactly - // the bug that would make two-finger Look silently never fire. + // the bug that would make two-finger Exec silently never fire. allowedTouchTypes = [.indirect] // Without a pressure configuration the deep-press stages are the // system's business and stage 2 may never be delivered here. @@ -1737,15 +1739,17 @@ final class PardesView: NSView { /// flight. AppKit keeps sending stage-2 events while the finger stays down, /// so only the transition counts. /// - /// Whatever button is in flight is released before the middle one goes out: - /// a middle press arriving while the core holds a left select-drag is - /// acme's 1-2 chord, which is Cut. Releasing first costs a cursor move at - /// the click point — which is what clicking there would have done anyway. + /// Whatever button is in flight is released before the look one goes out: + /// a right press arriving while the core holds a left select-drag is + /// acme's 1-3 chord, which is Paste. Releasing first costs a cursor move + /// at the click point — which is what clicking there would have done + /// anyway; a multi-finger press released this way fires its own Exec, + /// which is what the fingers already asked for. /// /// Not gated on the press being a LEFT one, which is what stopped this /// working: on a Force Touch trackpad the deep press is just as likely to - /// have arrived on the right stream, and an in-flight Look upgraded by - /// pressing harder is precisely the gesture. Already-Exec is the only case + /// have arrived on the right stream, and an in-flight Exec upgraded by + /// pressing harder is precisely the gesture. Already-Look is the only case /// with nothing to do. override func pressureChange(with event: NSEvent) { trace("pressure: stage=\(event.stage) latched=\(String(describing: latchedButton?.rawValue))") @@ -1829,7 +1833,7 @@ final class PardesView: NSView { /// (secondary click, three-finger drag, force click, "look up"), none of /// which this process can read, and every one of which turns a gesture into /// a different NSEvent or into none at all. When someone reports that - /// two-finger Look does nothing, this is the only thing that can answer + /// two-finger Exec does nothing, this is the only thing that can answer /// whether AppKit saw two fingers, one, or no click at all. private func trace(_ message: @autoclosure () -> String) { guard PardesView.tracing else { return } diff --git a/test/macos-snapshots/trackpad.golden b/test/macos-snapshots/trackpad.golden index 631e5af9..b5f9f567 100644 --- a/test/macos-snapshots/trackpad.golden +++ b/test/macos-snapshots/trackpad.golden @@ -246,7 +246,7 @@ | 1 @p0:2:3-6 x MARK a | 2 @p0:4:3-6 x MARK b | 3 @p0:6:3-6 x MARK c -== snap force grid=100x30 cursor=57,2 +== snap exec3 grid=100x30 cursor=57,2 |New Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill | /private/tmp/pardes-macos-e2e/trackpad/cwd Save /private/tmp/pardes-macos-e2e/trackpad/cwd/+New | RK c\n' 1 diff --git a/test/macos-snapshots/trackpad.snap b/test/macos-snapshots/trackpad.snap index 05f46e1f..df843e5d 100644 --- a/test/macos-snapshots/trackpad.snap +++ b/test/macos-snapshots/trackpad.snap @@ -3,8 +3,8 @@ # # Trackpad.button(fingers:) is pure policy with no NSEvent in it, so a script # can name how many fingers were resting and get back the button a hand would -# have produced: two is acme's button 3 (Look), three or more is button 2 -# (Exec), and a deep press is button 2 by another route. pardes_take_haptic is +# have produced: two or more fingers is acme's button 2 (Exec), one finger is +# left to select, and a deep press is button 3 (Look). pardes_take_haptic is # the other half — it is how the pulse the core armed is read back on a machine # with nothing to feel it with. # @@ -36,9 +36,9 @@ haptic look snap adapter # Back to a clean slate for the finger-count cases below. haptic none -# TWO FINGERS RESTING = button 3 = Look. MARK names no file, so looking at it +# A DEEP PRESS = button 3 = Look. MARK names no file, so looking at it # searches, and the hits land in a +Search buffer split under the shell. -fingers 2 4 3 +force 4 3 wait 10000 @p0: stable 700 15000 haptic look @@ -70,18 +70,20 @@ snap scrolled wheel wheeldown 4 5 stable 700 10000 snap notched -# THREE FINGERS = button 2 = Exec. `New` in the top tag is a builtin, so this +# TWO FINGERS = button 2 = Exec. `New` in the top tag is a builtin, so this # spawns a pane rather than running something the machine would have to own. -fingers 3 1 0 +fingers 2 1 0 stable 700 20000 haptic exec snap exec -# A DEEP PRESS is the same button 2 by another route; `Newcol` proves it went +# THREE FINGERS are the same button 2, and that is the case the finger count +# exists for: macOS delivers them on the right stream exactly like two, so a +# view that trusted the stream would Look here. `Newcol` proves this one went # somewhere different from the click above. -force 5 0 +fingers 3 5 0 stable 700 20000 haptic exec -snap force +snap exec3 # One finger is still an ordinary left press, and selecting is not a verb the # hand should feel. fingers 1 4 3 diff --git a/test/macos_e2e.swift b/test/macos_e2e.swift index 800e2d43..87733ea9 100644 --- a/test/macos_e2e.swift +++ b/test/macos_e2e.swift @@ -658,16 +658,16 @@ private final class Driver: PardesViewDelegate { case "fingers": let view = try live() - // The whole two-finger-Look / three-finger-Exec feature, asserted - // without a trackpad: the policy is pure and lives in Trackpad, so a - // script names the finger count and gets the button a hand gets. + // The whole finger-count feature, asserted without a trackpad: the + // policy is pure and lives in Trackpad, so a script names the + // finger count and gets the button a hand gets. // // The stream is the one macOS actually uses for a multi-finger // click when its own secondary click is on, which is the default // and which is what real hardware was observed doing for BOTH two // and three fingers. Passing RIGHT here is therefore the harder // case: it is the one where a view that trusted the stream would - // call three fingers a Look. + // call every multi-finger click a Look instead of an Exec. let count = try int(args, 0, "fingers count") let cell = try gridPoint(args, 1, "fingers") let stream = count >= 2 ? PARDES_MOUSE_RIGHT : PARDES_MOUSE_LEFT |
