Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion apps/desktop/scripts/verify-shell.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,17 @@ assert.match(appSource, /setPointerCapture[\s\S]*manualResizeDragRef|manualResiz
assert.match(appSource, /requestManualResizeFlush[\s\S]*resizeCommandInFlight/, "manual macOS resize fallback must coalesce pointer deltas and keep only one resize IPC in flight");
assert.match(appSource, /flushManualResizeDelta[\s\S]*applyManualMainWindowResize/, "manual macOS resize fallback must flush coalesced deltas through the backend resize command");
assert.match(appSource, /window_set_resize_cursor/, "resize handles must explicitly set native cursor icons instead of relying only on CSS over transparent macOS overlays");
assert.match(appSource, /function\s+isMacosResizeRuntime[\s\S]*navigator\.platform[\s\S]*Mac/, "macOS resize fallback must not depend only on async platform support before choosing the manual resize path");
assert.match(appSource, /isMacosResizeRuntime\(platformOs\)[\s\S]*manualResizeDragRef/, "macOS resize handles must use the manual resize path even before platform support finishes loading");
assert.match(appSource, /(?:onPointerEnter=\{\(event\) => handleResizePointerEnter\(event, region\.direction\)\}|onPointerEnter:\s*handleResizePointerEnter[\s\S]*onPointerEnter=\{\(event\) => resizeRegionHandlers\.onPointerEnter\(event, region\.direction\)\}|onPointerEnter=\{\(event\) => resizeRegionHandlers\.onPointerEnter\(event, region\.direction\)\}[\s\S]*onPointerEnter:\s*handleResizePointerEnter)/, "resize handles must set the matching cursor as soon as the pointer enters the hit area");
assert.match(appSource, /onPointerMove=\{\(event\) => resizeRegionHandlers\.onPointerMove\(event, region\.direction\)\}/, "resize handles must refresh hover feedback on pointer move, not only on pointer enter");
assert.match(appSource, /function\s+handleResizePointerMove\([^)]*direction:\s*ResizeDirection[\s\S]*setResizeBoundaryFeedback\(direction\)/, "resize pointer move must keep the boundary hint active while the pointer remains inside a resize hit area");
assert.match(appSource, /function\s+resizeDirectionFromSurfacePointer[\s\S]*getBoundingClientRect[\s\S]*WINDOW_RESIZE_EDGE_HIT_PX/, "overlay shell must detect resize edges by pointer coordinates instead of relying only on narrow transparent resize div hit-testing");
assert.match(appSource, /onPointerDownCapture[\s\S]*handleResizeSurfacePointerDown/, "overlay shell must capture edge pointerdown before the full-surface drag region starts window dragging");
assert.match(appSource, /onPointerMoveCapture[\s\S]*handleResizeSurfacePointerMove/, "overlay shell must refresh resize feedback from capture-phase pointer movement across the full transparent overlay");
assert.match(appSource, /function\s+handleResizeSurfacePointerMove[\s\S]*manualResizeDragRef\.current[\s\S]*handleResizePointerMove\(event,\s*pendingResize\.direction\)/, "manual macOS resize drags must continue with the captured direction after the pointer leaves the edge hit zone");
assert.match(appSource, /function\s+isPointerNearViewportResizeEdge[\s\S]*WINDOW_RESIZE_EDGE_HIT_PX/, "global pointerout handling must recognize resize-edge pointer positions before clearing hover feedback");
assert.match(appSource, /handleWindowPointerExit[\s\S]*isPointerNearViewportResizeEdge\(event\)[\s\S]*return/, "resize hover feedback must not be cleared by window pointerout while the pointer remains on a resize edge");
assert.match(appSource, /(?:onPointerLeave=\{handleResizePointerLeave\}|onPointerLeave:\s*handleResizePointerLeave[\s\S]*onPointerLeave=\{resizeRegionHandlers\.onPointerLeave\}|onPointerLeave=\{resizeRegionHandlers\.onPointerLeave\}[\s\S]*onPointerLeave:\s*handleResizePointerLeave)/, "resize handles must restore the default cursor when the pointer leaves the hit area");
assert.match(appSource, /resizeFeedback/, "resize handles must maintain visible in-app resize feedback state for macOS when native cursor icons are unavailable");
assert.match(appSource, /resize-feedback--active/, "resize dragging must strengthen the in-app resize feedback");
Expand Down Expand Up @@ -828,6 +838,10 @@ assert.match(styles, /\.history-section/, "styles must include playback history
assert.match(styles, /\.history-item/, "styles must include playback history item styling");
assert.match(styles, /\.drag-region\s*\{[\s\S]*inset:\s*0/, "drag region must cover the non-control player surface, not just the title strip");
assert.match(styles, /\.resize-region\s*\{[\s\S]*z-index:\s*95/, "overlay must keep resize hit areas above modal backdrops");
assert.match(styles, /\.resize-region\s*\{[\s\S]*background:\s*rgba\(\s*0,\s*0,\s*0,\s*0\.004\s*\)/, "transparent overlay resize hit areas must paint a near-invisible hit-test surface so macOS WebView does not pass through to the drag region");
assert.match(styles, /\.resize-region\s*\{[\s\S]*touch-action:\s*none/, "resize hit areas must keep browser gesture handling from stealing edge drags");
assert.match(styles, /\.resize-region--east,\s*[\s\S]*\.resize-region--west\s*\{[\s\S]*width:\s*16px/, "side resize hit areas must be wide enough to catch macOS transparent overlay edge drags");
assert.match(styles, /\.resize-region--north-east,\s*[\s\S]*\.resize-region--south-west\s*\{[\s\S]*width:\s*24px[\s\S]*height:\s*24px/, "corner resize hit areas must be large enough to catch macOS transparent overlay corner drags");
assert.match(styles, /\.resize-region--south-east/, "overlay must include corner resize hit areas");
assert.match(styles, /\.resize-feedback\s*\{[\s\S]*pointer-events:\s*none/, "resize feedback must be visible without blocking media controls or resize hit areas");
assert.match(styles, /\.resize-feedback\s*\{[\s\S]*z-index:\s*96/, "resize feedback must render above transparent resize hit areas so edge hints are complete");
Expand Down Expand Up @@ -1065,6 +1079,9 @@ assert.match(tauriRuntimeSource, /VK_OEM_5/, "Windows shortcut bridge must recog
assert.match(tauriRuntimeSource, /openplayer-native-shortcut/, "native shortcut bridge must emit actions to the overlay frontend");
assert.match(tauriRuntimeSource, /sync_overlay_to_main[\s\S]*focus_overlay_window\(app\)/, "overlay sync must return keyboard focus to the controls window");
assert.match(tauriRuntimeSource, /openplayer_macos_prepare_overlay_window/, "macOS overlay window must be marked as a fullscreen auxiliary child of the video window");
assert.match(tauriRuntimeSource, /WebviewWindowBuilder::new\([\s\S]*"overlay"[\s\S]*\.resizable\(cfg!\(target_os = "macos"\)\)/, "macOS overlay window must be natively resizable because it is the front window receiving edge hit-tests");
assert.match(tauriRuntimeSource, /fn sync_main_to_overlay_after_resize[\s\S]*main\.set_position[\s\S]*main\.set_size/, "macOS native overlay resize must resize the main video window to match the front controls window");
assert.match(tauriRuntimeSource, /overlay\.on_window_event\([\s\S]*WindowEvent::(?:Moved|Resized)[\s\S]*sync_main_to_overlay_after_resize/, "overlay native resize and move events must synchronize the main video window");
assert.match(macosGlViewSource, /NSWindowCollectionBehaviorFullScreenAuxiliary[\s\S]*addChildWindow/, "macOS overlay must join the main video fullscreen space instead of creating a separate fullscreen space");
assert.match(tauriRuntimeSource, /WindowEvent::Focused\(true\)[\s\S]*focus_overlay_window\(&app_handle\)/, "clicking the video/main window must return keyboard focus to the overlay shortcut handler");
assert.match(tauriRuntimeSource, /WindowEvent::CloseRequested[\s\S]*overlay_window\(&app_handle\)[\s\S]*overlay\.close\(\)/, "closing the main video window through OS chrome or Alt+F4 must also close the overlay controls window");
Expand All @@ -1082,7 +1099,8 @@ assert.match(mpvEmbedRuntimeSource, /mpv_embed_recording_state[\s\S]*mpv_embed_s
assert.match(tauriRuntimeSource, /window_start_resize[\s\S]*start_resize_dragging/, "desktop backend must start resizing the main video window from overlay hit areas");
assert.match(tauriRuntimeSource, /fn window_set_resize_cursor[\s\S]*CursorIcon::NeResize[\s\S]*CursorIcon::Default/, "desktop backend must expose native resize cursor icons for overlay hit areas");
assert.match(windowApplyResizeDeltaSource, /set_position[\s\S]*set_size/, "macOS manual resize fallback must resize the main video window");
assert.doesNotMatch(windowApplyResizeDeltaSource, /sync_overlay_to_main|sync_mpv_video_host/, "macOS manual resize deltas must not run immediate duplicate overlay/mpv sync on every pointermove");
assert.match(windowApplyResizeDeltaSource, /sync_overlay_to_main_after_resize/, "macOS manual resize fallback must resize the front overlay window after resizing the main video window");
assert.doesNotMatch(windowApplyResizeDeltaSource, /sync_mpv_video_host|schedule_mpv_video_host_sync/, "macOS manual resize deltas must not run immediate duplicate mpv host sync on every pointermove");
assert.match(tauriRuntimeSource, /window_close/, "desktop backend must keep close command");
assert.match(windowStartDragSource, /main_window\(&app\)\?[\s\S]*start_dragging/, "backend must drag the main video window when overlay drag strip is used");
assert.match(tauriRuntimeSource, /fn app_version/, "desktop backend must expose app version metadata for the about page");
Expand Down
48 changes: 42 additions & 6 deletions apps/desktop/src-tauri/src/window/overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use tauri::{AppHandle, Manager, PhysicalPosition, PhysicalSize, Position, Size};
use tauri::{WebviewUrl, WebviewWindowBuilder, WindowEvent, utils::config::Color};

use super::{
WindowState, begin_window_close, main_window,
MIN_MAIN_WINDOW_HEIGHT, MIN_MAIN_WINDOW_WIDTH, WindowState, begin_window_close, main_window,
overlay_platform::prepare_macos_main_window_chrome, overlay_platform::set_overlay_owner,
overlay_window,
};
Expand All @@ -25,6 +25,38 @@ fn sync_overlay_to_main_without_focus(app: &AppHandle) {
sync_overlay_to_main_with_focus(app, false);
}

pub(super) fn sync_overlay_to_main_after_resize(app: &AppHandle) {
sync_overlay_to_main_without_focus(app);
}

fn sync_main_to_overlay_after_resize(app: &AppHandle) {
let Ok(main) = main_window(app) else {
return;
};
if main.is_fullscreen().unwrap_or(false) || main.is_maximized().unwrap_or(false) {
return;
}
let Some(overlay) = overlay_window(app) else {
return;
};
let Ok(position) = overlay.outer_position() else {
return;
};
let Ok(size) = overlay.outer_size() else {
return;
};
let _ = main.set_position(Position::Physical(PhysicalPosition {
x: position.x,
y: position.y,
}));
let _ = main.set_size(Size::Physical(PhysicalSize {
width: size.width,
height: size.height,
}));
#[cfg(feature = "mpv-embed")]
schedule_mpv_video_host_sync(app);
}

fn sync_overlay_to_main_with_focus(app: &AppHandle, focus_overlay: bool) {
let Ok(main) = main_window(app) else {
return;
Expand Down Expand Up @@ -112,7 +144,8 @@ pub(crate) fn setup_overlay_window(app: &mut tauri::App) -> Result<(), String> {
.decorations(false)
.transparent(true)
.shadow(false)
.resizable(false)
.resizable(cfg!(target_os = "macos"))
.min_inner_size(MIN_MAIN_WINDOW_WIDTH as f64, MIN_MAIN_WINDOW_HEIGHT as f64)
.skip_taskbar(true)
.background_color(Color(0, 0, 0, 0))
.visible(false)
Expand All @@ -125,20 +158,23 @@ pub(crate) fn setup_overlay_window(app: &mut tauri::App) -> Result<(), String> {
sync_overlay_to_main(&app_handle);
let _ = overlay.show();
set_overlay_owner(&window, &overlay);
let app_handle_for_overlay_close = app_handle.clone();
let app_handle_for_overlay = app_handle.clone();
overlay.on_window_event(move |event| {
if matches!(event, WindowEvent::CloseRequested { .. }) {
let window_state = app_handle_for_overlay_close.state::<WindowState>();
let window_state = app_handle_for_overlay.state::<WindowState>();
if !begin_window_close(window_state.inner()) {
return;
}

let _ = stop_embedded_player_for_close(&app_handle_for_overlay_close);
let Ok(main) = main_window(&app_handle_for_overlay_close) else {
let _ = stop_embedded_player_for_close(&app_handle_for_overlay);
let Ok(main) = main_window(&app_handle_for_overlay) else {
return;
};
let _ = main.close();
}
if matches!(event, WindowEvent::Moved(_) | WindowEvent::Resized(_)) {
sync_main_to_overlay_after_resize(&app_handle_for_overlay);
}
});
window.on_window_event(move |event| {
if matches!(event, WindowEvent::CloseRequested { .. }) {
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src-tauri/src/window/resize.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use tauri::{AppHandle, CursorIcon, PhysicalPosition, PhysicalSize, Position, Size};
use tauri_runtime::ResizeDirection;

use super::{MIN_MAIN_WINDOW_HEIGHT, MIN_MAIN_WINDOW_WIDTH, main_window, overlay_window};
use super::{MIN_MAIN_WINDOW_HEIGHT, MIN_MAIN_WINDOW_WIDTH, main_window, overlay, overlay_window};

pub(crate) fn window_start_resize(app: AppHandle, direction: String) -> Result<(), String> {
let direction = resize_direction_from_str(&direction)?;
Expand Down Expand Up @@ -120,6 +120,7 @@ pub(crate) fn window_apply_resize_delta(
height: height as u32,
}))
.map_err(|error| error.to_string())?;
overlay::sync_overlay_to_main_after_resize(&app);
Ok(())
}

Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/app/constants/timing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export const DEFAULT_VOLUME_STEP = 0.05;
export const WINDOW_DRAG_START_DISTANCE_PX = 4;
export const WINDOW_DOUBLE_CLICK_MAX_MS = 650;
export const WINDOW_DOUBLE_CLICK_MAX_DISTANCE_PX = 24;
export const WINDOW_RESIZE_EDGE_HIT_PX = 24;
export const SUBTITLE_DELAY_STEP_SECONDS = 0.1;
8 changes: 6 additions & 2 deletions apps/desktop/src/components/player/PlayerAppView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ type ShellHandlers = Pick<
| "onDrop"
| "onKeyDown"
| "onPointerDown"
| "onPointerDownCapture"
| "onPointerLeave"
| "onPointerMove"
| "onPointerMoveCapture"
| "onPointerUpCapture"
| "onPointerCancelCapture"
| "onWheel"
>;

Expand All @@ -37,7 +41,7 @@ type ResizeRegionHandlers = {
onPointerEnter: (event: PointerEvent<HTMLDivElement>, direction: ResizeDirection) => void;
onPointerLeave: ComponentProps<"div">["onPointerLeave"];
onPointerDown: (event: PointerEvent<HTMLDivElement>, direction: ResizeDirection) => void;
onPointerMove: ComponentProps<"div">["onPointerMove"];
onPointerMove: (event: PointerEvent<HTMLDivElement>, direction: ResizeDirection) => void;
onPointerUp: ComponentProps<"div">["onPointerUp"];
onPointerCancel: ComponentProps<"div">["onPointerCancel"];
};
Expand Down Expand Up @@ -101,7 +105,7 @@ export function PlayerAppView({
onPointerEnter={(event) => resizeRegionHandlers.onPointerEnter(event, region.direction)}
onPointerLeave={resizeRegionHandlers.onPointerLeave}
onPointerDown={(event) => resizeRegionHandlers.onPointerDown(event, region.direction)}
onPointerMove={resizeRegionHandlers.onPointerMove}
onPointerMove={(event) => resizeRegionHandlers.onPointerMove(event, region.direction)}
onPointerUp={resizeRegionHandlers.onPointerUp}
onPointerCancel={resizeRegionHandlers.onPointerCancel}
/>
Expand Down
18 changes: 17 additions & 1 deletion apps/desktop/src/hooks/useChromeAutoHide.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { AUTO_HIDE_CONTROLS_MS } from "../app/constants";
import { AUTO_HIDE_CONTROLS_MS, WINDOW_RESIZE_EDGE_HIT_PX } from "../app/constants";
import type { ShortcutAction } from "../app/types";

type UseChromeAutoHideOptions = {
Expand All @@ -9,6 +9,19 @@ type UseChromeAutoHideOptions = {
onPointerExit?: () => void;
};

function isPointerNearViewportResizeEdge(event: MouseEvent | PointerEvent) {
return (
event.clientX >= 0 &&
event.clientY >= 0 &&
event.clientX <= window.innerWidth &&
event.clientY <= window.innerHeight &&
(event.clientX <= WINDOW_RESIZE_EDGE_HIT_PX ||
event.clientY <= WINDOW_RESIZE_EDGE_HIT_PX ||
window.innerWidth - event.clientX <= WINDOW_RESIZE_EDGE_HIT_PX ||
window.innerHeight - event.clientY <= WINDOW_RESIZE_EDGE_HIT_PX)
);
}

export function useChromeAutoHide({ mediaId, isChromePinned, quietKeyboardControls, onPointerExit }: UseChromeAutoHideOptions) {
const [isChromeVisible, setIsChromeVisible] = useState(true);
const chromeHideTimerRef = useRef<number | null>(null);
Expand Down Expand Up @@ -69,6 +82,9 @@ export function useChromeAutoHide({ mediaId, isChromePinned, quietKeyboardContro
if (relatedTarget instanceof Node && document.documentElement.contains(relatedTarget)) {
return;
}
if (isPointerNearViewportResizeEdge(event)) {
return;
}
hideChromeForPointerExit();
};

Expand Down
15 changes: 14 additions & 1 deletion apps/desktop/src/hooks/usePlayerChromeInteractions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ export function usePlayerChromeInteractions({
handleResizePointerEnter,
handleResizePointerLeave,
handleResizePointerDown,
handleResizeSurfacePointerDown,
handleResizeSurfacePointerMove,
handleResizeSurfacePointerEnd,
handleResizePointerMove,
handleResizePointerEnd,
clearResizeHoverFeedback,
Expand All @@ -91,7 +94,7 @@ export function usePlayerChromeInteractions({
onUserActivity: recordUserActivity,
});
clearResizeHoverFeedbackRef.current = clearResizeHoverFeedback;
const shellHandlers = useAppShellHandlers({
const baseShellHandlers = useAppShellHandlers({
contextMenu,
activePluginView,
isSettingsOpen,
Expand All @@ -106,6 +109,13 @@ export function usePlayerChromeInteractions({
handleShellPointerLeave,
setVolume,
});
const shellHandlers = {
...baseShellHandlers,
onPointerDownCapture: handleResizeSurfacePointerDown,
onPointerMoveCapture: handleResizeSurfacePointerMove,
onPointerUpCapture: handleResizeSurfacePointerEnd,
onPointerCancelCapture: handleResizeSurfacePointerEnd,
};

return {
isChromePinned,
Expand All @@ -120,6 +130,9 @@ export function usePlayerChromeInteractions({
handleResizePointerEnter,
handleResizePointerLeave,
handleResizePointerDown,
handleResizeSurfacePointerDown,
handleResizeSurfacePointerMove,
handleResizeSurfacePointerEnd,
handleResizePointerMove,
handleResizePointerEnd,
clearWindowFrameInteraction,
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/hooks/useWindowFrameInteractions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export function useWindowFrameInteractions({
handleResizePointerEnter,
handleResizePointerLeave,
handleResizePointerDown,
handleResizeSurfacePointerDown,
handleResizeSurfacePointerMove,
handleResizeSurfacePointerEnd,
handleResizePointerMove,
handleResizePointerEnd,
clearResizeHoverFeedback,
Expand Down Expand Up @@ -54,6 +57,9 @@ export function useWindowFrameInteractions({
handleResizePointerEnter,
handleResizePointerLeave,
handleResizePointerDown,
handleResizeSurfacePointerDown,
handleResizeSurfacePointerMove,
handleResizeSurfacePointerEnd,
handleResizePointerMove,
handleResizePointerEnd,
clearResizeHoverFeedback,
Expand Down
Loading