fix(apple): capture the PS/Home button + fullscreen only while streaming
ci / web (push) Successful in 26s
ci / docs-site (push) Successful in 30s
apple / swift (push) Successful in 1m16s
ci / bench (push) Successful in 1m34s
ci / rust (push) Successful in 2m11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 2m26s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m53s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m21s
ci / web (push) Successful in 26s
ci / docs-site (push) Successful in 30s
apple / swift (push) Successful in 1m16s
ci / bench (push) Successful in 1m34s
ci / rust (push) Successful in 2m11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 2m26s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m53s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m21s
Two issues from live Mac testing, plus a requested fullscreen option: - PS button: the Home/PS button (→ guide; the host maps it to the DualSense PS bit) does not reliably fire GCExtendedGamepad.valueChangedHandler on macOS, so its presses were dropped. Add a dedicated buttonHome.pressedChangedHandler that re-syncs. The host already maps BTN_GUIDE→PS, so this is the missing client half. - Fullscreen: a macOS FullscreenController (NSViewRepresentable) takes the window fullscreen while a session is up (incl. the trust prompt over the blurred stream) and restores it on the host list — so only the stream is fullscreen, not the picker. New `fullscreenWhileStreaming` setting (default on) + a Settings "Window" toggle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ struct ContentView: View {
|
|||||||
@AppStorage(DefaultsKey.compositor) private var compositor = 0
|
@AppStorage(DefaultsKey.compositor) private var compositor = 0
|
||||||
@AppStorage(DefaultsKey.gamepadType) private var gamepadType = 0
|
@AppStorage(DefaultsKey.gamepadType) private var gamepadType = 0
|
||||||
@AppStorage(DefaultsKey.bitrateKbps) private var bitrateKbps = 0
|
@AppStorage(DefaultsKey.bitrateKbps) private var bitrateKbps = 0
|
||||||
|
@AppStorage(DefaultsKey.fullscreenWhileStreaming) private var fullscreenWhileStreaming = true
|
||||||
@State private var showAddHost = false
|
@State private var showAddHost = false
|
||||||
@State private var pairingTarget: StoredHost?
|
@State private var pairingTarget: StoredHost?
|
||||||
@State private var speedTestTarget: StoredHost?
|
@State private var speedTestTarget: StoredHost?
|
||||||
@@ -58,6 +59,11 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onDisappear { model.disconnect() } // window closed mid-session (Cmd+N spawns more)
|
.onDisappear { model.disconnect() } // window closed mid-session (Cmd+N spawns more)
|
||||||
|
#if os(macOS)
|
||||||
|
// Fullscreen only while a session is up (incl. the trust prompt over the blurred stream),
|
||||||
|
// windowed on the host list — so the picker isn't forced fullscreen. Opt-out in Settings.
|
||||||
|
.background(FullscreenController(active: fullscreenWhileStreaming && model.connection != nil))
|
||||||
|
#endif
|
||||||
// On the outer Group so the sheet survives the trust-prompt → home transition
|
// On the outer Group so the sheet survives the trust-prompt → home transition
|
||||||
// (the "Pair with PIN instead" path disconnects first — the host's accept loop
|
// (the "Pair with PIN instead" path disconnects first — the host's accept loop
|
||||||
// is sequential, a pairing connection would queue behind the live session).
|
// is sequential, a pairing connection would queue behind the live session).
|
||||||
@@ -287,3 +293,24 @@ struct ContentView: View {
|
|||||||
autoTrust: true)
|
autoTrust: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
/// Drives the hosting window in/out of native fullscreen from SwiftUI state. Mounted invisibly in
|
||||||
|
/// the view tree; on each `active` change it captures the window and toggles fullscreen only when
|
||||||
|
/// the current state differs (so it never fights a toggle already in flight, and never touches a
|
||||||
|
/// window the user fullscreened manually unless `active` says otherwise).
|
||||||
|
private struct FullscreenController: NSViewRepresentable {
|
||||||
|
let active: Bool
|
||||||
|
|
||||||
|
func makeNSView(context: Context) -> NSView { NSView() }
|
||||||
|
|
||||||
|
func updateNSView(_ view: NSView, context: Context) {
|
||||||
|
let want = active
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let window = view.window else { return }
|
||||||
|
let isFull = window.styleMask.contains(.fullScreen)
|
||||||
|
if want != isFull { window.toggleFullScreen(nil) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ struct SettingsView: View {
|
|||||||
@AppStorage(DefaultsKey.presenter) private var presenter = "stage1"
|
@AppStorage(DefaultsKey.presenter) private var presenter = "stage1"
|
||||||
@AppStorage(DefaultsKey.cursorMode) private var cursorMode = "auto"
|
@AppStorage(DefaultsKey.cursorMode) private var cursorMode = "auto"
|
||||||
@AppStorage(DefaultsKey.libraryEnabled) private var libraryEnabled = false
|
@AppStorage(DefaultsKey.libraryEnabled) private var libraryEnabled = false
|
||||||
|
@AppStorage(DefaultsKey.fullscreenWhileStreaming) private var fullscreenWhileStreaming = true
|
||||||
@AppStorage(DefaultsKey.micEnabled) private var micEnabled = true
|
@AppStorage(DefaultsKey.micEnabled) private var micEnabled = true
|
||||||
@ObservedObject private var gamepads = GamepadManager.shared
|
@ObservedObject private var gamepads = GamepadManager.shared
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@@ -374,6 +375,16 @@ struct SettingsView: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
Section {
|
||||||
|
Toggle("Fullscreen while streaming", isOn: $fullscreenWhileStreaming)
|
||||||
|
} header: {
|
||||||
|
Text("Window")
|
||||||
|
} footer: {
|
||||||
|
Text("Take the window fullscreen when a session starts and restore it on the host "
|
||||||
|
+ "list, so only the stream is fullscreen — not the picker.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
Section {
|
Section {
|
||||||
Picker("Cursor in stream", selection: $cursorMode) {
|
Picker("Cursor in stream", selection: $cursorMode) {
|
||||||
Text("Auto (gamescope)").tag("auto")
|
Text("Auto (gamescope)").tag("auto")
|
||||||
|
|||||||
@@ -24,4 +24,6 @@ public enum DefaultsKey {
|
|||||||
public static let cursorMode = "punktfunk.cursorMode"
|
public static let cursorMode = "punktfunk.cursorMode"
|
||||||
/// Experimental: show the host's game library (browsed over the management API). Off by default.
|
/// Experimental: show the host's game library (browsed over the management API). Off by default.
|
||||||
public static let libraryEnabled = "punktfunk.libraryEnabled"
|
public static let libraryEnabled = "punktfunk.libraryEnabled"
|
||||||
|
/// macOS: take the window fullscreen while streaming and restore it on the host list. On by default.
|
||||||
|
public static let fullscreenWhileStreaming = "punktfunk.fullscreenWhileStreaming"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,6 +169,15 @@ public final class GamepadCapture {
|
|||||||
ext.valueChangedHandler = { [weak self] g, _ in
|
ext.valueChangedHandler = { [weak self] g, _ in
|
||||||
MainActor.assumeIsolated { self?.sync(g) }
|
MainActor.assumeIsolated { self?.sync(g) }
|
||||||
}
|
}
|
||||||
|
// The Home/PS button (→ guide; the host maps it to the DualSense PS / Xbox guide bit) does
|
||||||
|
// NOT reliably fire the gamepad's valueChangedHandler on macOS, so its presses were dropped.
|
||||||
|
// A dedicated handler re-syncs on every Home transition.
|
||||||
|
ext.buttonHome?.pressedChangedHandler = { [weak self] _, _, _ in
|
||||||
|
MainActor.assumeIsolated {
|
||||||
|
guard let self, let g = self.bound?.extendedGamepad else { return }
|
||||||
|
self.sync(g)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Wake the host pad immediately (pads are created lazily from the first event;
|
// Wake the host pad immediately (pads are created lazily from the first event;
|
||||||
// a DualSense's UHID handshake + initial lightbar write only start then).
|
// a DualSense's UHID handshake + initial lightbar write only start then).
|
||||||
connection.send(.gamepadAxis(GamepadWire.axisLSX, value: 0, pad: 0))
|
connection.send(.gamepadAxis(GamepadWire.axisLSX, value: 0, pad: 0))
|
||||||
|
|||||||
Reference in New Issue
Block a user