From f07d22cb33267b729f01a8caca1372b50f158d19 Mon Sep 17 00:00:00 2001 From: Boof2015 <75185879+Boof2015@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:54:56 -0400 Subject: [PATCH] fix sizing issues --- src/main/index.ts | 201 ++++++++++++--------- src/renderer/popouts/ScopePopoutWindow.tsx | 163 +++++++++-------- src/renderer/styles/globals.css | 28 ++- 3 files changed, 213 insertions(+), 179 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index cf583d3..7294209 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -13,7 +13,6 @@ import type { ProfileMenuRequest } from '../types/profileMenu' import { SCOPE_KINDS, type ScopeKind } from '../types/scope' let mainWindow: BrowserWindow | null = null -let currentSettingsHeight = 0 let moveInterval: ReturnType | null = null let moveStartCursor: { x: number; y: number } | null = null let moveStartPosition: number[] | null = null @@ -21,6 +20,8 @@ let moveStartPosition: number[] | null = null const scopePopoutWindows = new Map() const scopePopoutCloseAllowed = new Set() const popoutBoundsTimers = new Map>() +const windowSettingsHeights = new Map() +const windowSettingsBottomAnchors = new Map() const WINDOW_DEFAULTS = { width: 900, @@ -48,6 +49,99 @@ function isMainRendererWindow(window: BrowserWindow | null): boolean { return window !== null && window === mainWindow } +function getBaseMinHeight(window: BrowserWindow): number { + return isMainRendererWindow(window) ? WINDOW_DEFAULTS.minHeight : POPOUT_DEFAULTS.minHeight +} + +function getSettingsHeight(window: BrowserWindow | null): number { + if (!window) return 0 + return windowSettingsHeights.get(window.id) ?? 0 +} + +function setSettingsHeightForWindow(window: BrowserWindow, height: number): void { + const nextHeight = Math.max(0, Math.round(height)) + if (nextHeight === 0) { + windowSettingsHeights.delete(window.id) + windowSettingsBottomAnchors.delete(window.id) + return + } + + windowSettingsHeights.set(window.id, nextHeight) +} + +function toLogicalBounds(window: BrowserWindow, bounds = window.getBounds()): WindowBounds { + return { + ...bounds, + height: Math.max(getBaseMinHeight(window), bounds.height - getSettingsHeight(window)), + } +} + +function applyLogicalBounds(window: BrowserWindow, bounds: WindowBounds): void { + window.setBounds({ + ...bounds, + height: bounds.height + getSettingsHeight(window), + }) +} + +function setWindowHeight(window: BrowserWindow, bounds: WindowBounds, height: number, y = bounds.y): void { + window.setBounds({ + x: bounds.x, + y, + width: bounds.width, + height, + }) +} + +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 (delta !== 0) { + const bounds = window.getBounds() + const newHeight = Math.max(baseMinHeight + nextSettingsHeight, bounds.height + delta) + const display = screen.getDisplayMatching(bounds) + const workArea = display.workArea + const workAreaBottom = workArea.y + workArea.height + const bottomEdge = bounds.y + newHeight + + if (delta > 0 && bottomEdge > workAreaBottom) { + if (!windowSettingsBottomAnchors.has(window.id)) { + windowSettingsBottomAnchors.set(window.id, bounds.y + bounds.height) + } + const newY = Math.max(workArea.y, workAreaBottom - newHeight) + setWindowHeight(window, bounds, newHeight, newY) + } else if (delta < 0) { + const anchoredBottom = windowSettingsBottomAnchors.get(window.id) + if (anchoredBottom !== undefined) { + const targetY = Math.max(workArea.y, Math.min(anchoredBottom - newHeight, workAreaBottom - newHeight)) + setWindowHeight(window, bounds, newHeight, targetY) + } else { + const baseHeight = newHeight - nextSettingsHeight + const naturalBottom = bounds.y + baseHeight + if (naturalBottom < workAreaBottom) { + const maxY = workAreaBottom - newHeight + if (bounds.y < maxY) { + setWindowHeight(window, bounds, newHeight) + } else { + setWindowHeight(window, bounds, newHeight, maxY) + } + } else { + setWindowHeight(window, bounds, newHeight) + } + } + } else { + setWindowHeight(window, bounds, newHeight) + } + } + + setSettingsHeightForWindow(window, nextSettingsHeight) +} + function sendToRenderer(sender: WebContents, channel: string, ...args: unknown[]): void { if (!sender.isDestroyed()) { sender.send(channel, ...args) @@ -189,8 +283,11 @@ function createMainWindow(): void { }) mainWindow.on('closed', () => { + if (mainWindow) { + windowSettingsHeights.delete(mainWindow.id) + windowSettingsBottomAnchors.delete(mainWindow.id) + } mainWindow = null - currentSettingsHeight = 0 for (const kind of SCOPE_KINDS) { destroyScopePopoutWindow(kind) @@ -219,7 +316,7 @@ function emitPopoutBoundsChanged(kind: ScopeKind, window: BrowserWindow): void { popoutBoundsTimers.delete(kind) if (!mainWindow || mainWindow.isDestroyed() || window.isDestroyed()) return - const bounds = window.getBounds() + const bounds = toLogicalBounds(window) mainWindow.webContents.send('scope-popout:bounds-changed', kind, bounds) }, 80) @@ -293,6 +390,7 @@ function createScopePopoutWindow(kind: ScopeKind, rawBounds?: WindowBounds): Bro } const popoutWindow = new BrowserWindow(options) + setSettingsHeightForWindow(popoutWindow, 0) scopePopoutWindows.set(kind, popoutWindow) popoutWindow.once('ready-to-show', () => { @@ -311,6 +409,8 @@ function createScopePopoutWindow(kind: ScopeKind, rawBounds?: WindowBounds): Bro }) popoutWindow.on('closed', () => { + windowSettingsHeights.delete(popoutWindow.id) + windowSettingsBottomAnchors.delete(popoutWindow.id) scopePopoutWindows.delete(kind) scopePopoutCloseAllowed.delete(kind) @@ -348,7 +448,7 @@ function syncScopePopouts(nextState: ScopePopoutSyncStateMap): void { || currentBounds.height !== nextBounds.height if (hasBoundsDelta) { - popoutWindow.setBounds(nextBounds) + applyLogicalBounds(popoutWindow, nextBounds) } } } @@ -486,30 +586,14 @@ function setupIPC(): void { const targetWindow = getWindowFromSender(event.sender) if (!targetWindow) return - if (isMainRendererWindow(targetWindow)) { - targetWindow.setBounds({ - ...bounds, - height: bounds.height + currentSettingsHeight, - }) - return - } - - targetWindow.setBounds(bounds) + applyLogicalBounds(targetWindow, bounds) }) ipcMain.handle('window:get-bounds', (event) => { const targetWindow = getWindowFromSender(event.sender) if (!targetWindow) return null - const bounds = targetWindow.getBounds() - if (!isMainRendererWindow(targetWindow)) { - return bounds - } - - return { - ...bounds, - height: bounds.height - currentSettingsHeight, - } + return toLogicalBounds(targetWindow) }) ipcMain.on('window:reposition', (event, position: 'top' | 'bottom') => { @@ -530,84 +614,23 @@ function setupIPC(): void { ipcMain.on('window:expand-settings', (event, panelHeight: number) => { const targetWindow = getWindowFromSender(event.sender) - if (!targetWindow || !isMainRendererWindow(targetWindow)) return + if (!targetWindow) return - const bounds = targetWindow.getBounds() - const [minW] = targetWindow.getMinimumSize() - const newHeight = bounds.height + panelHeight - targetWindow.setMinimumSize(minW, WINDOW_DEFAULTS.minHeight + panelHeight) - - const display = screen.getDisplayMatching(bounds) - const workArea = display.workArea - const bottomEdge = bounds.y + newHeight - if (bottomEdge > workArea.y + workArea.height) { - const newY = Math.max(workArea.y, workArea.y + workArea.height - newHeight) - targetWindow.setBounds({ x: bounds.x, y: newY, width: bounds.width, height: newHeight }) - } else { - targetWindow.setSize(bounds.width, newHeight, true) - } - currentSettingsHeight = Math.max(0, currentSettingsHeight + Math.round(panelHeight)) + applySettingsHeight(targetWindow, getSettingsHeight(targetWindow) + panelHeight) }) ipcMain.on('window:collapse-settings', (event, panelHeight: number) => { const targetWindow = getWindowFromSender(event.sender) - if (!targetWindow || !isMainRendererWindow(targetWindow)) return + if (!targetWindow) return - const bounds = targetWindow.getBounds() - const [minW] = targetWindow.getMinimumSize() - const newHeight = Math.max(WINDOW_DEFAULTS.minHeight, bounds.height - panelHeight) - targetWindow.setMinimumSize(minW, WINDOW_DEFAULTS.minHeight) - - const display = screen.getDisplayMatching(bounds) - const workArea = display.workArea - const wasAtBottom = bounds.y + bounds.height >= workArea.y + workArea.height - 10 - if (wasAtBottom) { - const newY = Math.min(bounds.y + panelHeight, workArea.y + workArea.height - newHeight) - targetWindow.setBounds({ x: bounds.x, y: newY, width: bounds.width, height: newHeight }) - } else { - targetWindow.setSize(bounds.width, newHeight, true) - } - currentSettingsHeight = Math.max(0, currentSettingsHeight - Math.round(panelHeight)) + applySettingsHeight(targetWindow, getSettingsHeight(targetWindow) - panelHeight) }) ipcMain.on('window:set-settings-height', (event, panelHeight: number) => { const targetWindow = getWindowFromSender(event.sender) - if (!targetWindow || !isMainRendererWindow(targetWindow)) return + if (!targetWindow) return - const nextHeight = Math.max(0, Math.round(panelHeight)) - const delta = nextHeight - currentSettingsHeight - const [width, height] = targetWindow.getSize() - const [minW] = targetWindow.getMinimumSize() - - targetWindow.setMinimumSize(minW, WINDOW_DEFAULTS.minHeight + nextHeight) - if (delta !== 0) { - const newHeight = Math.max(WINDOW_DEFAULTS.minHeight, height + delta) - const bounds = targetWindow.getBounds() - const display = screen.getDisplayMatching(bounds) - const workArea = display.workArea - const bottomEdge = bounds.y + newHeight - if (delta > 0 && bottomEdge > workArea.y + workArea.height) { - const newY = Math.max(workArea.y, workArea.y + workArea.height - newHeight) - targetWindow.setBounds({ x: bounds.x, y: newY, width, height: newHeight }) - } else if (delta < 0) { - const baseHeight = newHeight - nextHeight - const naturalBottom = bounds.y + baseHeight - if (naturalBottom < workArea.y + workArea.height) { - const maxY = workArea.y + workArea.height - newHeight - if (bounds.y < maxY) { - targetWindow.setSize(width, newHeight, true) - } else { - targetWindow.setBounds({ x: bounds.x, y: maxY, width, height: newHeight }) - } - } else { - targetWindow.setSize(width, newHeight, true) - } - } else { - targetWindow.setSize(width, newHeight, true) - } - } - - currentSettingsHeight = nextHeight + applySettingsHeight(targetWindow, panelHeight) }) ipcMain.on('scope-popout:sync', (event, state: ScopePopoutSyncStateMap) => { diff --git a/src/renderer/popouts/ScopePopoutWindow.tsx b/src/renderer/popouts/ScopePopoutWindow.tsx index 4586c45..c1ffb75 100644 --- a/src/renderer/popouts/ScopePopoutWindow.tsx +++ b/src/renderer/popouts/ScopePopoutWindow.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState, type JSX, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent } from 'react' +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type JSX, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent } from 'react' import type { ScopePopoutSnapshot } from '../../types/popout' import { SCOPE_LABELS, type ScopeKind } from '../../types/scope' import { DEFAULT_SCOPE_SETTINGS, type ScopeSettings } from '../../types/settings' @@ -41,11 +41,12 @@ interface ScopePopoutWindowProps { scopeKind: ScopeKind } +const POPOUT_SETTINGS_EXPAND_HEIGHT = 260 + export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps): JSX.Element { const [snapshot, setSnapshot] = useState | null>(null) const [miniSettingsOpen, setMiniSettingsOpen] = useState(false) - const [chromeHeight, setChromeHeight] = useState(0) - const chromeRef = useRef(null) + const prevMiniSettingsOpenRef = useRef(false) const dataSource = useMemo(() => new ScopePopoutDataSource(scopeKind), [scopeKind]) useEffect(() => { @@ -74,6 +75,7 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps) const effectiveAccent = snapshot?.accent ?? '#38bdf8' const effectiveSettings = (snapshot?.settings ?? DEFAULT_SCOPE_SETTINGS[scopeKind]) as ScopeSettings[ScopeKind] + const settingsHeight = miniSettingsOpen ? POPOUT_SETTINGS_EXPAND_HEIGHT : 0 const handleUpdateScopeSettings = (kind: K, partial: Partial): void => { if (kind !== scopeKind) return @@ -121,26 +123,21 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps) window.electronAPI.stopWindowMove() }, []) - useEffect(() => { - const chrome = chromeRef.current - if (!chrome) return - - const updateHeight = (): void => { - setChromeHeight(chrome.scrollHeight) + useLayoutEffect(() => { + if (miniSettingsOpen && !prevMiniSettingsOpenRef.current) { + window.electronAPI.expandSettings(POPOUT_SETTINGS_EXPAND_HEIGHT) + } else if (!miniSettingsOpen && prevMiniSettingsOpenRef.current) { + window.electronAPI.collapseSettings(POPOUT_SETTINGS_EXPAND_HEIGHT) } - updateHeight() - - const observer = typeof ResizeObserver === 'undefined' - ? null - : new ResizeObserver(() => updateHeight()) - observer?.observe(chrome) - - return () => observer?.disconnect() - }, [miniSettingsOpen, snapshot]) + prevMiniSettingsOpenRef.current = miniSettingsOpen + }, [miniSettingsOpen]) useEffect(() => { return () => { + if (prevMiniSettingsOpenRef.current) { + window.electronAPI.setSettingsHeight(0) + } window.electronAPI.stopWindowMove() } }, []) @@ -152,57 +149,77 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps) onMouseUp={handleAltDragEnd} >
-
-
- -
- {snapshot?.label ?? SCOPE_LABELS[scopeKind]} - Detached Scope +
+
+
+ +
+ {snapshot?.label ?? SCOPE_LABELS[scopeKind]} + Detached Scope +
-
-
- - -
-
+
+ + +
+ +
- {miniSettingsOpen && ( +
+
+ +
+
+ + + {miniSettingsOpen && ( +
- )} -
- -
-
-
-
+ )} ) } diff --git a/src/renderer/styles/globals.css b/src/renderer/styles/globals.css index bb581b2..eba95e3 100644 --- a/src/renderer/styles/globals.css +++ b/src/renderer/styles/globals.css @@ -817,6 +817,13 @@ select { color: var(--text-primary); } +.scope-popout__viewport { + position: relative; + flex: 0 0 auto; + min-height: 0; + overflow: hidden; +} + .scope-popout__chrome { position: absolute; top: 0; @@ -831,7 +838,7 @@ select { transition: max-height 160ms ease, opacity 120ms ease, transform 160ms ease; } -.scope-popout:hover .scope-popout__chrome, +.scope-popout__viewport:hover .scope-popout__chrome, .scope-popout__chrome:hover, .scope-popout__chrome.is-expanded { max-height: 58px; @@ -840,10 +847,6 @@ select { transform: translateY(0); } -.scope-popout__chrome.is-expanded { - max-height: 420px; -} - .scope-popout__header { display: flex; align-items: center; @@ -951,12 +954,12 @@ select { } .scope-popout__settings-panel { - flex-shrink: 0; + height: 100%; overflow-y: auto; overflow-x: hidden; background: linear-gradient(180deg, rgba(6, 8, 11, 0.98), rgba(4, 5, 7, 0.98)); - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - padding: 12px 0 8px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + padding: 12px 0 10px; } .scope-popout__settings-panel .settings-scope-section { @@ -964,12 +967,17 @@ select { padding: 0 14px 12px; } +.scope-popout__settings-region { + flex: 0 0 auto; + min-height: 0; + overflow: hidden; +} + .scope-popout__content { - flex: 1; + height: 100%; min-height: 0; display: flex; flex-direction: column; - transition: padding-top 160ms ease; } .scope-popout__canvas-region {