cocoanut 0.2.0

A minimal, declarative macOS GUI framework for Rust
# Cocoanut Architecture

## Overview

```
┌─────────────────────────────────────────┐
│            User Code (Rust)             │
│                                         │
│  app("My App")                          │
│    .build()                             │
│    .root(View::vstack()                 │
│      .child(View::text("Hello"))        │
│      .child(View::button("Click")))     │
│    .run()                               │
├─────────────────────────────────────────┤
│  lib.rs      → exports & prelude          │
│  view.rs     → View + ViewKind enum       │
│  app.rs      → App + Appearance lifecycle │
│  renderer.rs → View tree → AppKit         │
│  event.rs    → Callback registry + ObjC   │
│  state.rs    → Reactive State<T>          │
│  menu.rs     → Menu + MenuItem            │
│  error.rs    → CocoanutError + Result     │
├─────────────────────────────────────────┤
│            AppKit (macOS)               │
│  NSApplication, NSWindow, NSButton, ... │
└─────────────────────────────────────────┘
```

## Design Principles

Learned from [swiftui-rs](../swiftui-rs):

- **KISS** — One `View` type, one `ViewKind` enum. Not 23 separate structs.
- **DRY** — Single renderer maps all view kinds to AppKit. No duplication.
- **SoC**`view.rs` defines *what* to show. `renderer.rs` decides *how*.

## Components

### `view.rs` — The Core Type

Everything is a `View`:

```rust
pub struct View {
    kind: ViewKind,       // What kind of view
    children: Vec<View>,  // Composable children
    style: ViewStyle,     // Visual properties
    event: Option<EventBinding>,  // Interaction
}
```

`ViewKind` is a single enum with all 26 view types (Text, Button, VStack, Slider, DatePicker, etc.).

Views are created with static constructors and composed with `.child()`:

```rust
View::vstack()
    .child(View::text("Hello").bold())
    .child(View::button("OK").on_click(1))
```

### `renderer.rs` — Single Rendering Pass

One function walks the view tree and creates AppKit NSViews:

```rust
pub fn render(root: &View, content_view: *mut Object, bounds: NSRect) -> Result<()>
```

Each `ViewKind` maps to exactly one AppKit class. Layout containers (VStack, HStack) calculate child positions and recurse.

### `app.rs` — Lifecycle

Builder pattern for app creation:

```rust
app("Title").size(800.0, 600.0).build().root(view_tree).run()
```

`run()` initializes NSApplication, creates NSWindow, calls `renderer::render()`, and starts the event loop.

### `event.rs` — Callback System

Global callback registry + custom ObjC class:

```rust
event::register(1, || println!("clicked!"));
// Renderer wires NSButton target/action → CocoanutActionHandler → dispatch_by_tag → closure
```

### `state.rs` — Reactive State

```rust
let count = counter_state(1, 2, 3); // auto-wires +1/-1/reset callbacks
count.on_change(|v| println!("Count: {}", v));
bind_label(&count, 100, |v| format!("Count: {}", v)); // updates NSTextField with tag 100
// In the view tree: View::label("Count: 0").tag(100)
```

### `menu.rs` — Menu System

Declarative menu construction with action dispatch:

```rust
Menu::new("File")
    .item(MenuItem::new("Open").shortcut("o").on_action(100))
    .separator()
    .item(MenuItem::new("Quit").shortcut("q"))
```

### `error.rs` — Error Handling

Single `CocoanutError` enum with `thiserror` derive. `Result<T>` type alias.

## Data Flow

```
View::vstack().child(View::text("Hi"))
  → View { kind: VStack, children: [View { kind: Text("Hi") }] }
  → renderer::render() walks tree recursively
  → VStack calculates child bounds, recurses
  → Text creates NSTextField, adds to parent NSView
  → NSApplication.run() starts event loop
```

## Key Decisions

- **View tree over individual component structs** — simpler, composable, no duplication
- **Single renderer** — all AppKit mapping in one file, easy to maintain
- **ObjC target/action for events** — one custom class dispatches all control actions to Rust closures
- **Reactive State<T>** — generic state container with listener pattern, no full re-render needed
- **Dark mode via NSAppearance** — per-window appearance, switchable at runtime
- **JSON serializable**`ViewDesc` enables debugging and potential bridging
- **test-mock feature** — renderer validates tree structure without AppKit in tests
- **Minimal deps** — only objc, cocoa, thiserror, serde, serde_json

## File Summary

| File | Lines | Purpose |
|------|-------|---------|
| `lib.rs` | ~45 | Exports, prelude |
| `view.rs` | ~600 | View, ViewKind (26 types), ViewStyle, ViewDesc |
| `renderer.rs` | ~600 | View tree → AppKit NSViews |
| `app.rs` | ~220 | App lifecycle, Appearance, dark mode |
| `event.rs` | ~150 | Callback registry, ObjC action handler |
| `state.rs` | ~180 | Reactive State<T>, bind_label |
| `menu.rs` | ~160 | Menu bar with action dispatch |
| `error.rs` | ~65 | Error types |
| **Total** | **~2020** | |

Previous version had 114 files / 26,878 lines. This is a **92% reduction**.