mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor(overlay): split bounds ownership by layer for TASK-20.1
This commit is contained in:
@@ -6,7 +6,7 @@ milestones: []
|
|||||||
date_format: yyyy-mm-dd
|
date_format: yyyy-mm-dd
|
||||||
max_column_width: 20
|
max_column_width: 20
|
||||||
default_editor: "nvim"
|
default_editor: "nvim"
|
||||||
auto_open_browser: true
|
auto_open_browser: false
|
||||||
default_port: 6420
|
default_port: 6420
|
||||||
remote_operations: true
|
remote_operations: true
|
||||||
auto_commit: false
|
auto_commit: false
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
id: TASK-20.1
|
id: TASK-20.1
|
||||||
title: Refactor overlay runtime to use per-layer window bounds ownership
|
title: Refactor overlay runtime to use per-layer window bounds ownership
|
||||||
status: To Do
|
status: Done
|
||||||
assignee: []
|
assignee: []
|
||||||
created_date: '2026-02-12 08:47'
|
created_date: '2026-02-12 08:47'
|
||||||
updated_date: '2026-02-12 09:42'
|
updated_date: '2026-02-12 10:12'
|
||||||
labels: []
|
labels: []
|
||||||
dependencies: []
|
dependencies: []
|
||||||
parent_task_id: TASK-20
|
parent_task_id: TASK-20
|
||||||
@@ -19,8 +19,24 @@ Refactor overlay runtime so each overlay layer owns and applies its bounds indep
|
|||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
<!-- AC:BEGIN -->
|
<!-- AC:BEGIN -->
|
||||||
- [ ] #1 `updateOverlayBoundsService` no longer applies the same bounds to every overlay window by default.
|
- [x] #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.
|
- [x] #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.
|
- [x] #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] #4 Single-layer behavior (visible-only or invisible-only) remains unchanged from user perspective.
|
||||||
<!-- AC:END -->
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
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.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
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.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export {
|
|||||||
createOverlayWindowService,
|
createOverlayWindowService,
|
||||||
enforceOverlayLayerOrderService,
|
enforceOverlayLayerOrderService,
|
||||||
ensureOverlayWindowLevelService,
|
ensureOverlayWindowLevelService,
|
||||||
updateOverlayBoundsService,
|
updateOverlayWindowBoundsService,
|
||||||
} from "./overlay-window-service";
|
} from "./overlay-window-service";
|
||||||
export { initializeOverlayRuntimeService } from "./overlay-runtime-init-service";
|
export { initializeOverlayRuntimeService } from "./overlay-runtime-init-service";
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ test("overlay manager stores window references and returns stable window order",
|
|||||||
|
|
||||||
assert.equal(manager.getMainWindow(), visibleWindow);
|
assert.equal(manager.getMainWindow(), visibleWindow);
|
||||||
assert.equal(manager.getInvisibleWindow(), invisibleWindow);
|
assert.equal(manager.getInvisibleWindow(), invisibleWindow);
|
||||||
|
assert.equal(manager.getOverlayWindow("visible"), visibleWindow);
|
||||||
|
assert.equal(manager.getOverlayWindow("invisible"), invisibleWindow);
|
||||||
assert.deepEqual(manager.getOverlayWindows(), [visibleWindow, 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"]]);
|
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", () => {
|
test("runtime-option and debug broadcasts use expected channels", () => {
|
||||||
const broadcasts: unknown[][] = [];
|
const broadcasts: unknown[][] = [];
|
||||||
broadcastRuntimeOptionsChangedRuntimeService(
|
broadcastRuntimeOptionsChangedRuntimeService(
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { BrowserWindow } from "electron";
|
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 {
|
export interface OverlayManagerService {
|
||||||
getMainWindow: () => BrowserWindow | null;
|
getMainWindow: () => BrowserWindow | null;
|
||||||
setMainWindow: (window: BrowserWindow | null) => void;
|
setMainWindow: (window: BrowserWindow | null) => void;
|
||||||
getInvisibleWindow: () => BrowserWindow | null;
|
getInvisibleWindow: () => BrowserWindow | null;
|
||||||
setInvisibleWindow: (window: BrowserWindow | null) => void;
|
setInvisibleWindow: (window: BrowserWindow | null) => void;
|
||||||
|
getOverlayWindow: (layer: OverlayLayer) => BrowserWindow | null;
|
||||||
|
setOverlayWindowBounds: (layer: OverlayLayer, geometry: WindowGeometry) => void;
|
||||||
getVisibleOverlayVisible: () => boolean;
|
getVisibleOverlayVisible: () => boolean;
|
||||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||||
getInvisibleOverlayVisible: () => boolean;
|
getInvisibleOverlayVisible: () => boolean;
|
||||||
@@ -29,6 +34,14 @@ export function createOverlayManagerService(): OverlayManagerService {
|
|||||||
setInvisibleWindow: (window) => {
|
setInvisibleWindow: (window) => {
|
||||||
invisibleWindow = window;
|
invisibleWindow = window;
|
||||||
},
|
},
|
||||||
|
getOverlayWindow: (layer) =>
|
||||||
|
layer === "visible" ? mainWindow : invisibleWindow,
|
||||||
|
setOverlayWindowBounds: (layer, geometry) => {
|
||||||
|
updateOverlayWindowBoundsService(
|
||||||
|
geometry,
|
||||||
|
layer === "visible" ? mainWindow : invisibleWindow,
|
||||||
|
);
|
||||||
|
},
|
||||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||||
setVisibleOverlayVisible: (visible) => {
|
setVisibleOverlayVisible: (visible) => {
|
||||||
visibleOverlayVisible = visible;
|
visibleOverlayVisible = visible;
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export function initializeOverlayRuntimeService(options: {
|
|||||||
createMainWindow: () => void;
|
createMainWindow: () => void;
|
||||||
createInvisibleWindow: () => void;
|
createInvisibleWindow: () => void;
|
||||||
registerGlobalShortcuts: () => void;
|
registerGlobalShortcuts: () => void;
|
||||||
updateOverlayBounds: (geometry: WindowGeometry) => void;
|
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||||
|
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||||
isVisibleOverlayVisible: () => boolean;
|
isVisibleOverlayVisible: () => boolean;
|
||||||
isInvisibleOverlayVisible: () => boolean;
|
isInvisibleOverlayVisible: () => boolean;
|
||||||
updateVisibleOverlayVisibility: () => void;
|
updateVisibleOverlayVisibility: () => void;
|
||||||
@@ -45,10 +46,12 @@ export function initializeOverlayRuntimeService(options: {
|
|||||||
options.setWindowTracker(windowTracker);
|
options.setWindowTracker(windowTracker);
|
||||||
if (windowTracker) {
|
if (windowTracker) {
|
||||||
windowTracker.onGeometryChange = (geometry: WindowGeometry) => {
|
windowTracker.onGeometryChange = (geometry: WindowGeometry) => {
|
||||||
options.updateOverlayBounds(geometry);
|
options.updateVisibleOverlayBounds(geometry);
|
||||||
|
options.updateInvisibleOverlayBounds(geometry);
|
||||||
};
|
};
|
||||||
windowTracker.onWindowFound = (geometry: WindowGeometry) => {
|
windowTracker.onWindowFound = (geometry: WindowGeometry) => {
|
||||||
options.updateOverlayBounds(geometry);
|
options.updateVisibleOverlayBounds(geometry);
|
||||||
|
options.updateInvisibleOverlayBounds(geometry);
|
||||||
if (options.isVisibleOverlayVisible()) {
|
if (options.isVisibleOverlayVisible()) {
|
||||||
options.updateVisibleOverlayVisibility();
|
options.updateVisibleOverlayVisibility();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function updateVisibleOverlayVisibilityService(args: {
|
|||||||
mpvConnected: boolean;
|
mpvConnected: boolean;
|
||||||
mpvSend: (payload: MpvCommandSender) => void;
|
mpvSend: (payload: MpvCommandSender) => void;
|
||||||
secondarySubVisibilityRequestId: number;
|
secondarySubVisibilityRequestId: number;
|
||||||
updateOverlayBounds: (geometry: WindowGeometry) => void;
|
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||||
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
||||||
enforceOverlayLayerOrder: () => void;
|
enforceOverlayLayerOrder: () => void;
|
||||||
syncOverlayShortcuts: () => void;
|
syncOverlayShortcuts: () => void;
|
||||||
@@ -62,7 +62,7 @@ export function updateVisibleOverlayVisibilityService(args: {
|
|||||||
args.setTrackerNotReadyWarningShown(false);
|
args.setTrackerNotReadyWarningShown(false);
|
||||||
const geometry = args.windowTracker.getGeometry();
|
const geometry = args.windowTracker.getGeometry();
|
||||||
if (geometry) {
|
if (geometry) {
|
||||||
args.updateOverlayBounds(geometry);
|
args.updateVisibleOverlayBounds(geometry);
|
||||||
}
|
}
|
||||||
args.ensureOverlayWindowLevel(args.mainWindow);
|
args.ensureOverlayWindowLevel(args.mainWindow);
|
||||||
args.mainWindow.show();
|
args.mainWindow.show();
|
||||||
@@ -88,7 +88,7 @@ export function updateVisibleOverlayVisibilityService(args: {
|
|||||||
const cursorPoint = screen.getCursorScreenPoint();
|
const cursorPoint = screen.getCursorScreenPoint();
|
||||||
const display = screen.getDisplayNearestPoint(cursorPoint);
|
const display = screen.getDisplayNearestPoint(cursorPoint);
|
||||||
const fallbackBounds = display.workArea;
|
const fallbackBounds = display.workArea;
|
||||||
args.updateOverlayBounds({
|
args.updateVisibleOverlayBounds({
|
||||||
x: fallbackBounds.x,
|
x: fallbackBounds.x,
|
||||||
y: fallbackBounds.y,
|
y: fallbackBounds.y,
|
||||||
width: fallbackBounds.width,
|
width: fallbackBounds.width,
|
||||||
@@ -106,7 +106,7 @@ export function updateInvisibleOverlayVisibilityService(args: {
|
|||||||
visibleOverlayVisible: boolean;
|
visibleOverlayVisible: boolean;
|
||||||
invisibleOverlayVisible: boolean;
|
invisibleOverlayVisible: boolean;
|
||||||
windowTracker: BaseWindowTracker | null;
|
windowTracker: BaseWindowTracker | null;
|
||||||
updateOverlayBounds: (geometry: WindowGeometry) => void;
|
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||||
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
||||||
enforceOverlayLayerOrder: () => void;
|
enforceOverlayLayerOrder: () => void;
|
||||||
syncOverlayShortcuts: () => void;
|
syncOverlayShortcuts: () => void;
|
||||||
@@ -140,7 +140,7 @@ export function updateInvisibleOverlayVisibilityService(args: {
|
|||||||
if (args.windowTracker && args.windowTracker.isTracking()) {
|
if (args.windowTracker && args.windowTracker.isTracking()) {
|
||||||
const geometry = args.windowTracker.getGeometry();
|
const geometry = args.windowTracker.getGeometry();
|
||||||
if (geometry) {
|
if (geometry) {
|
||||||
args.updateOverlayBounds(geometry);
|
args.updateInvisibleOverlayBounds(geometry);
|
||||||
}
|
}
|
||||||
showInvisibleWithoutFocus();
|
showInvisibleWithoutFocus();
|
||||||
args.syncOverlayShortcuts();
|
args.syncOverlayShortcuts();
|
||||||
@@ -156,7 +156,7 @@ export function updateInvisibleOverlayVisibilityService(args: {
|
|||||||
const cursorPoint = screen.getCursorScreenPoint();
|
const cursorPoint = screen.getCursorScreenPoint();
|
||||||
const display = screen.getDisplayNearestPoint(cursorPoint);
|
const display = screen.getDisplayNearestPoint(cursorPoint);
|
||||||
const fallbackBounds = display.workArea;
|
const fallbackBounds = display.workArea;
|
||||||
args.updateOverlayBounds({
|
args.updateInvisibleOverlayBounds({
|
||||||
x: fallbackBounds.x,
|
x: fallbackBounds.x,
|
||||||
y: fallbackBounds.y,
|
y: fallbackBounds.y,
|
||||||
width: fallbackBounds.width,
|
width: fallbackBounds.width,
|
||||||
|
|||||||
@@ -4,19 +4,17 @@ import { WindowGeometry } from "../../types";
|
|||||||
|
|
||||||
export type OverlayWindowKind = "visible" | "invisible";
|
export type OverlayWindowKind = "visible" | "invisible";
|
||||||
|
|
||||||
export function updateOverlayBoundsService(
|
export function updateOverlayWindowBoundsService(
|
||||||
geometry: WindowGeometry,
|
geometry: WindowGeometry,
|
||||||
getOverlayWindows: () => BrowserWindow[],
|
window: BrowserWindow | null,
|
||||||
): void {
|
): void {
|
||||||
if (!geometry) return;
|
if (!geometry || !window || window.isDestroyed()) return;
|
||||||
for (const window of getOverlayWindows()) {
|
window.setBounds({
|
||||||
window.setBounds({
|
x: geometry.x,
|
||||||
x: geometry.x,
|
y: geometry.y,
|
||||||
y: geometry.y,
|
width: geometry.width,
|
||||||
width: geometry.width,
|
height: geometry.height,
|
||||||
height: geometry.height,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureOverlayWindowLevelService(window: BrowserWindow): void {
|
export function ensureOverlayWindowLevelService(window: BrowserWindow): void {
|
||||||
|
|||||||
20
src/main.ts
20
src/main.ts
@@ -154,7 +154,6 @@ import {
|
|||||||
updateCurrentMediaPathService,
|
updateCurrentMediaPathService,
|
||||||
updateInvisibleOverlayVisibilityService,
|
updateInvisibleOverlayVisibilityService,
|
||||||
updateLastCardFromClipboardService,
|
updateLastCardFromClipboardService,
|
||||||
updateOverlayBoundsService,
|
|
||||||
updateVisibleOverlayVisibilityService,
|
updateVisibleOverlayVisibilityService,
|
||||||
} from "./core/services";
|
} from "./core/services";
|
||||||
import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-handler";
|
import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-handler";
|
||||||
@@ -763,8 +762,12 @@ async function tokenizeSubtitle(text: string): Promise<SubtitleData> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateOverlayBounds(geometry: WindowGeometry): void {
|
function updateVisibleOverlayBounds(geometry: WindowGeometry): void {
|
||||||
updateOverlayBoundsService(geometry, () => getOverlayWindows());
|
overlayManager.setOverlayWindowBounds("visible", geometry);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateInvisibleOverlayBounds(geometry: WindowGeometry): void {
|
||||||
|
overlayManager.setOverlayWindowBounds("invisible", geometry);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureOverlayWindowLevel(window: BrowserWindow): void {
|
function ensureOverlayWindowLevel(window: BrowserWindow): void {
|
||||||
@@ -856,8 +859,11 @@ function initializeOverlayRuntime(): void {
|
|||||||
registerGlobalShortcuts: () => {
|
registerGlobalShortcuts: () => {
|
||||||
registerGlobalShortcuts();
|
registerGlobalShortcuts();
|
||||||
},
|
},
|
||||||
updateOverlayBounds: (geometry) => {
|
updateVisibleOverlayBounds: (geometry) => {
|
||||||
updateOverlayBounds(geometry);
|
updateVisibleOverlayBounds(geometry);
|
||||||
|
},
|
||||||
|
updateInvisibleOverlayBounds: (geometry) => {
|
||||||
|
updateInvisibleOverlayBounds(geometry);
|
||||||
},
|
},
|
||||||
isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
isInvisibleOverlayVisible: () =>
|
isInvisibleOverlayVisible: () =>
|
||||||
@@ -1181,7 +1187,7 @@ function updateVisibleOverlayVisibility(): void {
|
|||||||
mpvClient.send(payload);
|
mpvClient.send(payload);
|
||||||
},
|
},
|
||||||
secondarySubVisibilityRequestId: MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
secondarySubVisibilityRequestId: MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||||
updateOverlayBounds: (geometry) => updateOverlayBounds(geometry),
|
updateVisibleOverlayBounds: (geometry) => updateVisibleOverlayBounds(geometry),
|
||||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||||
enforceOverlayLayerOrder: () => enforceOverlayLayerOrder(),
|
enforceOverlayLayerOrder: () => enforceOverlayLayerOrder(),
|
||||||
syncOverlayShortcuts: () => syncOverlayShortcuts(),
|
syncOverlayShortcuts: () => syncOverlayShortcuts(),
|
||||||
@@ -1196,7 +1202,7 @@ function updateInvisibleOverlayVisibility(): void {
|
|||||||
visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(),
|
visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(),
|
||||||
invisibleOverlayVisible: overlayManager.getInvisibleOverlayVisible(),
|
invisibleOverlayVisible: overlayManager.getInvisibleOverlayVisible(),
|
||||||
windowTracker,
|
windowTracker,
|
||||||
updateOverlayBounds: (geometry) => updateOverlayBounds(geometry),
|
updateInvisibleOverlayBounds: (geometry) => updateInvisibleOverlayBounds(geometry),
|
||||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||||
enforceOverlayLayerOrder: () => enforceOverlayLayerOrder(),
|
enforceOverlayLayerOrder: () => enforceOverlayLayerOrder(),
|
||||||
syncOverlayShortcuts: () => syncOverlayShortcuts(),
|
syncOverlayShortcuts: () => syncOverlayShortcuts(),
|
||||||
|
|||||||
Reference in New Issue
Block a user