# Window State Machine Architecture

This document describes the architecture of the Notch UI window system - how it manages window phases, frame calculations, and animations while maintaining clean separation from business logic.

## Design Philosophy

**The window layer is like MS Word's document engine** - it doesn't know about paragraphs, images, or formatting. It just knows how to render a canvas of a given size and manage window state.

Similarly, our window layer doesn't know about incidents, escalations, or subscriptions. It receives sizes and renders accordingly. This means:

- A developer adding new content types never touches window code
- A developer working on animations never needs to understand business logic
- Each layer can evolve independently

## Architecture Overview

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                           BUSINESS LOGIC LAYER                               │
│                                                                              │
│  IslandViewModel                                                             │
│  ├── effectiveExpandedSize: CGSize    (what size when open?)                │
│  ├── effectiveCollapsedSize: CGSize   (what size when closed?)              │
│  ├── requestExpand() / requestCollapse() via windowIntent                   │
│  └── Conforms to: WindowSizeProvider protocol                               │
│                                                                              │
│  ContentType enum                                                            │
│  └── preferredSize: CGSize            (fallback before measurement)         │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                         Sizes via │ protocol
                                   │
                    Intent via ────┘ WindowIntentReceiver
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                            WINDOW LAYER                                      │
│                                                                              │
│  WindowPhaseController                                                       │
│  ├── phase: WindowPhase               (collapsed/expanding/expanded/...)    │
│  ├── All frame coordination           (Combine pipelines, frame updates)    │
│  └── Frame-first pattern              (resize before SwiftUI animates)      │
│                                                                              │
│  WindowFrameCalculator                                                       │
│  └── calculateFrame(phase, expandedSize, collapsedSize, screen) -> NSRect   │
│                                                                              │
│  IslandWindowController                                                      │
│  └── Thin glue: creates window, wires components                            │
└─────────────────────────────────────────────────────────────────────────────┘
```

## Core Concepts

### Window Phases

The window has four phases, managed by `WindowPhaseController`:

```
collapsed ──► expanding ──► expanded ──► collapsing ──► collapsed
    │                           │
    └───────────────────────────┘
         (can interrupt)
```

| Phase | Frame | Interactive | Duration |
|-------|-------|-------------|----------|
| `collapsed` | Small (around notch) | No | Indefinite |
| `expanding` | Expanded | No | ~0.15s |
| `expanded` | Expanded | Yes | Indefinite |
| `collapsing` | Expanded→Small | No | ~0.25s |

### Intent-Based Communication

Business logic sends **intents**, not commands:

```swift
// Business logic says WHAT it wants
viewModel.windowIntent?.requestExpand()   // "I want the window open"
viewModel.windowIntent?.requestCollapse() // "I want the window closed"

// Window layer decides HOW and WHEN
// - If already expanded, ignore expand request
// - If mid-collapse, interrupt and expand
// - Handle timing, animations, frame updates
```

### Frame-First Pattern

The window frame must expand BEFORE SwiftUI animates content into it. Otherwise, content clips during the animation.

```
1. User clicks notch
2. WindowPhaseController.requestExpand() called
3. IMMEDIATELY: Window frame expands to target size (synchronous)
4. THEN: Phase changes to .expanding
5. THEN: SwiftUI animates content into the already-sized window
```

This is handled in `WindowPhaseController.transition()`:
```swift
private func transition(to target: WindowPhase) {
    // Frame-first: Update BEFORE phase changes
    updateFrameForTargetPhase(target)

    // Now update phase (triggers SwiftUI animation)
    phase = target
}
```

### Size Provider Protocol

The window layer receives sizes via protocol, not by knowing content types:

```swift
@MainActor
protocol WindowSizeProvider: AnyObject {
    var effectiveExpandedSize: CGSize { get }
    var effectiveCollapsedSize: CGSize { get }
    var measuredContentSizePublisher: Published<CGSize>.Publisher { get }
    func screenDidChange(_ screen: NSScreen)
}
```

`IslandViewModel` conforms to this. The window layer sees only this interface.

## File Reference

| File | Purpose |
|------|---------|
| `WindowPhase.swift` | Phase enum with transition rules and timing |
| `WindowIntent.swift` | Protocols for business↔window communication |
| `WindowPhaseController.swift` | FSM + frame coordination (the brain) |
| `WindowFrameCalculator.swift` | Pure frame calculation from sizes |

## Sizing Deep Dive

### Three Size Concepts

1. **`measuredContentSize`** - Actual size from SwiftUI measurement (ground truth)
2. **`ContentType.preferredSize`** - Fallback estimate before measurement
3. **`effectiveExpandedSize`** - Combined: measured if available, else preferred

### Why `.zero` for Collapsed?

When phase doesn't need expanded frame, we pass `.zero`:
```swift
let size = phase.needsExpandedFrame ? effectiveExpandedSize : .zero
```

`WindowFrameCalculator` interprets `.zero` as "use collapsed frame calculation."

### Collapsed Size Varies

The collapsed bar width changes based on state:
- Normal: notch width (~200px)
- Subscribed to incident: wider bar (~280px)

Business logic knows *why* it's wider. Window layer just receives the size.

## For Business Logic Developers

### Adding a New Content Type

1. Add case to `ContentType` enum
2. Add `preferredSize` for it
3. Build your SwiftUI view
4. Done.

```swift
// In IslandViewModel.swift
enum ContentType {
    // ... existing cases ...
    case newFeature(SomeData)
}

extension ContentType {
    var preferredSize: CGSize {
        switch self {
        // ... existing cases ...
        case .newFeature:
            return CGSize(width: Constants.Island.maxContentWidth, height: 300)
        }
    }
}
```

You never need to touch:
- WindowPhaseController
- WindowFrameCalculator
- IslandWindowController
- Any animation or timing code

### Opening/Closing the Island

```swift
// To open
viewModel.windowIntent?.requestExpand()
viewModel.status = .opened

// To close
viewModel.windowIntent?.requestCollapse()
viewModel.status = .closed
```

### Checking Window State

```swift
// For background tasks that shouldn't interrupt the user
if viewModel.windowAppearsCollapsed {
    // Safe to show notification
}
```

## For Window Layer Developers

### How Frame Updates Work

1. `WindowPhaseController.configure()` connects to size provider
2. Combine pipelines observe `$phase` and `measuredContentSizePublisher`
3. On change: calculate frame via `WindowFrameCalculator`, apply to window
4. Frame-first: `updateFrameForTargetPhase()` runs BEFORE phase changes

### Adding New Phase Behavior

1. Add case to `WindowPhase` enum
2. Define transitions in `canTransition(to:)`
3. Set `autoTransitionDelay` and `autoTransitionTarget` if needed
4. Handle in `WindowPhaseController.requestExpand()`/`requestCollapse()`

You never need to touch:
- ContentType enum
- IslandViewModel business logic
- What incidents, escalations, or events are

## Testing Considerations

### Business Logic Tests
- Mock `WindowIntentReceiver` protocol
- Verify correct intents sent for user actions
- Don't test animation timing

### Window Layer Tests
- Mock `WindowSizeProvider` protocol
- Test phase transitions
- Test frame calculations with known sizes
- Don't test business logic

## Common Issues

### Content Clips During Open Animation
Frame-first pattern isn't working. Check:
1. `updateFrameForTargetPhase()` is called BEFORE `phase = target`
2. Size provider returns correct `effectiveExpandedSize`

### Window Doesn't Resize When Content Changes
Check:
1. `measuredContentSizePublisher` is being observed
2. `updateContentSize()` is being called from SwiftUI
3. Only updates when `status == .opened`

### Collapsed Bar Wrong Width
Check:
1. `effectiveCollapsedSize` returns correct width for current state
2. Business logic is updating `contentType` correctly