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