From c59bed8c8e6572ad675dfc2c79ac8814eaa90409 Mon Sep 17 00:00:00 2001 From: sudacode Date: Thu, 12 Feb 2026 02:17:30 -0800 Subject: [PATCH] refactor(overlay): split bounds ownership by layer for TASK-20.1 --- ...keybinds-whenever-app-runtime-is-active.md | 0 backlog/config.yml | 2 +- ...o-use-per-layer-window-bounds-ownership.md | 28 +++++++++++--- src/core/services/index.ts | 2 +- .../services/overlay-manager-service.test.ts | 38 +++++++++++++++++++ src/core/services/overlay-manager-service.ts | 15 +++++++- .../services/overlay-runtime-init-service.ts | 9 +++-- .../services/overlay-visibility-service.ts | 12 +++--- src/core/services/overlay-window-service.ts | 20 +++++----- src/main.ts | 20 ++++++---- 10 files changed, 110 insertions(+), 36 deletions(-) rename backlog/{ => archive}/tasks/task-19 - Enable-overlay-keybinds-whenever-app-runtime-is-active.md (100%) diff --git a/backlog/tasks/task-19 - Enable-overlay-keybinds-whenever-app-runtime-is-active.md b/backlog/archive/tasks/task-19 - Enable-overlay-keybinds-whenever-app-runtime-is-active.md similarity index 100% rename from backlog/tasks/task-19 - Enable-overlay-keybinds-whenever-app-runtime-is-active.md rename to backlog/archive/tasks/task-19 - Enable-overlay-keybinds-whenever-app-runtime-is-active.md diff --git a/backlog/config.yml b/backlog/config.yml index 32ff981..de5fb9e 100644 --- a/backlog/config.yml +++ b/backlog/config.yml @@ -6,7 +6,7 @@ milestones: [] date_format: yyyy-mm-dd max_column_width: 20 default_editor: "nvim" -auto_open_browser: true +auto_open_browser: false default_port: 6420 remote_operations: true auto_commit: false diff --git a/backlog/tasks/task-20.1 - Refactor-overlay-runtime-to-use-per-layer-window-bounds-ownership.md b/backlog/tasks/task-20.1 - Refactor-overlay-runtime-to-use-per-layer-window-bounds-ownership.md index 3ee146e..e6b48a1 100644 --- a/backlog/tasks/task-20.1 - Refactor-overlay-runtime-to-use-per-layer-window-bounds-ownership.md +++ b/backlog/tasks/task-20.1 - Refactor-overlay-runtime-to-use-per-layer-window-bounds-ownership.md @@ -1,10 +1,10 @@ --- id: TASK-20.1 title: Refactor overlay runtime to use per-layer window bounds ownership -status: To Do +status: Done assignee: [] created_date: '2026-02-12 08:47' -updated_date: '2026-02-12 09:42' +updated_date: '2026-02-12 10:12' labels: [] dependencies: [] parent_task_id: TASK-20 @@ -19,8 +19,24 @@ Refactor overlay runtime so each overlay layer owns and applies its bounds indep ## Acceptance Criteria -- [ ] #1 `updateOverlayBoundsService` no longer applies the same bounds to every overlay window by default. -- [ ] #2 Main runtime/manager exposes per-layer bounds update paths for visible and invisible overlays. -- [ ] #3 Window tracker updates feed shared origin data; each layer applies its own computed bounds. -- [ ] #4 Single-layer behavior (visible-only or invisible-only) remains unchanged from user perspective. +- [x] #1 `updateOverlayBoundsService` no longer applies the same bounds to every overlay window by default. +- [x] #2 Main runtime/manager exposes per-layer bounds update paths for visible and invisible overlays. +- [x] #3 Window tracker updates feed shared origin data; each layer applies its own computed bounds. +- [x] #4 Single-layer behavior (visible-only or invisible-only) remains unchanged from user perspective. + +## Implementation Notes + + +Started implementation for per-layer overlay bounds ownership refactor. + +Implemented per-layer bounds ownership path: visible and invisible layers now update bounds independently through overlay manager/runtime plumbing, while preserving existing geometry source behavior. + +Replaced shared all-window bounds application with per-window bound application service and layer-specific runtime calls from visibility/tracker flows. + + +## Final Summary + + +Refactored overlay bounds ownership to per-layer update paths. Tracker geometry remains shared input, but visible/invisible windows apply bounds independently via explicit layer routes. Existing single-layer UX behavior is preserved. + diff --git a/src/core/services/index.ts b/src/core/services/index.ts index 356eb56..396e9df 100644 --- a/src/core/services/index.ts +++ b/src/core/services/index.ts @@ -53,7 +53,7 @@ export { createOverlayWindowService, enforceOverlayLayerOrderService, ensureOverlayWindowLevelService, - updateOverlayBoundsService, + updateOverlayWindowBoundsService, } from "./overlay-window-service"; export { initializeOverlayRuntimeService } from "./overlay-runtime-init-service"; export { diff --git a/src/core/services/overlay-manager-service.test.ts b/src/core/services/overlay-manager-service.test.ts index 1b92694..c1b11e5 100644 --- a/src/core/services/overlay-manager-service.test.ts +++ b/src/core/services/overlay-manager-service.test.ts @@ -25,6 +25,8 @@ test("overlay manager stores window references and returns stable window order", assert.equal(manager.getMainWindow(), visibleWindow); assert.equal(manager.getInvisibleWindow(), invisibleWindow); + assert.equal(manager.getOverlayWindow("visible"), visibleWindow); + assert.equal(manager.getOverlayWindow("invisible"), invisibleWindow); assert.deepEqual(manager.getOverlayWindows(), [visibleWindow, invisibleWindow]); }); @@ -70,6 +72,42 @@ test("overlay manager broadcasts to non-destroyed windows", () => { assert.deepEqual(calls, [["x", 1, "a"]]); }); +test("overlay manager applies bounds by layer", () => { + const manager = createOverlayManagerService(); + const visibleCalls: Electron.Rectangle[] = []; + const invisibleCalls: Electron.Rectangle[] = []; + const visibleWindow = { + isDestroyed: () => false, + setBounds: (bounds: Electron.Rectangle) => { + visibleCalls.push(bounds); + }, + } as unknown as Electron.BrowserWindow; + const invisibleWindow = { + isDestroyed: () => false, + setBounds: (bounds: Electron.Rectangle) => { + invisibleCalls.push(bounds); + }, + } as unknown as Electron.BrowserWindow; + manager.setMainWindow(visibleWindow); + manager.setInvisibleWindow(invisibleWindow); + + manager.setOverlayWindowBounds("visible", { + x: 10, + y: 20, + width: 30, + height: 40, + }); + manager.setOverlayWindowBounds("invisible", { + x: 1, + y: 2, + width: 3, + height: 4, + }); + + assert.deepEqual(visibleCalls, [{ x: 10, y: 20, width: 30, height: 40 }]); + assert.deepEqual(invisibleCalls, [{ x: 1, y: 2, width: 3, height: 4 }]); +}); + test("runtime-option and debug broadcasts use expected channels", () => { const broadcasts: unknown[][] = []; broadcastRuntimeOptionsChangedRuntimeService( diff --git a/src/core/services/overlay-manager-service.ts b/src/core/services/overlay-manager-service.ts index 401cde3..00c251d 100644 --- a/src/core/services/overlay-manager-service.ts +++ b/src/core/services/overlay-manager-service.ts @@ -1,11 +1,16 @@ import { BrowserWindow } from "electron"; -import { RuntimeOptionState } from "../../types"; +import { RuntimeOptionState, WindowGeometry } from "../../types"; +import { updateOverlayWindowBoundsService } from "./overlay-window-service"; + +type OverlayLayer = "visible" | "invisible"; export interface OverlayManagerService { getMainWindow: () => BrowserWindow | null; setMainWindow: (window: BrowserWindow | null) => void; getInvisibleWindow: () => BrowserWindow | null; setInvisibleWindow: (window: BrowserWindow | null) => void; + getOverlayWindow: (layer: OverlayLayer) => BrowserWindow | null; + setOverlayWindowBounds: (layer: OverlayLayer, geometry: WindowGeometry) => void; getVisibleOverlayVisible: () => boolean; setVisibleOverlayVisible: (visible: boolean) => void; getInvisibleOverlayVisible: () => boolean; @@ -29,6 +34,14 @@ export function createOverlayManagerService(): OverlayManagerService { setInvisibleWindow: (window) => { invisibleWindow = window; }, + getOverlayWindow: (layer) => + layer === "visible" ? mainWindow : invisibleWindow, + setOverlayWindowBounds: (layer, geometry) => { + updateOverlayWindowBoundsService( + geometry, + layer === "visible" ? mainWindow : invisibleWindow, + ); + }, getVisibleOverlayVisible: () => visibleOverlayVisible, setVisibleOverlayVisible: (visible) => { visibleOverlayVisible = visible; diff --git a/src/core/services/overlay-runtime-init-service.ts b/src/core/services/overlay-runtime-init-service.ts index 75b3be6..33bcd14 100644 --- a/src/core/services/overlay-runtime-init-service.ts +++ b/src/core/services/overlay-runtime-init-service.ts @@ -14,7 +14,8 @@ export function initializeOverlayRuntimeService(options: { createMainWindow: () => void; createInvisibleWindow: () => void; registerGlobalShortcuts: () => void; - updateOverlayBounds: (geometry: WindowGeometry) => void; + updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; + updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void; isVisibleOverlayVisible: () => boolean; isInvisibleOverlayVisible: () => boolean; updateVisibleOverlayVisibility: () => void; @@ -45,10 +46,12 @@ export function initializeOverlayRuntimeService(options: { options.setWindowTracker(windowTracker); if (windowTracker) { windowTracker.onGeometryChange = (geometry: WindowGeometry) => { - options.updateOverlayBounds(geometry); + options.updateVisibleOverlayBounds(geometry); + options.updateInvisibleOverlayBounds(geometry); }; windowTracker.onWindowFound = (geometry: WindowGeometry) => { - options.updateOverlayBounds(geometry); + options.updateVisibleOverlayBounds(geometry); + options.updateInvisibleOverlayBounds(geometry); if (options.isVisibleOverlayVisible()) { options.updateVisibleOverlayVisibility(); } diff --git a/src/core/services/overlay-visibility-service.ts b/src/core/services/overlay-visibility-service.ts index 6fbc860..ac606c5 100644 --- a/src/core/services/overlay-visibility-service.ts +++ b/src/core/services/overlay-visibility-service.ts @@ -19,7 +19,7 @@ export function updateVisibleOverlayVisibilityService(args: { mpvConnected: boolean; mpvSend: (payload: MpvCommandSender) => void; secondarySubVisibilityRequestId: number; - updateOverlayBounds: (geometry: WindowGeometry) => void; + updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; ensureOverlayWindowLevel: (window: BrowserWindow) => void; enforceOverlayLayerOrder: () => void; syncOverlayShortcuts: () => void; @@ -62,7 +62,7 @@ export function updateVisibleOverlayVisibilityService(args: { args.setTrackerNotReadyWarningShown(false); const geometry = args.windowTracker.getGeometry(); if (geometry) { - args.updateOverlayBounds(geometry); + args.updateVisibleOverlayBounds(geometry); } args.ensureOverlayWindowLevel(args.mainWindow); args.mainWindow.show(); @@ -88,7 +88,7 @@ export function updateVisibleOverlayVisibilityService(args: { const cursorPoint = screen.getCursorScreenPoint(); const display = screen.getDisplayNearestPoint(cursorPoint); const fallbackBounds = display.workArea; - args.updateOverlayBounds({ + args.updateVisibleOverlayBounds({ x: fallbackBounds.x, y: fallbackBounds.y, width: fallbackBounds.width, @@ -106,7 +106,7 @@ export function updateInvisibleOverlayVisibilityService(args: { visibleOverlayVisible: boolean; invisibleOverlayVisible: boolean; windowTracker: BaseWindowTracker | null; - updateOverlayBounds: (geometry: WindowGeometry) => void; + updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void; ensureOverlayWindowLevel: (window: BrowserWindow) => void; enforceOverlayLayerOrder: () => void; syncOverlayShortcuts: () => void; @@ -140,7 +140,7 @@ export function updateInvisibleOverlayVisibilityService(args: { if (args.windowTracker && args.windowTracker.isTracking()) { const geometry = args.windowTracker.getGeometry(); if (geometry) { - args.updateOverlayBounds(geometry); + args.updateInvisibleOverlayBounds(geometry); } showInvisibleWithoutFocus(); args.syncOverlayShortcuts(); @@ -156,7 +156,7 @@ export function updateInvisibleOverlayVisibilityService(args: { const cursorPoint = screen.getCursorScreenPoint(); const display = screen.getDisplayNearestPoint(cursorPoint); const fallbackBounds = display.workArea; - args.updateOverlayBounds({ + args.updateInvisibleOverlayBounds({ x: fallbackBounds.x, y: fallbackBounds.y, width: fallbackBounds.width, diff --git a/src/core/services/overlay-window-service.ts b/src/core/services/overlay-window-service.ts index 957e24e..caa988e 100644 --- a/src/core/services/overlay-window-service.ts +++ b/src/core/services/overlay-window-service.ts @@ -4,19 +4,17 @@ import { WindowGeometry } from "../../types"; export type OverlayWindowKind = "visible" | "invisible"; -export function updateOverlayBoundsService( +export function updateOverlayWindowBoundsService( geometry: WindowGeometry, - getOverlayWindows: () => BrowserWindow[], + window: BrowserWindow | null, ): void { - if (!geometry) return; - for (const window of getOverlayWindows()) { - window.setBounds({ - x: geometry.x, - y: geometry.y, - width: geometry.width, - height: geometry.height, - }); - } + if (!geometry || !window || window.isDestroyed()) return; + window.setBounds({ + x: geometry.x, + y: geometry.y, + width: geometry.width, + height: geometry.height, + }); } export function ensureOverlayWindowLevelService(window: BrowserWindow): void { diff --git a/src/main.ts b/src/main.ts index 07b54df..6392952 100644 --- a/src/main.ts +++ b/src/main.ts @@ -154,7 +154,6 @@ import { updateCurrentMediaPathService, updateInvisibleOverlayVisibilityService, updateLastCardFromClipboardService, - updateOverlayBoundsService, updateVisibleOverlayVisibilityService, } from "./core/services"; import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-handler"; @@ -763,8 +762,12 @@ async function tokenizeSubtitle(text: string): Promise { ); } -function updateOverlayBounds(geometry: WindowGeometry): void { - updateOverlayBoundsService(geometry, () => getOverlayWindows()); +function updateVisibleOverlayBounds(geometry: WindowGeometry): void { + overlayManager.setOverlayWindowBounds("visible", geometry); +} + +function updateInvisibleOverlayBounds(geometry: WindowGeometry): void { + overlayManager.setOverlayWindowBounds("invisible", geometry); } function ensureOverlayWindowLevel(window: BrowserWindow): void { @@ -856,8 +859,11 @@ function initializeOverlayRuntime(): void { registerGlobalShortcuts: () => { registerGlobalShortcuts(); }, - updateOverlayBounds: (geometry) => { - updateOverlayBounds(geometry); + updateVisibleOverlayBounds: (geometry) => { + updateVisibleOverlayBounds(geometry); + }, + updateInvisibleOverlayBounds: (geometry) => { + updateInvisibleOverlayBounds(geometry); }, isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), isInvisibleOverlayVisible: () => @@ -1181,7 +1187,7 @@ function updateVisibleOverlayVisibility(): void { mpvClient.send(payload); }, secondarySubVisibilityRequestId: MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY, - updateOverlayBounds: (geometry) => updateOverlayBounds(geometry), + updateVisibleOverlayBounds: (geometry) => updateVisibleOverlayBounds(geometry), ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window), enforceOverlayLayerOrder: () => enforceOverlayLayerOrder(), syncOverlayShortcuts: () => syncOverlayShortcuts(), @@ -1196,7 +1202,7 @@ function updateInvisibleOverlayVisibility(): void { visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(), invisibleOverlayVisible: overlayManager.getInvisibleOverlayVisible(), windowTracker, - updateOverlayBounds: (geometry) => updateOverlayBounds(geometry), + updateInvisibleOverlayBounds: (geometry) => updateInvisibleOverlayBounds(geometry), ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window), enforceOverlayLayerOrder: () => enforceOverlayLayerOrder(), syncOverlayShortcuts: () => syncOverlayShortcuts(),