From 69607414d5cb40c5357d296571dd634950b4f8e6 Mon Sep 17 00:00:00 2001 From: David Yen Date: Sat, 20 Sep 2025 22:59:50 -0700 Subject: [PATCH] Add smart window auto sizing --- README.md | 1 + src/components/DesktopWindow.tsx | 166 +++++++++++++++++++++++++------ 2 files changed, 138 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 947a74e..f5ec4ac 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ This is a "bug bounty hackathon" project that ironically has more bugs than it f - 🧱 **Tailwind Detox**: The entire interface now leans on React95 and styled-components—no utility classes in sight - 🪟 **Pixel-Perfect Window Controls**: Title bars now rock the classic `_`, `□`, and `X` glyphs straight out of Windows 95 - 🖱️ **Authentic Desktop Shell**: Draggable, resizable windows with minimize/maximize controls, a living taskbar, and a Start menu that launches every feature +- 🧊 **Smart Window Auto-Sizing**: Newly opened apps now expand to fit their content so you're not hunting for UI that slipped outside the frame - 🗃️ **Zustand State Management**: Because Redux wasn't complicated enough, we needed something "simpler" - ⚡ **Blazing Fast Vite**: The only thing fast about this project - 🎭 **TypeScript**: Added types everywhere except where they're actually needed diff --git a/src/components/DesktopWindow.tsx b/src/components/DesktopWindow.tsx index 06ccb18..f4f588f 100644 --- a/src/components/DesktopWindow.tsx +++ b/src/components/DesktopWindow.tsx @@ -1,4 +1,4 @@ -import { Suspense } from 'react' +import { Suspense, useCallback, useEffect, useRef } from 'react' import { Rnd } from 'react-rnd' import { styled } from 'styled-components' import { Button, Window, WindowContent, WindowHeader } from 'react95' @@ -46,7 +46,7 @@ const ControlGroup = styled.div` const Content = styled(WindowContent)` flex: 1; width: 100%; - overflow: hidden; + overflow: auto; ` const LoadingFallback = styled.div` @@ -68,7 +68,114 @@ export default function DesktopWindow({ windowState, containerSize }: Props) { setWindowTitle, } = useWindowManager() + const { + id, + position: windowPosition, + size, + minimized, + maximized, + zIndex, + context, + } = windowState + + const windowShellRef = useRef(null) + const contentRef = useRef(null) + const autoSizeEnabledRef = useRef(true) + const definition = WINDOW_APPS[windowState.appId] + + const adjustToContentSize = useCallback(() => { + if (maximized || minimized || !autoSizeEnabledRef.current) { + return + } + + if (containerSize.width <= 0 || containerSize.height <= 0) { + return + } + + const shellEl = windowShellRef.current + const contentEl = contentRef.current + + if (!shellEl || !contentEl) { + return + } + + const widthExtra = shellEl.offsetWidth - contentEl.offsetWidth + const heightExtra = shellEl.offsetHeight - contentEl.offsetHeight + + const requiredWidth = Math.ceil( + contentEl.scrollWidth + Math.max(0, widthExtra) + ) + const requiredHeight = Math.ceil( + contentEl.scrollHeight + Math.max(0, heightExtra) + ) + + const nextWidth = Math.min( + Math.max(size.width, requiredWidth), + containerSize.width + ) + const nextHeight = Math.min( + Math.max(size.height, requiredHeight), + containerSize.height + ) + + let nextX = windowPosition.x + let nextY = windowPosition.y + + if (nextX + nextWidth > containerSize.width) { + nextX = Math.max(0, containerSize.width - nextWidth) + } + + if (nextY + nextHeight > containerSize.height) { + nextY = Math.max(0, containerSize.height - nextHeight) + } + + const sizeChanged = nextWidth > size.width || nextHeight > size.height + const positionChanged = + nextX !== windowPosition.x || nextY !== windowPosition.y + + if (sizeChanged || positionChanged) { + const maybePosition = positionChanged ? { x: nextX, y: nextY } : undefined + setWindowSize(id, { width: nextWidth, height: nextHeight }, maybePosition) + } + }, [ + containerSize.height, + containerSize.width, + id, + maximized, + minimized, + setWindowSize, + size.height, + size.width, + windowPosition.x, + windowPosition.y, + ]) + + useEffect(() => { + if (typeof window === 'undefined' || maximized || minimized) { + return + } + + const contentEl = contentRef.current + if (!contentEl) { + return + } + + adjustToContentSize() + + if (typeof ResizeObserver === 'undefined') { + const rafId = window.requestAnimationFrame(() => adjustToContentSize()) + return () => window.cancelAnimationFrame(rafId) + } + + const observer = new ResizeObserver(() => { + adjustToContentSize() + }) + + observer.observe(contentEl) + return () => observer.disconnect() + }, [adjustToContentSize, maximized, minimized]) + if (!definition) { return null } @@ -76,38 +183,43 @@ export default function DesktopWindow({ windowState, containerSize }: Props) { const { Component } = definition const handleMaximize = () => { - toggleMaximize(windowState.id, containerSize) + autoSizeEnabledRef.current = false + toggleMaximize(id, containerSize) } return ( focusWindow(windowState.id)} - onDragStop={(_, data) => - setWindowPosition(windowState.id, { x: data.x, y: data.y }) - } - onResizeStart={() => focusWindow(windowState.id)} + size={{ width: size.width, height: size.height }} + position={{ x: windowPosition.x, y: windowPosition.y }} + onDragStart={() => { + autoSizeEnabledRef.current = false + focusWindow(id) + }} + onDragStop={(_, data) => setWindowPosition(id, { x: data.x, y: data.y })} + onResizeStart={() => { + autoSizeEnabledRef.current = false + focusWindow(id) + }} onResize={(_, __, ref, ___, position) => setWindowSize( - windowState.id, + id, { width: ref.offsetWidth, height: ref.offsetHeight }, position ) } - onMouseDown={() => focusWindow(windowState.id)} + onMouseDown={() => focusWindow(id)} dragHandleClassName="win95-title-bar" - disableDragging={windowState.maximized} - enableResizing={!windowState.maximized} + disableDragging={maximized} + enableResizing={!maximized} minWidth={360} minHeight={240} style={{ - display: windowState.minimized ? 'none' : undefined, - zIndex: windowState.zIndex, + display: minimized ? 'none' : undefined, + zIndex, }} > - + {windowState.title} @@ -115,38 +227,34 @@ export default function DesktopWindow({ windowState, containerSize }: Props) { square size="sm" aria-label="Minimize" - onClick={() => toggleMinimize(windowState.id)} + onClick={() => toggleMinimize(id)} > - + Loading...}> setWindowTitle(windowState.id, title)} + windowId={id} + context={context} + setTitle={title => setWindowTitle(id, title)} />