feat(windows): surface shortcuts on a Help screen; exit fullscreen when the stream stops
apple / swift (push) Successful in 1m10s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m13s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m13s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 44s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 41s
android / android (push) Successful in 4m11s
ci / rust (push) Failing after 1m21s
ci / web (push) Successful in 55s
apple / screenshots (push) Successful in 5m52s
ci / docs-site (push) Successful in 1m2s
arch / build-publish (push) Has been cancelled
ci / bench (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
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
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
apple / swift (push) Successful in 1m10s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m13s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m13s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 44s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 41s
android / android (push) Successful in 4m11s
ci / rust (push) Failing after 1m21s
ci / web (push) Successful in 55s
apple / screenshots (push) Successful in 5m52s
ci / docs-site (push) Successful in 1m2s
arch / build-publish (push) Has been cancelled
ci / bench (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
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
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
The in-stream keyboard shortcuts were only reachable deep in Settings > Input. Move them to a dedicated Help screen reached from a Help button on the host list (mirrors the Licenses screen — a hook-free page routed from root), and drop the reference card out of Settings so there's one home for it. Fullscreen (F11) was a window-style toggle that stream stop never undid, so disconnecting while fullscreen left the GUI borderless-fullscreen. Split toggle_fullscreen into is_fullscreen/enter_fullscreen/exit_fullscreen sharing a module-level SAVED_PLACEMENT, and have input::uninstall() call exit_fullscreen() on teardown. exit_fullscreen is a no-op when nothing was saved, so it's safe on every stream stop and idempotent with F11 — fullscreen is now streaming-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -180,6 +180,9 @@ pub fn uninstall() {
|
||||
}
|
||||
if let Some(mut st) = STATE.lock().unwrap().take() {
|
||||
set_captured(&mut st, false); // hand the cursor back + flush held state
|
||||
// Fullscreen is a streaming-only mode: if F11 put us there, drop back to a normal window
|
||||
// so the GUI (the host list) is never left borderless-fullscreen after the stream ends.
|
||||
exit_fullscreen(HWND(st.hwnd as *mut _));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,73 +247,104 @@ fn set_locked(st: &mut State, on: bool) {
|
||||
st.locked = on;
|
||||
}
|
||||
|
||||
/// Toggle borderless fullscreen for our top-level window (F11). The classic Win32 dance: entering,
|
||||
/// save the window placement and strip `WS_OVERLAPPEDWINDOW`, then size the window to the whole
|
||||
/// monitor; exiting, restore the style and the saved placement. The window's own style bit doubles
|
||||
/// as the fullscreen flag, so no extra state beyond the saved placement is needed. windows-reactor
|
||||
/// owns the WinUI window but exposes no fullscreen API, so we drive the HWND directly (parity with
|
||||
/// the GTK client's F11). The SwapChainPanel follows the resulting `WM_SIZE` like any window resize.
|
||||
fn toggle_fullscreen(hwnd: isize) {
|
||||
/// The pre-fullscreen window placement, saved on entering fullscreen and restored on leaving it.
|
||||
/// Module-level (not a `toggle_fullscreen`-local static) so the F11 toggle and the stream-stop exit
|
||||
/// ([`uninstall`]) share the one saved placement, and its presence is also the "are we fullscreen?"
|
||||
/// flag for [`exit_fullscreen`]. Only ever touched on the UI thread (the hook proc / the stream
|
||||
/// page's unmount), but a Mutex keeps the static sound + `Sync`.
|
||||
static SAVED_PLACEMENT: Mutex<Option<windows::Win32::UI::WindowsAndMessaging::WINDOWPLACEMENT>> =
|
||||
Mutex::new(None);
|
||||
|
||||
/// Whether our top-level window is currently borderless-fullscreen. Entering strips
|
||||
/// `WS_OVERLAPPEDWINDOW`, so its absence is the flag — no extra state beyond [`SAVED_PLACEMENT`].
|
||||
fn is_fullscreen(hwnd: HWND) -> bool {
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GetWindowLongPtrW, GWL_STYLE, WS_OVERLAPPEDWINDOW,
|
||||
};
|
||||
let overlapped = WS_OVERLAPPEDWINDOW.0 as isize;
|
||||
unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) & overlapped == 0 }
|
||||
}
|
||||
|
||||
/// Enter borderless fullscreen: remember the window placement, drop the frame
|
||||
/// (`WS_OVERLAPPEDWINDOW`), and size the window to cover the whole monitor. windows-reactor owns
|
||||
/// the WinUI window but exposes no fullscreen API, so we drive the HWND directly (parity with the
|
||||
/// GTK client's F11). The SwapChainPanel follows the resulting `WM_SIZE` like any window resize.
|
||||
fn enter_fullscreen(hwnd: HWND) {
|
||||
use windows::Win32::Graphics::Gdi::{
|
||||
GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTOPRIMARY,
|
||||
};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GetWindowLongPtrW, GetWindowPlacement, SetWindowLongPtrW, SetWindowPlacement, SetWindowPos,
|
||||
GWL_STYLE, SWP_FRAMECHANGED, SWP_NOMOVE, SWP_NOOWNERZORDER, SWP_NOSIZE, SWP_NOZORDER,
|
||||
WINDOWPLACEMENT, WS_OVERLAPPEDWINDOW,
|
||||
GetWindowLongPtrW, GetWindowPlacement, SetWindowLongPtrW, SetWindowPos, GWL_STYLE,
|
||||
SWP_FRAMECHANGED, SWP_NOOWNERZORDER, SWP_NOZORDER, WINDOWPLACEMENT, WS_OVERLAPPEDWINDOW,
|
||||
};
|
||||
// The pre-fullscreen placement, so exiting restores the exact windowed size + position. Only
|
||||
// ever touched on the UI thread (the hook proc), but a Mutex keeps the static sound + `Sync`.
|
||||
static SAVED: Mutex<Option<WINDOWPLACEMENT>> = Mutex::new(None);
|
||||
let hwnd = HWND(hwnd as *mut _);
|
||||
let overlapped = WS_OVERLAPPEDWINDOW.0 as isize;
|
||||
unsafe {
|
||||
let style = GetWindowLongPtrW(hwnd, GWL_STYLE);
|
||||
if style & overlapped != 0 {
|
||||
// Windowed → fullscreen: remember where we were, drop the frame, cover the monitor.
|
||||
let mut wp = WINDOWPLACEMENT {
|
||||
length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
let mut mi = MONITORINFO {
|
||||
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
let mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY);
|
||||
if GetWindowPlacement(hwnd, &mut wp).is_ok() && GetMonitorInfoW(mon, &mut mi).as_bool()
|
||||
{
|
||||
*SAVED.lock().unwrap() = Some(wp);
|
||||
SetWindowLongPtrW(hwnd, GWL_STYLE, style & !overlapped);
|
||||
let r = mi.rcMonitor;
|
||||
let _ = SetWindowPos(
|
||||
hwnd,
|
||||
None,
|
||||
r.left,
|
||||
r.top,
|
||||
r.right - r.left,
|
||||
r.bottom - r.top,
|
||||
SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Fullscreen → windowed: restore the frame, then the saved placement.
|
||||
SetWindowLongPtrW(hwnd, GWL_STYLE, style | overlapped);
|
||||
if let Some(wp) = SAVED.lock().unwrap().take() {
|
||||
let _ = SetWindowPlacement(hwnd, &wp);
|
||||
}
|
||||
let mut wp = WINDOWPLACEMENT {
|
||||
length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
let mut mi = MONITORINFO {
|
||||
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
let mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY);
|
||||
if GetWindowPlacement(hwnd, &mut wp).is_ok() && GetMonitorInfoW(mon, &mut mi).as_bool() {
|
||||
*SAVED_PLACEMENT.lock().unwrap() = Some(wp);
|
||||
SetWindowLongPtrW(hwnd, GWL_STYLE, style & !overlapped);
|
||||
let r = mi.rcMonitor;
|
||||
let _ = SetWindowPos(
|
||||
hwnd,
|
||||
None,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED,
|
||||
r.left,
|
||||
r.top,
|
||||
r.right - r.left,
|
||||
r.bottom - r.top,
|
||||
SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Leave borderless fullscreen: restore the frame style and the saved placement. A no-op when we
|
||||
/// aren't fullscreen (nothing saved), so it's safe to call unconditionally on stream stop.
|
||||
fn exit_fullscreen(hwnd: HWND) {
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GetWindowLongPtrW, SetWindowLongPtrW, SetWindowPlacement, SetWindowPos, GWL_STYLE,
|
||||
SWP_FRAMECHANGED, SWP_NOMOVE, SWP_NOOWNERZORDER, SWP_NOSIZE, SWP_NOZORDER,
|
||||
WS_OVERLAPPEDWINDOW,
|
||||
};
|
||||
let Some(wp) = SAVED_PLACEMENT.lock().unwrap().take() else {
|
||||
return; // never went fullscreen — nothing to restore
|
||||
};
|
||||
let overlapped = WS_OVERLAPPEDWINDOW.0 as isize;
|
||||
unsafe {
|
||||
let style = GetWindowLongPtrW(hwnd, GWL_STYLE);
|
||||
SetWindowLongPtrW(hwnd, GWL_STYLE, style | overlapped);
|
||||
let _ = SetWindowPlacement(hwnd, &wp);
|
||||
let _ = SetWindowPos(
|
||||
hwnd,
|
||||
None,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle borderless fullscreen for our top-level window (F11), the classic Win32 dance split into
|
||||
/// [`enter_fullscreen`] / [`exit_fullscreen`] so the stream-stop path can force windowed too.
|
||||
fn toggle_fullscreen(hwnd: isize) {
|
||||
let hwnd = HWND(hwnd as *mut _);
|
||||
if is_fullscreen(hwnd) {
|
||||
exit_fullscreen(hwnd);
|
||||
} else {
|
||||
enter_fullscreen(hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
fn send(c: &NativeClient, kind: InputKind, code: u32, x: i32, y: i32, flags: u32) {
|
||||
let _ = c.send_input(&InputEvent {
|
||||
kind,
|
||||
|
||||
Reference in New Issue
Block a user