import { Button, Container, Label, Node } from "./domlink.ts"; class TitleBar extends Container { label: Label; closeButton: Button; constructor(title: string, closeCallback: EventListener) { super(); this.class("LWindowHandle"); this.label = new Label(title); this.closeButton = new Button("x", closeCallback); this.with(this.label,this.closeButton); } get closable(): boolean { return this.closeButton.wraps.style.display != "none"; } set closable(x: boolean) { this.closeButton.wraps.style.display = x ? "inline-block" : "none"; } } export class Window extends Container { // i know that i dont technically need to have these per-intance (there's no more than one cursor), // but i still kind of feel like i should. private static currentlyDragged: Window | null = null; private static mouseRelX = 0; private static mouseRelY = 0; private static zIndexCounter = 100; titleBar: TitleBar; content = new Container().class("LWindowContent"); constructor(title: string = "New Window", height: number = 300, width: number = 400) { super(); this.class("LWindow"); this.titleBar = new TitleBar(title, ()=>{this.wraps.remove();}); this.add(this.titleBar); this.titleBar.wraps.addEventListener("mousedown", this.titleGrabHandler.bind(this)); this.wraps.addEventListener("mousedown", this.front); this.front(); this.add(this.content); this.style((s)=>{ s.width = `${width}px`; s.height = `${height}px`; s.top = "0px"; s.left = "0px"; }); globalThis.window.addEventListener("resize", ()=>{ // deno-lint-ignore no-self-assign this.position = this.position; }); } front() { this.style((s)=>{ s.zIndex = `${Window.zIndexCounter++}`; }); } override with(...nodes: (Node | string)[]): this { const w = this.wraps.style.width; const h = this.wraps.style.height; this.content.with(...nodes); this.wraps.style.width = w; this.wraps.style.height = h; return this; } get title() { return (this.titleBar.children[0] as Label).text; } set title(newTitle: string) { (this.titleBar.children[0] as Label).text = newTitle; } public closable(can: boolean = true) { this.titleBar.closable = can; return this; } private titleGrabHandler(ev: MouseEvent) { if (ev.button !== 0) return; Window.currentlyDragged = this; this.titleBar.wraps.style.cursor = 'grabbing'; const rect = this.wraps.getBoundingClientRect(); Window.mouseRelX = ev.clientX - rect.left; Window.mouseRelY = ev.clientY - rect.top; ev.preventDefault(); } private static onMouseMove(ev: MouseEvent) { const draggedWindow = Window.currentlyDragged; if (draggedWindow) { const newLeft = ev.clientX - Window.mouseRelX; const newTop = ev.clientY - Window.mouseRelY; draggedWindow.position = [newLeft, newTop]; } } set position([x, y]: [number, number]) { const viewportWidth = document.documentElement.clientWidth; const viewportHeight = document.documentElement.clientHeight; const clampedLeft = Math.min(Math.max(0, x), viewportWidth - this.wraps.offsetWidth); const clampedTop = Math.min(Math.max(0, y), viewportHeight - this.wraps.offsetHeight); this.wraps.style.left = `${clampedLeft}px`; this.wraps.style.top = `${clampedTop}px`; } public get position(): [number, number] { const rect = this.wraps.getBoundingClientRect(); return [rect.left, rect.top]; } private static onMouseUp(_ev: MouseEvent) { if (Window.currentlyDragged) { Window.currentlyDragged.titleBar.wraps.style.cursor = 'grab'; Window.currentlyDragged = null; } } // A single place to initialize global listeners private static _initialized = false; private static initializeGlobalListeners() { if (this._initialized) return; globalThis.addEventListener("mousemove", this.onMouseMove); globalThis.addEventListener("mouseup", this.onMouseUp); this._initialized = true; } // Static initializer block to set up listeners once static { this.initializeGlobalListeners(); } }