235 lines
7.4 KiB
JavaScript
235 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 };
|
|||
|
|
})();
|