desktop: services/window-manager.service.ts — window lifecycle (open/close/focus/minimize/maximize/drag/resize)
This commit is contained in:
parent
4041625f06
commit
9d8aaa5c1a
145
desktop/src/app/services/window-manager.service.ts
Normal file
145
desktop/src/app/services/window-manager.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user