diff --git a/apps/stage-tamagotchi/src/main/services/airi/onboarding/index.ts b/apps/stage-tamagotchi/src/main/services/airi/onboarding/index.ts index d398b2681..b35688680 100644 --- a/apps/stage-tamagotchi/src/main/services/airi/onboarding/index.ts +++ b/apps/stage-tamagotchi/src/main/services/airi/onboarding/index.ts @@ -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 | 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 diff --git a/apps/stage-tamagotchi/src/main/windows/shared/display.ts b/apps/stage-tamagotchi/src/main/windows/shared/display.ts index e90a3cfb2..9501ae70d 100644 --- a/apps/stage-tamagotchi/src/main/windows/shared/display.ts +++ b/apps/stage-tamagotchi/src/main/windows/shared/display.ts @@ -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 } + } } }