fix(apple): recover dropped macOS client features lost in the W7/W8 refactor
apple / swift (push) Successful in 1m20s
android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 1m17s
ci / rust (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Successful in 24s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 18s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 18s
docker / deploy-docs (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
release / apple (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
apple / swift (push) Successful in 1m20s
android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 1m17s
ci / rust (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Successful in 24s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 18s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 18s
docker / deploy-docs (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
release / apple (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
The W7/W8 client refactor (extracting FullscreenController/ApprovalRequest out of ContentView, splitting Settings) landed the DefaultsKeys + scaffolding but dropped the feature WIRING, which was uncommitted WIP shelved in a stash during the 16:28 clipboard/v4 reconcile. Recovered from that stash (+ its untracked-files parent for the whole new ModifierLayout.swift) and re-ported onto the refactored tree: - invertScroll: applied in InputCapture.sendScroll (the one scroll sink) + Settings toggle - modifierLayout (Cmd/Option switch): restored ModifierLayout.swift enum + KeyMaps .applyModifierLayout + InputCapture.emitKey wire-boundary + Settings picker - fullscreen shortcut (Ctrl+Cmd+F): InputCapture ⌃⌘F interception + onToggleFullscreen + StreamView wiring + Stream-menu item (the .punktfunkToggleFullscreen sink + FullscreenController observer already survived on main) - render scale: Settings picker + MatchWindowFollower renderScale/maxDimension application + StreamView/StreamViewIOS plumbing (RenderScale.swift already on main) StreamCommands re-ported by hand (the stash hunk deleted the since-landed clipboard item — kept it, added the fullscreen item alongside). Recovered ModifierLayout + RenderScale tests. swift build green; 15 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@ import UIKit
|
||||
import Foundation
|
||||
import GameController
|
||||
import PunktfunkCore
|
||||
import PunktfunkShared
|
||||
import os
|
||||
|
||||
/// Diagnostic logging for the input path. Off by default (input is high-rate); set
|
||||
@@ -125,6 +126,12 @@ public final class InputCapture {
|
||||
public var onDisconnect: (() -> Void)?
|
||||
public var onCycleStats: (() -> Void)?
|
||||
|
||||
/// Fired on ⌃⌘F (macOS) — toggle the streaming window in/out of fullscreen. Detected in the
|
||||
/// monitor only WHILE FORWARDING, for the same reason as the ⌃⌥⇧ combos: a captured stream view
|
||||
/// swallows keys, so the Stream menu's identical ⌃⌘F equivalent never reaches it; released, the
|
||||
/// menu handles it. Main queue.
|
||||
public var onToggleFullscreen: (() -> Void)?
|
||||
|
||||
#if os(iOS)
|
||||
/// Windows VKs of the three modifier classes in the ⌃⌥⇧Q release chord, both L/R sides:
|
||||
/// control (0xA2/0xA3), option (0xA4/0xA5), shift (0xA0/0xA1). Used to sift the HID key stream.
|
||||
@@ -273,6 +280,14 @@ public final class InputCapture {
|
||||
break
|
||||
}
|
||||
}
|
||||
// ⌃⌘F toggles the streaming window's fullscreen. Intercepted only while forwarding (the
|
||||
// captured stream view swallows the menu's identical equivalent); the F is latched so its
|
||||
// keyUp can't type into the host. keyCode 3 = kVK_ANSI_F (layout-independent).
|
||||
if self.forwarding, flags == [.control, .command], event.keyCode == 3 /* F */ {
|
||||
self.suppressedVK = 0x46 // VK_F — the same physical F is en route via GC
|
||||
self.onToggleFullscreen?()
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
}
|
||||
#endif
|
||||
@@ -318,7 +333,7 @@ public final class InputCapture {
|
||||
chordModifiersDown.removeAll()
|
||||
suppressedVK = nil
|
||||
for vk in pressedVKs {
|
||||
connection.send(.key(vk, down: false))
|
||||
emitKey(vk, down: false)
|
||||
}
|
||||
for button in pressedButtons {
|
||||
connection.send(.mouseButton(button, down: false))
|
||||
@@ -331,6 +346,15 @@ public final class InputCapture {
|
||||
residualScrollY = 0
|
||||
}
|
||||
|
||||
/// The single wire boundary for a key event. Every `.key` send funnels through here so the
|
||||
/// active location-based modifier layout is applied in exactly one place while all internal
|
||||
/// press/release bookkeeping (`pressedVKs`, `cmdKeysDown`, `resolveModifier`'s `isDown`) stays on
|
||||
/// the physical VK. Read live from the setting so a mid-session change (rare) takes on the next
|
||||
/// key without re-arming capture. Non-modifier VKs pass through untouched.
|
||||
private func emitKey(_ vk: UInt32, down: Bool) {
|
||||
connection.send(.key(Self.applyModifierLayout(vk, ModifierLayout.current), down: down))
|
||||
}
|
||||
|
||||
/// Release any held MOUSE buttons host-side, leaving keyboard state untouched. Used when
|
||||
/// the iPad pointer lock drops while a GCMouse button is held: by then the GCMouse release
|
||||
/// handler is gated off (`gcMouseForwarding` is false), so it can't deliver the release
|
||||
@@ -399,7 +423,7 @@ public final class InputCapture {
|
||||
inputLog.debug(
|
||||
"key \(vk, privacy: .public) \(down ? "down" : "up", privacy: .public) sent")
|
||||
}
|
||||
connection.send(.key(vk, down: down))
|
||||
emitKey(vk, down: down)
|
||||
}
|
||||
|
||||
/// NSEvent modifier path (macOS): modifier keys never fire keyDown/keyUp — they arrive
|
||||
@@ -566,8 +590,15 @@ public final class InputCapture {
|
||||
/// Moonlight's convention). Fed by StreamLayerView.scrollWheel — the only delivery
|
||||
/// path that covers trackpad/Magic Mouse gestures (GCMouse never reports them).
|
||||
/// Fractional remainders accumulate so slow two-finger scrolling isn't truncated away.
|
||||
public func sendScroll(dx: Float, dy: Float) {
|
||||
public func sendScroll(dx rawDx: Float, dy rawDy: Float) {
|
||||
guard forwarding else { return }
|
||||
// Optionally invert both axes (read live). This is the ONE scroll sink for every platform —
|
||||
// the macOS wheel, the iOS trackpad pan, and a GCMouse wheel all land here — so the toggle
|
||||
// flips them consistently. Residuals are accumulated AFTER inversion so a direction change
|
||||
// between events doesn't strand a fractional remainder of the old sign.
|
||||
let invert = UserDefaults.standard.bool(forKey: DefaultsKey.invertScroll)
|
||||
let dx = invert ? -rawDx : rawDx
|
||||
let dy = invert ? -rawDy : rawDy
|
||||
let fy = dy + residualScrollY
|
||||
let fx = dx + residualScrollX
|
||||
let iy = fy.rounded(.towardZero)
|
||||
@@ -643,7 +674,7 @@ public final class InputCapture {
|
||||
} else {
|
||||
self.pressedVKs.remove(vk)
|
||||
}
|
||||
self.connection.send(.key(vk, down: pressed))
|
||||
self.emitKey(vk, down: pressed)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
// InputCapture's static keymap tables: HID usage → Windows VK (the GCKeyboard path on all
|
||||
// platforms) and, on macOS, NSEvent.keyCode → Windows VK (the NSEvent key path).
|
||||
|
||||
import PunktfunkShared
|
||||
|
||||
extension InputCapture {
|
||||
/// Remap one modifier VK for the active location-based [`ModifierLayout`] just before it goes on
|
||||
/// the wire. `.mac` (and every non-modifier VK) is the identity. `.windows` swaps the Alt vs
|
||||
/// Super/Windows ROLE between the Option and Command keys while KEEPING the side, so a Windows
|
||||
/// user's `⌘ = Alt, ⌥ = Win` muscle memory lands correctly:
|
||||
/// L Command 0x5B ↔ L Alt 0xA4 · R Command 0x5C ↔ R Alt 0xA5.
|
||||
/// It's applied ONLY at the send boundary (`InputCapture.emitKey`) — all press/release
|
||||
/// bookkeeping stays on the physical VK, so modifier direction tracking and the client-local
|
||||
/// ⌘-based shortcuts are untouched. The swap is its own inverse: a key that went down remapped
|
||||
/// goes up remapped, so the host never sees a stuck modifier.
|
||||
static func applyModifierLayout(_ vk: UInt32, _ layout: ModifierLayout) -> UInt32 {
|
||||
guard layout == .windows else { return vk }
|
||||
switch vk {
|
||||
case 0x5B: return 0xA4 // L Command → L Alt (VK_LWIN → VK_LMENU)
|
||||
case 0x5C: return 0xA5 // R Command → R Alt (VK_RWIN → VK_RMENU)
|
||||
case 0xA4: return 0x5B // L Option → L Win (VK_LMENU → VK_LWIN)
|
||||
case 0xA5: return 0x5C // R Option → R Win (VK_RMENU → VK_RWIN)
|
||||
default: return vk
|
||||
}
|
||||
}
|
||||
/// HID usage (GCKeyCode raw) → Windows VK (the host maps VK → evdev; every VK emitted
|
||||
/// here exists in punktfunk-host/src/inject.rs::vk_to_evdev — extend the two together).
|
||||
static let hidToVK: [Int: UInt32] = {
|
||||
|
||||
Reference in New Issue
Block a user