desktop: services/window-manager.service.ts — window lifecycle (open/close/focus/minimize/maximize/drag/resize)

This commit is contained in:
Butterfly Dev 2026-04-07 03:26:59 +00:00
parent 4041625f06
commit 9d8aaa5c1a

View File

@ -0,0 +1,145 @@
import { Injectable, signal, computed } from '@angular/core';
/** Represents an open window on the desktop. */
export interface WindowState {
id: string;
title: string;
icon: string;
/** Which built-in app or remote session this window shows. */
appId: string;
x: number;
y: number;
width: number;
height: number;
minWidth: number;
minHeight: number;
zIndex: number;
isMinimized: boolean;
isMaximized: boolean;
isActive: boolean;
/** Session id if this is a remote display window. */
sessionId?: string;
}
let nextWindowId = 1;
let topZIndex = 100;
@Injectable({ providedIn: 'root' })
export class WindowManagerService {
/** All open windows. */
private windows = signal<WindowState[]>([]);
/** The currently active (focused) window. */
readonly activeWindowId = signal<string | null>(null);
/** Computed list of non-minimized windows sorted by z-index. */
readonly visibleWindows = computed(() =>
this.windows().filter((w) => !w.isMinimized).sort((a, b) => a.zIndex - b.zIndex),
);
/** All windows for the taskbar. */
readonly allWindows = computed(() => this.windows());
/**
* Open a new window for the given app.
* @returns The window id.
*/
openWindow(opts: {
title: string;
icon: string;
appId: string;
sessionId?: string;
width?: number;
height?: number;
}): string {
const id = `win-${nextWindowId++}`;
const offset = (this.windows().length % 8) * 30;
const w: WindowState = {
id,
title: opts.title,
icon: opts.icon,
appId: opts.appId,
x: 120 + offset,
y: 60 + offset,
width: opts.width ?? 800,
height: opts.height ?? 520,
minWidth: 300,
minHeight: 200,
zIndex: ++topZIndex,
isMinimized: false,
isMaximized: false,
isActive: true,
sessionId: opts.sessionId,
};
// Deactivate all others.
this.windows.update((list) =>
list.map((win) => ({ ...win, isActive: false, zIndex: win.zIndex })),
);
this.windows.update((list) => [...list, w]);
this.activeWindowId.set(id);
return id;
}
/** Close a window by id. */
closeWindow(id: string): void {
this.windows.update((list) => list.filter((w) => w.id !== id));
if (this.activeWindowId() === id) {
const remaining = this.windows();
this.activeWindowId.set(remaining.length > 0 ? remaining[remaining.length - 1].id : null);
}
}
/** Focus a window (bring to front). */
focusWindow(id: string): void {
this.windows.update((list) =>
list.map((w) => ({
...w,
isActive: w.id === id,
zIndex: w.id === id ? ++topZIndex : w.zIndex,
isMinimized: w.id === id ? false : w.isMinimized,
})),
);
this.activeWindowId.set(id);
}
/** Minimize a window. */
minimizeWindow(id: string): void {
this.windows.update((list) =>
list.map((w) => (w.id === id ? { ...w, isMinimized: true, isActive: false } : w)),
);
// Focus the next visible window.
const visible = this.windows().filter((w) => !w.isMinimized && w.id !== id);
if (visible.length > 0) {
const top = visible.reduce((a, b) => (a.zIndex > b.zIndex ? a : b));
this.focusWindow(top.id);
} else {
this.activeWindowId.set(null);
}
}
/** Toggle maximize for a window. */
toggleMaximize(id: string): void {
this.windows.update((list) =>
list.map((w) => (w.id === id ? { ...w, isMaximized: !w.isMaximized } : w)),
);
}
/** Update window position and size (from dragging/resizing). */
updateWindowGeometry(id: string, changes: Partial<Pick<WindowState, 'x' | 'y' | 'width' | 'height'>>): void {
this.windows.update((list) =>
list.map((w) => (w.id === id ? { ...w, ...changes } : w)),
);
}
/** Update window title. */
updateWindowTitle(id: string, title: string): void {
this.windows.update((list) => list.map((w) => (w.id === id ? { ...w, title } : w)));
}
/** Get a specific window's state. */
getWindow(id: string): WindowState | undefined {
return this.windows().find((w) => w.id === id);
}
}