Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/components/Desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default function Desktop() {
const containerRef = useRef<HTMLDivElement | null>(null)
const [containerSize, setContainerSize] = useState<WindowSize>(DEFAULT_SIZE)
const [iconSize, setIconSize] = useState(DEFAULT_ICON_SIZE)
const { windows, openWindow } = useWindowManager()
const { windows, openWindow, setDesktopBounds } = useWindowManager()
const initializedRef = useRef(false)
const firstIconRef = useRef<HTMLButtonElement | null>(null)

Expand All @@ -62,7 +62,9 @@ export default function Desktop() {

const updateSize = () => {
const rect = element.getBoundingClientRect()
setContainerSize({ width: rect.width, height: rect.height })
const bounds = { width: rect.width, height: rect.height }
setContainerSize(bounds)
setDesktopBounds(bounds)
}

updateSize()
Expand All @@ -76,7 +78,7 @@ export default function Desktop() {
const handleResize = () => updateSize()
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
}, [setDesktopBounds])

useEffect(() => {
const element = firstIconRef.current
Expand Down
29 changes: 29 additions & 0 deletions src/contexts/WindowManagerContext.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'

import {
WindowManagerProvider,
useWindowManager,
} from './WindowManagerContext.tsx'
import type { WindowSize } from '../types/window.ts'

const h = React.createElement

type CaptureComponent = (() => null) & {
lastValue?: (bounds: WindowSize) => void
}

const Capture: CaptureComponent = () => {
const manager = useWindowManager()
Capture.lastValue = manager.setDesktopBounds
return null
}

test('WindowManagerProvider exposes setDesktopBounds', () => {
Capture.lastValue = undefined
renderToStaticMarkup(h(WindowManagerProvider, null, h(Capture)))

assert.equal(typeof Capture.lastValue, 'function')
})
188 changes: 173 additions & 15 deletions src/contexts/WindowManagerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type WindowManagerValue = {
setWindowPosition: (id: string, position: WindowPoint) => void
setWindowSize: (id: string, size: WindowSize, position?: WindowPoint) => void
setWindowTitle: (id: string, title: string) => void
setDesktopBounds: (bounds: WindowSize) => void
apps: typeof WINDOW_APPS
}

Expand All @@ -43,6 +44,10 @@ const randomId = () => Math.random().toString(36).slice(2, 7)
export function WindowManagerProvider({ children }: PropsWithChildren) {
const [windows, setWindows] = useState<WindowState[]>([])
const [activeWindowId, setActiveWindowIdState] = useState<string | null>(null)
const [desktopBounds, setDesktopBoundsState] = useState<WindowSize>({
width: Number.POSITIVE_INFINITY,
height: Number.POSITIVE_INFINITY,
})
const activeIdRef = useRef<string | null>(null)
const zIndexRef = useRef(100)

Expand Down Expand Up @@ -105,15 +110,17 @@ export function WindowManagerProvider({ children }: PropsWithChildren) {
y: 80 + prev.length * 24,
}

const requestedWidth =
options?.size?.width ?? definition.defaultSize.width
const requestedHeight =
options?.size?.height ?? definition.defaultSize.height

const boundedWidth = Math.min(requestedWidth, desktopBounds.width)
const boundedHeight = Math.min(requestedHeight, desktopBounds.height)

const size = {
width: Math.max(
MIN_WIDTH,
options?.size?.width ?? definition.defaultSize.width
),
height: Math.max(
MIN_HEIGHT,
options?.size?.height ?? definition.defaultSize.height
),
width: Math.max(MIN_WIDTH, boundedWidth),
height: Math.max(MIN_HEIGHT, boundedHeight),
}

const id = `${appId}-${Date.now()}-${randomId()}`
Expand All @@ -139,7 +146,12 @@ export function WindowManagerProvider({ children }: PropsWithChildren) {
setActiveWindowId(createdId)
return createdId
},
[getNextZIndex, setActiveWindowId]
[
desktopBounds.height,
desktopBounds.width,
getNextZIndex,
setActiveWindowId,
]
)

const toggleMinimize = useCallback(
Expand Down Expand Up @@ -236,20 +248,164 @@ export function WindowManagerProvider({ children }: PropsWithChildren) {
return window
}

const limitedWidth = Math.min(size.width, desktopBounds.width)
const limitedHeight = Math.min(size.height, desktopBounds.height)

const nextSize = {
width: Math.max(MIN_WIDTH, limitedWidth),
height: Math.max(MIN_HEIGHT, limitedHeight),
}

const nextPosition = position ?? window.position
const maxX = Math.max(0, desktopBounds.width - nextSize.width)
const maxY = Math.max(0, desktopBounds.height - nextSize.height)

const clampedPosition = {
x: Math.min(Math.max(0, nextPosition.x), maxX),
y: Math.min(Math.max(0, nextPosition.y), maxY),
}

return {
...window,
size: {
width: Math.max(MIN_WIDTH, size.width),
height: Math.max(MIN_HEIGHT, size.height),
},
position: position ?? window.position,
size: nextSize,
position: clampedPosition,
}
})
)
},
[]
[desktopBounds.height, desktopBounds.width]
)

const setDesktopBounds = useCallback((bounds: WindowSize) => {
setDesktopBoundsState(prevBounds => {
const hasWidthChange = bounds.width !== prevBounds.width
const hasHeightChange = bounds.height !== prevBounds.height

if (!hasWidthChange && !hasHeightChange) {
return prevBounds
}

const widthShrunk = bounds.width < prevBounds.width
const heightShrunk = bounds.height < prevBounds.height

setWindows(prevWindows => {
let changed = false

const updated = prevWindows.map(window => {
if (window.maximized) {
const maximizeSize = {
width: Math.max(MIN_WIDTH, bounds.width),
height: Math.max(MIN_HEIGHT, bounds.height),
}

let nextPrevious = window.previous
let previousChanged = false

if (window.previous) {
const limitedPrevWidth = Math.min(
window.previous.size.width,
bounds.width
)
const limitedPrevHeight = Math.min(
window.previous.size.height,
bounds.height
)

const nextPreviousSize = {
width: Math.max(MIN_WIDTH, limitedPrevWidth),
height: Math.max(MIN_HEIGHT, limitedPrevHeight),
}

const maxPrevX = Math.max(
0,
bounds.width - nextPreviousSize.width
)
const maxPrevY = Math.max(
0,
bounds.height - nextPreviousSize.height
)

const nextPreviousPosition = {
x: Math.min(Math.max(0, window.previous.position.x), maxPrevX),
y: Math.min(Math.max(0, window.previous.position.y), maxPrevY),
}

previousChanged =
nextPreviousSize.width !== window.previous.size.width ||
nextPreviousSize.height !== window.previous.size.height ||
nextPreviousPosition.x !== window.previous.position.x ||
nextPreviousPosition.y !== window.previous.position.y

if (previousChanged) {
nextPrevious = {
position: nextPreviousPosition,
size: nextPreviousSize,
}
}
}

const frameChanged =
window.position.x !== 0 ||
window.position.y !== 0 ||
window.size.width !== maximizeSize.width ||
window.size.height !== maximizeSize.height

if (frameChanged || previousChanged) {
changed = true
return {
...window,
position: { x: 0, y: 0 },
size: maximizeSize,
Comment on lines +354 to +358

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clamp maximized windows without refreshing their restore state

When setDesktopBounds shrinks the desktop, maximized windows have their position and size reset but their saved previous snapshot is left untouched. Later, toggleMaximize restores from that stale snapshot, so unmaximizing after a viewport shrink will reopen the window at the old off‑screen coordinates and dimensions, undoing the clamping. Consider updating the previous values whenever a maximized window is resized so restore always respects the current desktop bounds.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex please fix this

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Testing

  • npm run format
  • npm run lint
  • ⚠️ npm test (fails: node: bad option: --experimental-transform-types, --test-coverage-include=src/components/, --test-coverage-include=src/routes/)

View task →

previous: nextPrevious,
}
}
return window
}

if (!widthShrunk && !heightShrunk) {
return window
}

const limitedWidth = Math.min(window.size.width, bounds.width)
const limitedHeight = Math.min(window.size.height, bounds.height)

const nextSize = {
width: Math.max(MIN_WIDTH, limitedWidth),
height: Math.max(MIN_HEIGHT, limitedHeight),
}

const maxX = Math.max(0, bounds.width - nextSize.width)
const maxY = Math.max(0, bounds.height - nextSize.height)

const nextPosition = {
x: Math.min(Math.max(0, window.position.x), maxX),
y: Math.min(Math.max(0, window.position.y), maxY),
}

if (
nextSize.width !== window.size.width ||
nextSize.height !== window.size.height ||
nextPosition.x !== window.position.x ||
nextPosition.y !== window.position.y
) {
changed = true
return {
...window,
size: nextSize,
position: nextPosition,
}
}

return window
})

return changed ? updated : prevWindows
})

return bounds
})
}, [])

const closeWindow = useCallback(
(id: string) => {
let nextActiveId: string | null = activeIdRef.current
Expand Down Expand Up @@ -288,6 +444,7 @@ export function WindowManagerProvider({ children }: PropsWithChildren) {
setWindowPosition,
setWindowSize,
setWindowTitle,
setDesktopBounds,
apps: WINDOW_APPS,
}),
[
Expand All @@ -301,6 +458,7 @@ export function WindowManagerProvider({ children }: PropsWithChildren) {
setWindowPosition,
setWindowSize,
setWindowTitle,
setDesktopBounds,
]
)

Expand Down