diff --git a/changes/fix-macos-overlay-passthrough.md b/changes/fix-macos-overlay-passthrough.md new file mode 100644 index 00000000..50a81545 --- /dev/null +++ b/changes/fix-macos-overlay-passthrough.md @@ -0,0 +1,4 @@ +type: fixed +area: overlay + +- Fixed macOS overlay passthrough so mpv controls remain clickable before hovering subtitle bars. diff --git a/src/core/services/overlay-visibility.test.ts b/src/core/services/overlay-visibility.test.ts index 7254d687..26f40d10 100644 --- a/src/core/services/overlay-visibility.test.ts +++ b/src/core/services/overlay-visibility.test.ts @@ -883,7 +883,7 @@ test('visible overlay stays hidden while a modal window is active', () => { assert.ok(!calls.includes('update-bounds')); }); -test('macOS tracked visible overlay stays interactive without passively stealing focus', () => { +test('macOS tracked visible overlay starts click-through without passively stealing focus', () => { const { window, calls } = createMainWindowRecorder(); const tracker: WindowTrackerStub = { isTracking: () => true, @@ -915,12 +915,52 @@ test('macOS tracked visible overlay stays interactive without passively stealing isWindowsPlatform: false, } as never); - assert.ok(calls.includes('mouse-ignore:false:plain')); + assert.ok(calls.includes('mouse-ignore:true:forward')); assert.ok(calls.includes('show')); assert.ok(!calls.includes('focus')); }); -test('macOS keeps active mpv overlay visible and interactive during tracker refresh', () => { +test('macOS tracked visible overlay remains click-through even if the overlay had focus', () => { + const { window, calls, setFocused } = createMainWindowRecorder(); + const tracker: WindowTrackerStub = { + isTracking: () => true, + getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }), + isTargetWindowFocused: () => true, + }; + + setFocused(true); + + updateVisibleOverlayVisibility({ + visibleOverlayVisible: true, + mainWindow: window as never, + windowTracker: tracker as never, + trackerNotReadyWarningShown: false, + setTrackerNotReadyWarningShown: () => {}, + updateVisibleOverlayBounds: () => { + calls.push('update-bounds'); + }, + ensureOverlayWindowLevel: () => { + calls.push('ensure-level'); + }, + syncPrimaryOverlayWindowLayer: () => { + calls.push('sync-layer'); + }, + enforceOverlayLayerOrder: () => { + calls.push('enforce-order'); + }, + syncOverlayShortcuts: () => { + calls.push('sync-shortcuts'); + }, + isMacOSPlatform: true, + isWindowsPlatform: false, + } as never); + + assert.ok(calls.includes('mouse-ignore:true:forward')); + assert.ok(calls.includes('ensure-level')); + assert.ok(!calls.includes('focus')); +}); + +test('macOS keeps active mpv overlay visible and click-through during tracker refresh', () => { const { window, calls } = createMainWindowRecorder(); const osdMessages: string[] = []; const tracker: WindowTrackerStub = { @@ -961,7 +1001,7 @@ test('macOS keeps active mpv overlay visible and interactive during tracker refr assert.ok(calls.includes('update-bounds')); assert.ok(calls.includes('sync-layer')); - assert.ok(calls.includes('mouse-ignore:false:plain')); + assert.ok(calls.includes('mouse-ignore:true:forward')); assert.ok(calls.includes('ensure-level')); assert.ok(calls.includes('enforce-order')); assert.ok(calls.includes('sync-shortcuts')); @@ -1060,7 +1100,7 @@ test('macOS preserves an already visible active mpv overlay while tracker is tem assert.equal(trackerWarning, false); assert.ok(calls.includes('sync-layer')); - assert.ok(calls.includes('mouse-ignore:false:plain')); + assert.ok(calls.includes('mouse-ignore:true:forward')); assert.ok(calls.includes('ensure-level')); assert.ok(calls.includes('sync-shortcuts')); assert.ok(!calls.includes('hide')); @@ -1390,7 +1430,7 @@ test('macOS preserves visible overlay during transient tracker loss with retaine assert.deepEqual(osdMessages, []); assert.ok(calls.includes('update-bounds')); assert.ok(calls.includes('sync-layer')); - assert.ok(calls.includes('mouse-ignore:false:plain')); + assert.ok(calls.includes('mouse-ignore:true:forward')); assert.ok(calls.includes('ensure-level')); assert.ok(calls.includes('enforce-order')); assert.ok(calls.includes('sync-shortcuts')); @@ -1438,7 +1478,7 @@ test('macOS preserves visible overlay level during non-minimized tracker loss', } as never); assert.ok(calls.includes('sync-layer')); - assert.ok(calls.includes('mouse-ignore:false:plain')); + assert.ok(calls.includes('mouse-ignore:true:forward')); assert.ok(calls.includes('ensure-level')); assert.ok(calls.includes('enforce-order')); assert.ok(calls.includes('sync-shortcuts')); diff --git a/src/core/services/overlay-visibility.ts b/src/core/services/overlay-visibility.ts index f9c885d9..e05cd7e5 100644 --- a/src/core/services/overlay-visibility.ts +++ b/src/core/services/overlay-visibility.ts @@ -98,8 +98,7 @@ export function updateVisibleOverlayVisibility(args: { const canReportMacOSTargetMinimized = args.isMacOSPlatform && typeof windowTracker?.isTargetWindowMinimized === 'function'; const isTrackedMacOSTargetMinimized = - canReportMacOSTargetMinimized && - windowTracker?.isTargetWindowMinimized() === true; + canReportMacOSTargetMinimized && windowTracker?.isTargetWindowMinimized() === true; const hasTransientMacOSTrackerLoss = args.isMacOSPlatform && canReportMacOSTargetMinimized && @@ -117,6 +116,8 @@ export function updateVisibleOverlayVisibility(args: { !hasTransientMacOSTrackerLoss && !isVisibleOverlayFocused && !isTrackedMacOSTargetFocused; + // Renderer hover tracking temporarily disables this for subtitle and popup interaction. + const shouldUseMacOSMousePassthrough = args.isMacOSPlatform; const shouldDefaultToPassthrough = args.isWindowsPlatform || forceMousePassthrough || shouldReleaseMacOSOverlayLevel; const windowsForegroundProcessName = @@ -141,6 +142,7 @@ export function updateVisibleOverlayVisibility(args: { (args.windowTracker.isTracking() || args.windowTracker.getGeometry() !== null); const shouldForcePassiveReshow = args.isWindowsPlatform && !wasVisible; const shouldIgnoreMouseEvents = + shouldUseMacOSMousePassthrough || forceMousePassthrough || (shouldDefaultToPassthrough && (!isVisibleOverlayFocused || shouldForcePassiveReshow)); const shouldBindTrackedWindowsOverlay = args.isWindowsPlatform && !!args.windowTracker; @@ -291,8 +293,7 @@ export function updateVisibleOverlayVisibility(args: { const canReportMacOSTargetMinimized = args.isMacOSPlatform && typeof args.windowTracker.isTargetWindowMinimized === 'function'; const isTrackedMacOSTargetMinimized = - canReportMacOSTargetMinimized && - args.windowTracker.isTargetWindowMinimized(); + canReportMacOSTargetMinimized && args.windowTracker.isTargetWindowMinimized(); const shouldPreserveTransientTrackedOverlay = (args.isMacOSPlatform && !isTrackedMacOSTargetMinimized && diff --git a/src/renderer/handlers/keyboard.test.ts b/src/renderer/handlers/keyboard.test.ts index 0147002c..a0b1e1be 100644 --- a/src/renderer/handlers/keyboard.test.ts +++ b/src/renderer/handlers/keyboard.test.ts @@ -822,6 +822,7 @@ test('default keybindings dispatch through overlay keyboard handling', async () testGlobals.sessionActions.map((action) => action.actionId).sort(), expectedSessionActions.sort(), ); + testGlobals.dispatchKeydown({ key: 'Escape', code: 'Escape' }); } finally { testGlobals.restore(); }