280 lines
10 KiB
JavaScript
280 lines
10 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;
|
|
|
|
console.log(`[MovementManager] Moving ${direction} from (${targetX}, ${targetY})`);
|
|
|
|
const windowSize = {
|
|
width: currentBounds.width,
|
|
height: currentBounds.height
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
// Find the display that contains or is nearest to the target position
|
|
const nearestDisplay = screen.getDisplayNearestPoint({ x: targetX, y: targetY });
|
|
const { x: workAreaX, y: workAreaY, width: workAreaWidth, height: workAreaHeight } = nearestDisplay.workArea;
|
|
|
|
// Only clamp if the target position would actually go out of bounds
|
|
let clampedX = targetX;
|
|
let clampedY = targetY;
|
|
|
|
// Check horizontal bounds
|
|
if (targetX < workAreaX) {
|
|
clampedX = workAreaX;
|
|
} else if (targetX + currentBounds.width > workAreaX + workAreaWidth) {
|
|
clampedX = workAreaX + workAreaWidth - currentBounds.width;
|
|
}
|
|
|
|
// Check vertical bounds
|
|
if (targetY < workAreaY) {
|
|
clampedY = workAreaY;
|
|
console.log(`[MovementManager] Clamped Y to top edge: ${clampedY}`);
|
|
} else if (targetY + currentBounds.height > workAreaY + workAreaHeight) {
|
|
clampedY = workAreaY + workAreaHeight - currentBounds.height;
|
|
console.log(`[MovementManager] Clamped Y to bottom edge: ${clampedY}`);
|
|
}
|
|
|
|
console.log(`[MovementManager] Final position: (${clampedX}, ${clampedY}), Work area: ${workAreaX},${workAreaY} ${workAreaWidth}x${workAreaHeight}`);
|
|
|
|
// Only move if there's an actual change in position
|
|
if (clampedX === this.headerPosition.x && clampedY === this.headerPosition.y) {
|
|
console.log(`[MovementManager] No position change, skipping animation`);
|
|
return;
|
|
}
|
|
|
|
this.animateToPosition(header, clampedX, clampedY);
|
|
}
|
|
|
|
animateToPosition(header, targetX, targetY, windowSize) {
|
|
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;
|
|
const { width, height } = windowSize || header.getBounds();
|
|
header.setBounds({
|
|
x: Math.round(currentX),
|
|
y: Math.round(currentY),
|
|
width,
|
|
height
|
|
});
|
|
|
|
if (progress < 1) {
|
|
this.animationFrameId = setTimeout(animate, 8);
|
|
} else {
|
|
this.animationFrameId = null;
|
|
this.isAnimating = false;
|
|
if (Number.isFinite(targetX) && Number.isFinite(targetY)) {
|
|
if (!this._isWindowValid(header)) return;
|
|
header.setPosition(Math.round(targetX), Math.round(targetY));
|
|
// Update header position to the actual final position
|
|
this.headerPosition = { x: Math.round(targetX), y: Math.round(targetY) };
|
|
}
|
|
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 currentBounds = header.getBounds();
|
|
|
|
const windowSize = {
|
|
width: currentBounds.width,
|
|
height: currentBounds.height
|
|
};
|
|
|
|
let targetX = currentBounds.x;
|
|
let targetY = currentBounds.y;
|
|
|
|
switch (direction) {
|
|
case 'left':
|
|
targetX = workAreaX;
|
|
break;
|
|
case 'right':
|
|
targetX = workAreaX + width - windowSize.width;
|
|
break;
|
|
case 'up':
|
|
targetY = workAreaY;
|
|
break;
|
|
case 'down':
|
|
targetY = workAreaY + height - windowSize.height;
|
|
break;
|
|
}
|
|
|
|
header.setBounds({
|
|
x: Math.round(targetX),
|
|
y: Math.round(targetY),
|
|
width: windowSize.width,
|
|
height: windowSize.height
|
|
});
|
|
|
|
this.headerPosition = { x: targetX, y: targetY };
|
|
this.updateLayout();
|
|
}
|
|
|
|
destroy() {
|
|
if (this.animationFrameId) {
|
|
clearTimeout(this.animationFrameId);
|
|
this.animationFrameId = null;
|
|
}
|
|
this.isAnimating = false;
|
|
console.log('[Movement] Manager destroyed');
|
|
}
|
|
}
|
|
|
|
module.exports = SmoothMovementManager;
|