227 lines
8.5 KiB
JavaScript
227 lines
8.5 KiB
JavaScript
const { screen } = require('electron');
|
|
|
|
class SmoothMovementManager {
|
|
constructor(windowPool, getDisplayById, getCurrentDisplay, updateLayout) {
|
|
this.windowPool = windowPool;
|
|
this.getDisplayById = getDisplayById;
|
|
this.getCurrentDisplay = getCurrentDisplay;
|
|
this.updateLayout = updateLayout;
|
|
this.stepSize = 80;
|
|
this.animationDuration = 300;
|
|
this.headerPosition = { x: 0, y: 0 };
|
|
this.isAnimating = false;
|
|
this.hiddenPosition = null;
|
|
this.lastVisiblePosition = null;
|
|
this.currentDisplayId = null;
|
|
this.animationFrameId = null;
|
|
}
|
|
|
|
/**
|
|
* @param {BrowserWindow} win
|
|
* @returns {boolean}
|
|
*/
|
|
_isWindowValid(win) {
|
|
if (!win || win.isDestroyed()) {
|
|
if (this.isAnimating) {
|
|
console.warn('[MovementManager] Window destroyed mid-animation. Halting.');
|
|
this.isAnimating = false;
|
|
if (this.animationFrameId) {
|
|
clearTimeout(this.animationFrameId);
|
|
this.animationFrameId = null;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
moveToDisplay(displayId) {
|
|
const header = this.windowPool.get('header');
|
|
if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
|
|
|
|
const targetDisplay = this.getDisplayById(displayId);
|
|
if (!targetDisplay) return;
|
|
|
|
const currentBounds = header.getBounds();
|
|
const currentDisplay = this.getCurrentDisplay(header);
|
|
|
|
if (currentDisplay.id === targetDisplay.id) return;
|
|
|
|
const relativeX = (currentBounds.x - currentDisplay.workArea.x) / currentDisplay.workAreaSize.width;
|
|
const relativeY = (currentBounds.y - currentDisplay.workArea.y) / currentDisplay.workAreaSize.height;
|
|
const targetX = targetDisplay.workArea.x + targetDisplay.workAreaSize.width * relativeX;
|
|
const targetY = targetDisplay.workArea.y + targetDisplay.workAreaSize.height * relativeY;
|
|
|
|
const finalX = Math.max(targetDisplay.workArea.x, Math.min(targetDisplay.workArea.x + targetDisplay.workAreaSize.width - currentBounds.width, targetX));
|
|
const finalY = Math.max(targetDisplay.workArea.y, Math.min(targetDisplay.workArea.y + targetDisplay.workAreaSize.height - currentBounds.height, targetY));
|
|
|
|
this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
|
|
this.animateToPosition(header, finalX, finalY);
|
|
this.currentDisplayId = targetDisplay.id;
|
|
}
|
|
|
|
hideToEdge(edge, callback, { instant = false } = {}) {
|
|
const header = this.windowPool.get('header');
|
|
if (!header || header.isDestroyed()) {
|
|
if (typeof callback === 'function') callback();
|
|
return;
|
|
}
|
|
|
|
const { x, y } = header.getBounds();
|
|
this.lastVisiblePosition = { x, y };
|
|
this.hiddenPosition = { edge };
|
|
|
|
if (instant) {
|
|
header.hide();
|
|
if (typeof callback === 'function') callback();
|
|
return;
|
|
}
|
|
|
|
header.webContents.send('window-hide-animation');
|
|
|
|
setTimeout(() => {
|
|
if (!header.isDestroyed()) header.hide();
|
|
if (typeof callback === 'function') callback();
|
|
}, 5);
|
|
}
|
|
|
|
showFromEdge(callback) {
|
|
const header = this.windowPool.get('header');
|
|
if (!header || header.isDestroyed()) {
|
|
if (typeof callback === 'function') callback();
|
|
return;
|
|
}
|
|
|
|
// 숨기기 전에 기억해둔 위치 복구
|
|
if (this.lastVisiblePosition) {
|
|
header.setPosition(
|
|
this.lastVisiblePosition.x,
|
|
this.lastVisiblePosition.y,
|
|
false // animate: false
|
|
);
|
|
}
|
|
|
|
header.show();
|
|
header.webContents.send('window-show-animation');
|
|
|
|
// 내부 상태 초기화
|
|
this.hiddenPosition = null;
|
|
this.lastVisiblePosition = null;
|
|
|
|
if (typeof callback === 'function') callback();
|
|
}
|
|
|
|
moveStep(direction) {
|
|
const header = this.windowPool.get('header');
|
|
if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
|
|
|
|
const currentBounds = header.getBounds();
|
|
this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
|
|
let targetX = this.headerPosition.x;
|
|
let targetY = this.headerPosition.y;
|
|
|
|
switch (direction) {
|
|
case 'left': targetX -= this.stepSize; break;
|
|
case 'right': targetX += this.stepSize; break;
|
|
case 'up': targetY -= this.stepSize; break;
|
|
case 'down': targetY += this.stepSize; break;
|
|
default: return;
|
|
}
|
|
|
|
const displays = screen.getAllDisplays();
|
|
let validPosition = displays.some(d => (
|
|
targetX >= d.workArea.x && targetX + currentBounds.width <= d.workArea.x + d.workArea.width &&
|
|
targetY >= d.workArea.y && targetY + currentBounds.height <= d.workArea.y + d.workArea.height
|
|
));
|
|
|
|
if (!validPosition) {
|
|
const nearestDisplay = screen.getDisplayNearestPoint({ x: targetX, y: targetY });
|
|
const { x, y, width, height } = nearestDisplay.workArea;
|
|
targetX = Math.max(x, Math.min(x + width - currentBounds.width, targetX));
|
|
targetY = Math.max(y, Math.min(y + height - currentBounds.height, targetY));
|
|
}
|
|
|
|
if (targetX === this.headerPosition.x && targetY === this.headerPosition.y) return;
|
|
this.animateToPosition(header, targetX, targetY);
|
|
}
|
|
|
|
animateToPosition(header, targetX, targetY) {
|
|
if (!this._isWindowValid(header)) return;
|
|
|
|
this.isAnimating = true;
|
|
const startX = this.headerPosition.x;
|
|
const startY = this.headerPosition.y;
|
|
const startTime = Date.now();
|
|
|
|
if (!Number.isFinite(targetX) || !Number.isFinite(targetY) || !Number.isFinite(startX) || !Number.isFinite(startY)) {
|
|
this.isAnimating = false;
|
|
return;
|
|
}
|
|
|
|
const animate = () => {
|
|
if (!this._isWindowValid(header)) return;
|
|
|
|
const elapsed = Date.now() - startTime;
|
|
const progress = Math.min(elapsed / this.animationDuration, 1);
|
|
const eased = 1 - Math.pow(1 - progress, 3);
|
|
const currentX = startX + (targetX - startX) * eased;
|
|
const currentY = startY + (targetY - startY) * eased;
|
|
|
|
if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) {
|
|
this.isAnimating = false;
|
|
return;
|
|
}
|
|
|
|
if (!this._isWindowValid(header)) return;
|
|
header.setPosition(Math.round(currentX), Math.round(currentY));
|
|
|
|
if (progress < 1) {
|
|
this.animationFrameId = setTimeout(animate, 8);
|
|
} else {
|
|
this.animationFrameId = null;
|
|
this.headerPosition = { x: targetX, y: targetY };
|
|
if (Number.isFinite(targetX) && Number.isFinite(targetY)) {
|
|
if (!this._isWindowValid(header)) return;
|
|
header.setPosition(Math.round(targetX), Math.round(targetY));
|
|
}
|
|
this.isAnimating = false;
|
|
this.updateLayout();
|
|
}
|
|
};
|
|
animate();
|
|
}
|
|
|
|
moveToEdge(direction) {
|
|
const header = this.windowPool.get('header');
|
|
if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
|
|
|
|
const display = this.getCurrentDisplay(header);
|
|
const { width, height } = display.workAreaSize;
|
|
const { x: workAreaX, y: workAreaY } = display.workArea;
|
|
const headerBounds = header.getBounds();
|
|
const currentBounds = header.getBounds();
|
|
let targetX = currentBounds.x;
|
|
let targetY = currentBounds.y;
|
|
|
|
switch (direction) {
|
|
case 'left': targetX = workAreaX; break;
|
|
case 'right': targetX = workAreaX + width - headerBounds.width; break;
|
|
case 'up': targetY = workAreaY; break;
|
|
case 'down': targetY = workAreaY + height - headerBounds.height; break;
|
|
}
|
|
|
|
this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
|
|
this.animateToPosition(header, targetX, targetY);
|
|
}
|
|
|
|
destroy() {
|
|
if (this.animationFrameId) {
|
|
clearTimeout(this.animationFrameId);
|
|
this.animationFrameId = null;
|
|
}
|
|
this.isAnimating = false;
|
|
console.log('[Movement] Manager destroyed');
|
|
}
|
|
}
|
|
|
|
module.exports = SmoothMovementManager; |