/** * CatOS Window Management Module * Handles window creation, positioning, resizing, and lifecycle */ class WindowManager { constructor(core) { this.core = core; this.windowInteractions = new WindowInteractions(); } // Window Management createWindow(options) { const windowId = `window-${this.core.nextWindowId++}`; const window = document.createElement('div'); window.className = 'window focused'; window.id = windowId; window.innerHTML = `
${options.icon} ${options.title}
${options.content}
`; // Position window with smart cascade const offset = (this.core.nextWindowId - 2) * 30; const maxOffset = 200; // Prevent windows from going off-screen const actualOffset = offset % maxOffset; window.style.left = (100 + actualOffset) + 'px'; window.style.top = (50 + actualOffset) + 'px'; window.style.width = options.width || '600px'; window.style.height = options.height || '400px'; // Add to container document.getElementById('windows-container').appendChild(window); // Store window data this.core.windows.set(windowId, { element: window, title: options.title, icon: options.icon, appType: options.appType, minimized: false, maximized: false, eventHandlers: new Map() // Store event handlers for cleanup }); // Setup window events this.setupWindowEvents(windowId); this.focusWindow(windowId); return windowId; } setupWindowEvents(windowId) { const window = this.core.windows.get(windowId); const element = window.element; // Window controls element.querySelectorAll('.window-control').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const action = btn.dataset.action; switch(action) { case 'minimize': this.minimizeWindow(windowId); break; case 'maximize': this.toggleMaximizeWindow(windowId); break; case 'close': this.closeWindow(windowId); break; } }); }); // Window focus element.addEventListener('mousedown', () => { this.focusWindow(windowId); }); // Window dragging const titlebar = element.querySelector('.window-titlebar'); this.windowInteractions.setupDragging(windowId, element, titlebar, this.core); // Window resizing this.windowInteractions.setupResizing(windowId, element, this.core); } focusWindow(windowId) { // Unfocus all windows document.querySelectorAll('.window').forEach(w => { w.classList.remove('focused'); }); // Update taskbar document.querySelectorAll('.taskbar-app').forEach(app => { app.classList.remove('focused'); }); // Focus target window const window = this.core.windows.get(windowId); if (window) { window.element.classList.add('focused'); this.core.focusedWindow = windowId; // Update taskbar const taskbarApp = document.querySelector(`[data-window-id="${windowId}"]`); if (taskbarApp) { taskbarApp.classList.add('focused'); } } } minimizeWindow(windowId) { const window = this.core.windows.get(windowId); if (window) { window.element.classList.add('minimized'); window.minimized = true; // Focus another window if this was focused if (this.core.focusedWindow === windowId) { this.core.focusedWindow = null; // Find another window to focus for (let [id, win] of this.core.windows) { if (id !== windowId && !win.minimized) { this.focusWindow(id); break; } } } } } toggleMaximizeWindow(windowId) { const window = this.core.windows.get(windowId); if (window) { if (window.maximized) { // Restore window window.element.classList.remove('maximized'); window.maximized = false; } else { // Maximize window window.element.classList.add('maximized'); window.maximized = true; } } } closeWindow(windowId) { const window = this.core.windows.get(windowId); if (window) { // Clean up event handlers this.windowInteractions.cleanup(windowId, window); // Remove from DOM window.element.remove(); // Remove from taskbar const taskbarApp = document.querySelector(`[data-window-id="${windowId}"]`); if (taskbarApp) { taskbarApp.remove(); } // Remove from memory this.core.windows.delete(windowId); // Focus another window if this was focused if (this.core.focusedWindow === windowId) { this.core.focusedWindow = null; // Find another window to focus for (let [id, win] of this.core.windows) { if (!win.minimized) { this.focusWindow(id); break; } } } } } addToTaskbar(windowId, app) { const taskbar = document.getElementById('taskbar-apps'); const taskbarApp = document.createElement('div'); taskbarApp.className = 'taskbar-app focused'; taskbarApp.dataset.windowId = windowId; taskbarApp.innerHTML = ` ${app.icon} ${app.title} `; taskbarApp.addEventListener('click', () => { const window = this.core.windows.get(windowId); if (window) { if (window.minimized) { // Restore window window.element.classList.remove('minimized'); window.minimized = false; this.focusWindow(windowId); } else if (this.core.focusedWindow === windowId) { // If window is focused, minimize it this.minimizeWindow(windowId); } else { // If window exists but not focused, focus it this.focusWindow(windowId); } } }); taskbar.appendChild(taskbarApp); } } /** * Window Interactions Handler - Modular event handling for drag/resize */ class WindowInteractions { constructor() { this.activeWindows = new Map(); // Store per-window interaction state } setupDragging(windowId, element, handle, core) { const interaction = { isDragging: false, startMouseX: 0, startMouseY: 0, startWindowX: 0, startWindowY: 0, handlers: new Map() }; const dragStart = (e) => { const window = core.windows.get(windowId); if (window && window.maximized) return; if (e.target === handle || (handle.contains(e.target) && !e.target.classList.contains('window-control'))) { e.preventDefault(); interaction.isDragging = true; if (e.type === "touchstart") { interaction.startMouseX = e.touches[0].clientX; interaction.startMouseY = e.touches[0].clientY; } else { interaction.startMouseX = e.clientX; interaction.startMouseY = e.clientY; } const rect = element.getBoundingClientRect(); interaction.startWindowX = rect.left; interaction.startWindowY = rect.top; document.body.classList.add('dragging'); element.classList.add('being-dragged'); } }; const drag = (e) => { if (!interaction.isDragging) return; e.preventDefault(); let currentMouseX, currentMouseY; if (e.type === "touchmove") { currentMouseX = e.touches[0].clientX; currentMouseY = e.touches[0].clientY; } else { currentMouseX = e.clientX; currentMouseY = e.clientY; } const deltaX = currentMouseX - interaction.startMouseX; const deltaY = currentMouseY - interaction.startMouseY; let newX = interaction.startWindowX + deltaX; let newY = interaction.startWindowY + deltaY; const maxX = window.innerWidth - element.offsetWidth; const maxY = window.innerHeight - element.offsetHeight - 48; newX = Math.max(0, Math.min(newX, maxX)); newY = Math.max(0, Math.min(newY, maxY)); requestAnimationFrame(() => { element.style.left = newX + "px"; element.style.top = newY + "px"; }); }; const dragEnd = () => { if (interaction.isDragging) { interaction.isDragging = false; document.body.classList.remove('dragging'); element.classList.remove('being-dragged'); } }; // Store handlers for cleanup interaction.handlers.set('mousedown', dragStart); interaction.handlers.set('touchstart', dragStart); interaction.handlers.set('mousemove', drag); interaction.handlers.set('touchmove', drag); interaction.handlers.set('mouseup', dragEnd); interaction.handlers.set('touchend', dragEnd); // Add event listeners handle.addEventListener("mousedown", dragStart); handle.addEventListener("touchstart", dragStart); document.addEventListener("mousemove", drag); document.addEventListener("touchmove", drag); document.addEventListener("mouseup", dragEnd); document.addEventListener("touchend", dragEnd); this.activeWindows.set(windowId, interaction); } setupResizing(windowId, element, core) { const resizeHandles = element.querySelectorAll('.window-resize-handle'); const interaction = { isResizing: false, resizeDirection: '', startX: 0, startY: 0, startWidth: 0, startHeight: 0, startLeft: 0, startTop: 0, handlers: new Map() }; const resizeStart = (e) => { const windowData = core.windows.get(windowId); if (windowData && windowData.maximized) return; e.preventDefault(); e.stopPropagation(); interaction.isResizing = true; interaction.resizeDirection = e.target.dataset.direction; interaction.startX = e.clientX; interaction.startY = e.clientY; interaction.startWidth = parseInt(getComputedStyle(element).width, 10); interaction.startHeight = parseInt(getComputedStyle(element).height, 10); interaction.startLeft = parseInt(getComputedStyle(element).left, 10); interaction.startTop = parseInt(getComputedStyle(element).top, 10); element.classList.add('resizing'); document.body.style.cursor = e.target.style.cursor; document.body.style.userSelect = 'none'; }; const resize = (e) => { if (!interaction.isResizing) return; e.preventDefault(); const deltaX = e.clientX - interaction.startX; const deltaY = e.clientY - interaction.startY; let newWidth = interaction.startWidth; let newHeight = interaction.startHeight; let newLeft = interaction.startLeft; let newTop = interaction.startTop; if (interaction.resizeDirection.includes('e')) { newWidth = Math.max(300, interaction.startWidth + deltaX); } if (interaction.resizeDirection.includes('w')) { newWidth = Math.max(300, interaction.startWidth - deltaX); newLeft = interaction.startLeft + (interaction.startWidth - newWidth); } if (interaction.resizeDirection.includes('s')) { newHeight = Math.max(200, interaction.startHeight + deltaY); } if (interaction.resizeDirection.includes('n')) { newHeight = Math.max(200, interaction.startHeight - deltaY); newTop = interaction.startTop + (interaction.startHeight - newHeight); } const maxWidth = window.innerWidth - newLeft; const maxHeight = window.innerHeight - newTop - 48; newWidth = Math.min(newWidth, maxWidth); newHeight = Math.min(newHeight, maxHeight); requestAnimationFrame(() => { element.style.width = newWidth + 'px'; element.style.height = newHeight + 'px'; element.style.left = newLeft + 'px'; element.style.top = newTop + 'px'; }); }; const resizeEnd = () => { if (!interaction.isResizing) return; interaction.isResizing = false; interaction.resizeDirection = ''; element.classList.remove('resizing'); document.body.style.cursor = ''; document.body.style.userSelect = ''; }; // Store handlers for cleanup interaction.handlers.set('resizeStart', resizeStart); interaction.handlers.set('resize', resize); interaction.handlers.set('resizeEnd', resizeEnd); // Add event listeners resizeHandles.forEach(handle => { handle.addEventListener('mousedown', resizeStart); }); document.addEventListener('mousemove', resize); document.addEventListener('mouseup', resizeEnd); // Store resize interaction separately or merge with drag interaction if (this.activeWindows.has(windowId)) { // Merge with existing interaction const existing = this.activeWindows.get(windowId); existing.resizeInteraction = interaction; } else { this.activeWindows.set(windowId, { resizeInteraction: interaction }); } } cleanup(windowId, windowData) { const interaction = this.activeWindows.get(windowId); if (interaction) { // Clean up drag handlers if (interaction.handlers) { const handles = windowData.element.querySelectorAll('.window-titlebar'); handles.forEach(handle => { if (interaction.handlers.has('mousedown')) { handle.removeEventListener('mousedown', interaction.handlers.get('mousedown')); } if (interaction.handlers.has('touchstart')) { handle.removeEventListener('touchstart', interaction.handlers.get('touchstart')); } }); // Clean up document listeners ['mousemove', 'touchmove', 'mouseup', 'touchend'].forEach(event => { if (interaction.handlers.has(event)) { document.removeEventListener(event, interaction.handlers.get(event)); } }); } // Clean up resize handlers if (interaction.resizeInteraction) { const resizeHandles = windowData.element.querySelectorAll('.window-resize-handle'); resizeHandles.forEach(handle => { if (interaction.resizeInteraction.handlers.has('resizeStart')) { handle.removeEventListener('mousedown', interaction.resizeInteraction.handlers.get('resizeStart')); } }); if (interaction.resizeInteraction.handlers.has('resize')) { document.removeEventListener('mousemove', interaction.resizeInteraction.handlers.get('resize')); } if (interaction.resizeInteraction.handlers.has('resizeEnd')) { document.removeEventListener('mouseup', interaction.resizeInteraction.handlers.get('resizeEnd')); } } this.activeWindows.delete(windowId); } } } // Export for use in main system window.CatOSWindowManager = WindowManager;