ryOS ryOS / Docs
GitHub Launch

Window Management

ryOS implements a sophisticated multi-instance window manager with theme-aware chrome, animations, and desktop-like behaviors.

Architecture Overview

graph TB
    subgraph "State Layer"
        AppStore[useAppStore
Zustand Store] end subgraph "Logic Layer" WM[useWindowManager
Hook] WI[useWindowInsets
Hook] end subgraph "Presentation Layer" WF[WindowFrame
Component] AR[appRegistry
Config] end subgraph "App Layer" App1[App Component 1] App2[App Component 2] AppN[App Component N] end AppStore --> WM AppStore --> WF AR --> WM AR --> WF WI --> WM WI --> WF WM --> WF WF --> App1 WF --> App2 WF --> AppN

Core Components

WindowFrame Component

The WindowFrame component (src/components/layout/WindowFrame.tsx) renders window chrome, handles user interactions, and provides animations.

Props Interface

interface WindowFrameProps {
  children: React.ReactNode;
  title: string;
  onClose?: () => void;
  isForeground?: boolean;
  appId: AppId;
  isShaking?: boolean;
  /** Window material: "default" (opaque), "transparent", "notitlebar" (immersive), "brushedmetal" (macOS aluminum) */
  material?: "default" | "transparent" | "notitlebar" | "brushedmetal";
  skipInitialSound?: boolean;
  windowConstraints?: {
    minWidth?: number;
    minHeight?: number;
    maxWidth?: number | string;
    maxHeight?: number | string;
  };
  instanceId?: string;
  onNavigateNext?: () => void;
  onNavigatePrevious?: () => void;
  interceptClose?: boolean;
  menuBar?: React.ReactNode;
  keepMountedWhenMinimized?: boolean;
  onFullscreenToggle?: () => void;
  disableTitlebarAutoHide?: boolean;
  titleBarRightContent?: React.ReactNode;
}

Material Modes

ModeDescriptionUse Case
defaultStandard opaque window backgroundMost apps
transparentSemi-transparent backgroundiPod, Photo Booth
notitlebarImmersive mode with floating titlebar on hoverVideos, games
brushedmetalmacOS brushed aluminum surfaceTerminal, Synth

useWindowManager Hook

The useWindowManager hook (src/hooks/useWindowManager.ts) manages window positioning, dragging, and resizing.

interface UseWindowManagerProps {
  appId: AppId;
  instanceId?: string;
}

// Returns
interface UseWindowManagerReturn {
  windowPosition: { x: number; y: number };
  windowSize: { width: number; height: number };
  isDragging: boolean;
  resizeType: ResizeType;
  handleMouseDown: (e: MouseEvent) => void;
  handleResizeStart: (e: MouseEvent, type: ResizeType) => void;
  setWindowSize: (size: WindowSize) => void;
  setWindowPosition: (pos: WindowPosition) => void;
  maximizeWindowHeight: (maxConstraint?: number) => void;
  getSafeAreaBottomInset: () => number;
  snapZone: "left" | "right" | null;
  computeInsets: () => WindowInsets;
}

Window Instance State

Each window instance maintains comprehensive state:

interface AppInstance {
  instanceId: string;        // Unique identifier (numeric string, e.g., "12")
  appId: AppId;              // App type identifier
  isOpen: boolean;           // Visibility state
  isForeground: boolean;     // Focus state
  isMinimized?: boolean;     // Dock/taskbar minimize state
  isLoading?: boolean;       // For lazy-loaded apps
  position?: { x: number; y: number };
  size?: { width: number; height: number };
  title?: string;            // Static title set at creation
  displayTitle?: string;     // Dynamic title (updated by WindowFrame)
  createdAt: number;         // For stable ordering in taskbar
  initialData?: unknown;     // App-specific data passed at launch
  launchOrigin?: {           // Source icon rect for open-from-icon animation
    x: number;
    y: number;
    width: number;
    height: number;
  };
}

Window Lifecycle

State Transitions

stateDiagram-v2
    [*] --> Loading: launchApp()
    Loading --> Normal: App loaded
    Normal --> Minimized: minimize()
    Minimized --> Normal: restore()
    Normal --> Maximized: maximize()
    Maximized --> Normal: restore()
    Normal --> Foreground: bringToFront()
    Foreground --> Background: Another window focused
    Background --> Foreground: bringToFront()
    Normal --> [*]: closeApp()
    Minimized --> [*]: closeApp()
    Maximized --> [*]: closeApp()

Creation Flow

sequenceDiagram
    participant U as User
    participant S as useAppStore
    participant R as appRegistry
    participant WF as WindowFrame
    participant App as App Component
    
    U->>S: launchApp(appId)
    S->>S: createAppInstance()
    S->>S: Generate instanceId
    S->>S: Calculate cascade position
    S->>R: Get app config
    R-->>S: Component, constraints
    S->>WF: Render WindowFrame
    WF->>WF: Play window open sound
    WF->>App: Mount app component
    App->>S: markInstanceAsLoaded()

Cascade Positioning

New windows are automatically positioned with an offset to prevent exact overlap:

const computeDefaultWindowState = () => {
  const appIndex = appIds.indexOf(appId);
  const offsetIndex = appIndex >= 0 ? appIndex : 0;

  return {
    position: {
      x: isMobile ? 0 : 16 + offsetIndex * 32,
      y: isMobile ? 28 : 40 + offsetIndex * 20,
    },
    size: isMobile ? getMobileWindowSize(appId) : config.defaultSize,
  };
};

Z-Index Management

Z-index is calculated from position in the instanceOrder array. The last item in the array is the topmost window.

graph TB
    subgraph instanceOrder["instanceOrder Array"]
        direction TB
        A["[0] finder-1"] --> B["[1] textedit-2"]
        B --> C["[2] ipod-3"]
        C --> D["[3] chats-4 ← foreground"]
    end
    
    subgraph zIndex["Z-Index Stack (visual)"]
        direction TB
        Z4["z:103 chats-4 (top)"]
        Z3["z:102 ipod-3"]
        Z2["z:101 textedit-2"]
        Z1["z:100 finder-1 (bottom)"]
    end
    
    instanceOrder -.->|"z = BASE + index"| zIndex

Bringing to Front

bringInstanceToForeground: (instanceId) => {
  set((state) => {
    const instances = { ...state.instances };
    let order = [...state.instanceOrder];
    
    // Update foreground flags
    Object.keys(instances).forEach((id) => {
      instances[id] = {
        ...instances[id],
        isForeground: id === instanceId,
      };
    });
    
    // Move to end of order array (top of stack)
    order = [...order.filter((id) => id !== instanceId), instanceId];
    
    return { instances, instanceOrder: order, foregroundInstanceId: instanceId };
  });
}

Window Constraints

Apps define window constraints in the app registry:

interface WindowConstraints {
  minSize?: { width: number; height: number };
  maxSize?: { width: number; height: number };
  defaultSize: { width: number; height: number };
  mobileDefaultSize?: { width: number; height: number };
  mobileSquare?: boolean;  // If true, height = width on mobile
}
PropertyDescription
minSizeMinimum window dimensions during resize
maxSizeMaximum window dimensions
defaultSizeInitial size on desktop
mobileDefaultSizeInitial size on mobile devices
mobileSquareForce square aspect ratio on mobile

Resize Behavior

Resize Handles

WindowFrame renders 8 resize handles (N, S, E, W, NE, NW, SE, SW) as invisible hit areas:

// Handle positions expand during active resize for smoother tracking
const handleClass = cn(
  "absolute cursor-n-resize pointer-events-auto",
  resizeType?.includes("n")
    ? "top-[-100px] h-[200px]"  // Expanded during resize
    : "top-1 h-2"               // Normal state
);

Double-Click Behaviors

TargetAction
Top/bottom resize handleMaximize height only
Title barToggle full maximize
Side resize handleNo special action

Snap-to-Edge

Windows can snap to screen edges during drag:

// Detect snap zones - trigger when cursor within 20px of edge
const SNAP_THRESHOLD = 20;

if (clientX <= SNAP_THRESHOLD) {
  setSnapZone("left");
} else if (clientX >= window.innerWidth - SNAP_THRESHOLD) {
  setSnapZone("right");
} else {
  setSnapZone(null);
}

On drag end:

  • Window snaps to half-screen width
  • Pre-snap state saved for restore
  • Visual indicator shown during drag

Theme-Specific Chrome

WindowFrame renders different title bars based on the active theme:

macOS (Aqua) Theme

  • Traffic light buttons (close/minimize/zoom) on LEFT
  • Centered title text
  • Pinstripe background texture
  • Rounded corners

macOS (System 7) Theme

  • Close box only on LEFT
  • Left-aligned title
  • Dotted pattern background

Windows (XP/98) Theme

  • Title bar with icon
  • Minimize/maximize/close buttons on RIGHT
  • Gradient title bar background
  • System font styling
graph LR
    subgraph Mac["Mac Themes"]
        M1[Traffic Lights - Left]
        M2[Centered Title]
        M3[Pinstripe/Pattern]
    end
    
    subgraph Windows["Windows Themes"]
        W1[System Buttons - Right]
        W2[Icon + Left Title]
        W3[Gradient Title Bar]
    end

Animations

WindowFrame uses Framer Motion for all animations:

Open Animation

initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.15 }}

Close Animation

exit={{ scale: 0.95, opacity: 0 }}
transition={{ duration: 0.15 }}

Minimize Animation

// Animate toward dock icon position
exit={{
  scale: 0.1,
  opacity: 0,
  x: dockIconPosition.x - windowCenter.x,
  y: dockIconPosition.y - windowCenter.y,
}}

Shake/Nudge Animation

// Horizontal shake for alerts
animate={{
  x: [0, -4, 4, -4, 4, -2, 2, 0],
}}
transition={{ duration: 0.4 }}

Sound & Haptic Feedback

Every window operation has corresponding feedback:

OperationSoundHaptic
OpenWINDOW_OPENLight
CloseWINDOW_CLOSELight
MaximizeWINDOW_EXPANDMedium
RestoreWINDOW_COLLAPSEMedium
MinimizeWINDOW_ZOOM_MINIMIZELight
Start dragWINDOW_MOVE_MOVINGNone
End dragWINDOW_MOVE_STOPLight
Start resizeWINDOW_RESIZE_RESIZINGNone
End resizeWINDOW_RESIZE_STOPLight

Multi-Instance Apps

Only specific apps support multiple windows:

const supportsMultiWindow =
  multiWindow ||
  appId === "textedit" ||
  appId === "finder" ||
  appId === "applet-viewer";

When launching a multi-instance app:

  • If no instance exists: Create new instance
  • If instance exists but minimized: Restore existing
  • If instance exists and visible: Create new instance (if supported)

Expose Mode (Mission Control)

Windows can enter Expose mode for an overview of all open windows. This is managed via useAppStore:

// In useAppStore
exposeMode: boolean;
setExposeMode: (v: boolean) => void;

When expose mode is active, windows are arranged in a grid layout:

const exposeTransform = useMemo(() => {
  if (!exposeMode || !instanceId) return null;
  
  const openInstances = Object.values(instances)
    .filter(inst => inst.isOpen && !inst.isMinimized);
  const myIndex = openInstances.findIndex(inst => inst.instanceId === instanceId);
  
  if (myIndex === -1 || openInstances.length === 0) return null;
  
  const grid = calculateExposeGrid(
    openInstances.length,
    window.innerWidth,
    window.innerHeight,
    60, // padding
    24, // gap
    isMobile
  );
  
  return getExposeTransform(
    windowPosition.x,
    windowPosition.y,
    windowSize.width,
    windowSize.height,
    myIndex,
    grid,
    window.innerWidth,
    window.innerHeight
  );
}, [exposeMode, instanceId, instances, windowPosition, windowSize, isMobile]);

Windows can be selected in expose mode by clicking on them, which emits a typed exposeWindowSelect app event and then brings the selected instance to foreground while exiting expose mode.

Close Interception

Apps can intercept close to show confirmation dialogs:

// In app component
useEffect(() => {
  if (!interceptClose) return;

  const handlePerformClose = () => performClose();
  
  window.addEventListener(
    `closeWindow-${instanceId}`,
    handlePerformClose
  );

  return () => {
    window.removeEventListener(`closeWindow-${instanceId}`, handlePerformClose);
  };
}, [instanceId, performClose, interceptClose]);

Apps set interceptClose={true} on WindowFrame and listen for the close event to show unsaved changes dialogs.

In addition, app-level crash isolation is handled outside WindowFrame: each rendered app instance is wrapped with AppErrorBoundary in AppManager, so a runtime crash in one window presents a crash dialog with both Relaunch and Quit options without crashing the desktop. The crash dialog adapts to the current OS theme and reports errors via reportRuntimeCrash.

Mobile Adaptations

AdaptationDescription
Full-width windowsWindows stretch to screen width on mobile
Touch-sized handlesLarger resize handle hit areas
Swipe navigationHorizontal swipe between apps (phone only)
Mobile default sizesPer-app optimized dimensions
Simplified controlsReduced window chrome on small screens

Related Documentation