fix popout scopes issues

This commit is contained in:
Boof2015
2026-05-15 15:35:59 -04:00
parent 023b87801a
commit 2ac1554e8a
6 changed files with 367 additions and 33 deletions
+20 -5
View File
@@ -26,6 +26,7 @@ import { resolveNativeThemeSource } from '../shared/themeState'
import { resolveWindowCapabilities } from '../shared/windowCapabilities'
import {
clampDraggedMainWindowBounds,
clampRestoredWindowBounds,
raiseWindowAboveNormalPopouts,
resolveExpandedMainWindowBounds,
} from '../shared/windowGeometry'
@@ -98,6 +99,7 @@ const NOW_PLAYING_CONFIG_DEFAULTS = {
const STATIC_APP_ICON_FILENAME = 'icon.png'
const MAIN_WINDOW_SYNC_SUPPRESSION_MS = 180
const MAIN_WINDOW_VISIBLE_GRAB_MARGIN = 64
const RESTORED_WINDOW_VISIBLE_MARGIN = 64
const runtimeWindowCapabilities = resolveWindowCapabilities({
platform: process.platform,
argv: process.argv,
@@ -582,7 +584,11 @@ function syncMainWindowLogicalBounds(window: BrowserWindow, bounds = window.getB
}
function applyMainWindowLogicalBounds(window: BrowserWindow, bounds: WindowBounds): void {
const logicalBounds = normalizeMainWindowBounds(bounds)
const logicalBounds = clampRestoredWindowBounds(
normalizeMainWindowBounds(bounds),
getDisplayWorkAreas(),
RESTORED_WINDOW_VISIBLE_MARGIN,
)
mainWindowLogicalBounds = logicalBounds
suppressMainWindowSync()
const expandedBounds = resolveExpandedMainWindowBounds(logicalBounds, getSettingsHeight(window), getDisplayWorkAreas())
@@ -605,10 +611,12 @@ function applyLogicalBounds(window: BrowserWindow, bounds: WindowBounds): void {
return
}
window.setBounds({
const nextBounds = clampRestoredWindowBounds({
...bounds,
height: nextHeight,
})
}, getDisplayWorkAreas(), RESTORED_WINDOW_VISIBLE_MARGIN)
window.setBounds(nextBounds)
}
function setWindowHeight(window: BrowserWindow, bounds: WindowBounds, height: number, y = bounds.y): void {
@@ -1121,9 +1129,12 @@ function createScopePopoutWindow(kind: ScopeKind, rawBounds?: WindowBounds): Bro
width: POPOUT_DEFAULTS.width,
height: POPOUT_DEFAULTS.height,
}
const bounds = shouldRestoreGeometry
const normalizedBounds = shouldRestoreGeometry
? normalizeBounds(rawBounds, fallbackBounds)
: fallbackBounds
const bounds = shouldRestoreGeometry
? clampRestoredWindowBounds(normalizedBounds, getDisplayWorkAreas(), RESTORED_WINDOW_VISIBLE_MARGIN)
: normalizedBounds
suppressNextPopoutBoundsEvents.add(kind)
const options: BrowserWindowConstructorOptions = {
@@ -1212,7 +1223,11 @@ function syncScopePopouts(nextState: ScopePopoutSyncStateMap): void {
if (supportsGeometryPersistence() && desired.bounds) {
const currentBounds = popoutWindow.getBounds()
const nextBounds = normalizeBounds(desired.bounds, currentBounds)
const nextBounds = clampRestoredWindowBounds(
normalizeBounds(desired.bounds, currentBounds),
getDisplayWorkAreas(),
RESTORED_WINDOW_VISIBLE_MARGIN,
)
const hasBoundsDelta =
currentBounds.x !== nextBounds.x
|| currentBounds.y !== nextBounds.y
@@ -60,6 +60,7 @@ const defaultTheme = resolveTheme(createDefaultTheme())
export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps): JSX.Element {
const [snapshot, setSnapshot] = useState<ScopePopoutSnapshot<ScopeKind> | null>(null)
const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(false)
const [cursorInsideWindow, setCursorInsideWindow] = useState(false)
const prevMiniSettingsOpenRef = useRef(false)
const frameTarget = usePerformanceStore((s) => s.frameTarget)
const miniSettingsOpen = useUiStore((s) => s.settingsOpen)
@@ -78,6 +79,30 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps)
frameScheduler.setFrameTarget(frameTarget)
}, [frameScheduler, frameTarget])
useEffect(() => {
let isDisposed = false
const syncCursorInsideWindow = (): void => {
void window.electronAPI.isCursorInsideWindow()
.then((isInside) => {
if (!isDisposed) {
setCursorInsideWindow(isInside)
}
})
.catch(() => {
// Renderer pointer events still update the chrome when cursor polling is unavailable.
})
}
syncCursorInsideWindow()
const interval = setInterval(syncCursorInsideWindow, 120)
return () => {
isDisposed = true
clearInterval(interval)
}
}, [])
useEffect(() => {
const unsubscribeSnapshot = window.electronAPI.onScopePopoutSnapshot((nextSnapshot) => {
if (nextSnapshot.kind !== scopeKind) return
@@ -182,6 +207,9 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps)
return (
<div
className="scope-popout"
onMouseEnter={() => setCursorInsideWindow(true)}
onMouseMove={() => setCursorInsideWindow(true)}
onMouseLeave={() => setCursorInsideWindow(false)}
onMouseDown={useNativeDragRegions ? undefined : handleAltDragStart}
onMouseUp={useNativeDragRegions ? undefined : handleAltDragEnd}
>
@@ -193,6 +221,7 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps)
className={[
'scope-popout__chrome',
miniSettingsOpen ? 'is-expanded' : '',
cursorInsideWindow ? 'is-cursor-inside' : '',
].join(' ').trim()}
>
<header className={`scope-popout__header ${useNativeDragRegions ? 'is-native-drag' : ''}`.trim()}>
+84 -10
View File
@@ -30,6 +30,8 @@ const ACTIVE_PROFILE_KEY = 'prism:activeProfile'
const PROFILE_GEOMETRY_SYNC_WINDOW_MS = 800
interface PersistedSettingsState {
activeProfileId?: string | null
profileBaselineSignature?: string
scopeOrder: ScopeKind[]
hiddenScopes: ScopeKind[]
widthWeights: Record<ScopeKind, number>
@@ -112,6 +114,29 @@ function buildPersistedScopePopouts(scopePopouts: ScopePopoutStateMap): ScopePop
: stripScopePopoutBounds(normalizedScopePopouts)
}
function buildProfileBaselineSignature(profile: Profile | null): string | undefined {
if (!profile) {
return undefined
}
const normalizedProfile = normalizeProfile(profile, profile.name)
const scopePopouts = SCOPE_KINDS.reduce((acc, kind) => {
acc[kind] = {
poppedOut: normalizedProfile.scopePopouts[kind]?.poppedOut === true,
}
return acc
}, {} as Record<ScopeKind, { poppedOut: boolean }>)
return JSON.stringify({
name: normalizedProfile.name,
scopeOrder: normalizeScopeOrder(normalizedProfile.scopeOrder),
hiddenScopes: normalizeHiddenScopes(normalizedProfile.hiddenScopes),
widthWeights: normalizeWidthWeights(normalizedProfile.widthWeights),
scopeSettings: mergeScopeSettings(normalizedProfile.scopeSettings),
scopePopouts,
})
}
function restoreBaselineScopePopoutBounds(
scopePopouts: ScopePopoutStateMap,
baseline: Profile | null,
@@ -144,6 +169,30 @@ function restoreBaselineGeometry(
}
}
function restoreBaselinePopoutOpenState(
state: Pick<SettingsState, 'savedProfileBaseline'>,
workingState: WorkingSettingsState,
): WorkingSettingsState {
const baseline = state.savedProfileBaseline
if (!baseline) {
return workingState
}
const baselinePopouts = normalizeScopePopouts(baseline.scopePopouts)
return {
...workingState,
scopePopouts: SCOPE_KINDS.reduce((acc, kind) => {
acc[kind] = {
...workingState.scopePopouts[kind],
poppedOut: baselinePopouts[kind]?.poppedOut === true,
windowBounds: workingState.scopePopouts[kind]?.windowBounds
?? baselinePopouts[kind]?.windowBounds,
}
return acc
}, {} as ScopePopoutStateMap),
}
}
function loadFromStorage(): Partial<PersistedSettingsState> {
if (!canUseBrowserStorage()) {
return {}
@@ -165,7 +214,9 @@ function loadFromStorage(): Partial<PersistedSettingsState> {
return {}
}
function saveToStorage(state: WorkingSettingsState): void {
function saveToStorage(
state: WorkingSettingsState & Pick<SettingsState, 'activeProfileId' | 'savedProfileBaseline'>,
): void {
if (!canUseBrowserStorage()) {
return
}
@@ -177,6 +228,8 @@ function saveToStorage(state: WorkingSettingsState): void {
: undefined
localStorage.setItem(STORAGE_KEY, JSON.stringify({
activeProfileId: state.activeProfileId,
profileBaselineSignature: buildProfileBaselineSignature(state.savedProfileBaseline),
scopeOrder: state.scopeOrder,
hiddenScopes: Array.from(state.hiddenScopes),
widthWeights: state.widthWeights,
@@ -189,7 +242,9 @@ function saveToStorage(state: WorkingSettingsState): void {
}
}
function persistWorkingState(state: WorkingSettingsState): void {
function persistWorkingState(
state: WorkingSettingsState & Pick<SettingsState, 'activeProfileId' | 'savedProfileBaseline'>,
): void {
saveToStorage(state)
}
@@ -202,6 +257,26 @@ function hasPersistedWorkingState(state: Partial<PersistedSettingsState>): boole
|| 'windowBounds' in state
}
function canRestorePersistedWorkingState(
state: Partial<PersistedSettingsState>,
snapshot: ProfileLibrarySnapshot,
): boolean {
if (!hasPersistedWorkingState(state)) {
return false
}
if (!snapshot.activeProfileId || state.activeProfileId !== snapshot.activeProfileId) {
return false
}
const activeProfile = snapshot.profiles[snapshot.activeProfileId]
if (!activeProfile || typeof state.profileBaselineSignature !== 'string') {
return false
}
return state.profileBaselineSignature === buildProfileBaselineSignature(activeProfile)
}
function loadLegacyProfileMigrationPayload(): LegacyProfileMigrationPayload | null {
try {
const rawProfiles = localStorage.getItem(PROFILES_STORAGE_KEY)
@@ -558,19 +633,18 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
}
}
// If localStorage has a working state from a previous session, preserve it so the
// user picks up exactly where they left off (dirty or not). The saved profile becomes
// the baseline for dirty-state comparison but the working values are left as-is.
// On first launch (no stored state) we load the profile normally.
// Preserve a previous working draft only when it belongs to the current active
// profile baseline. Legacy or stale state should not override saved popout state.
const storedWorkingState = loadFromStorage()
const hasStoredWorkingState = hasPersistedWorkingState(storedWorkingState)
applyProfileSnapshot(set, snapshot, { loadActiveProfile: !hasStoredWorkingState })
if (hasStoredWorkingState) {
const shouldRestoreWorkingState = canRestorePersistedWorkingState(storedWorkingState, snapshot)
applyProfileSnapshot(set, snapshot, { loadActiveProfile: !shouldRestoreWorkingState })
if (shouldRestoreWorkingState) {
set((state) => {
const nextWorkingState = restoreBaselineGeometry(
const persistedWorkingState = restoreBaselineGeometry(
state,
createWorkingStateFromPersistedState(storedWorkingState),
)
const nextWorkingState = restoreBaselinePopoutOpenState(state, persistedWorkingState)
return commitWorkingState(state, nextWorkingState, state.savedProfileBaseline)
})
+2
View File
@@ -1977,6 +1977,8 @@ button.toolbar__version:hover {
.scope-popout__viewport:hover .scope-popout__chrome,
.scope-popout__chrome:hover,
.scope-popout__chrome:focus-within,
.scope-popout__chrome.is-cursor-inside,
.scope-popout__chrome.is-expanded {
max-height: 58px;
opacity: 1;
+9
View File
@@ -145,6 +145,15 @@ export function clampDraggedMainWindowBounds(
return clampBoundsWithVisibleMargin(actualBounds, envelope, visibleMargin)
}
export function clampRestoredWindowBounds(
bounds: WindowBounds,
workAreas: readonly WorkAreaBounds[],
visibleMargin: number,
): WindowBounds {
const envelope = buildDisplayEnvelope(bounds, bounds, workAreas)
return clampBoundsWithVisibleMargin(bounds, envelope, visibleMargin)
}
export function raiseWindowAboveNormalPopouts(
mainWindow: StackableWindowLike | null,
popouts: Iterable<StackableWindowLike>,