glass/src/electron/smoothMovementManager.js

312 lines
13 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) {
const header = this.windowPool.get('header');
if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
const currentBounds = header.getBounds();
const display = this.getCurrentDisplay(header);
if (
!currentBounds || typeof currentBounds.x !== 'number' || typeof currentBounds.y !== 'number' ||
!display || !display.workArea || !display.workAreaSize ||
typeof display.workArea.x !== 'number' || typeof display.workArea.y !== 'number' ||
typeof display.workAreaSize.width !== 'number' || typeof display.workAreaSize.height !== 'number'
) {
console.error('[MovementManager] Invalid bounds or display info for hideToEdge. Aborting.');
return;
}
this.lastVisiblePosition = { x: currentBounds.x, y: currentBounds.y };
this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
const { width: screenWidth, height: screenHeight } = display.workAreaSize;
const { x: workAreaX, y: workAreaY } = display.workArea;
let targetX = this.headerPosition.x;
let targetY = this.headerPosition.y;
switch (edge) {
case 'top': targetY = workAreaY - currentBounds.height - 20; break;
case 'bottom': targetY = workAreaY + screenHeight + 20; break;
case 'left': targetX = workAreaX - currentBounds.width - 20; break;
case 'right': targetX = workAreaX + screenWidth + 20; break;
}
this.hiddenPosition = { x: targetX, y: targetY, edge };
this.isAnimating = true;
const startX = this.headerPosition.x;
const startY = this.headerPosition.y;
const duration = 400;
const startTime = Date.now();
const animate = () => {
if (!this._isWindowValid(header)) return;
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = progress * progress * progress;
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;
if (typeof callback === 'function') callback();
}
};
animate();
}
showFromEdge(callback) {
const header = this.windowPool.get('header');
if (
!this._isWindowValid(header) || this.isAnimating ||
!this.hiddenPosition || !this.lastVisiblePosition ||
typeof this.hiddenPosition.x !== 'number' || typeof this.hiddenPosition.y !== 'number' ||
typeof this.lastVisiblePosition.x !== 'number' || typeof this.lastVisiblePosition.y !== 'number'
) {
console.error('[MovementManager] Invalid state for showFromEdge. Aborting.');
this.isAnimating = false;
this.hiddenPosition = null;
this.lastVisiblePosition = null;
return;
}
if (!this._isWindowValid(header)) return;
header.setPosition(this.hiddenPosition.x, this.hiddenPosition.y);
this.headerPosition = { x: this.hiddenPosition.x, y: this.hiddenPosition.y };
const targetX = this.lastVisiblePosition.x;
const targetY = this.lastVisiblePosition.y;
this.isAnimating = true;
const startX = this.headerPosition.x;
const startY = this.headerPosition.y;
const duration = 500;
const startTime = Date.now();
const animate = () => {
if (!this._isWindowValid(header)) return;
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const c1 = 1.70158;
const c3 = c1 + 1;
const eased = 1 + c3 * Math.pow(progress - 1, 3) + c1 * Math.pow(progress - 1, 2);
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.hiddenPosition = null;
this.lastVisiblePosition = null;
if (callback) callback();
}
};
animate();
}
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;