feat(stage-tamagotchi): better transforming strategy for main-stage window
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { createContext } from '@moeru/eventa/adapters/electron/main'
|
||||
import type { BrowserWindow } from 'electron'
|
||||
import type { BrowserWindow, Rectangle } from 'electron'
|
||||
|
||||
import type { OnboardingWindowManager } from '../../../windows/onboarding'
|
||||
|
||||
@@ -12,26 +12,33 @@ import { computeAdjacentPosition } from '../../../windows/shared/display'
|
||||
|
||||
const ANIMATION_DURATION = 350
|
||||
|
||||
function animateWindowPosition(
|
||||
function animateWindowBounds(
|
||||
window: BrowserWindow,
|
||||
targetX: number,
|
||||
targetY: number,
|
||||
target: Rectangle,
|
||||
): ReturnType<typeof animate> | undefined {
|
||||
if (window.isDestroyed())
|
||||
return undefined
|
||||
|
||||
const bounds = window.getBounds()
|
||||
const state = { x: bounds.x, y: bounds.y }
|
||||
const current = window.getBounds()
|
||||
const state = { x: current.x, y: current.y, w: current.width, h: current.height }
|
||||
|
||||
return animate(state, {
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
w: target.width,
|
||||
h: target.height,
|
||||
duration: ANIMATION_DURATION,
|
||||
ease: 'outCubic',
|
||||
modifier: utils.round(0),
|
||||
onRender: () => {
|
||||
if (!window.isDestroyed())
|
||||
window.setPosition(Math.round(state.x), Math.round(state.y))
|
||||
if (!window.isDestroyed()) {
|
||||
window.setBounds({
|
||||
x: Math.round(state.x),
|
||||
y: Math.round(state.y),
|
||||
width: Math.round(state.w),
|
||||
height: Math.round(state.h),
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -51,14 +58,19 @@ export function createOnboardingService(params: {
|
||||
const onboardingBounds = onboardingWindow.getBounds()
|
||||
const display = screen.getDisplayMatching(onboardingBounds)
|
||||
|
||||
const newPosition = computeAdjacentPosition(
|
||||
const adjacent = computeAdjacentPosition(
|
||||
onboardingBounds,
|
||||
{ width: savedBounds.width, height: savedBounds.height },
|
||||
display.workArea,
|
||||
)
|
||||
|
||||
currentAnimation?.pause()
|
||||
currentAnimation = animateWindowPosition(params.mainWindow, newPosition.x, newPosition.y)
|
||||
currentAnimation = animateWindowBounds(params.mainWindow, {
|
||||
x: adjacent.x,
|
||||
y: adjacent.y,
|
||||
width: adjacent.width,
|
||||
height: adjacent.height,
|
||||
})
|
||||
|
||||
let userMovedManually = false
|
||||
let ignoreNextMoves = true
|
||||
@@ -70,6 +82,7 @@ export function createOnboardingService(params: {
|
||||
}
|
||||
|
||||
params.mainWindow.on('move', moveListener)
|
||||
params.mainWindow.on('resize', moveListener)
|
||||
setTimeout(() => {
|
||||
ignoreNextMoves = false
|
||||
}, ANIMATION_DURATION + 50)
|
||||
@@ -78,10 +91,11 @@ export function createOnboardingService(params: {
|
||||
|
||||
cleanupOnClosed = params.onboardingWindowManager.onClosed(() => {
|
||||
params.mainWindow.removeListener('move', moveListener)
|
||||
params.mainWindow.removeListener('resize', moveListener)
|
||||
|
||||
if (!userMovedManually && !params.mainWindow.isDestroyed()) {
|
||||
currentAnimation?.pause()
|
||||
currentAnimation = animateWindowPosition(params.mainWindow, savedBounds.x, savedBounds.y)
|
||||
currentAnimation = animateWindowBounds(params.mainWindow, savedBounds)
|
||||
}
|
||||
|
||||
cleanupOnClosed = undefined
|
||||
|
||||
@@ -141,39 +141,77 @@ export function widthFrom(bounds: Rectangle, sizeOptions: Size & { min?: Size, m
|
||||
return val
|
||||
}
|
||||
|
||||
export interface AdjacentPositionResult {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
scale: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a position for `target` adjacent to `anchor`, staying within `workArea`.
|
||||
*
|
||||
* Compares available space on right, left, and bottom of the anchor and picks the
|
||||
* side with the most room. Tie-breaking preference: right > left > bottom.
|
||||
*
|
||||
* If the target doesn't fit at full size on the best side, it is scaled down
|
||||
* (preserving aspect ratio) to fit, respecting `minScale`.
|
||||
*/
|
||||
export function computeAdjacentPosition(
|
||||
anchorBounds: Rectangle,
|
||||
targetSize: { width: number, height: number },
|
||||
workArea: Rectangle,
|
||||
offset = 16,
|
||||
): { x: number, y: number } {
|
||||
const centerY = anchorBounds.y + Math.floor((anchorBounds.height - targetSize.height) / 2)
|
||||
const clampY = (y: number) => Math.min(
|
||||
Math.max(y, workArea.y),
|
||||
workArea.y + workArea.height - targetSize.height,
|
||||
)
|
||||
options?: { margin?: number, minScale?: number },
|
||||
): AdjacentPositionResult {
|
||||
const margin = options?.margin ?? 16
|
||||
const minScale = options?.minScale ?? 0.5
|
||||
|
||||
// Try right
|
||||
const rightX = anchorBounds.x + anchorBounds.width + offset
|
||||
if (rightX + targetSize.width <= workArea.x + workArea.width) {
|
||||
return { x: rightX, y: clampY(centerY) }
|
||||
const waRight = workArea.x + workArea.width
|
||||
const waBottom = workArea.y + workArea.height
|
||||
|
||||
const rightSpace = { w: waRight - (anchorBounds.x + anchorBounds.width + margin), h: workArea.height }
|
||||
const leftSpace = { w: anchorBounds.x - workArea.x - margin, h: workArea.height }
|
||||
const bottomSpace = { w: workArea.width, h: waBottom - (anchorBounds.y + anchorBounds.height + margin) }
|
||||
|
||||
function maxScale(space: { w: number, h: number }): number {
|
||||
if (space.w <= 0 || space.h <= 0)
|
||||
return 0
|
||||
return Math.min(space.w / targetSize.width, space.h / targetSize.height, 1)
|
||||
}
|
||||
|
||||
// Try left
|
||||
const leftX = anchorBounds.x - targetSize.width - offset
|
||||
if (leftX >= workArea.x) {
|
||||
return { x: leftX, y: clampY(centerY) }
|
||||
}
|
||||
const candidates: { side: 'right' | 'left' | 'bottom', scale: number }[] = [
|
||||
{ side: 'right', scale: maxScale(rightSpace) },
|
||||
{ side: 'left', scale: maxScale(leftSpace) },
|
||||
{ side: 'bottom', scale: maxScale(bottomSpace) },
|
||||
]
|
||||
|
||||
// Fall back to below
|
||||
const belowY = anchorBounds.y + anchorBounds.height + offset
|
||||
const belowX = anchorBounds.x + Math.floor((anchorBounds.width - targetSize.width) / 2)
|
||||
return {
|
||||
x: Math.min(Math.max(belowX, workArea.x), workArea.x + workArea.width - targetSize.width),
|
||||
y: Math.min(belowY, workArea.y + workArea.height - targetSize.height),
|
||||
candidates.sort((a, b) => b.scale - a.scale)
|
||||
const best = candidates[0]!
|
||||
|
||||
const scale = Math.max(best.scale, minScale)
|
||||
const w = Math.round(targetSize.width * scale)
|
||||
const h = Math.round(targetSize.height * scale)
|
||||
|
||||
const clampX = (x: number) => Math.min(Math.max(x, workArea.x), waRight - w)
|
||||
const clampY = (y: number) => Math.min(Math.max(y, workArea.y), waBottom - h)
|
||||
|
||||
const centerY = anchorBounds.y + Math.floor((anchorBounds.height - h) / 2)
|
||||
|
||||
switch (best.side) {
|
||||
case 'right': {
|
||||
const x = anchorBounds.x + anchorBounds.width + margin
|
||||
return { x: clampX(x), y: clampY(centerY), width: w, height: h, scale }
|
||||
}
|
||||
case 'left': {
|
||||
const x = anchorBounds.x - w - margin
|
||||
return { x: clampX(x), y: clampY(centerY), width: w, height: h, scale }
|
||||
}
|
||||
case 'bottom': {
|
||||
const y = anchorBounds.y + anchorBounds.height + margin
|
||||
const x = anchorBounds.x + Math.floor((anchorBounds.width - w) / 2)
|
||||
return { x: clampX(x), y: clampY(y), width: w, height: h, scale }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user