From 702b4c862d7eb6ca91a1053c4a0b00df04788fe5 Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Wed, 19 Aug 2026 23:30:24 +0800 Subject: [PATCH] feat(stage-tamagotchi): adaptive controls island --- apps/stage-tamagotchi/src/main/tray/index.ts | 3 +- .../src/main/windows/shared/display.ts | 50 +--- .../control-button-tooltip.vue | 26 +- .../controls-island-profile-picker.vue | 19 +- .../controls-island-root.test.ts | 261 ++++++++++++++++++ .../controls-island/controls-island-root.vue | 135 +++++++++ .../controls-island-stop-speaking.test.ts | 13 +- .../controls-island-stop-speaking.vue | 2 +- .../stage-islands/controls-island/index.vue | 104 +++++-- .../use-controls-island-placement.ts | 99 +++++++ .../stage-islands/status-island/index.vue | 2 +- .../src/renderer/pages/index.vue | 12 +- .../src/shared/utils/electron/display.ts | 42 +++ docs/ai/context/ui-components.md | 2 + .../misc/profile-switcher-popover.vue | 26 +- .../ui/src/components/form/select/select.vue | 10 +- 16 files changed, 719 insertions(+), 87 deletions(-) create mode 100644 apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/controls-island-root.test.ts create mode 100644 apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/controls-island-root.vue create mode 100644 apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/use-controls-island-placement.ts create mode 100644 apps/stage-tamagotchi/src/shared/utils/electron/display.ts diff --git a/apps/stage-tamagotchi/src/main/tray/index.ts b/apps/stage-tamagotchi/src/main/tray/index.ts index 61bda9759..e8ada28d5 100644 --- a/apps/stage-tamagotchi/src/main/tray/index.ts +++ b/apps/stage-tamagotchi/src/main/tray/index.ts @@ -20,10 +20,11 @@ import { isMacOS } from 'std-env' import icon from '../../../resources/icon.png?asset' import macOSTrayIcon from '../../../resources/tray-icon-macos.png?asset' +import { findDominantDisplayArea } from '../../shared/utils/electron/display' import { onAppBeforeQuit } from '../libs/bootkit/lifecycle' import { setupInlayWindow } from '../windows/inlay' import { Animator } from '../windows/shared/animator' -import { computeResizedBoundsAnchoredToDominantDisplay, findDominantDisplayArea } from '../windows/shared/display' +import { computeResizedBoundsAnchoredToDominantDisplay } from '../windows/shared/display' import { toggleWindowShow } from '../windows/shared/window' const RECOMMENDED_WIDTH = 450 diff --git a/apps/stage-tamagotchi/src/main/windows/shared/display.ts b/apps/stage-tamagotchi/src/main/windows/shared/display.ts index 4dbf6f4e8..85d1ef83c 100644 --- a/apps/stage-tamagotchi/src/main/windows/shared/display.ts +++ b/apps/stage-tamagotchi/src/main/windows/shared/display.ts @@ -1,7 +1,11 @@ import type { BrowserWindow, Rectangle } from 'electron' +import type { DisplayArea } from '../../../shared/utils/electron/display' + import { screen } from 'electron' +import { findDominantDisplayArea } from '../../../shared/utils/electron/display' + export function currentDisplayBounds(window: BrowserWindow) { const bounds = window.getBounds() const nearbyDisplay = screen.getDisplayMatching(bounds) @@ -65,20 +69,13 @@ export function centerWindowOnDisplay(window: Pick /** Displays from Electron screen APIs. */ - displays: readonly ResizableDisplayArea[] + displays: readonly DisplayArea[] } /** @@ -138,43 +135,6 @@ export function computeResizedBoundsAnchoredToDominantDisplay(options: DominantD } } -/** - * 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) } diff --git a/apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/control-button-tooltip.vue b/apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/control-button-tooltip.vue index a8e9ab64f..3ffd402ef 100644 --- a/apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/control-button-tooltip.vue +++ b/apps/stage-tamagotchi/src/renderer/components/stage-islands/controls-island/control-button-tooltip.vue @@ -1,9 +1,25 @@