NativeDesktop

Widgets

Which element to use, virtual lists, and app-defined native widgets that never echo.

Which element to use, how lists work, and how to add a widget that the framework doesn't ship. For the short list of rules, see donts.md.

Use a built-in first

Every built-in maps to the platform's own control where one exists (an NSButton, an NSSlider, an NSOutlineView) and draws a matching control elsewhere. Pick by intent.

NeedElement
Window structurescaffold, toolbar, sidebar, inspector, titlebar, window_controls, split, tab_view
Navigationsource_list (sections of items), source_tree (flat id/parent nodes, expansion, row actions), tab_strip, command_palette
Buttonsbutton, icon_button, link, button_group
Text inputtext_field, secure_field, search_field, text_area, combo_box
Choicesswitch, checkbox, radio_group, segmented, select, slider, stepper, date_picker, color_well
Settingsform, form_section, form_row, settings_group, switch_row, sheet_layout
Feedbackprogress, spinner, toast, banner, status_page, window.alert(..)
Datalist, table, grid, tree (virtual, see below)
Mediaimage, avatar, seek_bar, player_bar, lyrics_view, shelf, chat_bubble, composer
Surfacesglass, glass_group, webview (feature webview), native_surface

Icons come from the Icon enum (Lucide names, SF Symbols on macOS). icon(Icon::Moon, 12.) shows one on its own, tinted by text_color. Don't use inline SVG (inline-svg).

Stateful elements take an id in their constructor: text_field("title"), list("notes", ..), webview("page", &page). The id keeps the native view, its focus, its IME state, and its scroll position when a sibling appears or disappears above it. Give every interactive element an id or a label. The id is its test id and the label is its accessible name, so both tests and screen readers can find it.

Lists: virtual, keyed, never a scroll div

Anything that can grow past 50 rows is a list() (or table, grid, tree) over a ListState<K>. Rows render only near the viewport. On macOS, the presenter is a real NSTableView.

pub struct Inbox {
    messages: Entity<Store>,
    list: ListState<MessageId>,
}

impl Inbox {
    fn new(store: Entity<Store>, cx: &mut Context<Self>) -> Self {
        let mut list = ListState::new(cx).follow(Follow::Tail).estimated_row_height(px(44.));
        list.set_items_versioned(store.read(cx).rows(), cx);
        cx.observe(&store, |this, store, cx| {
            let rows = store.read(cx).rows();
            this.list.set_items_versioned(rows, cx);
        }).detach();
        Self { messages: store, list }
    }

    fn row(&mut self, id: MessageId, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let message = self.messages.read(cx).get(id);
        text(message.subject.clone())
    }
}

// in render:
list("messages", &self.list, cx.processor(Self::row))
    .selection(Selection::single())
    .on_select(cx.listener(|this, e: &SelectEvent<MessageId>, _, cx| this.open(e.key, cx)))

Don't:

div().overflow_y_scroll().children(messages.iter().map(|m| row(m)))

That builds every row on every render, loses the scroll anchor on updates, and fails scroll-list.

The app owns sorting. table(..) reports SortChanged, and the app re-sorts its keys and calls set_items again. Expansion in tree/source_tree is app state too, so an unrelated render never collapses a branch that the user opened.

App-defined native widgets

A widget that the framework doesn't ship (a map, a gauge, a platform picker) implements the same NativeWidget trait that every built-in uses, and you call it like any built-in:

pub struct MapWidget;

impl NativeWidget for MapWidget {
    type Props = MapProps;   // Clone + PartialEq: only changed props cross to the backend
    type Event = MapEvent;   // std::convert::Infallible when it emits nothing

    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
    }
}

Status: the trait and the element shape are fixed. Backends don't mount app-defined widgets yet (nd::native is at its call-site stage), so check the widget in a real window before you rely on it.

Native callbacks never call app code

AppKit fires delegates from inside setters (setRegion calls regionDidChange, selectRowIndexes calls selectionDidChange). In an nd widget:

// update(): set only when the control does not already hold the value.
state.viewport.borrow_mut().apply(&(new.center, new.zoom), |&(center, zoom)| {
    cx.programmatic(|| unsafe { map.setRegion_animated(region_for(center, zoom), true) });
    read_back(map) // what the control holds now, after it normalised the value
});

// delegate: record what the user produced, then emit it.
self.ivars().viewport.borrow_mut().report((center, zoom));
self.ivars().emit.emit(MapEvent::RegionChanged(RegionChanged { center, zoom }));

apply skips the setter when the value equals what the control holds (the last applied or last reported value). When the control changes the value (a map fitting a region to its aspect ratio, a slider snapping to a step), apply returns the normalized value so that the widget can report it. An app that feeds the reported value back causes no second set and no loop. To test it, set a value that the control normalizes from inside the widget's own event handler, and assert one event and one set.

Native surfaces

webview(id, &page) shows a WebView that the app owns. Unmounting parks the page instead of reloading it. native_surface(id, SurfaceSource::..) hosts other platform views. Both sit in the same layout and z-order as everything else, and overlays above them get the input instead of the page.