diff --git a/desktop/src/app/services/window-manager.service.ts b/desktop/src/app/services/window-manager.service.ts new file mode 100644 index 0000000..7c4138f --- /dev/null +++ b/desktop/src/app/services/window-manager.service.ts @@ -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([]); + + /** The currently active (focused) window. */ + readonly activeWindowId = signal(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>): 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); + } +}