|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WindowManager { |
|
|
constructor(core) { |
|
|
this.core = core; |
|
|
this.windowInteractions = new WindowInteractions(); |
|
|
} |
|
|
|
|
|
|
|
|
createWindow(options) { |
|
|
const windowId = `window-${this.core.nextWindowId++}`; |
|
|
const window = document.createElement('div'); |
|
|
window.className = 'window focused'; |
|
|
window.id = windowId; |
|
|
window.innerHTML = ` |
|
|
<div class="window-titlebar"> |
|
|
<div class="window-title"> |
|
|
<span class="window-icon">${options.icon}</span> |
|
|
${options.title} |
|
|
</div> |
|
|
<div class="window-controls"> |
|
|
<button class="window-control minimize" data-action="minimize">−</button> |
|
|
<button class="window-control maximize" data-action="maximize">□</button> |
|
|
<button class="window-control close" data-action="close">×</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="window-content"> |
|
|
${options.content} |
|
|
</div> |
|
|
<div class="window-resize-handle resize-handle-n" data-direction="n"></div> |
|
|
<div class="window-resize-handle resize-handle-s" data-direction="s"></div> |
|
|
<div class="window-resize-handle resize-handle-e" data-direction="e"></div> |
|
|
<div class="window-resize-handle resize-handle-w" data-direction="w"></div> |
|
|
<div class="window-resize-handle resize-handle-ne" data-direction="ne"></div> |
|
|
<div class="window-resize-handle resize-handle-nw" data-direction="nw"></div> |
|
|
<div class="window-resize-handle resize-handle-se" data-direction="se"></div> |
|
|
<div class="window-resize-handle resize-handle-sw" data-direction="sw"></div> |
|
|
`; |
|
|
|
|
|
|
|
|
const offset = (this.core.nextWindowId - 2) * 30; |
|
|
const maxOffset = 200; |
|
|
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'; |
|
|
|
|
|
|
|
|
document.getElementById('windows-container').appendChild(window); |
|
|
|
|
|
|
|
|
this.core.windows.set(windowId, { |
|
|
element: window, |
|
|
title: options.title, |
|
|
icon: options.icon, |
|
|
appType: options.appType, |
|
|
minimized: false, |
|
|
maximized: false, |
|
|
eventHandlers: new Map() |
|
|
}); |
|
|
|
|
|
|
|
|
this.setupWindowEvents(windowId); |
|
|
this.focusWindow(windowId); |
|
|
|
|
|
return windowId; |
|
|
} |
|
|
|
|
|
setupWindowEvents(windowId) { |
|
|
const window = this.core.windows.get(windowId); |
|
|
const element = window.element; |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
element.addEventListener('mousedown', () => { |
|
|
this.focusWindow(windowId); |
|
|
}); |
|
|
|
|
|
|
|
|
const titlebar = element.querySelector('.window-titlebar'); |
|
|
this.windowInteractions.setupDragging(windowId, element, titlebar, this.core); |
|
|
|
|
|
|
|
|
this.windowInteractions.setupResizing(windowId, element, this.core); |
|
|
} |
|
|
|
|
|
focusWindow(windowId) { |
|
|
|
|
|
document.querySelectorAll('.window').forEach(w => { |
|
|
w.classList.remove('focused'); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.taskbar-app').forEach(app => { |
|
|
app.classList.remove('focused'); |
|
|
}); |
|
|
|
|
|
|
|
|
const window = this.core.windows.get(windowId); |
|
|
if (window) { |
|
|
window.element.classList.add('focused'); |
|
|
this.core.focusedWindow = windowId; |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
if (this.core.focusedWindow === windowId) { |
|
|
this.core.focusedWindow = null; |
|
|
|
|
|
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) { |
|
|
|
|
|
window.element.classList.remove('maximized'); |
|
|
window.maximized = false; |
|
|
} else { |
|
|
|
|
|
window.element.classList.add('maximized'); |
|
|
window.maximized = true; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
closeWindow(windowId) { |
|
|
const window = this.core.windows.get(windowId); |
|
|
if (window) { |
|
|
|
|
|
this.windowInteractions.cleanup(windowId, window); |
|
|
|
|
|
|
|
|
window.element.remove(); |
|
|
|
|
|
|
|
|
const taskbarApp = document.querySelector(`[data-window-id="${windowId}"]`); |
|
|
if (taskbarApp) { |
|
|
taskbarApp.remove(); |
|
|
} |
|
|
|
|
|
|
|
|
this.core.windows.delete(windowId); |
|
|
|
|
|
|
|
|
if (this.core.focusedWindow === windowId) { |
|
|
this.core.focusedWindow = null; |
|
|
|
|
|
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 = ` |
|
|
<span class="taskbar-app-icon">${app.icon}</span> |
|
|
<span class="taskbar-app-name">${app.title}</span> |
|
|
`; |
|
|
|
|
|
taskbarApp.addEventListener('click', () => { |
|
|
const window = this.core.windows.get(windowId); |
|
|
if (window) { |
|
|
if (window.minimized) { |
|
|
|
|
|
window.element.classList.remove('minimized'); |
|
|
window.minimized = false; |
|
|
this.focusWindow(windowId); |
|
|
} else if (this.core.focusedWindow === windowId) { |
|
|
|
|
|
this.minimizeWindow(windowId); |
|
|
} else { |
|
|
|
|
|
this.focusWindow(windowId); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
taskbar.appendChild(taskbarApp); |
|
|
} |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WindowInteractions { |
|
|
constructor() { |
|
|
this.activeWindows = new Map(); |
|
|
} |
|
|
|
|
|
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'); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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 = ''; |
|
|
}; |
|
|
|
|
|
|
|
|
interaction.handlers.set('resizeStart', resizeStart); |
|
|
interaction.handlers.set('resize', resize); |
|
|
interaction.handlers.set('resizeEnd', resizeEnd); |
|
|
|
|
|
|
|
|
resizeHandles.forEach(handle => { |
|
|
handle.addEventListener('mousedown', resizeStart); |
|
|
}); |
|
|
|
|
|
document.addEventListener('mousemove', resize); |
|
|
document.addEventListener('mouseup', resizeEnd); |
|
|
|
|
|
|
|
|
if (this.activeWindows.has(windowId)) { |
|
|
|
|
|
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) { |
|
|
|
|
|
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')); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
['mousemove', 'touchmove', 'mouseup', 'touchend'].forEach(event => { |
|
|
if (interaction.handlers.has(event)) { |
|
|
document.removeEventListener(event, interaction.handlers.get(event)); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
window.CatOSWindowManager = WindowManager; |