# Native widgets

Write a widget as a plain function or `RenderOnce` struct when `div`, `text` and `canvas` are
enough. Implement `NativeWidget` when the OS has a real control you want, such as a map, or when
the widget must be a native view to behave like one. The framework lays out, focuses and
z-orders both kinds the same way, and a call site cannot tell your widget from a built-in. Every
built-in control in `nd-components` is written against this same trait.

The Places example has a map and a gauge. On macOS the map is MapKit's `MKMapView` through
objc2 and the gauge is a SwiftUI `Gauge` ([Swift](https://nativedesktop.formalsnake.dev/concepts/swift.md)). Elsewhere both are drawn.

On AppKit a widget with an objc2 or Swift implementation mounts as its own view, which the
backend frames, measures and updates like a built-in control. Everywhere else, and on AppKit
when a Swift widget's symbols are not in the binary, the element mounts its drawn tree inside a
container that takes the element's id, layout style and semantics.

> **Example: Places.** Source in [examples/native-widget](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/native-widget). Live on the web and captured on macOS, Linux and Windows in the docs app.


## The trait

A widget names its props, its events, what assistive technology and automation should see, and
its implementations:

```rust
pub struct MapWidget;

impl NativeWidget for MapWidget {
    type Props = MapProps;
    type Event = MapEvent;

    fn semantics(props: &MapProps) -> Semantics {
        Semantics::new(Role::Group).label("Map").value(format!("{} places", props.pins.len()))
    }

    fn drawn(props: &MapProps, emit: Emit<MapEvent>, window: &mut Window, cx: &mut App) -> AnyElement {
        drawn::render(props, emit, window, cx)
    }

    fn backends() -> Backends<Self> {
        let backends = Backends::drawn_only();
        #[cfg(target_os = "macos")]
        let backends = backends.appkit(appkit::MapKit);
        backends
    }
}
```

<sub>From [examples/native-widget/src/map_view/mod.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/native-widget/src/map_view/mod.rs), line 49.</sub>


- `Props` is compared with `PartialEq` on every render; the native side is touched only when it
  changed.
- `Event` is what the widget can tell the app. A widget with no events uses
  `std::convert::Infallible`.
- `semantics` feeds the semantic tree, so tests and the MCP server see a "Map" with a place count
  and not an opaque view.
- `drawn` is the fallback for every backend that has no native version, and for the web. It is
  an ordinary element tree and can read `cx.theme()`.
- `backends` registers the native versions. `Backends::drawn_only()` is the default;
  `.appkit(..)` adds an AppKit one on macOS and `.swift(..)` a Swift one.

An element type wraps the props and implements `NativeElement` (`base_mut` and `into_parts`).
`NativeBase<W>` carries the id, layout style and event handlers, and `#[derive(IntoElement)]`
mounts it. Builder methods such as `.zoom(..)` and `.pins(..)` set props, and
`.on_region_change(..)` registers an event handler. See
[map_view/mod.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/native-widget/src/map_view/mod.rs) for the full
set; the call site reads like any built-in:

```rust
.content(
    map_view("map", self.center)
        .zoom(self.zoom)
        .pins(pins)
        .size_full()
        .on_region_change(cx.listener(|this, e: &RegionChanged, _window, cx| {
            this.center = e.center;
            this.zoom = e.zoom;
            cx.notify();
        }))
        .on_pin_select(cx.listener(|this, e: &PinSelected, _window, cx| {
            if let Some(place) = this.places.iter_mut().find(|p| p.name == e.title.as_ref()) {
                place.visited = true;
                cx.notify();
            }
        })),
)
```

<sub>From [examples/native-widget/src/main.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/native-widget/src/main.rs), line 65.</sub>


## The drawn fallback

The fallback in [drawn.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/native-widget/src/map_view/drawn.rs) paints a 3x3
block of OpenStreetMap tiles around the centre with a `canvas`, draws the pins on top and pans on
drag. It is a real map, with the attribution OpenStreetMap requires, and it is the version the web
and Linux builds ship. Write it first: it defines the props and events, and the native version
then has to match them.

## The AppKit side

`AppKitWidget` creates an `NSView`, updates it when props change and reports its size:

```rust
fn create(&self, props: &MapProps, cx: &mut AppKitCx<MapWidget>) -> (Retained<MKMapView>, Self::State) {
    let map = unsafe { MKMapView::new(cx.mtm()) };
    let delegate = MapDelegate::new(cx.emitter(), cx.mtm());
    // MKMapView holds its delegate weakly; the node state keeps it alive.
    unsafe { map.setDelegate(Some(ProtocolObject::from_ref(&*delegate))) };
    apply(&map, &delegate, None, props, cx);
    (map, delegate)
}
```

<sub>From [examples/native-widget/src/map_view/appkit.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/native-widget/src/map_view/appkit.rs), line 31.</sub>


`AppKitCx` gives the main-thread marker, `emitter()` for delegates the view installs, and
`programmatic(..)`. `State` is whatever you want kept beside the view; here it is the delegate,
because `MKMapView` holds its delegate weakly. `update` receives the old and new props and applies
the difference:

```rust
fn apply(
    map: &MKMapView,
    delegate: &MapDelegate,
    old: Option<&MapProps>,
    new: &MapProps,
    cx: &mut AppKitCx<MapWidget>,
) {
    let viewport: Viewport = (new.center, new.zoom);
    delegate.ivars().viewport.borrow_mut().apply(&viewport, |&(center, zoom)| {
        let region = MKCoordinateRegion { center: to_cl(center), span: span_for_zoom(zoom) };
        // Programmatic: the regionDidChange this triggers is reported, not emitted.
        cx.programmatic(|| unsafe { map.setRegion_animated(region, old.is_some()) });
        shown(map)
    });
    if old.is_none_or(|o| o.pins != new.pins) {
        set_pins(map, &new.pins);
    }
}
```

<sub>From [examples/native-widget/src/map_view/appkit.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/native-widget/src/map_view/appkit.rs), line 59.</sub>


`measure` returns the view's natural size. A map has none, so it returns zero and the app sizes it
with flex or explicit dimensions.

## Callbacks and controlled values

Two rules keep a native view from fighting the app's state.

No native callback calls app code. AppKit invokes delegates inside setters and layout:
`setRegion` fires `regionDidChange` before it returns, while the app's view is mid-update.
`Emit` queues the event, and the queue drains after the update, outside any borrow of the app's
state. Call `emit(..)` from a delegate and never run app code there.

```rust
define_class!(
    #[unsafe(super(NSObject))]
    #[thread_kind = MainThreadOnly]
    #[ivars = MapIvars]
    pub struct MapDelegate;

    unsafe impl NSObjectProtocol for MapDelegate {}

    unsafe impl MKMapViewDelegate for MapDelegate {
        #[unsafe(method(mapView:regionDidChangeAnimated:))]
        fn region_did_change(&self, map: &MKMapView, _animated: bool) {
            let (center, zoom) = shown(map);
            // Busy while `apply` runs the setter, which records what the map shows itself.
            if let Ok(mut viewport) = self.ivars().viewport.try_borrow_mut() {
                viewport.report((center, zoom));
            }
            // Queued, never delivered inside this callback; dropped if a programmatic set caused it.
            self.ivars().emit.emit(MapEvent::RegionChanged(RegionChanged { center, zoom }));
        }

        #[unsafe(method(mapView:didSelectAnnotation:))]
        fn did_select(&self, _map: &MKMapView, annotation: &ProtocolObject<dyn MKAnnotation>) {
            if let Some(title) = unsafe { annotation.title() } {
                self.ivars().emit.emit(MapEvent::PinSelected(PinSelected { title: title.to_string().into() }));
            }
        }
    }
);
```

<sub>From [examples/native-widget/src/map_view/appkit.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/native-widget/src/map_view/appkit.rs), line 97.</sub>


A controlled value never echoes. The map is controlled: the app owns `center` and `zoom`, and the
user's pan emits `RegionChanged`, which the app feeds back in. Without care that loops, and
MapKit makes it worse by fitting any region to its aspect ratio, so the region it reports is
never the one you set. `Synced<T>` holds what the native control holds. `apply` calls the setter
only when the new value differs from the last one applied or reported, `report` records what the
control produced, and the setter runs inside `cx.programmatic(..)` so the callback it triggers is
dropped. Feeding the reported region back then causes no second `setRegion`.

```rust
/// What the map shows, in the props' own terms. MKMapView fits any region to its aspect ratio,
/// so the region it reports differs from the one we set; `Synced` remembers the reported one and
/// the app feeding it back causes no second `setRegion`.
type Viewport = (LatLng, f64);
```

<sub>From [examples/native-widget/src/map_view/appkit.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/native-widget/src/map_view/appkit.rs), line 20.</sub>


Do the same for any prop the user can also change: a slider's step snapping and a split divider's
clamp are the same case.

## Build and run

The map crate depends on `objc2-map-kit` only for macOS, and the native-widget package is named
`places` with an `m6` feature that gates the binary:

```sh
cargo run -p places --features m6
```

`objc2` types are re-exported under `nd::native::appkit`, so no extra dependency is needed for
AppKit itself.
