234 lines
7.4 KiB
JavaScript
234 lines
7.4 KiB
JavaScript
/* ═══════════════════════════════════════════
|
||
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 };
|
||
})();
|