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
|
||||
max_column_width: 20
|
||||
default_editor: "nvim"
|
||||
auto_open_browser: true
|
||||
auto_open_browser: false
|
||||
default_port: 6420
|
||||
remote_operations: true
|
||||
auto_commit: false
|
||||
|
||||
@@ -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
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #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.
|
||||
<!-- 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,
|
||||
enforceOverlayLayerOrderService,
|
||||
ensureOverlayWindowLevelService,
|
||||
updateOverlayBoundsService,
|
||||
updateOverlayWindowBoundsService,
|
||||
} from "./overlay-window-service";
|
||||
export { initializeOverlayRuntimeService } from "./overlay-runtime-init-service";
|
||||
export {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,12 +4,11 @@ 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()) {
|
||||
if (!geometry || !window || window.isDestroyed()) return;
|
||||
window.setBounds({
|
||||
x: geometry.x,
|
||||
y: geometry.y,
|
||||
@@ -17,7 +16,6 @@ export function updateOverlayBoundsService(
|
||||
height: geometry.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureOverlayWindowLevelService(window: BrowserWindow): void {
|
||||
if (process.platform === "darwin") {
|
||||
|
||||
20
src/main.ts
20
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<SubtitleData> {
|
||||
);
|
||||
}
|
||||
|
||||
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(),
|
||||
|
||||
Reference in New Issue
Block a user