From bbdd98cbff85ac7a43079e2edf02116b3c1ad454 Mon Sep 17 00:00:00 2001 From: sudacode Date: Sat, 21 Mar 2026 19:34:00 -0700 Subject: [PATCH] fix(renderer): sync embedded sidebar mouse passthrough --- src/renderer/handlers/mouse.ts | 61 +++++++++++++++++++++------- src/renderer/overlay-mouse-ignore.ts | 42 +++++++++++++++++++ src/renderer/state.ts | 2 + 3 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 src/renderer/overlay-mouse-ignore.ts diff --git a/src/renderer/handlers/mouse.ts b/src/renderer/handlers/mouse.ts index 0d66d3a..14b1940 100644 --- a/src/renderer/handlers/mouse.ts +++ b/src/renderer/handlers/mouse.ts @@ -1,4 +1,5 @@ import type { ModalStateReader, RendererContext } from '../context'; +import { syncOverlayMouseIgnoreState } from '../overlay-mouse-ignore.js'; import { YOMITAN_POPUP_HIDDEN_EVENT, YOMITAN_POPUP_SHOWN_EVENT, @@ -25,6 +26,19 @@ export function createMouseHandlers( let pausedBySubtitleHover = false; let pausedByYomitanPopup = false; + function isWithinOtherSubtitleContainer( + relatedTarget: EventTarget | null, + otherContainer: HTMLElement, + ): boolean { + if (relatedTarget === otherContainer) { + return true; + } + if (typeof Node !== 'undefined' && relatedTarget instanceof Node) { + return otherContainer.contains(relatedTarget); + } + return false; + } + function maybeResumeHoverPause(): void { if (!pausedBySubtitleHover) return; if (pausedByYomitanPopup) return; @@ -80,10 +94,7 @@ export function createMouseHandlers( function enablePopupInteraction(): void { yomitanPopupVisible = true; ctx.state.yomitanPopupVisible = true; - ctx.dom.overlay.classList.add('interactive'); - if (ctx.platform.shouldToggleMouseIgnore) { - window.electronAPI.setIgnoreMouseEvents(false); - } + syncOverlayMouseIgnoreState(ctx); if (ctx.platform.isMacOSPlatform) { window.focus(); } @@ -101,20 +112,18 @@ export function createMouseHandlers( popupPauseRequestId += 1; maybeResumeYomitanPopupPause(); maybeResumeHoverPause(); - if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) { - ctx.dom.overlay.classList.remove('interactive'); - if (ctx.platform.shouldToggleMouseIgnore) { - window.electronAPI.setIgnoreMouseEvents(true, { forward: true }); - } - } + syncOverlayMouseIgnoreState(ctx); } - async function handleMouseEnter(): Promise { + async function handleMouseEnter( + _event?: MouseEvent, + showSecondaryHover = false, + ): Promise { ctx.state.isOverSubtitle = true; - ctx.dom.overlay.classList.add('interactive'); - if (ctx.platform.shouldToggleMouseIgnore) { - window.electronAPI.setIgnoreMouseEvents(false); + if (showSecondaryHover) { + ctx.dom.secondarySubContainer.classList.add('secondary-sub-hover-active'); } + syncOverlayMouseIgnoreState(ctx); if (yomitanPopupVisible && options.getYomitanPopupAutoPauseEnabled()) { return; @@ -124,6 +133,10 @@ export function createMouseHandlers( return; } + if (pausedBySubtitleHover) { + return; + } + const requestId = ++hoverPauseRequestId; let paused: boolean | null = null; try { @@ -141,8 +154,22 @@ export function createMouseHandlers( pausedBySubtitleHover = true; } - async function handleMouseLeave(): Promise { + async function handleMouseLeave( + _event?: MouseEvent, + hideSecondaryHover = false, + ): Promise { + const relatedTarget = _event?.relatedTarget ?? null; + const otherContainer = hideSecondaryHover + ? ctx.dom.subtitleContainer + : ctx.dom.secondarySubContainer; + if (relatedTarget && isWithinOtherSubtitleContainer(relatedTarget, otherContainer)) { + return; + } + ctx.state.isOverSubtitle = false; + if (hideSecondaryHover) { + ctx.dom.secondarySubContainer.classList.remove('secondary-sub-hover-active'); + } hoverPauseRequestId += 1; maybeResumeHoverPause(); if (yomitanPopupVisible) return; @@ -246,6 +273,10 @@ export function createMouseHandlers( } return { + handlePrimaryMouseEnter: (event?: MouseEvent) => handleMouseEnter(event, false), + handlePrimaryMouseLeave: (event?: MouseEvent) => handleMouseLeave(event, false), + handleSecondaryMouseEnter: (event?: MouseEvent) => handleMouseEnter(event, true), + handleSecondaryMouseLeave: (event?: MouseEvent) => handleMouseLeave(event, true), handleMouseEnter, handleMouseLeave, setupDragging, diff --git a/src/renderer/overlay-mouse-ignore.ts b/src/renderer/overlay-mouse-ignore.ts new file mode 100644 index 0000000..dcd798b --- /dev/null +++ b/src/renderer/overlay-mouse-ignore.ts @@ -0,0 +1,42 @@ +import type { RendererContext } from './context'; +import type { RendererState } from './state'; + +function isBlockingOverlayModalOpen(state: RendererState): boolean { + const embeddedSidebarOpen = + state.subtitleSidebarModalOpen && state.subtitleSidebarConfig?.layout === 'embedded'; + + return Boolean( + state.controllerSelectModalOpen || + state.controllerDebugModalOpen || + state.jimakuModalOpen || + state.kikuModalOpen || + state.runtimeOptionsModalOpen || + state.subsyncModalOpen || + state.sessionHelpModalOpen || + (state.subtitleSidebarModalOpen && !embeddedSidebarOpen), + ); +} + +export function syncOverlayMouseIgnoreState(ctx: RendererContext): void { + const shouldStayInteractive = + ctx.state.isOverSubtitle || + ctx.state.isOverSubtitleSidebar || + ctx.state.yomitanPopupVisible || + isBlockingOverlayModalOpen(ctx.state); + + if (shouldStayInteractive) { + ctx.dom.overlay.classList.add('interactive'); + } else { + ctx.dom.overlay.classList.remove('interactive'); + } + if (!ctx.platform?.shouldToggleMouseIgnore) { + return; + } + + if (shouldStayInteractive) { + window.electronAPI.setIgnoreMouseEvents(false); + return; + } + + window.electronAPI.setIgnoreMouseEvents(true, { forward: true }); +} diff --git a/src/renderer/state.ts b/src/renderer/state.ts index de48297..5f5f517 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -25,6 +25,7 @@ export type ChordAction = export type RendererState = { isOverSubtitle: boolean; + isOverSubtitleSidebar: boolean; isDragging: boolean; dragStartY: number; startYPercent: number; @@ -115,6 +116,7 @@ export type RendererState = { export function createRendererState(): RendererState { return { isOverSubtitle: false, + isOverSubtitleSidebar: false, isDragging: false, dragStartY: 0, startYPercent: 0,