fix(stage-tamagotchi): anchor tray resize to dominant display (#2012)

---

Co-authored-by-agent: Codex
This commit is contained in:
Neko
2026-06-27 01:54:39 +08:00
committed by GitHub
parent 22d0711dc0
commit 1065ed565b
3 changed files with 186 additions and 16 deletions
+17 -15
View File
@@ -22,6 +22,7 @@ import macOSTrayIcon from '../../../resources/tray-icon-macos.png?asset'
import { onAppBeforeQuit } from '../libs/bootkit/lifecycle'
import { setupInlayWindow } from '../windows/inlay'
import { computeResizedBoundsAnchoredToDominantDisplay, findDominantDisplayArea } from '../windows/shared/display'
import { toggleWindowShow } from '../windows/shared/window'
const RECOMMENDED_WIDTH = 450
@@ -35,21 +36,20 @@ function applyWindowSize(window: BrowserWindow, width: number, height: number, x
window.setResizable(true)
const bounds = {
width: Math.round(width),
height: Math.round(height),
} as Electron.Rectangle
if (x !== undefined && y !== undefined) {
bounds.x = Math.round(x)
bounds.y = Math.round(y)
}
const bounds = x !== undefined && y !== undefined
? {
x: Math.round(x),
y: Math.round(y),
width: Math.round(width),
height: Math.round(height),
}
: computeResizedBoundsAnchoredToDominantDisplay({
currentBounds: window.getBounds(),
targetSize: { width, height },
displays: screen.getAllDisplays(),
})
window.setBounds(bounds)
if (x === undefined || y === undefined) {
window.center()
}
window.show()
}
@@ -109,8 +109,10 @@ export function setupTray(params: {
return
}
const { x: areaX, y: areaY, width: areaWidth, height: areaHeight } = screen.getPrimaryDisplay().workArea
const { width: windowWidth, height: windowHeight } = params.mainWindow.getBounds()
const mainWindowBounds = params.mainWindow.getBounds()
const currentDisplay = findDominantDisplayArea(mainWindowBounds, screen.getAllDisplays()) ?? screen.getDisplayMatching(mainWindowBounds)
const { x: areaX, y: areaY, width: areaWidth, height: areaHeight } = currentDisplay.workArea
const { width: windowWidth, height: windowHeight } = mainWindowBounds
const fullHeightTarget = areaHeight
const fullWidthTarget = Math.floor(areaHeight * ASPECT_RATIO)
@@ -2,7 +2,7 @@ import type { Rectangle } from 'electron'
import { describe, expect, it, vi } from 'vitest'
import { heightFrom, mapForBreakpoints, widthFrom } from './display'
import { computeResizedBoundsAnchoredToDominantDisplay, heightFrom, mapForBreakpoints, widthFrom } from './display'
// NOTICE:
// Mocking 'electron' is needed to prevent Vitest from attempting to resolve/load the real Electron binary during tests.
@@ -78,3 +78,57 @@ describe('heightFrom', () => {
expect(heightFrom({ height: 1000 } as Rectangle, { actual: 350, max: 400 })).toBe(350)
})
})
describe('computeResizedBoundsAnchoredToDominantDisplay', () => {
const primaryDisplay = {
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
workArea: { x: 0, y: 25, width: 1920, height: 1055 },
}
const secondaryDisplay = {
bounds: { x: 1920, y: 0, width: 1920, height: 1080 },
workArea: { x: 1920, y: 0, width: 1920, height: 1040 },
}
const topDisplay = {
bounds: { x: 0, y: -900, width: 1600, height: 900 },
workArea: { x: 0, y: -900, width: 1600, height: 860 },
}
it('uses the display with the largest overlap when resizing a window across two displays', () => {
const bounds = computeResizedBoundsAnchoredToDominantDisplay({
currentBounds: { x: 1700, y: 220, width: 500, height: 600 },
targetSize: { width: 450, height: 600 },
displays: [primaryDisplay, secondaryDisplay],
})
expect(bounds.x).toBe(1920)
expect(bounds.y).toBe(220)
expect(bounds.width).toBe(450)
expect(bounds.height).toBe(600)
})
it('uses the display with the largest overlap across three displays', () => {
const bounds = computeResizedBoundsAnchoredToDominantDisplay({
currentBounds: { x: 1100, y: -700, width: 380, height: 620 },
targetSize: { width: 450, height: 600 },
displays: [primaryDisplay, secondaryDisplay, topDisplay],
})
expect(bounds.x).toBe(1030)
expect(bounds.y).toBe(-680)
expect(bounds.width).toBe(450)
expect(bounds.height).toBe(600)
})
it('keeps the matching display bottom-right corner anchored when resizing in the bottom-right quadrant', () => {
const bounds = computeResizedBoundsAnchoredToDominantDisplay({
currentBounds: { x: 3420, y: 740, width: 300, height: 250 },
targetSize: { width: 450, height: 600 },
displays: [primaryDisplay, secondaryDisplay],
})
expect(bounds.x).toBe(3270)
expect(bounds.y).toBe(390)
expect(bounds.width).toBe(450)
expect(bounds.height).toBe(600)
})
})
@@ -9,6 +9,120 @@ export function currentDisplayBounds(window: BrowserWindow) {
return nearbyDisplay.bounds
}
export interface ResizableDisplayArea {
/** Full display bounds used to decide which physical display owns most of a window. */
bounds: Rectangle
/** Usable display area used for quadrant anchoring and final window clamping. */
workArea: Rectangle
}
export interface DominantDisplayResizeOptions {
/** Current window bounds in Electron display coordinates. */
currentBounds: Rectangle
/** Desired size before display work-area clamping. */
targetSize: Pick<Rectangle, 'width' | 'height'>
/** Displays from Electron screen APIs. */
displays: readonly ResizableDisplayArea[]
}
/**
* Computes resize bounds from the display that owns most of the current window.
*/
export function computeResizedBoundsAnchoredToDominantDisplay(options: DominantDisplayResizeOptions): Rectangle {
const targetWidth = Math.round(options.targetSize.width)
const targetHeight = Math.round(options.targetSize.height)
const display = findDominantDisplayArea(options.currentBounds, options.displays)
if (!display) {
return {
...options.currentBounds,
width: targetWidth,
height: targetHeight,
}
}
const workArea = display.workArea
// Target sizes may come from a larger display preset. Clamp them before
// deriving anchors so the right/bottom edge math never asks for coordinates
// outside the selected display's usable area.
const width = Math.min(targetWidth, workArea.width)
const height = Math.min(targetHeight, workArea.height)
const workAreaRight = workArea.x + workArea.width
const workAreaBottom = workArea.y + workArea.height
const currentRight = options.currentBounds.x + options.currentBounds.width
const currentBottom = options.currentBounds.y + options.currentBounds.height
// The quadrant is based on the current window center, not the top-left
// corner, so a window crossing displays behaves according to where most of
// the visible window lives inside the selected work area.
const currentCenterX = options.currentBounds.x + options.currentBounds.width / 2
const currentCenterY = options.currentBounds.y + options.currentBounds.height / 2
const workAreaCenterX = workArea.x + workArea.width / 2
const workAreaCenterY = workArea.y + workArea.height / 2
// Left/top quadrants keep the original x/y. Right/bottom quadrants keep the
// opposite edge visually fixed by subtracting the new size from the current
// right/bottom edge.
const x = currentCenterX > workAreaCenterX
? currentRight - width
: options.currentBounds.x
const y = currentCenterY > workAreaCenterY
? currentBottom - height
: options.currentBounds.y
// The anchor can still land just outside the work area when the previous
// window crossed a screen boundary. Clamp after anchoring so resize intent
// wins first, then display safety.
return {
x: Math.round(clamp(x, workArea.x, workAreaRight - width)),
y: Math.round(clamp(y, workArea.y, workAreaBottom - height)),
width,
height,
}
}
/**
* Finds the display that owns the largest visible share of `bounds`.
*/
export function findDominantDisplayArea(bounds: Rectangle, displays: readonly ResizableDisplayArea[]): ResizableDisplayArea | undefined {
let dominantDisplay: ResizableDisplayArea | undefined
let dominantArea = -1
for (const display of displays) {
// Use full display bounds, not workArea. Menu bars and docks shrink
// workArea, but they should not change which physical display owns a
// cross-screen window.
const area = intersectionArea(bounds, display.bounds)
if (area > dominantArea) {
dominantDisplay = display
dominantArea = area
}
}
return dominantDisplay
}
function intersectionArea(a: Rectangle, b: Rectangle): number {
// Each side of the overlap rectangle is the inner edge from the two source
// rectangles. If the right edge crosses the left edge, or bottom crosses top,
// the rectangles do not overlap.
const left = Math.max(a.x, b.x)
const top = Math.max(a.y, b.y)
const right = Math.min(a.x + a.width, b.x + b.width)
const bottom = Math.min(a.y + a.height, b.y + b.height)
if (right <= left || bottom <= top) {
return 0
}
return (right - left) * (bottom - top)
}
function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max)
}
interface SizeActual { actual: number }
interface SizePercentage { percentage: number }
type Size = SizeActual | SizePercentage | number