/** * pc-live | Window Manager * Handles: create, focus, drag, resize, maximize, minimize, close, taskbar */ const WindowManager = (() => { const layer = document.getElementById('window-layer'); const taskbarApps = document.getElementById('taskbar-apps'); let zCounter = 10; const windows = new Map(); // id -> { el, tbBtn, minimized, maximized, preMax, title, icon } /* ── Helpers ──────────────────────────────────────────── */ function nextZ() { return ++zCounter; } function clampPos(el) { const lw = layer.clientWidth; const lh = layer.clientHeight; let l = parseInt(el.style.left) || 0; let t = parseInt(el.style.top) || 0; const w = el.offsetWidth; const h = el.offsetHeight; el.style.left = Math.min(Math.max(0, l), lw - w) + 'px'; el.style.top = Math.min(Math.max(0, t), lh - Math.max(40, h - 300)) + 'px'; } /* ── Drag ─────────────────────────────────────────────── */ function makeDraggable(el, handle) { let startX, startY, startL, startT; handle.addEventListener('mousedown', (e) => { if (e.target.classList.contains('wc-btn')) return; if (e.target.classList.contains('wc-resize')) return; const win = windows.get(el.dataset.wid); if (win && win.maximized) return; startX = e.clientX; startY = e.clientY; startL = parseInt(el.style.left) || 0; startT = parseInt(el.style.top) || 0; focus(el.dataset.wid); const mm = (e2) => { el.style.left = (startL + e2.clientX - startX) + 'px'; el.style.top = (startT + e2.clientY - startY) + 'px'; }; const mu = () => { clampPos(el); document.removeEventListener('mousemove', mm); document.removeEventListener('mouseup', mu); }; document.addEventListener('mousemove', mm); document.addEventListener('mouseup', mu); }); // Double-click titlebar → toggle maximize handle.addEventListener('dblclick', (e) => { if (e.target.classList.contains('wc-btn')) return; toggleMaximize(el.dataset.wid); }); } /* ── Resize ───────────────────────────────────────────── */ function makeResizable(el, id) { const dirs = ['n', 's', 'e', 'w', 'nw', 'ne', 'sw', 'se']; dirs.forEach(dir => { const h = document.createElement('div'); h.className = 'wc-resize wc-resize-' + dir; el.appendChild(h); let startX, startY, startL, startT, startW, startH; const MIN_W = 320, MIN_H = 200; h.addEventListener('mousedown', (e) => { const win = windows.get(id); if (!win || win.maximized) return; e.stopPropagation(); focus(id); startX = e.clientX; startY = e.clientY; startL = parseInt(el.style.left) || 0; startT = parseInt(el.style.top) || 0; startW = el.offsetWidth; startH = el.offsetHeight; const mm = (e2) => { const dx = e2.clientX - startX; const dy = e2.clientY - startY; let newL = startL, newT = startT, newW = startW, newH = startH; if (dir.includes('e')) newW = Math.max(MIN_W, startW + dx); if (dir.includes('w')) { newW = Math.max(MIN_W, startW - dx); newL = startL + startW - newW; } if (dir.includes('s')) newH = Math.max(MIN_H, startH + dy); if (dir.includes('n')) { newH = Math.max(MIN_H, startH - dy); newT = startT + startH - newH; } el.style.left = newL + 'px'; el.style.top = newT + 'px'; el.style.width = newW + 'px'; el.style.height = newH + 'px'; }; const mu = () => { document.removeEventListener('mousemove', mm); document.removeEventListener('mouseup', mu); }; document.addEventListener('mousemove', mm); document.addEventListener('mouseup', mu); }); }); } /* ── Focus ────────────────────────────────────────────── */ function focus(id) { const win = windows.get(id); if (!win) return; windows.forEach((w, wid) => { w.el.classList.toggle('focused', wid === id); w.tbBtn && w.tbBtn.classList.toggle('active', wid === id); }); win.el.style.zIndex = nextZ(); } /* ── Maximize / Restore ───────────────────────────────── */ function maximize(id) { const win = windows.get(id); if (!win || win.maximized) return; win.preMax = { left: win.el.style.left, top: win.el.style.top, width: win.el.style.width, height: win.el.style.height, }; win.maximized = true; win.el.classList.add('maximized'); win.el.style.left = '0'; win.el.style.top = '0'; win.el.style.width = layer.clientWidth + 'px'; win.el.style.height = layer.clientHeight + 'px'; const btn = win.el.querySelector('.wc-maximize'); if (btn) btn.title = 'Restore'; focus(id); } function unmaximize(id) { const win = windows.get(id); if (!win || !win.maximized || !win.preMax) return; win.maximized = false; win.el.classList.remove('maximized'); win.el.style.left = win.preMax.left; win.el.style.top = win.preMax.top; win.el.style.width = win.preMax.width; win.el.style.height = win.preMax.height; win.preMax = null; const btn = win.el.querySelector('.wc-maximize'); if (btn) btn.title = 'Maximize'; focus(id); } function toggleMaximize(id) { const win = windows.get(id); if (!win) return; if (win.maximized) unmaximize(id); else maximize(id); } /* ── Create ───────────────────────────────────────────── */ function create({ id, title, icon = '🖥', width = 700, height = 480, content }) { if (windows.has(id)) { restore(id); return windows.get(id).el; } const lw = layer.clientWidth; const lh = layer.clientHeight; const left = Math.max(20, Math.floor(lw / 2 - width / 2)); const top = Math.max(20, Math.floor(lh / 2 - height / 2)); const el = document.createElement('div'); el.className = 'window fade-in'; el.dataset.wid = id; el.style.cssText = `left:${left}px;top:${top}px;width:${width}px;height:${height}px;z-index:${nextZ()}`; el.innerHTML = `
${icon} ${title}
${content || ''}
`; el.querySelector('.wc-minimize').addEventListener('click', () => minimize(id)); el.querySelector('.wc-maximize').addEventListener('click', () => toggleMaximize(id)); el.querySelector('.wc-close').addEventListener('click', () => close(id)); el.addEventListener('mousedown', () => focus(id)); makeDraggable(el, el.querySelector('.window-titlebar')); makeResizable(el, id); layer.appendChild(el); const tbBtn = document.createElement('div'); tbBtn.className = 'taskbar-btn'; tbBtn.innerHTML = `${icon}${title}`; tbBtn.addEventListener('click', () => toggle(id)); taskbarApps.appendChild(tbBtn); windows.set(id, { el, tbBtn, minimized: false, maximized: false, preMax: null, title, icon }); focus(id); return el; } /* ── Minimize / restore / toggle ────────────────────────── */ function minimize(id) { const win = windows.get(id); if (!win) return; win.minimized = true; win.el.classList.add('minimized'); win.tbBtn && win.tbBtn.classList.remove('active'); win.tbBtn && win.tbBtn.classList.add('minimized'); } function restore(id) { const win = windows.get(id); if (!win) return; win.minimized = false; win.el.classList.remove('minimized'); win.tbBtn && win.tbBtn.classList.remove('minimized'); focus(id); } function toggle(id) { const win = windows.get(id); if (!win) return; if (win.minimized) restore(id); else minimize(id); } /* ── Close ───────────────────────────────────────────────── */ function close(id) { const win = windows.get(id); if (!win) return; win.el.remove(); win.tbBtn && win.tbBtn.remove(); windows.delete(id); } function closeAll() { windows.forEach((_, id) => close(id)); } /* ── Get body element ────────────────────────────────────── */ function getBody(id) { const win = windows.get(id); return win ? win.el.querySelector('.window-body') : null; } return { create, close, closeAll, focus, minimize, restore, toggle, maximize, unmaximize, toggleMaximize, getBody }; })();