feat(clients/windows): WinUI UX batch - tile hover, Settings NavigationView, modal slide-up
audit / cargo-audit (push) Successful in 1m13s
apple / swift (push) Successful in 1m14s
release / apple (push) Successful in 8m2s
android / android (push) Successful in 10m42s
ci / web (push) Successful in 48s
ci / docs-site (push) Successful in 58s
ci / rust (push) Successful in 12m23s
apple / screenshots (push) Successful in 5m27s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m43s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m21s
ci / bench (push) Successful in 4m49s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m10s
deb / build-publish (push) Successful in 4m0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
decky / build-publish (push) Successful in 26s
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 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m20s
windows-host / package (push) Failing after 23s
flatpak / build-publish (push) Successful in 4m39s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m42s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m16s
docker / deploy-docs (push) Successful in 34s
audit / cargo-audit (push) Successful in 1m13s
apple / swift (push) Successful in 1m14s
release / apple (push) Successful in 8m2s
android / android (push) Successful in 10m42s
ci / web (push) Successful in 48s
ci / docs-site (push) Successful in 58s
ci / rust (push) Successful in 12m23s
apple / screenshots (push) Successful in 5m27s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m43s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m21s
ci / bench (push) Successful in 4m49s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m10s
deb / build-publish (push) Successful in 4m0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
decky / build-publish (push) Successful in 26s
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 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m20s
windows-host / package (push) Failing after 23s
flatpak / build-publish (push) Successful in 4m39s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m42s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m16s
docker / deploy-docs (push) Successful in 34s
Bump windows-reactor + windows to a4f7b2cb (from b4129fcc) for the new PointerEntered/PointerExited events; migration is mechanical renames only (SymbolGlyph->Symbol, placeholder->placeholder_text, on_changed-> on_text_changed/on_toggled, on_menu_item_clicked->on_item_clicked, on_ready->on_mounted). New runtime model: reactor lost its build.rs, so the client build.rs stages the WinAppSDK bootstrap via windows-reactor-setup::as_framework_dependent() and main calls windows_reactor::bootstrap() (missing either = 0x80040154 at launch); staged filenames unchanged, so pack-msix and the MSIX manifest are untouched. - Host tiles: WinUI pointer-over fill (ControlFillSecondary) via the new pointer enter/exit events, hover id in root state (backend-wired handlers bypass the reconciler flush, like the flyout clicks). - Settings: stock NavigationView sidebar (Windows-Settings pattern) with Display/Video/Input/Audio/About panes, built-in back arrow, wide content column, and a per-section content slide-up tween. The section card is KEYED by section: an in-place diff across sections re-sets a reused ComboBox's items (clearing WinUI's selection) but skips selected_index when the values compare equal, rendering a blank selection - the key forces a remount. Card titles/descriptions dropped; per-control guidance moved to hover tooltips (ToolTipService). - New "Show the stats overlay (HUD)" setting (show_hud, default on), honored mid-stream via the 400 ms HUD re-render. - Add-host modal: entrance fade + slide-up tween (scrim fades with it). - Self-initiated disconnect (Ctrl+Alt+Shift+D -> Ended(None)) returns to the host list silently instead of raising the error banner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -274,9 +274,17 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc
|
|||||||
compiler + a per-arch `FFMPEG_DIR` ARM64 tree, SDL3/libopus build-from-source cross-compile
|
compiler + a per-arch `FFMPEG_DIR` ARM64 tree, SDL3/libopus build-from-source cross-compile
|
||||||
cleanly), and both ship as signed MSIX (`windows-msix.yml` matrix → `..._x64.msix`/`..._arm64.msix`,
|
cleanly), and both ship as signed MSIX (`windows-msix.yml` matrix → `..._x64.msix`/`..._arm64.msix`,
|
||||||
verified: ARM64 binaries + manifest arch). **windows-reactor is unpublished** (git
|
verified: ARM64 binaries + manifest arch). **windows-reactor is unpublished** (git
|
||||||
dep pinned to commit `b4129fcc`; `windows` pinned to the SAME commit so `IDXGISwapChain1` unifies
|
dep pinned to commit `a4f7b2cb`, bumped 2026-07-02 from `b4129fcc` for `on_pointer_entered`/
|
||||||
with `set_swap_chain`); its `build.rs` downloads the Win App SDK NuGets + needs `CARGO_WORKSPACE_DIR`
|
`on_pointer_exited` hover events — mechanical renames only: `SymbolGlyph`→`Symbol`,
|
||||||
set (in the VM build env; `/temp`+`/winmd` gitignored). Gotcha: `CARGO_HOME` must be an ASCII path
|
`placeholder`→`placeholder_text`, TextBox `on_changed`→`on_text_changed`, ToggleSwitch
|
||||||
|
`on_changed`→`on_toggled`, `on_menu_item_clicked`→`on_item_clicked`, SwapChainPanel
|
||||||
|
`on_ready`→`on_mounted`; `windows` pinned to the SAME commit so `IDXGISwapChain1` unifies with
|
||||||
|
`set_swap_chain`). New-model runtime staging: reactor has NO build.rs anymore — the app's own
|
||||||
|
`build.rs` calls `windows_reactor_setup::as_framework_dependent()` (same-rev build-dep, stages
|
||||||
|
the bootstrap DLL + resources.pri that pack-msix expects) and `main` calls
|
||||||
|
`windows_reactor::bootstrap()` before `App` (packaged MSIX: a no-op, the manifest's
|
||||||
|
`Microsoft.WindowsAppRuntime.2` dependency resolves the runtime). `CARGO_WORKSPACE_DIR` is no
|
||||||
|
longer required (harmless where still set). Gotcha: `CARGO_HOME` must be an ASCII path
|
||||||
— the `ü` in the dev box's username breaks SDL3's MSVC precompiled-header build. **Parity/cleanup
|
— the `ü` in the dev box's username breaks SDL3's MSVC precompiled-header build. **Parity/cleanup
|
||||||
batch (2026-07-02)**: `app.rs` split into per-screen `app/` modules (mod=root/router · hosts ·
|
batch (2026-07-02)**: `app.rs` split into per-screen `app/` modules (mod=root/router · hosts ·
|
||||||
connect · pair · speed · settings · licenses · stream · style; thread-driven state lives in ROOT
|
connect · pair · speed · settings · licenses · stream · style; thread-driven state lives in ROOT
|
||||||
@@ -294,7 +302,19 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc
|
|||||||
loopback E2E (TOFU connect → clock skew → HEVC negotiate → shared-D3D11 + D3D11VA init → WASAPI →
|
loopback E2E (TOFU connect → clock skew → HEVC negotiate → shared-D3D11 + D3D11VA init → WASAPI →
|
||||||
session end; synthetic payload isn't decodable so decode output stays unvalidated), speed-test
|
session end; synthetic payload isn't decodable so decode output stays unvalidated), speed-test
|
||||||
E2E. The WinUI window itself CANNOT be launched from SSH (session-0 → WinAppSDK 0x80070005,
|
E2E. The WinUI window itself CANNOT be launched from SSH (session-0 → WinAppSDK 0x80070005,
|
||||||
pre-existing; needs the console session, e.g. PsExec -i 1).
|
pre-existing; needs the console session, e.g. PsExec -i 1). **UX batch (2026-07-02 evening,
|
||||||
|
UIA-smoke-tested on the hybrid laptop)**: host tiles get the WinUI pointer-over fill
|
||||||
|
(`on_pointer_entered`/`exited` → root hover state → `ControlFillSecondary`), Settings is a stock
|
||||||
|
**NavigationView** sidebar (Windows-Settings pattern: Display/Video/Input/Audio/About panes,
|
||||||
|
built-in back arrow, section in root state; the section card is **keyed by section** — an
|
||||||
|
in-place diff across sections re-sets a reused ComboBox's items, clearing WinUI's selection,
|
||||||
|
but skips `selected_index` when the values compare equal → blank selection; the key forces a
|
||||||
|
remount — and the content column rides its own section-switch slide-up tween), new
|
||||||
|
**"Show the stats overlay (HUD)"** toggle
|
||||||
|
(`Settings::show_hud`, applies mid-stream via the 400 ms HUD re-render), the Add-host modal
|
||||||
|
slides up + fades in (root margin/opacity tween, same pattern as screen navigation), and a
|
||||||
|
self-initiated disconnect (Ctrl+Alt+Shift+D → `Ended(None)`) returns to the host list silently
|
||||||
|
instead of raising the error banner.
|
||||||
Next: **HDR on-glass validation** (Windows host with `PUNKTFUNK_10BIT` → the HDR laptop
|
Next: **HDR on-glass validation** (Windows host with `PUNKTFUNK_10BIT` → the HDR laptop
|
||||||
display), then RAWINPUT relative-mouse pointer-lock.
|
display), then RAWINPUT relative-mouse pointer-lock.
|
||||||
**Android stage 1 done** (`clients/android`, Kotlin app + `native/` Rust JNI core linking
|
**Android stage 1 done** (`clients/android`, Kotlin app + `native/` Rust JNI core linking
|
||||||
|
|||||||
Generated
+47
-40
@@ -2780,8 +2780,9 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"wasapi",
|
"wasapi",
|
||||||
"windows 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-reactor",
|
"windows-reactor",
|
||||||
|
"windows-reactor-setup",
|
||||||
"winresource",
|
"winresource",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4622,12 +4623,12 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.62.2"
|
version = "0.62.2"
|
||||||
source = "git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1#b4129fcc1ae81eec8bf1217539883db821bca3a1"
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-collections 0.3.2 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-collections 0.3.2 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-core 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-core 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-future 0.3.2 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-future 0.3.2 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-numerics 0.3.1 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-numerics 0.3.1 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-reference",
|
"windows-reference",
|
||||||
"windows-time",
|
"windows-time",
|
||||||
]
|
]
|
||||||
@@ -4644,9 +4645,9 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-collections"
|
name = "windows-collections"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
source = "git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1#b4129fcc1ae81eec8bf1217539883db821bca3a1"
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-core 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4665,13 +4666,13 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.62.2"
|
version = "0.62.2"
|
||||||
source = "git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1#b4129fcc1ae81eec8bf1217539883db821bca3a1"
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement 0.60.2 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-implement 0.60.2 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-interface 0.59.3 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-interface 0.59.3 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-link 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-link 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-result 0.4.1 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-result 0.4.1 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-strings 0.5.1 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-strings 0.5.1 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4688,11 +4689,11 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-future"
|
name = "windows-future"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
source = "git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1#b4129fcc1ae81eec8bf1217539883db821bca3a1"
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-core 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-link 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-link 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-threading 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-threading 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4709,7 +4710,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-implement"
|
name = "windows-implement"
|
||||||
version = "0.60.2"
|
version = "0.60.2"
|
||||||
source = "git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1#b4129fcc1ae81eec8bf1217539883db821bca3a1"
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -4730,7 +4731,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-interface"
|
name = "windows-interface"
|
||||||
version = "0.59.3"
|
version = "0.59.3"
|
||||||
source = "git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1#b4129fcc1ae81eec8bf1217539883db821bca3a1"
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -4746,7 +4747,7 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1#b4129fcc1ae81eec8bf1217539883db821bca3a1"
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-numerics"
|
name = "windows-numerics"
|
||||||
@@ -4761,33 +4762,38 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-numerics"
|
name = "windows-numerics"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
source = "git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1#b4129fcc1ae81eec8bf1217539883db821bca3a1"
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-core 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-link 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-link 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-reactor"
|
name = "windows-reactor"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
source = "git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1#b4129fcc1ae81eec8bf1217539883db821bca3a1"
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"windows-collections 0.3.2 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-collections 0.3.2 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-core 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-core 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-future 0.3.2 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-future 0.3.2 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-numerics 0.3.1 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-numerics 0.3.1 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-reference",
|
"windows-reference",
|
||||||
"windows-threading 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-threading 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-time",
|
"windows-time",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-reactor-setup"
|
||||||
|
version = "0.0.0"
|
||||||
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-reference"
|
name = "windows-reference"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1#b4129fcc1ae81eec8bf1217539883db821bca3a1"
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-core 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
"windows-time",
|
"windows-time",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4803,9 +4809,9 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
source = "git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1#b4129fcc1ae81eec8bf1217539883db821bca3a1"
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-link 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4831,9 +4837,9 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-strings"
|
name = "windows-strings"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
source = "git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1#b4129fcc1ae81eec8bf1217539883db821bca3a1"
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-link 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4941,17 +4947,18 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-threading"
|
name = "windows-threading"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1#b4129fcc1ae81eec8bf1217539883db821bca3a1"
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-link 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-time"
|
name = "windows-time"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1#b4129fcc1ae81eec8bf1217539883db821bca3a1"
|
source = "git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f#a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=b4129fcc1ae81eec8bf1217539883db821bca3a1)",
|
"windows-core 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
|
"windows-link 0.2.1 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ punktfunk-core = { path = "../../crates/punktfunk-core", features = ["quic"] }
|
|||||||
# `build.rs` downloads the Windows App SDK NuGets and stages the bootstrap DLL + resources.pri
|
# `build.rs` downloads the Windows App SDK NuGets and stages the bootstrap DLL + resources.pri
|
||||||
# next to the exe; it requires `CARGO_WORKSPACE_DIR` to be set in the build env. Unpublished
|
# next to the exe; it requires `CARGO_WORKSPACE_DIR` to be set in the build env. Unpublished
|
||||||
# (version 0.0.0) and fast-moving, so pinned to a verified commit.
|
# (version 0.0.0) and fast-moving, so pinned to a verified commit.
|
||||||
windows-reactor = { git = "https://github.com/microsoft/windows-rs", rev = "b4129fcc1ae81eec8bf1217539883db821bca3a1" }
|
windows-reactor = { git = "https://github.com/microsoft/windows-rs", rev = "a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f" }
|
||||||
# Win32 / Direct3D11 / DXGI for the SwapChainPanel composition swapchain. Pulled from the SAME
|
# Win32 / Direct3D11 / DXGI for the SwapChainPanel composition swapchain. Pulled from the SAME
|
||||||
# windows-rs commit as windows-reactor so their `windows-core` unifies — the `IDXGISwapChain1`
|
# windows-rs commit as windows-reactor so their `windows-core` unifies — the `IDXGISwapChain1`
|
||||||
# we hand to `SwapChainPanelHandle::set_swap_chain` must satisfy reactor's `windows_core::Interface`.
|
# we hand to `SwapChainPanelHandle::set_swap_chain` must satisfy reactor's `windows_core::Interface`.
|
||||||
windows = { git = "https://github.com/microsoft/windows-rs", rev = "b4129fcc1ae81eec8bf1217539883db821bca3a1", features = [
|
windows = { git = "https://github.com/microsoft/windows-rs", rev = "a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f", features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
"Win32_Graphics_Dxgi",
|
"Win32_Graphics_Dxgi",
|
||||||
"Win32_Graphics_Dxgi_Common",
|
"Win32_Graphics_Dxgi_Common",
|
||||||
@@ -69,5 +69,8 @@ tracing = "0.1"
|
|||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
# Embeds the app icon as an exe resource (build.rs) — Windows hosts only (rc.exe from the SDK).
|
# Embeds the app icon as an exe resource (build.rs) — Windows hosts only (rc.exe from the SDK).
|
||||||
|
# windows-reactor-setup stages the Windows App SDK runtime bootstrap (framework-dependent) next
|
||||||
|
# to the exe — the new-model replacement for the old windows-reactor build.rs; same pinned rev.
|
||||||
[target.'cfg(windows)'.build-dependencies]
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
winresource = "0.1"
|
winresource = "0.1"
|
||||||
|
windows-reactor-setup = { git = "https://github.com/microsoft/windows-rs", rev = "a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f" }
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ fn main() {
|
|||||||
// is the TARGET (both the x64 and the cross-compiled ARM64 Windows builds pass).
|
// is the TARGET (both the x64 and the cross-compiled ARM64 Windows builds pass).
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
if std::env::var_os("CARGO_CFG_WINDOWS").is_some() {
|
if std::env::var_os("CARGO_CFG_WINDOWS").is_some() {
|
||||||
|
// Stage the Windows App SDK runtime bootstrap + resources.pri next to the exe
|
||||||
|
// (framework-dependent deployment; `main` calls `windows_reactor::bootstrap()`).
|
||||||
|
windows_reactor_setup::as_framework_dependent();
|
||||||
|
|
||||||
let icon = "../../packaging/windows/branding/punktfunk.ico";
|
let icon = "../../packaging/windows/branding/punktfunk.ico";
|
||||||
println!("cargo:rerun-if-changed={icon}");
|
println!("cargo:rerun-if-changed={icon}");
|
||||||
winresource::WindowsResource::new()
|
winresource::WindowsResource::new()
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ stamps the manifest `ProcessorArchitecture` and names the output. See
|
|||||||
| File | Source |
|
| File | Source |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `punktfunk-client.exe` | the release build |
|
| `punktfunk-client.exe` | the release build |
|
||||||
| `Microsoft.WindowsAppRuntime.Bootstrap.dll`, `resources.pri` | auto-staged by windows-reactor's `build.rs` |
|
| `Microsoft.WindowsAppRuntime.Bootstrap.dll`, `resources.pri` | staged by the client's `build.rs` via `windows-reactor-setup::as_framework_dependent()` |
|
||||||
| `SDL3.dll` | auto-staged by the `sdl3` crate |
|
| `SDL3.dll` | auto-staged by the `sdl3` crate |
|
||||||
| `avcodec/avformat/avutil/swscale/swresample/...-*.dll` | `FFMPEG_DIR\bin` |
|
| `avcodec/avformat/avutil/swscale/swresample/...-*.dll` | `FFMPEG_DIR\bin` |
|
||||||
| `Assets\*.png` | checked-in tile/store logos (rasterized from `packaging/flatpak/io.unom.Punktfunk.svg`) |
|
| `Assets\*.png` | checked-in tile/store logos (rasterized from `packaging/flatpak/io.unom.Punktfunk.svg`) |
|
||||||
@@ -30,12 +30,13 @@ stamps the manifest `ProcessorArchitecture` and names the output. See
|
|||||||
|
|
||||||
### Why an "unpackaged" WinUI app packages cleanly
|
### Why an "unpackaged" WinUI app packages cleanly
|
||||||
|
|
||||||
windows-reactor calls `MddBootstrapInitialize2` with `OnPackageIdentity_NOOP`
|
`main` calls `windows_reactor::bootstrap()`, which runs `MddBootstrapInitialize2` with
|
||||||
(`crates/libs/reactor/src/app.rs`), so under MSIX **package identity** the App SDK bootstrapper is
|
`OnPackageIdentity_NOOP` (`crates/libs/reactor/src/bootstrap.rs`), so under MSIX **package
|
||||||
a no-op and the runtime is resolved from the manifest's `<PackageDependency>` on
|
identity** the App SDK bootstrapper is a no-op and the runtime is resolved from the manifest's
|
||||||
`Microsoft.WindowsAppRuntime.2` instead (reactor pins `WINDOWSAPPSDK_RELEASE_MAJORMINOR = 0x20000`
|
`<PackageDependency>` on `Microsoft.WindowsAppRuntime.2` instead (reactor pins
|
||||||
= 2.0). It's a full-trust Win32 app (`EntryPoint="Windows.FullTrustApplication"` + `runFullTrust`)
|
`WINDOWSAPPSDK_RELEASE_MAJORMINOR = 0x20000` = 2.0). It's a full-trust Win32 app
|
||||||
because it owns raw D3D11, Win32 low-level input hooks, WASAPI and SDL3.
|
(`EntryPoint="Windows.FullTrustApplication"` + `runFullTrust`) because it owns raw D3D11, Win32
|
||||||
|
low-level input hooks, WASAPI and SDL3.
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
|
|||||||
@@ -270,7 +270,9 @@ fn connect_with(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
SessionEvent::Ended(err) => {
|
SessionEvent::Ended(err) => {
|
||||||
st.call(err.unwrap_or_else(|| "Session ended".into()));
|
// `None` = the user ended the session themselves (the disconnect shortcut) —
|
||||||
|
// return to the host list silently; an error banner would read as a failure.
|
||||||
|
st.call(err.unwrap_or_default());
|
||||||
gamepad.detach();
|
gamepad.detach();
|
||||||
ss.call(Screen::Hosts);
|
ss.call(Screen::Hosts);
|
||||||
break;
|
break;
|
||||||
@@ -343,7 +345,7 @@ pub(crate) fn request_access_page(
|
|||||||
let cancel_btn = {
|
let cancel_btn = {
|
||||||
let (ctx, ss) = (ctx.clone(), set_screen.clone());
|
let (ctx, ss) = (ctx.clone(), set_screen.clone());
|
||||||
button("Cancel")
|
button("Cancel")
|
||||||
.icon(SymbolGlyph::Cancel)
|
.icon(Symbol::Cancel)
|
||||||
.on_click(move || {
|
.on_click(move || {
|
||||||
// Return the UI immediately; the parked connect is blocking with no abort, so trip
|
// Return the UI immediately; the parked connect is blocking with no abort, so trip
|
||||||
// the flag this request's event loop captured — it then tears down silently when
|
// the flag this request's event loop captured — it then tears down silently when
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use crate::discovery::DiscoveredHost;
|
|||||||
use crate::trust::KnownHosts;
|
use crate::trust::KnownHosts;
|
||||||
use windows_reactor::*;
|
use windows_reactor::*;
|
||||||
|
|
||||||
/// Overflow-menu item labels — `on_menu_item_clicked` reports the clicked item by its text.
|
/// Overflow-menu item labels — `on_item_clicked` reports the clicked item by its text.
|
||||||
const MENU_CONNECT: &str = "Connect";
|
const MENU_CONNECT: &str = "Connect";
|
||||||
const MENU_SPEED: &str = "Test network speed\u{2026}";
|
const MENU_SPEED: &str = "Test network speed\u{2026}";
|
||||||
const MENU_RENAME: &str = "Rename\u{2026}";
|
const MENU_RENAME: &str = "Rename\u{2026}";
|
||||||
@@ -42,9 +42,15 @@ pub(crate) struct HostsProps {
|
|||||||
/// own `use_state`: a child component's sync `SetState` marks its slot dirty but does not
|
/// own `use_state`: a child component's sync `SetState` marks its slot dirty but does not
|
||||||
/// re-render when its props are otherwise unchanged, so the toggle wouldn't take.
|
/// re-render when its props are otherwise unchanged, so the toggle wouldn't take.
|
||||||
pub(crate) show_add: bool,
|
pub(crate) show_add: bool,
|
||||||
|
/// The modal's entrance-tween progress (0 → 1, root-driven): opacity + slide-up offset.
|
||||||
|
pub(crate) add_anim: f64,
|
||||||
|
/// The hovered tile's stable id (saved: fp_hex, discovered: `addr:port`) — root state because
|
||||||
|
/// the pointer enter/exit handlers bypass the reconciler flush, like the flyout clicks above.
|
||||||
|
pub(crate) hover: Option<String>,
|
||||||
pub(crate) set_forget: AsyncSetState<Option<(String, String)>>,
|
pub(crate) set_forget: AsyncSetState<Option<(String, String)>>,
|
||||||
pub(crate) set_rename: AsyncSetState<Option<(String, String)>>,
|
pub(crate) set_rename: AsyncSetState<Option<(String, String)>>,
|
||||||
pub(crate) set_show_add: AsyncSetState<bool>,
|
pub(crate) set_show_add: AsyncSetState<bool>,
|
||||||
|
pub(crate) set_hover: AsyncSetState<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for HostsProps {
|
impl PartialEq for HostsProps {
|
||||||
@@ -56,6 +62,8 @@ impl PartialEq for HostsProps {
|
|||||||
&& self.forget == other.forget
|
&& self.forget == other.forget
|
||||||
&& self.rename == other.rename
|
&& self.rename == other.rename
|
||||||
&& self.show_add == other.show_add
|
&& self.show_add == other.show_add
|
||||||
|
&& self.add_anim == other.add_anim
|
||||||
|
&& self.hover == other.hover
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +71,12 @@ impl PartialEq for HostsProps {
|
|||||||
/// optional "…" menu button are SIBLINGS overlaid in one grid cell, never nested: WinUI bubbles
|
/// optional "…" menu button are SIBLINGS overlaid in one grid cell, never nested: WinUI bubbles
|
||||||
/// `Tapped` out of buttons (reactor doesn't mark it handled), so a button inside the tap target
|
/// `Tapped` out of buttons (reactor doesn't mark it handled), so a button inside the tap target
|
||||||
/// would fire both its own click and the tile's connect (the old forget-also-connects bug).
|
/// would fire both its own click and the tile's connect (the old forget-also-connects bug).
|
||||||
|
///
|
||||||
|
/// Hover renders the WinUI card pointer-over look — the card background lifts to the control
|
||||||
|
/// hover fill while the pointer is inside the tile (tracked via `hover`, see `HostsProps`).
|
||||||
fn host_tile(
|
fn host_tile(
|
||||||
|
id: &str,
|
||||||
|
hover: &Hover,
|
||||||
name: &str,
|
name: &str,
|
||||||
sub: &str,
|
sub: &str,
|
||||||
status_row: Element,
|
status_row: Element,
|
||||||
@@ -104,7 +117,27 @@ fn host_tile(
|
|||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
card_flush(grid(children)).into()
|
let mut tile = card_flush(grid(children));
|
||||||
|
if hover.current.as_deref() == Some(id) {
|
||||||
|
tile = tile.background(ThemeRef::ControlFillSecondary);
|
||||||
|
}
|
||||||
|
let enter = {
|
||||||
|
let (set, id) = (hover.set.clone(), id.to_string());
|
||||||
|
move |_: PointerEventInfo| set.call(Some(id.clone()))
|
||||||
|
};
|
||||||
|
let exit = {
|
||||||
|
let set = hover.set.clone();
|
||||||
|
move || set.call(None)
|
||||||
|
};
|
||||||
|
tile.on_pointer_entered(enter)
|
||||||
|
.on_pointer_exited(exit)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hover-tracking pair `host_tile` needs: the currently hovered tile id + its root setter.
|
||||||
|
pub(crate) struct Hover {
|
||||||
|
pub(crate) current: Option<String>,
|
||||||
|
pub(crate) set: AsyncSetState<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The status row at the bottom of a tile: presence dot + Online/Offline, plus the trust chip.
|
/// The status row at the bottom of a tile: presence dot + Online/Offline, plus the trust chip.
|
||||||
@@ -179,12 +212,12 @@ fn rename_editor(
|
|||||||
card(
|
card(
|
||||||
vstack((
|
vstack((
|
||||||
text_box(draft)
|
text_box(draft)
|
||||||
.placeholder("Host name")
|
.placeholder_text("Host name")
|
||||||
.on_changed(on_changed),
|
.on_text_changed(on_changed),
|
||||||
hstack((
|
hstack((
|
||||||
button("Save")
|
button("Save")
|
||||||
.accent()
|
.accent()
|
||||||
.icon(SymbolGlyph::Accept)
|
.icon(Symbol::Accept)
|
||||||
.on_click(commit),
|
.on_click(commit),
|
||||||
button("Cancel")
|
button("Cancel")
|
||||||
.subtle()
|
.subtle()
|
||||||
@@ -213,6 +246,10 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
let rename = props.rename.clone();
|
let rename = props.rename.clone();
|
||||||
let set_forget = &props.set_forget;
|
let set_forget = &props.set_forget;
|
||||||
let set_rename = &props.set_rename;
|
let set_rename = &props.set_rename;
|
||||||
|
let hover = Hover {
|
||||||
|
current: props.hover.clone(),
|
||||||
|
set: props.set_hover.clone(),
|
||||||
|
};
|
||||||
let known = KnownHosts::load();
|
let known = KnownHosts::load();
|
||||||
|
|
||||||
// Responsive column count from the live window width (re-renders on resize): as many
|
// Responsive column count from the live window width (re-renders on resize): as many
|
||||||
@@ -235,14 +272,11 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
.grid_column(0)
|
.grid_column(0)
|
||||||
.vertical_alignment(VerticalAlignment::Center),
|
.vertical_alignment(VerticalAlignment::Center),
|
||||||
hstack((
|
hstack((
|
||||||
button("Add host")
|
button("Add host").accent().icon(Symbol::Add).on_click({
|
||||||
.accent()
|
let sa = set_show_add.clone();
|
||||||
.icon(SymbolGlyph::Add)
|
move || sa.call(true)
|
||||||
.on_click({
|
}),
|
||||||
let sa = set_show_add.clone();
|
button("Settings").icon(Symbol::Setting).on_click({
|
||||||
move || sa.call(true)
|
|
||||||
}),
|
|
||||||
button("Settings").icon(SymbolGlyph::Setting).on_click({
|
|
||||||
let ss = set_screen.clone();
|
let ss = set_screen.clone();
|
||||||
move || ss.call(Screen::Settings)
|
move || ss.call(Screen::Settings)
|
||||||
}),
|
}),
|
||||||
@@ -293,7 +327,7 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
let (sf, sr) = (set_forget.clone(), set_rename.clone());
|
let (sf, sr) = (set_forget.clone(), set_rename.clone());
|
||||||
let (fp, name) = (k.fp_hex.clone(), k.name.clone());
|
let (fp, name) = (k.fp_hex.clone(), k.name.clone());
|
||||||
button("")
|
button("")
|
||||||
.icon(SymbolGlyph::More)
|
.icon(Symbol::More)
|
||||||
.subtle()
|
.subtle()
|
||||||
.tooltip("More options")
|
.tooltip("More options")
|
||||||
.automation_name("More options")
|
.automation_name("More options")
|
||||||
@@ -304,7 +338,7 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
menu_separator(),
|
menu_separator(),
|
||||||
menu_item(MENU_FORGET),
|
menu_item(MENU_FORGET),
|
||||||
])
|
])
|
||||||
.on_menu_item_clicked(move |item: String| match item.as_str() {
|
.on_item_clicked(move |item: String| match item.as_str() {
|
||||||
MENU_CONNECT => {
|
MENU_CONNECT => {
|
||||||
initiate(&svc.ctx, target.clone(), &svc.set_screen, &svc.set_status)
|
initiate(&svc.ctx, target.clone(), &svc.set_screen, &svc.set_status)
|
||||||
}
|
}
|
||||||
@@ -325,6 +359,8 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
};
|
};
|
||||||
let (ctx2, ss, st) = (ctx.clone(), set_screen.clone(), set_status.clone());
|
let (ctx2, ss, st) = (ctx.clone(), set_screen.clone(), set_status.clone());
|
||||||
tiles.push(host_tile(
|
tiles.push(host_tile(
|
||||||
|
&k.fp_hex,
|
||||||
|
&hover,
|
||||||
&k.name,
|
&k.name,
|
||||||
&format!("{}:{}", k.addr, k.port),
|
&format!("{}:{}", k.addr, k.port),
|
||||||
status_row(
|
status_row(
|
||||||
@@ -378,6 +414,8 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
("Open", Pill::Neutral)
|
("Open", Pill::Neutral)
|
||||||
};
|
};
|
||||||
tiles.push(host_tile(
|
tiles.push(host_tile(
|
||||||
|
&format!("{}:{}", h.addr, h.port),
|
||||||
|
&hover,
|
||||||
&h.name,
|
&h.name,
|
||||||
&format!("{}:{}", h.addr, h.port),
|
&format!("{}:{}", h.addr, h.port),
|
||||||
status_row(None, badge, kind),
|
status_row(None, badge, kind),
|
||||||
@@ -466,13 +504,13 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
.foreground(ThemeRef::SecondaryText),
|
.foreground(ThemeRef::SecondaryText),
|
||||||
text_box(manual)
|
text_box(manual)
|
||||||
.header("Address")
|
.header("Address")
|
||||||
.placeholder("192.168.1.20 or my-pc.local")
|
.placeholder_text("192.168.1.20 or my-pc.local")
|
||||||
.on_changed(move |s| set_manual.call(s))
|
.on_text_changed(move |s| set_manual.call(s))
|
||||||
.margin(edges(0.0, 6.0, 0.0, 0.0)),
|
.margin(edges(0.0, 6.0, 0.0, 0.0)),
|
||||||
hstack((
|
hstack((
|
||||||
button("Connect")
|
button("Connect")
|
||||||
.accent()
|
.accent()
|
||||||
.icon(SymbolGlyph::Forward)
|
.icon(Symbol::Forward)
|
||||||
.on_click(connect_manual),
|
.on_click(connect_manual),
|
||||||
button("Cancel").on_click({
|
button("Cancel").on_click({
|
||||||
let sa = set_show_add.clone();
|
let sa = set_show_add.clone();
|
||||||
@@ -488,10 +526,20 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
.max_width(460.0)
|
.max_width(460.0)
|
||||||
.horizontal_alignment(HorizontalAlignment::Center)
|
.horizontal_alignment(HorizontalAlignment::Center)
|
||||||
.vertical_alignment(VerticalAlignment::Center)
|
.vertical_alignment(VerticalAlignment::Center)
|
||||||
.margin(uniform(24.0));
|
// Entrance: fade + slide up, driven by the root tween (`add_anim` 0 → 1). The card starts
|
||||||
|
// a bit low and rises to centre — for a centred element, extra top margin shifts it down by
|
||||||
|
// half the difference, so the offset is doubled.
|
||||||
|
.opacity(props.add_anim)
|
||||||
|
.margin(edges(
|
||||||
|
24.0,
|
||||||
|
24.0 + (1.0 - props.add_anim) * 56.0,
|
||||||
|
24.0,
|
||||||
|
24.0,
|
||||||
|
));
|
||||||
|
|
||||||
|
// The scrim fades in with the same tween.
|
||||||
let scrim = border(modal).background(Color {
|
let scrim = border(modal).background(Color {
|
||||||
a: 140,
|
a: (140.0 * props.add_anim) as u8,
|
||||||
r: 0,
|
r: 0,
|
||||||
g: 0,
|
g: 0,
|
||||||
b: 0,
|
b: 0,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const APP_LICENSE: &str = concat!(
|
|||||||
const THIRD_PARTY_NOTICES: &str = include_str!("../../../../THIRD-PARTY-NOTICES.txt");
|
const THIRD_PARTY_NOTICES: &str = include_str!("../../../../THIRD-PARTY-NOTICES.txt");
|
||||||
|
|
||||||
pub(crate) fn licenses_page(set_screen: &AsyncSetState<Screen>) -> Element {
|
pub(crate) fn licenses_page(set_screen: &AsyncSetState<Screen>) -> Element {
|
||||||
let back_btn = button("Back").accent().icon(SymbolGlyph::Back).on_click({
|
let back_btn = button("Back").accent().icon(Symbol::Back).on_click({
|
||||||
let ss = set_screen.clone();
|
let ss = set_screen.clone();
|
||||||
move || ss.call(Screen::Settings)
|
move || ss.call(Screen::Settings)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -200,6 +200,12 @@ fn root(cx: &mut RenderCx, ctx: &Arc<AppCtx>) -> Element {
|
|||||||
let (forget, set_forget) = cx.use_async_state(Option::<(String, String)>::None);
|
let (forget, set_forget) = cx.use_async_state(Option::<(String, String)>::None);
|
||||||
let (rename, set_rename) = cx.use_async_state(Option::<(String, String)>::None);
|
let (rename, set_rename) = cx.use_async_state(Option::<(String, String)>::None);
|
||||||
let (show_add, set_show_add) = cx.use_async_state(false);
|
let (show_add, set_show_add) = cx.use_async_state(false);
|
||||||
|
// Hovered host tile (its stable id), driving the WinUI-style card hover fill. Root state for
|
||||||
|
// the same reason as `forget`/`rename`: pointer enter/exit handlers are wired straight in the
|
||||||
|
// reactor backend, so only a root `AsyncSetState` reliably re-renders the page.
|
||||||
|
let (hover, set_hover) = cx.use_async_state(Option::<String>::None);
|
||||||
|
// Which Settings section the NavigationView shows (persists across visits this run).
|
||||||
|
let (settings_nav, set_settings_nav) = cx.use_async_state("display".to_string());
|
||||||
|
|
||||||
// Continuous LAN discovery (spawned once).
|
// Continuous LAN discovery (spawned once).
|
||||||
cx.use_effect((), {
|
cx.use_effect((), {
|
||||||
@@ -279,6 +285,70 @@ fn root(cx: &mut RenderCx, ctx: &Arc<AppCtx>) -> Element {
|
|||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Settings-section entrance: the same tween again, keyed on the selected section, so
|
||||||
|
// switching panes slides the CONTENT column up (the sidebar stays put — this must not wrap
|
||||||
|
// the NavigationView, so it can't ride the screen-level tween above). Entering Settings
|
||||||
|
// fresh leaves it settled at 1 (only the screen tween plays; no double animation).
|
||||||
|
let nav_gen = cx.use_ref(std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)));
|
||||||
|
let (nav_anim, set_nav_anim) = cx.use_async_state((String::new(), 1.0f64));
|
||||||
|
cx.use_effect(settings_nav.clone(), {
|
||||||
|
let (s, set_nav_anim, gen) = (
|
||||||
|
settings_nav.clone(),
|
||||||
|
set_nav_anim.clone(),
|
||||||
|
nav_gen.borrow().clone(),
|
||||||
|
);
|
||||||
|
move || {
|
||||||
|
use std::sync::atomic::Ordering::SeqCst;
|
||||||
|
let mine = gen.fetch_add(1, SeqCst) + 1;
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
const STEPS: u32 = 14;
|
||||||
|
for i in 0..=STEPS {
|
||||||
|
if gen.load(SeqCst) != mine {
|
||||||
|
return; // a newer section switch superseded this tween
|
||||||
|
}
|
||||||
|
let p = f64::from(i) / f64::from(STEPS);
|
||||||
|
let eased = 1.0 - (1.0 - p).powi(3);
|
||||||
|
set_nav_anim.call((s.clone(), eased));
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(16));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let nav_progress = if nav_anim.0 == settings_nav {
|
||||||
|
nav_anim.1
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// "Add host" modal entrance: the same manual tween as the screen navigation (see above for
|
||||||
|
// why it can't be a composition animation), stepping 0 → 1 when the modal opens. The hosts
|
||||||
|
// page maps it to the modal's opacity + a downward start offset (the slide-up) and the
|
||||||
|
// scrim's fade. Closing resets to 0 instantly — the modal unmounts, nothing to animate.
|
||||||
|
let add_gen = cx.use_ref(std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)));
|
||||||
|
let (add_anim, set_add_anim) = cx.use_async_state(0.0f64);
|
||||||
|
cx.use_effect(show_add, {
|
||||||
|
let (set_add_anim, gen) = (set_add_anim.clone(), add_gen.borrow().clone());
|
||||||
|
move || {
|
||||||
|
use std::sync::atomic::Ordering::SeqCst;
|
||||||
|
let mine = gen.fetch_add(1, SeqCst) + 1;
|
||||||
|
if !show_add {
|
||||||
|
set_add_anim.call(0.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
const STEPS: u32 = 12;
|
||||||
|
for i in 0..=STEPS {
|
||||||
|
if gen.load(SeqCst) != mine {
|
||||||
|
return; // reopened/closed mid-tween — a newer run owns the value
|
||||||
|
}
|
||||||
|
let p = f64::from(i) / f64::from(STEPS);
|
||||||
|
set_add_anim.call(1.0 - (1.0 - p).powi(3)); // ease-out cubic
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(16));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Each hook-using screen is mounted as its own component so its hooks are isolated from
|
// Each hook-using screen is mounted as its own component so its hooks are isolated from
|
||||||
// root's (root's own hooks above stay a stable prefix regardless of which screen renders).
|
// root's (root's own hooks above stay a stable prefix regardless of which screen renders).
|
||||||
let svc = Svc {
|
let svc = Svc {
|
||||||
@@ -297,16 +367,25 @@ fn root(cx: &mut RenderCx, ctx: &Arc<AppCtx>) -> Element {
|
|||||||
forget,
|
forget,
|
||||||
rename,
|
rename,
|
||||||
show_add,
|
show_add,
|
||||||
|
add_anim,
|
||||||
|
hover,
|
||||||
set_forget,
|
set_forget,
|
||||||
set_rename,
|
set_rename,
|
||||||
set_show_add,
|
set_show_add,
|
||||||
|
set_hover,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// connecting_page / request_access_page / settings_page / licenses_page use no hooks
|
// connecting_page / request_access_page / settings_page / licenses_page use no hooks
|
||||||
// (they never touch `cx`), so calling them inline is sound.
|
// (they never touch `cx`), so calling them inline is sound.
|
||||||
Screen::Connecting => connect::connecting_page(ctx, &status),
|
Screen::Connecting => connect::connecting_page(ctx, &status),
|
||||||
Screen::RequestAccess => connect::request_access_page(ctx, &set_screen),
|
Screen::RequestAccess => connect::request_access_page(ctx, &set_screen),
|
||||||
Screen::Settings => settings::settings_page(ctx, &set_screen),
|
Screen::Settings => settings::settings_page(
|
||||||
|
ctx,
|
||||||
|
&set_screen,
|
||||||
|
&settings_nav,
|
||||||
|
&set_settings_nav,
|
||||||
|
nav_progress,
|
||||||
|
),
|
||||||
Screen::Licenses => licenses::licenses_page(&set_screen),
|
Screen::Licenses => licenses::licenses_page(&set_screen),
|
||||||
Screen::Pair => component(pair::pair_page, svc),
|
Screen::Pair => component(pair::pair_page, svc),
|
||||||
Screen::SpeedTest => component(speed::speed_page, SpeedProps { svc, state: speed }),
|
Screen::SpeedTest => component(speed::speed_page, SpeedProps { svc, state: speed }),
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ pub(crate) fn pair_page(props: &Svc, cx: &mut RenderCx) -> Element {
|
|||||||
);
|
);
|
||||||
button("Pair & Connect")
|
button("Pair & Connect")
|
||||||
.accent()
|
.accent()
|
||||||
.icon(SymbolGlyph::Accept)
|
.icon(Symbol::Accept)
|
||||||
.on_click(move || {
|
.on_click(move || {
|
||||||
let pin = code2.trim().to_string();
|
let pin = code2.trim().to_string();
|
||||||
let (ctx3, ss, st, target3) =
|
let (ctx3, ss, st, target3) =
|
||||||
@@ -65,7 +65,7 @@ pub(crate) fn pair_page(props: &Svc, cx: &mut RenderCx) -> Element {
|
|||||||
let cancel_btn = {
|
let cancel_btn = {
|
||||||
let ss = set_screen.clone();
|
let ss = set_screen.clone();
|
||||||
button("Cancel")
|
button("Cancel")
|
||||||
.icon(SymbolGlyph::Cancel)
|
.icon(Symbol::Cancel)
|
||||||
.on_click(move || ss.call(Screen::Hosts))
|
.on_click(move || ss.call(Screen::Hosts))
|
||||||
};
|
};
|
||||||
// The no-PIN alternative offered alongside the PIN ceremony: open an identified connect that
|
// The no-PIN alternative offered alongside the PIN ceremony: open an identified connect that
|
||||||
@@ -73,7 +73,7 @@ pub(crate) fn pair_page(props: &Svc, cx: &mut RenderCx) -> Element {
|
|||||||
let request_btn = {
|
let request_btn = {
|
||||||
let (svc, target2) = (props.clone(), target.clone());
|
let (svc, target2) = (props.clone(), target.clone());
|
||||||
button("Request access without a PIN")
|
button("Request access without a PIN")
|
||||||
.icon(SymbolGlyph::Send)
|
.icon(Symbol::Send)
|
||||||
.on_click(move || request_access(&svc, &target2))
|
.on_click(move || request_access(&svc, &target2))
|
||||||
.horizontal_alignment(HorizontalAlignment::Stretch)
|
.horizontal_alignment(HorizontalAlignment::Stretch)
|
||||||
};
|
};
|
||||||
@@ -105,9 +105,9 @@ pub(crate) fn pair_page(props: &Svc, cx: &mut RenderCx) -> Element {
|
|||||||
.informational()
|
.informational()
|
||||||
.is_closable(false),
|
.is_closable(false),
|
||||||
text_box(code)
|
text_box(code)
|
||||||
.placeholder("PIN")
|
.placeholder_text("PIN")
|
||||||
.font_size(28.0)
|
.font_size(28.0)
|
||||||
.on_changed(move |s| set_code.call(s)),
|
.on_text_changed(move |s| set_code.call(s)),
|
||||||
hstack((pair_btn, cancel_btn)).spacing(8.0),
|
hstack((pair_btn, cancel_btn)).spacing(8.0),
|
||||||
text_block(
|
text_block(
|
||||||
"Don\u{2019}t have a PIN? Request access instead and approve this device on the host \
|
"Don\u{2019}t have a PIN? Request access instead and approve this device on the host \
|
||||||
|
|||||||
@@ -95,27 +95,33 @@ fn setting_toggle(
|
|||||||
.header(header)
|
.header(header)
|
||||||
.on_content("On")
|
.on_content("On")
|
||||||
.off_content("Off")
|
.off_content("Off")
|
||||||
.on_changed(move |v: bool| {
|
.on_toggled(move |v: bool| {
|
||||||
let mut s = ctx.settings.lock().unwrap();
|
let mut s = ctx.settings.lock().unwrap();
|
||||||
apply(&mut s, v);
|
apply(&mut s, v);
|
||||||
s.save();
|
s.save();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A titled settings card: bold heading, a secondary description, then the controls.
|
/// A settings card: just the controls. No heading (the section title is the NavigationView
|
||||||
fn settings_card(title: &str, blurb: &str, controls: Vec<Element>) -> Element {
|
/// header) and no description paragraph — per-control guidance is a `.tooltip(...)` on the
|
||||||
let mut children: Vec<Element> = vec![
|
/// control itself (a paragraph in the card reads as the first control's label).
|
||||||
text_block(title).font_size(15.0).semibold().into(),
|
fn settings_card(controls: Vec<Element>) -> Element {
|
||||||
text_block(blurb)
|
card(vstack(controls).spacing(10.0)).into()
|
||||||
.font_size(12.0)
|
|
||||||
.foreground(ThemeRef::SecondaryText)
|
|
||||||
.into(),
|
|
||||||
];
|
|
||||||
children.extend(controls);
|
|
||||||
card(vstack(children).spacing(10.0)).into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn settings_page(ctx: &Arc<AppCtx>, set_screen: &AsyncSetState<Screen>) -> Element {
|
/// The settings screen: a stock WinUI `NavigationView` (the Windows-Settings sidebar pattern) —
|
||||||
|
/// one pane item per section, the section's card as the content, the built-in back arrow
|
||||||
|
/// returning to the host list. `section`/`set_section` are the selected pane tag, held in ROOT
|
||||||
|
/// state (this page stays hook-free): `on_selection_changed` is wired in the reactor backend, so
|
||||||
|
/// only a root `AsyncSetState` reliably re-renders the new section in. `progress` is the
|
||||||
|
/// section-switch entrance tween (0 → 1), mapped onto the content column's opacity + offset.
|
||||||
|
pub(crate) fn settings_page(
|
||||||
|
ctx: &Arc<AppCtx>,
|
||||||
|
set_screen: &AsyncSetState<Screen>,
|
||||||
|
section: &str,
|
||||||
|
set_section: &AsyncSetState<String>,
|
||||||
|
progress: f64,
|
||||||
|
) -> Element {
|
||||||
let s = ctx.settings.lock().unwrap().clone();
|
let s = ctx.settings.lock().unwrap().clone();
|
||||||
|
|
||||||
// --- Display ---------------------------------------------------------------------------
|
// --- Display ---------------------------------------------------------------------------
|
||||||
@@ -138,7 +144,11 @@ pub(crate) fn settings_page(ctx: &Arc<AppCtx>, set_screen: &AsyncSetState<Screen
|
|||||||
};
|
};
|
||||||
let res_combo = setting_combo(ctx, "Resolution", res_names, res_i, |s, i| {
|
let res_combo = setting_combo(ctx, "Resolution", res_names, res_i, |s, i| {
|
||||||
(s.width, s.height) = RESOLUTIONS[i];
|
(s.width, s.height) = RESOLUTIONS[i];
|
||||||
});
|
})
|
||||||
|
.tooltip(
|
||||||
|
"The host creates a virtual display at exactly this size. \u{201C}Native display\u{201D} \
|
||||||
|
resolves to the monitor this window is on at connect.",
|
||||||
|
);
|
||||||
let (hz_names, hz_i) = {
|
let (hz_names, hz_i) = {
|
||||||
let names: Vec<String> = REFRESH
|
let names: Vec<String> = REFRESH
|
||||||
.iter()
|
.iter()
|
||||||
@@ -155,17 +165,26 @@ pub(crate) fn settings_page(ctx: &Arc<AppCtx>, set_screen: &AsyncSetState<Screen
|
|||||||
};
|
};
|
||||||
let hz_combo = setting_combo(ctx, "Refresh rate", hz_names, hz_i, |s, i| {
|
let hz_combo = setting_combo(ctx, "Refresh rate", hz_names, hz_i, |s, i| {
|
||||||
s.refresh_hz = REFRESH[i];
|
s.refresh_hz = REFRESH[i];
|
||||||
});
|
})
|
||||||
|
.tooltip("\u{201C}Native\u{201D} resolves to this display's refresh rate at connect.");
|
||||||
let (comp_names, comp_i) = presets(COMPOSITORS, |v| *v == s.compositor);
|
let (comp_names, comp_i) = presets(COMPOSITORS, |v| *v == s.compositor);
|
||||||
let comp_combo = setting_combo(ctx, "Host compositor", comp_names, comp_i, |s, i| {
|
let comp_combo = setting_combo(ctx, "Host compositor", comp_names, comp_i, |s, i| {
|
||||||
s.compositor = COMPOSITORS[i].0.to_string();
|
s.compositor = COMPOSITORS[i].0.to_string();
|
||||||
});
|
})
|
||||||
|
.tooltip(
|
||||||
|
"Linux hosts only, and advisory \u{2014} the host falls back to auto-detect when the \
|
||||||
|
choice is unavailable.",
|
||||||
|
);
|
||||||
|
|
||||||
// --- Video -----------------------------------------------------------------------------
|
// --- Video -----------------------------------------------------------------------------
|
||||||
let (dec_names, dec_i) = presets(DECODERS, |v| *v == s.decoder);
|
let (dec_names, dec_i) = presets(DECODERS, |v| *v == s.decoder);
|
||||||
let decoder_combo = setting_combo(ctx, "Video decoder", dec_names, dec_i, |s, i| {
|
let decoder_combo = setting_combo(ctx, "Video decoder", dec_names, dec_i, |s, i| {
|
||||||
s.decoder = DECODERS[i].0.to_string();
|
s.decoder = DECODERS[i].0.to_string();
|
||||||
});
|
})
|
||||||
|
.tooltip(
|
||||||
|
"Hardware decode (D3D11VA) is far lighter than software \u{2014} keep it on Automatic \
|
||||||
|
unless debugging.",
|
||||||
|
);
|
||||||
// GPU picker, only on a multi-GPU box (hybrid laptop, eGPU): which adapter decodes + presents.
|
// GPU picker, only on a multi-GPU box (hybrid laptop, eGPU): which adapter decodes + presents.
|
||||||
// Stored as the adapter description; empty = automatic (the window's monitor's adapter).
|
// Stored as the adapter description; empty = automatic (the window's monitor's adapter).
|
||||||
let gpus = crate::gpu::adapter_names();
|
let gpus = crate::gpu::adapter_names();
|
||||||
@@ -177,24 +196,25 @@ pub(crate) fn settings_page(ctx: &Arc<AppCtx>, set_screen: &AsyncSetState<Screen
|
|||||||
.position(|n| *n == s.adapter)
|
.position(|n| *n == s.adapter)
|
||||||
.map_or(0, |i| i + 1);
|
.map_or(0, |i| i + 1);
|
||||||
let gpus = gpus.clone();
|
let gpus = gpus.clone();
|
||||||
setting_combo(
|
setting_combo(ctx, "GPU", names, current, move |s, i| {
|
||||||
ctx,
|
s.adapter = if i == 0 {
|
||||||
"GPU (decode + present, applies to the next stream)",
|
String::new()
|
||||||
names,
|
} else {
|
||||||
current,
|
gpus[i - 1].clone()
|
||||||
move |s, i| {
|
};
|
||||||
s.adapter = if i == 0 {
|
})
|
||||||
String::new()
|
.tooltip(
|
||||||
} else {
|
"Which adapter decodes and presents the stream. Applies to the next stream; \
|
||||||
gpus[i - 1].clone()
|
Automatic uses the GPU driving this window's display.",
|
||||||
};
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let (codec_names, codec_i) = presets(CODECS, |v| *v == s.codec);
|
let (codec_names, codec_i) = presets(CODECS, |v| *v == s.codec);
|
||||||
let codec_combo = setting_combo(ctx, "Video codec", codec_names, codec_i, |s, i| {
|
let codec_combo = setting_combo(ctx, "Video codec", codec_names, codec_i, |s, i| {
|
||||||
s.codec = CODECS[i].0.to_string();
|
s.codec = CODECS[i].0.to_string();
|
||||||
});
|
})
|
||||||
|
.tooltip(
|
||||||
|
"A soft preference \u{2014} the host falls back to the best codec both sides support.",
|
||||||
|
);
|
||||||
// Free-form Mb/s (0 = host default) instead of presets, so a speed-test recommendation
|
// Free-form Mb/s (0 = host default) instead of presets, so a speed-test recommendation
|
||||||
// round-trips exactly.
|
// round-trips exactly.
|
||||||
let bitrate_box = {
|
let bitrate_box = {
|
||||||
@@ -207,10 +227,18 @@ pub(crate) fn settings_page(ctx: &Arc<AppCtx>, set_screen: &AsyncSetState<Screen
|
|||||||
s.bitrate_kbps = (v.clamp(0.0, 3000.0) * 1000.0) as u32;
|
s.bitrate_kbps = (v.clamp(0.0, 3000.0) * 1000.0) as u32;
|
||||||
s.save();
|
s.save();
|
||||||
})
|
})
|
||||||
|
.tooltip(
|
||||||
|
"0 lets the host decide. Run a per-host speed test from the host list for a \
|
||||||
|
recommendation.",
|
||||||
|
)
|
||||||
};
|
};
|
||||||
let hdr_toggle = setting_toggle(ctx, "HDR (10-bit, BT.2020 PQ)", s.hdr_enabled, |s, on| {
|
let hdr_toggle = setting_toggle(ctx, "HDR (10-bit, BT.2020 PQ)", s.hdr_enabled, |s, on| {
|
||||||
s.hdr_enabled = on
|
s.hdr_enabled = on
|
||||||
});
|
})
|
||||||
|
.tooltip(
|
||||||
|
"Advertise 10-bit HDR10 so the host upgrades HDR content. Needs a display in HDR mode; \
|
||||||
|
SDR content is unaffected.",
|
||||||
|
);
|
||||||
|
|
||||||
// --- Input -----------------------------------------------------------------------------
|
// --- Input -----------------------------------------------------------------------------
|
||||||
// Which physical controller forwards as pad 0: automatic = the most recently connected;
|
// Which physical controller forwards as pad 0: automatic = the most recently connected;
|
||||||
@@ -247,87 +275,129 @@ pub(crate) fn settings_page(ctx: &Arc<AppCtx>, set_screen: &AsyncSetState<Screen
|
|||||||
ids.get(sel - 1).copied()
|
ids.get(sel - 1).copied()
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
.tooltip(
|
||||||
|
"Exactly one controller is forwarded to the host; \u{201C}Automatic\u{201D} \
|
||||||
|
picks the most recently connected.",
|
||||||
|
)
|
||||||
};
|
};
|
||||||
let (pad_names, pad_i) = presets(GAMEPADS, |v| {
|
let (pad_names, pad_i) = presets(GAMEPADS, |v| {
|
||||||
GamepadPref::from_name(v) == GamepadPref::from_name(&s.gamepad)
|
GamepadPref::from_name(v) == GamepadPref::from_name(&s.gamepad)
|
||||||
});
|
});
|
||||||
let pad_combo = setting_combo(ctx, "Gamepad type", pad_names, pad_i, |s, i| {
|
let pad_combo = setting_combo(ctx, "Gamepad type", pad_names, pad_i, |s, i| {
|
||||||
s.gamepad = GAMEPADS[i].0.to_string();
|
s.gamepad = GAMEPADS[i].0.to_string();
|
||||||
});
|
})
|
||||||
|
.tooltip(
|
||||||
|
"The virtual pad the host creates. \u{201C}Automatic\u{201D} matches your physical \
|
||||||
|
controller.",
|
||||||
|
);
|
||||||
let shortcuts_toggle = setting_toggle(
|
let shortcuts_toggle = setting_toggle(
|
||||||
ctx,
|
ctx,
|
||||||
"Capture system shortcuts (Alt+Tab, Win, \u{2026})",
|
"Capture system shortcuts (Alt+Tab, Win, \u{2026})",
|
||||||
s.inhibit_shortcuts,
|
s.inhibit_shortcuts,
|
||||||
|s, on| s.inhibit_shortcuts = on,
|
|s, on| s.inhibit_shortcuts = on,
|
||||||
);
|
)
|
||||||
|
.tooltip("Off: Alt+Tab, Win & co. act on this machine while the stream input is captured.");
|
||||||
|
|
||||||
// --- Audio -----------------------------------------------------------------------------
|
// --- Audio -----------------------------------------------------------------------------
|
||||||
let (ac_names, ac_i) = presets(AUDIO_CHANNELS, |v| *v == s.audio_channels);
|
let (ac_names, ac_i) = presets(AUDIO_CHANNELS, |v| *v == s.audio_channels);
|
||||||
let channels_combo = setting_combo(ctx, "Audio channels", ac_names, ac_i, |s, i| {
|
let channels_combo = setting_combo(ctx, "Audio channels", ac_names, ac_i, |s, i| {
|
||||||
s.audio_channels = AUDIO_CHANNELS[i].0;
|
s.audio_channels = AUDIO_CHANNELS[i].0;
|
||||||
});
|
})
|
||||||
|
.tooltip("The host downmixes if its output has fewer channels.");
|
||||||
let mic_toggle = setting_toggle(
|
let mic_toggle = setting_toggle(
|
||||||
ctx,
|
ctx,
|
||||||
"Stream microphone to the host",
|
"Stream microphone to the host",
|
||||||
s.mic_enabled,
|
s.mic_enabled,
|
||||||
|s, on| s.mic_enabled = on,
|
|s, on| s.mic_enabled = on,
|
||||||
);
|
)
|
||||||
|
.tooltip("Sends the default microphone to the host's virtual mic source.");
|
||||||
|
|
||||||
|
let hud_toggle = setting_toggle(ctx, "Show the stats overlay (HUD)", s.show_hud, |s, on| {
|
||||||
|
s.show_hud = on
|
||||||
|
})
|
||||||
|
.tooltip("The in-stream overlay: mode, codec, fps, bitrate, latency, decode path.");
|
||||||
|
|
||||||
let back_btn = button("Back").accent().icon(SymbolGlyph::Back).on_click({
|
|
||||||
let ss = set_screen.clone();
|
|
||||||
move || ss.call(Screen::Hosts)
|
|
||||||
});
|
|
||||||
let licenses_button = {
|
let licenses_button = {
|
||||||
let ss = set_screen.clone();
|
let ss = set_screen.clone();
|
||||||
button("Third-party licenses").on_click(move || ss.call(Screen::Licenses))
|
button("Third-party licenses").on_click(move || ss.call(Screen::Licenses))
|
||||||
};
|
};
|
||||||
|
|
||||||
page(vec![
|
// The selected section's content — per-control guidance lives on hover tooltips, so the
|
||||||
page_header("Settings", back_btn),
|
// card is just the controls.
|
||||||
section("DISPLAY"),
|
let (title, card): (&str, Element) = match section {
|
||||||
settings_card(
|
"video" => (
|
||||||
"Display",
|
|
||||||
"The host creates a virtual display at exactly this mode. The compositor choice is \
|
|
||||||
advisory (Linux hosts only).",
|
|
||||||
vec![res_combo.into(), hz_combo.into(), comp_combo.into()],
|
|
||||||
),
|
|
||||||
section("VIDEO"),
|
|
||||||
settings_card(
|
|
||||||
"Video",
|
"Video",
|
||||||
"Hardware decode (D3D11VA) is far lighter than software — keep it on Automatic \
|
settings_card({
|
||||||
unless debugging. Run a per-host speed test (host list) before setting a high \
|
|
||||||
bitrate.",
|
|
||||||
{
|
|
||||||
let mut controls: Vec<Element> = vec![decoder_combo.into()];
|
let mut controls: Vec<Element> = vec![decoder_combo.into()];
|
||||||
if let Some(c) = gpu_combo {
|
if let Some(c) = gpu_combo {
|
||||||
controls.push(c.into());
|
controls.push(c.into());
|
||||||
}
|
}
|
||||||
controls.extend([codec_combo.into(), bitrate_box.into(), hdr_toggle.into()]);
|
controls.extend([
|
||||||
|
codec_combo.into(),
|
||||||
|
bitrate_box.into(),
|
||||||
|
hdr_toggle.into(),
|
||||||
|
hud_toggle.into(),
|
||||||
|
]);
|
||||||
controls
|
controls
|
||||||
},
|
}),
|
||||||
),
|
),
|
||||||
section("INPUT"),
|
"input" => (
|
||||||
settings_card(
|
|
||||||
"Input",
|
"Input",
|
||||||
"Exactly one controller is forwarded to the host; \u{201C}Automatic\u{201D} picks the \
|
settings_card(vec![
|
||||||
most recently connected. The gamepad type is the virtual pad the host creates.",
|
|
||||||
vec![
|
|
||||||
forward_combo.into(),
|
forward_combo.into(),
|
||||||
pad_combo.into(),
|
pad_combo.into(),
|
||||||
shortcuts_toggle.into(),
|
shortcuts_toggle.into(),
|
||||||
],
|
]),
|
||||||
),
|
),
|
||||||
section("AUDIO"),
|
"audio" => (
|
||||||
settings_card(
|
|
||||||
"Audio",
|
"Audio",
|
||||||
"Request stereo or surround — the host downmixes if its output has fewer.",
|
settings_card(vec![channels_combo.into(), mic_toggle.into()]),
|
||||||
vec![channels_combo.into(), mic_toggle.into()],
|
|
||||||
),
|
),
|
||||||
section("ABOUT"),
|
"about" => ("About", settings_card(vec![licenses_button.into()])),
|
||||||
settings_card(
|
_ => (
|
||||||
"About",
|
"Display",
|
||||||
"punktfunk is licensed under MIT OR Apache-2.0.",
|
settings_card(vec![res_combo.into(), hz_combo.into(), comp_combo.into()]),
|
||||||
vec![licenses_button.into()],
|
|
||||||
),
|
),
|
||||||
])
|
};
|
||||||
|
|
||||||
|
// The stock WinUI sidebar (Windows-Settings pattern): pane on the left, the section's card
|
||||||
|
// as content, the NavigationView's own back arrow returning to the host list. Auto display
|
||||||
|
// mode collapses the pane on a narrow window, exactly like Windows Settings.
|
||||||
|
let items = vec![
|
||||||
|
NavViewItem::new("Display")
|
||||||
|
.tag("display")
|
||||||
|
.icon(Symbol::FullScreen),
|
||||||
|
NavViewItem::new("Video").tag("video").icon(Symbol::Video),
|
||||||
|
NavViewItem::new("Input")
|
||||||
|
.tag("input")
|
||||||
|
.icon(Symbol::Keyboard),
|
||||||
|
NavViewItem::new("Audio").tag("audio").icon(Symbol::Volume),
|
||||||
|
NavViewItem::new("About").tag("about").icon(Symbol::Help),
|
||||||
|
];
|
||||||
|
// The card is KEYED by section so switching panes REMOUNTS it instead of diffing one
|
||||||
|
// section's controls into another's: an in-place diff re-sets a reused ComboBox's items
|
||||||
|
// (which clears WinUI's selection) but skips `selected_index` whenever the two sections'
|
||||||
|
// values compare equal — the combo then renders with no selected option. A fresh mount
|
||||||
|
// applies every prop, so the selection always displays.
|
||||||
|
//
|
||||||
|
// The content column (not the NavigationView — the sidebar must stay put) carries the
|
||||||
|
// section-switch entrance: fade + slide-up from the root-driven tween.
|
||||||
|
let content = page_wide(vec![card.with_key(section)])
|
||||||
|
.opacity(progress)
|
||||||
|
.margin(edges(0.0, (1.0 - progress) * 22.0, 0.0, 0.0));
|
||||||
|
NavigationView::new(items, content)
|
||||||
|
.pane_title("Settings")
|
||||||
|
.header(title)
|
||||||
|
.selected_tag(section)
|
||||||
|
.on_selection_changed({
|
||||||
|
let ss = set_section.clone();
|
||||||
|
move |tag: String| ss.call(tag)
|
||||||
|
})
|
||||||
|
.settings_visible(false)
|
||||||
|
.back_enabled(true)
|
||||||
|
.on_back_requested({
|
||||||
|
let ss = set_screen.clone();
|
||||||
|
move || ss.call(Screen::Hosts)
|
||||||
|
})
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ pub(crate) fn speed_page(props: &SpeedProps, cx: &mut RenderCx) -> Element {
|
|||||||
let back_btn = {
|
let back_btn = {
|
||||||
let ss = set_screen.clone();
|
let ss = set_screen.clone();
|
||||||
button("Close")
|
button("Close")
|
||||||
.icon(SymbolGlyph::Back)
|
.icon(Symbol::Back)
|
||||||
.on_click(move || ss.call(Screen::Hosts))
|
.on_click(move || ss.call(Screen::Hosts))
|
||||||
.horizontal_alignment(HorizontalAlignment::Center)
|
.horizontal_alignment(HorizontalAlignment::Center)
|
||||||
};
|
};
|
||||||
@@ -126,7 +126,7 @@ pub(crate) fn speed_page(props: &SpeedProps, cx: &mut RenderCx) -> Element {
|
|||||||
let (ctx, ss, kbps) = (ctx.clone(), set_screen.clone(), *recommended_kbps);
|
let (ctx, ss, kbps) = (ctx.clone(), set_screen.clone(), *recommended_kbps);
|
||||||
button(format!("Use {recommended_mbps:.0} Mb/s"))
|
button(format!("Use {recommended_mbps:.0} Mb/s"))
|
||||||
.accent()
|
.accent()
|
||||||
.icon(SymbolGlyph::Accept)
|
.icon(Symbol::Accept)
|
||||||
.on_click(move || {
|
.on_click(move || {
|
||||||
let mut s = ctx.settings.lock().unwrap();
|
let mut s = ctx.settings.lock().unwrap();
|
||||||
s.bitrate_kbps = kbps;
|
s.bitrate_kbps = kbps;
|
||||||
@@ -154,7 +154,7 @@ pub(crate) fn speed_page(props: &SpeedProps, cx: &mut RenderCx) -> Element {
|
|||||||
hstack((apply_btn, {
|
hstack((apply_btn, {
|
||||||
let ss = set_screen.clone();
|
let ss = set_screen.clone();
|
||||||
button("Close")
|
button("Close")
|
||||||
.icon(SymbolGlyph::Cancel)
|
.icon(Symbol::Cancel)
|
||||||
.on_click(move || ss.call(Screen::Hosts))
|
.on_click(move || ss.call(Screen::Hosts))
|
||||||
}))
|
}))
|
||||||
.spacing(8.0)
|
.spacing(8.0)
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ impl PartialEq for StreamProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
/// Frames + host clock offset, stashed by the mount effect for `on_ready` (which fires later,
|
/// Frames + host clock offset, stashed by the mount effect for `on_mounted` (which fires
|
||||||
/// once the native panel exists).
|
/// later, once the native panel exists).
|
||||||
static PENDING: RefCell<Option<(crate::session::FrameRx, i64)>> = const { RefCell::new(None) };
|
static PENDING: RefCell<Option<(crate::session::FrameRx, i64)>> = const { RefCell::new(None) };
|
||||||
/// The live render thread; stopped + joined by the unmount cleanup (before panel teardown).
|
/// The live render thread; stopped + joined by the unmount cleanup (before panel teardown).
|
||||||
static RENDER: RefCell<Option<RenderThread>> = const { RefCell::new(None) };
|
static RENDER: RefCell<Option<RenderThread>> = const { RefCell::new(None) };
|
||||||
@@ -65,7 +65,7 @@ fn window_dpi() -> u32 {
|
|||||||
pub(crate) fn stream_page(props: &StreamProps, cx: &mut RenderCx) -> Element {
|
pub(crate) fn stream_page(props: &StreamProps, cx: &mut RenderCx) -> Element {
|
||||||
let ctx = &props.svc.ctx;
|
let ctx = &props.svc.ctx;
|
||||||
// Take the connector + frames handoff once on mount; keep the connector alive (and for input)
|
// Take the connector + frames handoff once on mount; keep the connector alive (and for input)
|
||||||
// in a use_ref, stash frames for `on_ready`, install the input hooks. The cleanup stops the
|
// in a use_ref, stash frames for `on_mounted`, install the input hooks. The cleanup stops the
|
||||||
// render thread FIRST (it must not present into a panel that's tearing down), then removes
|
// render thread FIRST (it must not present into a panel that's tearing down), then removes
|
||||||
// the input hooks.
|
// the input hooks.
|
||||||
let connector_ref = cx.use_ref::<Option<Arc<NativeClient>>>(None);
|
let connector_ref = cx.use_ref::<Option<Arc<NativeClient>>>(None);
|
||||||
@@ -95,46 +95,48 @@ pub(crate) fn stream_page(props: &StreamProps, cx: &mut RenderCx) -> Element {
|
|||||||
|
|
||||||
let mode = connector_ref.borrow().as_ref().map(|c| c.mode());
|
let mode = connector_ref.borrow().as_ref().map(|c| c.mode());
|
||||||
let host = ctx.shared.target.lock().unwrap().name.clone();
|
let host = ctx.shared.target.lock().unwrap().name.clone();
|
||||||
grid((
|
// Read per render: this page re-renders on every HUD sample (~400 ms), so toggling the
|
||||||
swap_chain_panel()
|
// overlay in Settings takes effect mid-stream.
|
||||||
.on_ready(|panel| {
|
let show_hud = ctx.settings.lock().unwrap().show_hud;
|
||||||
// Placeholder size — the first `on_resize` (fired after the first layout pass)
|
let mut layers: Vec<Element> = vec![swap_chain_panel()
|
||||||
// resizes to the panel's real pixel size.
|
.on_mounted(|panel| {
|
||||||
let dpi = window_dpi();
|
// Placeholder size — the first `on_resize` (fired after the first layout pass)
|
||||||
match Presenter::new(1280, 720, dpi) {
|
// resizes to the panel's real pixel size.
|
||||||
Ok(p) => {
|
let dpi = window_dpi();
|
||||||
if let Err(e) = panel.set_swap_chain(p.swap_chain()) {
|
match Presenter::new(1280, 720, dpi) {
|
||||||
tracing::error!(error = %e, "set_swap_chain");
|
Ok(p) => {
|
||||||
return;
|
if let Err(e) = panel.set_swap_chain(p.swap_chain()) {
|
||||||
}
|
tracing::error!(error = %e, "set_swap_chain");
|
||||||
if let Some((frames, clock_offset)) =
|
return;
|
||||||
PENDING.with(|c| c.borrow_mut().take())
|
}
|
||||||
{
|
if let Some((frames, clock_offset)) = PENDING.with(|c| c.borrow_mut().take()) {
|
||||||
let shared = render::RenderShared::new(1280, 720, dpi);
|
let shared = render::RenderShared::new(1280, 720, dpi);
|
||||||
RENDER.with(|cell| {
|
RENDER.with(|cell| {
|
||||||
*cell.borrow_mut() =
|
*cell.borrow_mut() =
|
||||||
Some(render::spawn(p, frames, shared, clock_offset));
|
Some(render::spawn(p, frames, shared, clock_offset));
|
||||||
});
|
});
|
||||||
tracing::info!(dpi, "stream presenter bound — render thread started");
|
tracing::info!(dpi, "stream presenter bound — render thread started");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(e) => tracing::error!(error = %e, "create presenter"),
|
|
||||||
}
|
}
|
||||||
})
|
Err(e) => tracing::error!(error = %e, "create presenter"),
|
||||||
.on_resize(|w, h| {
|
}
|
||||||
// DIPs → physical pixels; the presenter maps back via SetMatrixTransform.
|
})
|
||||||
let dpi = window_dpi();
|
.on_resize(|w, h| {
|
||||||
let px = |v: f64| (v * f64::from(dpi) / 96.0).round() as u32;
|
// DIPs → physical pixels; the presenter maps back via SetMatrixTransform.
|
||||||
RENDER.with(|cell| {
|
let dpi = window_dpi();
|
||||||
if let Some(rt) = cell.borrow().as_ref() {
|
let px = |v: f64| (v * f64::from(dpi) / 96.0).round() as u32;
|
||||||
rt.shared().set_dpi(dpi);
|
RENDER.with(|cell| {
|
||||||
rt.shared().set_size(px(w), px(h));
|
if let Some(rt) = cell.borrow().as_ref() {
|
||||||
}
|
rt.shared().set_dpi(dpi);
|
||||||
});
|
rt.shared().set_size(px(w), px(h));
|
||||||
}),
|
}
|
||||||
hud_overlay(&props.hud, mode, &host),
|
});
|
||||||
))
|
})
|
||||||
.into()
|
.into()];
|
||||||
|
if show_hud {
|
||||||
|
layers.push(hud_overlay(&props.hud, mode, &host));
|
||||||
|
}
|
||||||
|
grid(layers).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A small chip for the dark HUD: coloured text on a translucent dark fill.
|
/// A small chip for the dark HUD: coloured text on a translucent dark fill.
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Windowed (default): the WinUI 3 app owns host selection, settings, and pairing.
|
// Windowed (default): the WinUI 3 app owns host selection, settings, and pairing.
|
||||||
|
// Framework-dependent deployment: initialize the Windows App SDK runtime before any WinUI
|
||||||
|
// call (build.rs stages the bootstrap DLL via windows-reactor-setup).
|
||||||
|
if let Err(e) = windows_reactor::bootstrap() {
|
||||||
|
tracing::error!(error = %e, "Windows App SDK bootstrap failed");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
let gamepad = gamepad::GamepadService::start();
|
let gamepad = gamepad::GamepadService::start();
|
||||||
if let Err(e) = app::run(identity, gamepad) {
|
if let Err(e) = app::run(identity, gamepad) {
|
||||||
tracing::error!(error = %e, "WinUI app failed");
|
tracing::error!(error = %e, "WinUI app failed");
|
||||||
|
|||||||
@@ -149,12 +149,19 @@ pub struct Settings {
|
|||||||
/// vanished adapter (eGPU unplugged) falls back to automatic.
|
/// vanished adapter (eGPU unplugged) falls back to automatic.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub adapter: String,
|
pub adapter: String,
|
||||||
|
/// Show the stats/info overlay (HUD) over the stream.
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub show_hud: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_codec() -> String {
|
fn default_codec() -> String {
|
||||||
"auto".into()
|
"auto".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
/// The `codec` setting as a `quic::CODEC_*` preference bit (`0` = auto).
|
/// The `codec` setting as a `quic::CODEC_*` preference bit (`0` = auto).
|
||||||
pub fn preferred_codec(&self) -> u8 {
|
pub fn preferred_codec(&self) -> u8 {
|
||||||
@@ -183,6 +190,7 @@ impl Default for Settings {
|
|||||||
decoder: "auto".into(),
|
decoder: "auto".into(),
|
||||||
codec: "auto".into(),
|
codec: "auto".into(),
|
||||||
adapter: String::new(),
|
adapter: String::new(),
|
||||||
|
show_hud: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user