# Native widgets

When `div`, `text`, and `canvas` are enough, write a widget as a plain function or a `RenderOnce`
struct. Implement `NativeWidget` when the OS has a real control that you want, such as a map, or
when the widget must be a native view to behave like one. The framework handles layout, focus, and
z-order the same way for both kinds, and a call site can't tell your widget from a built-in one.
Every built-in control in `nd-components` uses 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://native.formalsnake.dev/concepts/swift.md)). On other platforms, nd draws both.

On AppKit, a widget with an objc2 or Swift implementation mounts as its own view. The backend
frames, measures, and updates that view like a built-in control. Everywhere else, and on AppKit
when a Swift widget's symbols aren't 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/FormalStudio/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 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/FormalStudio/NativeDesktop/blob/main/examples/native-widget/src/map_view/mod.rs), line 49.</sub>


- nd compares `Props` with `PartialEq` on every render, and touches the native side only when
  the props 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
  instead of an opaque view.
- `drawn` is the fallback for the web and for every backend that has no native version. It's an
  ordinary element tree, and it can read `cx.theme()`.
- `backends` registers the native versions. `Backends::drawn_only()` is the default.
  `.appkit(..)` adds an AppKit version on macOS, and `.swift(..)` adds a Swift version.

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. For the full set, see
[map_view/mod.rs](https://github.com/FormalStudio/NativeDesktop/blob/main/examples/native-widget/src/map_view/mod.rs). 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/FormalStudio/NativeDesktop/blob/main/examples/native-widget/src/main.rs), line 65.</sub>


## The drawn fallback

The fallback in [drawn.rs](https://github.com/FormalStudio/NativeDesktop/blob/main/examples/native-widget/src/map_view/drawn.rs) uses a `canvas`
to paint a 3x3 block of OpenStreetMap tiles around the center. It draws the pins on top and pans
when you drag. It's a real map, with the attribution that OpenStreetMap requires, and the web and
Linux builds ship it. Write the fallback 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 the 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/FormalStudio/NativeDesktop/blob/main/examples/native-widget/src/map_view/appkit.rs), line 31.</sub>


`AppKitCx` gives you the main-thread marker, `emitter()` for delegates that the view installs, and
`programmatic(..)`. `State` is whatever you want to keep beside the view. Here, it's 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/FormalStudio/NativeDesktop/blob/main/examples/native-widget/src/map_view/appkit.rs), line 59.</sub>


`measure` returns the view's natural size. A map has no natural size, 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 in the middle of an
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/FormalStudio/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
when the user pans, the map emits `RegionChanged`, which the app feeds back in. Without care, that
loops. MapKit makes it worse, because it fits any region to its aspect ratio, so the region that 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 that was applied or
  reported.
- `report` records what the control produced.
- The setter runs inside `cx.programmatic(..)`, so nd drops the callback that it triggers.

When the app feeds the reported region back, no second `setRegion` happens.

```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/FormalStudio/NativeDesktop/blob/main/examples/native-widget/src/map_view/appkit.rs), line 20.</sub>


Do the same for any prop that 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. The native-widget package is named
`places`, and an `m6` feature gates the binary:

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

nd re-exports the `objc2` types under `nd::native::appkit`, so you don't need an extra dependency
for AppKit itself.
