mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-12 13:20:53 +02:00
improve settings popout on main window
This commit is contained in:
+132
-12
@@ -25,6 +25,11 @@ import { RESIZE_DIRECTIONS, type ResizeDirection } from '../types/windowResize'
|
||||
import type { DialogOptions, DialogResult } from '../types/dialog'
|
||||
import { normalizeProfile } from '../shared/profileState'
|
||||
import { resolveNativeThemeSource } from '../shared/themeState'
|
||||
import {
|
||||
clampDraggedMainWindowBounds,
|
||||
raiseWindowAboveNormalPopouts,
|
||||
resolveExpandedMainWindowBounds,
|
||||
} from '../shared/windowGeometry'
|
||||
import { calculateResizedWindowBounds } from '../shared/windowResize'
|
||||
import { FileBackedProfileLibrary } from './profileLibrary'
|
||||
import { AstraIntegrationService } from './services/astraIntegration'
|
||||
@@ -34,7 +39,7 @@ import { FileBackedWindowStateStore } from './windowStateStore'
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
let moveInterval: ReturnType<typeof setInterval> | null = null
|
||||
let moveStartCursor: { x: number; y: number } | null = null
|
||||
let moveStartPosition: number[] | null = null
|
||||
let moveStartBounds: WindowBounds | null = null
|
||||
let resizeInterval: ReturnType<typeof setInterval> | null = null
|
||||
let resizeWindow: BrowserWindow | null = null
|
||||
let resizeStartCursor: { x: number; y: number } | null = null
|
||||
@@ -44,7 +49,8 @@ let mainWindowBoundsTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let mainRendererReady = false
|
||||
let allowMainWindowClose = false
|
||||
let mainWindowClosePending = false
|
||||
let suppressNextMainWindowBoundsEvent = false
|
||||
let suppressMainWindowSyncUntil = 0
|
||||
let mainWindowLogicalBounds: WindowBounds | null = null
|
||||
|
||||
const scopePopoutWindows = new Map<ScopeKind, BrowserWindow>()
|
||||
const scopePopoutCloseAllowed = new Set<ScopeKind>()
|
||||
@@ -74,6 +80,9 @@ const POPOUT_DEFAULTS = {
|
||||
minHeight: 160,
|
||||
}
|
||||
|
||||
const MAIN_WINDOW_SYNC_SUPPRESSION_MS = 180
|
||||
const MAIN_WINDOW_VISIBLE_GRAB_MARGIN = 64
|
||||
|
||||
function getProfileLibrary(): FileBackedProfileLibrary {
|
||||
if (!profileLibrary) {
|
||||
profileLibrary = new FileBackedProfileLibrary(
|
||||
@@ -176,6 +185,7 @@ function focusMainWindow(): void {
|
||||
}
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
raiseMainWindowAboveNormalPopouts()
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string): string {
|
||||
@@ -244,8 +254,7 @@ function scheduleMainWindowBoundsSave(window: BrowserWindow): void {
|
||||
mainWindowBoundsTimer = setTimeout(() => {
|
||||
mainWindowBoundsTimer = null
|
||||
if (window.isDestroyed() || window.webContents.isDestroyed()) return
|
||||
if (suppressNextMainWindowBoundsEvent) {
|
||||
suppressNextMainWindowBoundsEvent = false
|
||||
if (isMainWindowSyncSuppressed()) {
|
||||
return
|
||||
}
|
||||
window.webContents.send('window:bounds-changed', toLogicalBounds(window))
|
||||
@@ -293,6 +302,32 @@ function getBaseMinHeight(window: BrowserWindow): number {
|
||||
return isMainRendererWindow(window) ? WINDOW_DEFAULTS.minHeight : POPOUT_DEFAULTS.minHeight
|
||||
}
|
||||
|
||||
function normalizeMainWindowBounds(bounds: WindowBounds): WindowBounds {
|
||||
return {
|
||||
x: Math.round(bounds.x),
|
||||
y: Math.round(bounds.y),
|
||||
width: Math.max(WINDOW_DEFAULTS.minWidth, Math.round(bounds.width)),
|
||||
height: Math.max(WINDOW_DEFAULTS.minHeight, Math.round(bounds.height)),
|
||||
}
|
||||
}
|
||||
|
||||
function getDisplayWorkAreas(): WindowBounds[] {
|
||||
return screen.getAllDisplays().map((display) => ({
|
||||
x: display.workArea.x,
|
||||
y: display.workArea.y,
|
||||
width: display.workArea.width,
|
||||
height: display.workArea.height,
|
||||
}))
|
||||
}
|
||||
|
||||
function suppressMainWindowSync(durationMs = MAIN_WINDOW_SYNC_SUPPRESSION_MS): void {
|
||||
suppressMainWindowSyncUntil = Math.max(suppressMainWindowSyncUntil, Date.now() + durationMs)
|
||||
}
|
||||
|
||||
function isMainWindowSyncSuppressed(): boolean {
|
||||
return suppressMainWindowSyncUntil > Date.now()
|
||||
}
|
||||
|
||||
function getSettingsHeight(window: BrowserWindow | null): number {
|
||||
if (!window) return 0
|
||||
return windowSettingsHeights.get(window.id) ?? 0
|
||||
@@ -310,13 +345,42 @@ function setSettingsHeightForWindow(window: BrowserWindow, height: number): void
|
||||
}
|
||||
|
||||
function toLogicalBounds(window: BrowserWindow, bounds = window.getBounds()): WindowBounds {
|
||||
if (isMainRendererWindow(window) && mainWindowLogicalBounds) {
|
||||
return { ...mainWindowLogicalBounds }
|
||||
}
|
||||
|
||||
return {
|
||||
...bounds,
|
||||
height: Math.max(getBaseMinHeight(window), bounds.height - getSettingsHeight(window)),
|
||||
}
|
||||
}
|
||||
|
||||
function syncMainWindowLogicalBounds(window: BrowserWindow, bounds = window.getBounds()): void {
|
||||
if (!isMainRendererWindow(window)) {
|
||||
return
|
||||
}
|
||||
|
||||
mainWindowLogicalBounds = normalizeMainWindowBounds({
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
width: bounds.width,
|
||||
height: Math.max(WINDOW_DEFAULTS.minHeight, bounds.height - getSettingsHeight(window)),
|
||||
})
|
||||
}
|
||||
|
||||
function applyMainWindowLogicalBounds(window: BrowserWindow, bounds: WindowBounds): void {
|
||||
const logicalBounds = normalizeMainWindowBounds(bounds)
|
||||
mainWindowLogicalBounds = logicalBounds
|
||||
suppressMainWindowSync()
|
||||
window.setBounds(resolveExpandedMainWindowBounds(logicalBounds, getSettingsHeight(window), getDisplayWorkAreas()))
|
||||
}
|
||||
|
||||
function applyLogicalBounds(window: BrowserWindow, bounds: WindowBounds): void {
|
||||
if (isMainRendererWindow(window)) {
|
||||
applyMainWindowLogicalBounds(window, bounds)
|
||||
return
|
||||
}
|
||||
|
||||
window.setBounds({
|
||||
...bounds,
|
||||
height: bounds.height + getSettingsHeight(window),
|
||||
@@ -335,12 +399,28 @@ function setWindowHeight(window: BrowserWindow, bounds: WindowBounds, height: nu
|
||||
function applySettingsHeight(window: BrowserWindow, rawNextHeight: number): void {
|
||||
const currentSettingsHeight = getSettingsHeight(window)
|
||||
const nextSettingsHeight = Math.max(0, Math.round(rawNextHeight))
|
||||
const delta = nextSettingsHeight - currentSettingsHeight
|
||||
const baseMinHeight = getBaseMinHeight(window)
|
||||
const [minW] = window.getMinimumSize()
|
||||
|
||||
window.setMinimumSize(minW, baseMinHeight + nextSettingsHeight)
|
||||
|
||||
if (currentSettingsHeight === nextSettingsHeight) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isMainRendererWindow(window)) {
|
||||
if (!mainWindowLogicalBounds) {
|
||||
syncMainWindowLogicalBounds(window)
|
||||
}
|
||||
setSettingsHeightForWindow(window, nextSettingsHeight)
|
||||
applyMainWindowLogicalBounds(window, toLogicalBounds(window))
|
||||
if (currentSettingsHeight === 0 && nextSettingsHeight > 0) {
|
||||
raiseMainWindowAboveNormalPopouts()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const delta = nextSettingsHeight - currentSettingsHeight
|
||||
if (delta !== 0) {
|
||||
const bounds = window.getBounds()
|
||||
const newHeight = Math.max(baseMinHeight + nextSettingsHeight, bounds.height + delta)
|
||||
@@ -382,6 +462,10 @@ function applySettingsHeight(window: BrowserWindow, rawNextHeight: number): void
|
||||
setSettingsHeightForWindow(window, nextSettingsHeight)
|
||||
}
|
||||
|
||||
function raiseMainWindowAboveNormalPopouts(): void {
|
||||
raiseWindowAboveNormalPopouts(mainWindow, scopePopoutWindows.values())
|
||||
}
|
||||
|
||||
function sendToRenderer(sender: WebContents, channel: string, ...args: unknown[]): void {
|
||||
if (!sender.isDestroyed()) {
|
||||
sender.send(channel, ...args)
|
||||
@@ -398,7 +482,7 @@ function stopWindowMoveController(): void {
|
||||
moveInterval = null
|
||||
}
|
||||
moveStartCursor = null
|
||||
moveStartPosition = null
|
||||
moveStartBounds = null
|
||||
}
|
||||
|
||||
function stopWindowResizeController(): void {
|
||||
@@ -651,6 +735,7 @@ function createMainWindow(): void {
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
})
|
||||
syncMainWindowLogicalBounds(mainWindow)
|
||||
|
||||
mainWindow.on('close', (event) => {
|
||||
if (allowMainWindowClose || !mainRendererReady || mainWindow?.webContents.isDestroyed()) {
|
||||
@@ -681,6 +766,8 @@ function createMainWindow(): void {
|
||||
windowSettingsHeights.delete(mainWindow.id)
|
||||
windowSettingsBottomAnchors.delete(mainWindow.id)
|
||||
}
|
||||
mainWindowLogicalBounds = null
|
||||
suppressMainWindowSyncUntil = 0
|
||||
mainRendererReady = false
|
||||
allowMainWindowClose = false
|
||||
mainWindowClosePending = false
|
||||
@@ -693,12 +780,21 @@ function createMainWindow(): void {
|
||||
|
||||
mainWindow.on('move', () => {
|
||||
if (!mainWindow) return
|
||||
if (!isMainWindowSyncSuppressed()) {
|
||||
syncMainWindowLogicalBounds(mainWindow)
|
||||
}
|
||||
scheduleMainWindowBoundsSave(mainWindow)
|
||||
})
|
||||
mainWindow.on('resize', () => {
|
||||
if (!mainWindow) return
|
||||
if (!isMainWindowSyncSuppressed()) {
|
||||
syncMainWindowLogicalBounds(mainWindow)
|
||||
}
|
||||
scheduleMainWindowBoundsSave(mainWindow)
|
||||
})
|
||||
mainWindow.on('focus', () => {
|
||||
raiseMainWindowAboveNormalPopouts()
|
||||
})
|
||||
|
||||
loadRendererTarget(mainWindow, { window: 'main' })
|
||||
}
|
||||
@@ -799,6 +895,7 @@ function createScopePopoutWindow(kind: ScopeKind, rawBounds?: WindowBounds): Bro
|
||||
popoutWindow.once('ready-to-show', () => {
|
||||
if (!popoutWindow.isDestroyed()) {
|
||||
popoutWindow.show()
|
||||
raiseMainWindowAboveNormalPopouts()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -924,14 +1021,26 @@ function setupIPC(): void {
|
||||
stopWindowMoveController()
|
||||
const cursor = screen.getCursorScreenPoint()
|
||||
moveStartCursor = { x: cursor.x, y: cursor.y }
|
||||
moveStartPosition = targetWindow.getPosition()
|
||||
moveStartBounds = targetWindow.getBounds()
|
||||
|
||||
moveInterval = setInterval(() => {
|
||||
if (!targetWindow || targetWindow.isDestroyed() || !moveStartCursor || !moveStartPosition) return
|
||||
if (!targetWindow || targetWindow.isDestroyed() || !moveStartCursor || !moveStartBounds) return
|
||||
const current = screen.getCursorScreenPoint()
|
||||
const dx = current.x - moveStartCursor.x
|
||||
const dy = current.y - moveStartCursor.y
|
||||
targetWindow.setPosition(moveStartPosition[0] + dx, moveStartPosition[1] + dy)
|
||||
|
||||
if (isMainRendererWindow(targetWindow)) {
|
||||
const nextBounds = clampDraggedMainWindowBounds({
|
||||
x: moveStartBounds.x + dx,
|
||||
y: moveStartBounds.y + dy,
|
||||
width: moveStartBounds.width,
|
||||
height: moveStartBounds.height,
|
||||
}, getDisplayWorkAreas(), MAIN_WINDOW_VISIBLE_GRAB_MARGIN)
|
||||
targetWindow.setPosition(nextBounds.x, nextBounds.y)
|
||||
return
|
||||
}
|
||||
|
||||
targetWindow.setPosition(moveStartBounds.x + dx, moveStartBounds.y + dy)
|
||||
}, 16)
|
||||
})
|
||||
|
||||
@@ -1212,9 +1321,6 @@ function setupIPC(): void {
|
||||
const targetWindow = getWindowFromSender(event.sender)
|
||||
if (!targetWindow) return
|
||||
|
||||
if (isMainRendererWindow(targetWindow)) {
|
||||
suppressNextMainWindowBoundsEvent = true
|
||||
}
|
||||
applyLogicalBounds(targetWindow, bounds)
|
||||
})
|
||||
|
||||
@@ -1231,6 +1337,20 @@ function setupIPC(): void {
|
||||
|
||||
const display = screen.getDisplayMatching(targetWindow.getBounds())
|
||||
const workArea = display.workArea
|
||||
|
||||
if (isMainRendererWindow(targetWindow)) {
|
||||
const logicalBounds = toLogicalBounds(targetWindow)
|
||||
applyLogicalBounds(targetWindow, {
|
||||
x: workArea.x,
|
||||
y: position === 'top'
|
||||
? workArea.y
|
||||
: workArea.y + workArea.height - logicalBounds.height,
|
||||
width: workArea.width,
|
||||
height: logicalBounds.height,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const [, height] = targetWindow.getSize()
|
||||
|
||||
if (position === 'top') {
|
||||
|
||||
+15
-13
@@ -6,14 +6,13 @@ import BottomBar from './components/BottomBar'
|
||||
import ScopePopoutBridge from './components/ScopePopoutBridge'
|
||||
import WindowResizeOverlay from './components/WindowResizeOverlay'
|
||||
import AppBanner from './components/AppBanner'
|
||||
import { resolveMainWindowSettingsHeight } from './mainWindowSettings'
|
||||
import { useSettingsStore } from './stores/settingsStore'
|
||||
import { useAstraStore } from './stores/astraStore'
|
||||
import { useAudioStore } from './stores/audioStore'
|
||||
import { useThemeStore } from './stores/themeStore'
|
||||
import { useUiStore } from './stores/uiStore'
|
||||
|
||||
const DEFAULT_SETTINGS_HEIGHT = 400
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
const [toolbarVisible, setToolbarVisible] = useState(false)
|
||||
const [settingsPanelHeight, setSettingsPanelHeight] = useState(0)
|
||||
@@ -136,11 +135,12 @@ export default function App(): JSX.Element {
|
||||
updateMainWindowBounds,
|
||||
])
|
||||
|
||||
const measuredSettingsHeight = settingsPanelHeight > 0 && bottomBarHeight > 0
|
||||
? settingsPanelHeight + bottomBarHeight
|
||||
: DEFAULT_SETTINGS_HEIGHT
|
||||
|
||||
const settingsHeight = settingsOpen ? measuredSettingsHeight : 0
|
||||
const settingsHeight = resolveMainWindowSettingsHeight(
|
||||
settingsOpen,
|
||||
settingsPanelHeight,
|
||||
bottomBarHeight,
|
||||
)
|
||||
const settingsVisible = settingsOpen && settingsHeight > 0
|
||||
|
||||
useLayoutEffect(() => {
|
||||
window.electronAPI.setSettingsHeight(settingsHeight)
|
||||
@@ -211,12 +211,14 @@ export default function App(): JSX.Element {
|
||||
|
||||
<AppBanner />
|
||||
|
||||
{settingsOpen && (
|
||||
<div className="prism-settings-region" style={{ height: settingsHeight }}>
|
||||
<SettingsPanel onHeightChange={setSettingsPanelHeight} />
|
||||
<BottomBar onClose={handleCloseSettings} onHeightChange={setBottomBarHeight} />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`prism-settings-region ${settingsVisible ? '' : 'is-hidden'}`.trim()}
|
||||
style={{ height: settingsHeight }}
|
||||
aria-hidden={!settingsVisible}
|
||||
>
|
||||
<SettingsPanel onHeightChange={setSettingsPanelHeight} />
|
||||
<BottomBar onClose={handleCloseSettings} onHeightChange={setBottomBarHeight} />
|
||||
</div>
|
||||
|
||||
<WindowResizeOverlay />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export function resolveMainWindowSettingsHeight(
|
||||
settingsOpen: boolean,
|
||||
settingsPanelHeight: number,
|
||||
bottomBarHeight: number,
|
||||
): number {
|
||||
if (!settingsOpen) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (settingsPanelHeight <= 0 || bottomBarHeight <= 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return Math.ceil(settingsPanelHeight) + Math.ceil(bottomBarHeight)
|
||||
}
|
||||
@@ -207,6 +207,13 @@ select {
|
||||
box-shadow: 0 -16px 34px rgba(0, 0, 0, 0.34);
|
||||
}
|
||||
|
||||
.prism-settings-region.is-hidden {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
border-top-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.app-banner-layer {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import type { WindowBounds } from '../types/popout'
|
||||
|
||||
export interface WorkAreaBounds {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface StackableWindowLike {
|
||||
isDestroyed(): boolean
|
||||
isAlwaysOnTop(): boolean
|
||||
moveTop(): void
|
||||
}
|
||||
|
||||
function getRight(bounds: WorkAreaBounds): number {
|
||||
return bounds.x + bounds.width
|
||||
}
|
||||
|
||||
function getBottom(bounds: WorkAreaBounds): number {
|
||||
return bounds.y + bounds.height
|
||||
}
|
||||
|
||||
function rangesOverlap(startA: number, endA: number, startB: number, endB: number): boolean {
|
||||
return startA < endB && startB < endA
|
||||
}
|
||||
|
||||
function rectsIntersect(a: WorkAreaBounds, b: WorkAreaBounds): boolean {
|
||||
return rangesOverlap(a.x, getRight(a), b.x, getRight(b))
|
||||
&& rangesOverlap(a.y, getBottom(a), b.y, getBottom(b))
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
if (min > max) {
|
||||
return Math.round((min + max) / 2)
|
||||
}
|
||||
|
||||
return Math.min(max, Math.max(min, value))
|
||||
}
|
||||
|
||||
function unionBounds(boundsList: readonly WorkAreaBounds[]): WorkAreaBounds {
|
||||
const left = Math.min(...boundsList.map((bounds) => bounds.x))
|
||||
const top = Math.min(...boundsList.map((bounds) => bounds.y))
|
||||
const right = Math.max(...boundsList.map((bounds) => getRight(bounds)))
|
||||
const bottom = Math.max(...boundsList.map((bounds) => getBottom(bounds)))
|
||||
|
||||
return {
|
||||
x: left,
|
||||
y: top,
|
||||
width: right - left,
|
||||
height: bottom - top,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWorkAreas(workAreas: readonly WorkAreaBounds[]): WorkAreaBounds[] {
|
||||
return workAreas
|
||||
.filter((bounds) => bounds.width > 0 && bounds.height > 0)
|
||||
.map((bounds) => ({
|
||||
x: Math.round(bounds.x),
|
||||
y: Math.round(bounds.y),
|
||||
width: Math.round(bounds.width),
|
||||
height: Math.round(bounds.height),
|
||||
}))
|
||||
}
|
||||
|
||||
export function buildDisplayEnvelope(
|
||||
anchorBounds: WindowBounds,
|
||||
projectedBounds: WorkAreaBounds,
|
||||
workAreas: readonly WorkAreaBounds[],
|
||||
): WorkAreaBounds {
|
||||
const normalizedWorkAreas = normalizeWorkAreas(workAreas)
|
||||
if (normalizedWorkAreas.length === 0) {
|
||||
return { ...projectedBounds }
|
||||
}
|
||||
|
||||
const relevant = normalizedWorkAreas.filter((workArea) => {
|
||||
return rectsIntersect(workArea, projectedBounds)
|
||||
|| rangesOverlap(workArea.x, getRight(workArea), anchorBounds.x, getRight(anchorBounds))
|
||||
})
|
||||
|
||||
return unionBounds(relevant.length > 0 ? relevant : normalizedWorkAreas)
|
||||
}
|
||||
|
||||
export function clampBoundsWithinEnvelope(
|
||||
bounds: WindowBounds,
|
||||
envelope: WorkAreaBounds,
|
||||
): WindowBounds {
|
||||
const maxX = getRight(envelope) - bounds.width
|
||||
const maxY = getBottom(envelope) - bounds.height
|
||||
|
||||
return {
|
||||
x: clamp(bounds.x, envelope.x, maxX),
|
||||
y: clamp(bounds.y, envelope.y, maxY),
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
}
|
||||
}
|
||||
|
||||
export function clampBoundsWithVisibleMargin(
|
||||
bounds: WindowBounds,
|
||||
envelope: WorkAreaBounds,
|
||||
visibleMargin: number,
|
||||
): WindowBounds {
|
||||
const margin = Math.max(0, Math.round(visibleMargin))
|
||||
const minX = envelope.x + margin - bounds.width
|
||||
const maxX = getRight(envelope) - margin
|
||||
const minY = envelope.y + margin - bounds.height
|
||||
const maxY = getBottom(envelope) - margin
|
||||
|
||||
return {
|
||||
x: clamp(bounds.x, minX, maxX),
|
||||
y: clamp(bounds.y, minY, maxY),
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveExpandedMainWindowBounds(
|
||||
logicalBounds: WindowBounds,
|
||||
settingsHeight: number,
|
||||
workAreas: readonly WorkAreaBounds[],
|
||||
): WindowBounds {
|
||||
const nextSettingsHeight = Math.max(0, Math.round(settingsHeight))
|
||||
if (nextSettingsHeight === 0) {
|
||||
return { ...logicalBounds }
|
||||
}
|
||||
|
||||
const projectedBounds: WindowBounds = {
|
||||
x: logicalBounds.x,
|
||||
y: logicalBounds.y,
|
||||
width: logicalBounds.width,
|
||||
height: logicalBounds.height + nextSettingsHeight,
|
||||
}
|
||||
|
||||
const envelope = buildDisplayEnvelope(logicalBounds, projectedBounds, workAreas)
|
||||
return clampBoundsWithinEnvelope(projectedBounds, envelope)
|
||||
}
|
||||
|
||||
export function clampDraggedMainWindowBounds(
|
||||
actualBounds: WindowBounds,
|
||||
workAreas: readonly WorkAreaBounds[],
|
||||
visibleMargin: number,
|
||||
): WindowBounds {
|
||||
const envelope = buildDisplayEnvelope(actualBounds, actualBounds, workAreas)
|
||||
return clampBoundsWithVisibleMargin(actualBounds, envelope, visibleMargin)
|
||||
}
|
||||
|
||||
export function raiseWindowAboveNormalPopouts(
|
||||
mainWindow: StackableWindowLike | null,
|
||||
popouts: Iterable<StackableWindowLike>,
|
||||
): boolean {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const popout of popouts) {
|
||||
if (!popout.isDestroyed() && !popout.isAlwaysOnTop()) {
|
||||
mainWindow.moveTop()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getHorizontalWheelScrollResult,
|
||||
normalizeWheelDelta,
|
||||
} from '../src/renderer/utils/horizontalWheelScroll'
|
||||
import { resolveMainWindowSettingsHeight } from '../src/renderer/mainWindowSettings'
|
||||
import {
|
||||
formatAstraTime,
|
||||
getAstraPlaybackProgress,
|
||||
@@ -17,6 +18,11 @@ import {
|
||||
import {
|
||||
createDefaultProfile,
|
||||
} from '../src/shared/profileState'
|
||||
import {
|
||||
clampDraggedMainWindowBounds,
|
||||
raiseWindowAboveNormalPopouts,
|
||||
resolveExpandedMainWindowBounds,
|
||||
} from '../src/shared/windowGeometry'
|
||||
import { calculateResizedWindowBounds } from '../src/shared/windowResize'
|
||||
import { createDefaultTheme, resolveNativeThemeSource, resolveTheme } from '../src/shared/themeState'
|
||||
import { usePerformanceStore } from '../src/renderer/stores/performanceStore'
|
||||
@@ -418,6 +424,28 @@ function createFakeTransportBridge(): {
|
||||
}
|
||||
}
|
||||
|
||||
function createFakeStackableWindow(alwaysOnTop = false): {
|
||||
getMoveTopCalls: () => number
|
||||
window: {
|
||||
isDestroyed: () => boolean
|
||||
isAlwaysOnTop: () => boolean
|
||||
moveTop: () => void
|
||||
}
|
||||
} {
|
||||
let moveTopCalls = 0
|
||||
|
||||
return {
|
||||
getMoveTopCalls: () => moveTopCalls,
|
||||
window: {
|
||||
isDestroyed: () => false,
|
||||
isAlwaysOnTop: () => alwaysOnTop,
|
||||
moveTop: () => {
|
||||
moveTopCalls += 1
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function readSpectrumMagnitudes(transport: NativeVisualizerTransport, size = 8): number[] {
|
||||
const output = new Float32Array(size)
|
||||
const count = transport.fillLatestSpectrumMagnitudes(output)
|
||||
@@ -1141,6 +1169,78 @@ test('toggleScope appends astra to the scope order when it is enabled from an op
|
||||
}
|
||||
})
|
||||
|
||||
test('resolveMainWindowSettingsHeight waits for real measurements instead of using a placeholder height', () => {
|
||||
assert.equal(resolveMainWindowSettingsHeight(false, 312, 96), 0)
|
||||
assert.equal(resolveMainWindowSettingsHeight(true, 0, 96), 0)
|
||||
assert.equal(resolveMainWindowSettingsHeight(true, 312, 0), 0)
|
||||
assert.equal(resolveMainWindowSettingsHeight(true, 312, 96), 408)
|
||||
})
|
||||
|
||||
test('expanded main-window bounds can push upward into an overlapping display above', () => {
|
||||
const resolved = resolveExpandedMainWindowBounds(
|
||||
{ x: 700, y: 1110, width: 900, height: 180 },
|
||||
400,
|
||||
[
|
||||
{ x: 0, y: 0, width: 1920, height: 1080 },
|
||||
{ x: 600, y: 1080, width: 720, height: 260 },
|
||||
],
|
||||
)
|
||||
|
||||
assert.equal(resolved.x, 700)
|
||||
assert.equal(resolved.y, 760)
|
||||
assert.equal(resolved.height, 580)
|
||||
})
|
||||
|
||||
test('expanded main-window bounds can span into a taller side display instead of clamping to one display', () => {
|
||||
const resolved = resolveExpandedMainWindowBounds(
|
||||
{ x: 650, y: 650, width: 400, height: 180 },
|
||||
300,
|
||||
[
|
||||
{ x: 0, y: 0, width: 800, height: 800 },
|
||||
{ x: 800, y: 0, width: 800, height: 1200 },
|
||||
],
|
||||
)
|
||||
|
||||
assert.equal(resolved.x, 650)
|
||||
assert.equal(resolved.y, 650)
|
||||
assert.equal(resolved.height, 480)
|
||||
})
|
||||
|
||||
test('dragged main-window bounds keep a visible grab margin without sticking at a display seam', () => {
|
||||
const clamped = clampDraggedMainWindowBounds(
|
||||
{ x: 760, y: 120, width: 400, height: 220 },
|
||||
[
|
||||
{ x: 0, y: 0, width: 800, height: 900 },
|
||||
{ x: 800, y: 0, width: 800, height: 900 },
|
||||
],
|
||||
64,
|
||||
)
|
||||
|
||||
assert.equal(clamped.x, 760)
|
||||
assert.equal(clamped.y, 120)
|
||||
})
|
||||
|
||||
test('raiseWindowAboveNormalPopouts raises the main window when an unpinned popout exists', () => {
|
||||
const main = createFakeStackableWindow()
|
||||
const normalPopout = createFakeStackableWindow(false)
|
||||
const pinnedPopout = createFakeStackableWindow(true)
|
||||
|
||||
const raised = raiseWindowAboveNormalPopouts(main.window, [normalPopout.window, pinnedPopout.window])
|
||||
|
||||
assert.equal(raised, true)
|
||||
assert.equal(main.getMoveTopCalls(), 1)
|
||||
})
|
||||
|
||||
test('raiseWindowAboveNormalPopouts leaves pinned popouts above the main window', () => {
|
||||
const main = createFakeStackableWindow()
|
||||
const pinnedPopout = createFakeStackableWindow(true)
|
||||
|
||||
const raised = raiseWindowAboveNormalPopouts(main.window, [pinnedPopout.window])
|
||||
|
||||
assert.equal(raised, false)
|
||||
assert.equal(main.getMoveTopCalls(), 0)
|
||||
})
|
||||
|
||||
test('main-window bounds updates persist working state in Electron mode', () => {
|
||||
const previousSettingsState = useSettingsStore.getState()
|
||||
const fakeStorage = installFakeLocalStorage()
|
||||
|
||||
Reference in New Issue
Block a user