/* ═══════════════════════════════════════════ Phosphor Terminal — Window Manager (wm.js) ═══════════════════════════════════════════ */ const WM = (() => { "use strict"; let topZ = 100; const state = new Map(); // wid → { el, hidden, maximized, savedStyle } let drag = null; let resize = null; /* ── Init ──────────────────────────────── */ function init() { document.querySelectorAll(".window").forEach(register); setupIcons(); document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onUp); // Focus topmost visible window on load const visible = [...state.values()].filter(s => !s.hidden); if (visible.length) focusEl(visible[visible.length - 1].el); } /* ── Build and inject window chrome ────── */ function buildChrome(win) { const title = win.dataset.title || win.dataset.wid || ""; const bar = document.createElement("div"); bar.className = "window-titlebar"; const maxBtn = document.createElement("button"); maxBtn.className = "win-btn maximize"; maxBtn.title = "Full screen"; maxBtn.textContent = "⤢"; const titleSpan = document.createElement("span"); titleSpan.className = "window-title"; titleSpan.textContent = title; bar.appendChild(maxBtn); bar.appendChild(titleSpan); win.insertBefore(bar, win.firstChild); const footer = document.createElement("div"); footer.className = "window-footer"; const closeBtn = document.createElement("button"); closeBtn.className = "win-btn close"; closeBtn.title = "Close"; closeBtn.textContent = "×"; const spacer = document.createElement("div"); spacer.className = "win-footer-spacer"; const resizeHandle = document.createElement("div"); resizeHandle.className = "win-resize"; resizeHandle.title = "Drag to resize"; resizeHandle.textContent = "◢"; footer.appendChild(closeBtn); footer.appendChild(spacer); footer.appendChild(resizeHandle); win.appendChild(footer); } /* ── Register a window element ─────────── */ function register(win) { const id = win.dataset.wid; if (!id || state.has(id)) return; buildChrome(win); const hidden = win.classList.contains("hidden"); state.set(id, { el: win, hidden, maximized: false, savedStyle: null }); // Draggable title bar const bar = win.querySelector(".window-titlebar"); bar.addEventListener("mousedown", e => { if (e.button !== 0 || e.target.closest(".win-btn")) return; startDrag(e, win); }); // Focus on any click inside window win.addEventListener("mousedown", () => { if (!state.get(id)?.hidden) focusEl(win); }); // Window buttons const q = s => win.querySelector(s); const closeAction = win.dataset.close; q(".win-btn.close") ?.addEventListener("click", () => closeAction === "back" ? history.back() : hide(id)); q(".win-btn.maximize")?.addEventListener("click", () => toggleMax(id)); // Resize handle q(".win-resize")?.addEventListener("mousedown", e => { if (e.button !== 0) return; e.preventDefault(); e.stopPropagation(); const r = win.getBoundingClientRect(); resize = { el: win, x0: e.clientX, y0: e.clientY, w0: r.width, h0: r.height }; }); } /* ── Focus ─────────────────────────────── */ function focusEl(win) { document.querySelectorAll(".window.focused") .forEach(w => w.classList.remove("focused")); win.classList.add("focused"); win.style.zIndex = ++topZ; } function focus(id) { const s = state.get(id); if (s && !s.hidden) focusEl(s.el); } /* ── Show / Hide ───────────────────────── */ function show(id) { const s = state.get(id); if (!s) return; s.el.classList.remove("hidden"); s.hidden = false; focusEl(s.el); } function hide(id) { const s = state.get(id); if (!s) return; s.el.classList.add("hidden"); s.hidden = true; } function toggle(id) { const s = state.get(id); if (!s) return; if (s.hidden) { show(id); } else if (s.el.classList.contains("focused")) { hide(id); } else { focusEl(s.el); } } /* ── Maximize / Restore ────────────────── */ function toggleMax(id) { const s = state.get(id); if (!s) return; const el = s.el; const area = document.querySelector(".desktop-area"); if (!s.maximized) { s.savedStyle = { top: el.style.top, left: el.style.left, width: el.style.width, height: el.style.height, transform: el.style.transform, }; el.style.transform = "none"; el.style.top = "0"; el.style.left = "0"; el.style.width = area.clientWidth + "px"; el.style.height = area.clientHeight + "px"; s.maximized = true; } else { const sv = s.savedStyle; el.style.top = sv.top; el.style.left = sv.left; el.style.width = sv.width; el.style.height = sv.height; el.style.transform = sv.transform; s.maximized = false; } focusEl(el); } /* ── Drag ──────────────────────────────── */ function startDrag(e, win) { e.preventDefault(); focusEl(win); // Freeze any CSS transform into explicit top/left so dragging is consistent const wr = win.getBoundingClientRect(); const pr = win.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 }; const left0 = wr.left - pr.left; const top0 = wr.top - pr.top; win.style.transform = "none"; win.style.left = left0 + "px"; win.style.top = top0 + "px"; drag = { el: win, x0: e.clientX, y0: e.clientY, left0, top0 }; document.body.style.cursor = "move"; } /* ── Mouse events ──────────────────────── */ function onMove(e) { if (drag) { const dx = e.clientX - drag.x0; const dy = e.clientY - drag.y0; drag.el.style.left = (drag.left0 + dx) + "px"; drag.el.style.top = (drag.top0 + dy) + "px"; } if (resize) { const dx = e.clientX - resize.x0; const dy = e.clientY - resize.y0; resize.el.style.width = Math.max(280, resize.w0 + dx) + "px"; resize.el.style.height = Math.max(140, resize.h0 + dy) + "px"; } } function onUp() { drag = null; resize = null; document.body.style.cursor = ""; } /* ── Desktop icons ─────────────────────── */ function setupIcons() { document.querySelectorAll(".desktop-icon[data-opens]").forEach(icon => { icon.addEventListener("click", () => { document.querySelectorAll(".desktop-icon.active") .forEach(i => i.classList.remove("active")); icon.classList.add("active"); show(icon.dataset.opens); }); }); } /* ── Boot: hide icons for hidden windows ─ */ document.addEventListener("DOMContentLoaded", init); return { show, hide, toggle, focus }; })();