/**
* 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;