refactor(overlay): split bounds ownership by layer for TASK-20.1

This commit is contained in:
2026-02-12 02:17:30 -08:00
parent 402788b1e2
commit dfb54630df
10 changed files with 110 additions and 36 deletions

View File

@@ -53,7 +53,7 @@ export {
createOverlayWindowService,
enforceOverlayLayerOrderService,
ensureOverlayWindowLevelService,
updateOverlayBoundsService,
updateOverlayWindowBoundsService,
} from "./overlay-window-service";
export { initializeOverlayRuntimeService } from "./overlay-runtime-init-service";
export {

View File

@@ -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(

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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,

View File

@@ -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 {