Skip to main content

Crate frust_native_widgets

Crate frust_native_widgets 

Source
Expand description

frust-native-widgets: render REAL platform widgets (Android Views / UIKit views / AppKit views) from pure Rust — native_button("Save"), native_switch(...) etc. compose the framework’s platform_view slots for placement, while this plugin creates and mutates the actual native views through direct, same-thread FFI: with_jni_env + hand-curated JNI bindings on Android, objc2-ui-kit on iOS (the factory itself a Rust define_class! class — zero Swift), and objc2-app-kit on macOS (a Rust frust_plugin::desktop::DesktopViewFactory the desktop Mode-A host resolves by view_type). This crate holds the retained-handle [registry] every control’s create/update/dispose path is built over, the runtime those paths dispatch through, the controls — eight shared (Button, Label, Switch, Slider, ProgressBar, Image, Spinner, DatePicker), the two iOS/macOS-only ones (Segmented, Stepper) and the iOS-only TabBar — that runtime serves, the typed events vocabulary their listeners decode into, the app-facing api builders, and the three platform arms’ factory glue. Each shared control carries one platform half per arm (Segmented/ Stepper have no Android half and TabBar has only the iOS one — their builders render a refusal banner elsewhere) — a #[cfg(target_os = "android")] mod platform, a #[cfg(target_os = "ios")] mod platform and a #[cfg(target_os = "macos")] mod platform, side by side in the same file, executing the same shared setter plan and reporting a tap/toggle/drag back to the app through the same event dispatch on every platform (crate::apple::events‘s and crate::appkit::events’ Rust target-action objects, mirroring Android’s shared listener). The macOS arm registers every kind iOS does except the iOS-only TabBar (macOS has no bottom-tab-bar idiom). The theme ladder’s two Apple arms are in: L1 pins brightness per view (crate::apple::theme’s overrideUserInterfaceStyle, crate::appkit::theme’s NSAppearance), re-pinned on every update; L2 applies the same folded Props tokens through typed objc2-ui-kit/objc2-app-kit/CALayer setters, including a themed background’s corner radius; L3 (crate::coretext, shared by both arms) resolves the embedded Glyph faces to a real CTFont via CoreText, degrading to the system font (one logged warning) on any resolution failure.

§One factory, one listener, N controls

Adding a control never adds Kotlin or Swift — and that holds for your components too: NativeComponent is the public trait a plugin author implements to drive a native view (or view hierarchy) from pure Rust, registered with register_component and served by the very same runtime, factory and listener as the built-in controls (see the component module’s own doc for the lifecycle contract; it ships with the frust-api feature, like the builders above).

One limit that doc states in full, repeated here because it decides whether the trait is for you at all. An app crate cannot implement it today: create has to name jni::objects::JObject on Android and objc2-ui-kit’s classes on iOS in the implementing crate, and this plugin re-exports neither FFI crate, so the practical audience today is plugin authors, not app authors (the only implementing type here is this crate’s own non-default demo-components composite).

A component hears its own views the way the built-in controls do: it attaches the platform’s one listener to any view it built (root or child) with ComponentCtx::attach_listener, the listener is bound to the slot’s own id by the context (never handed to the component), and the event reaches NativeComponent::on_event through the same slot-id routing — whose answer the app hears on NativeComponentView::on_event, the builders’ events-as-signals idiom (the component module doc’s Listener attachment).

Every control is a Rust NativeWidget impl registered under a kind string in the plugin-internal runtime, and the platform side is fixed forever at ONE generic factory class plus ONE generic listener class (Android: plugins/native-widgets/platform/android/, driven by this crate’s four JNI exports; iOS: a Rust define_class! factory registered straight into the Objective-C runtime, crate::apple::factory — zero Swift, and target -action instead of a listener class; macOS: a Rust DesktopViewFactory registered with frust_plugin::desktop, crate::appkit::factory, plus one target-action class). Which control a platform_view slot means travels in that slot’s params_json, under two reserved keys the api layer injects — the same payload that carries the differ’s slot id across a factory contract that does not pass it.

§Native presentations

Modal platform UI — an alert today — is not a platform_view slot: the differ owns a slot’s lifetime and knows no modal stacking, while a modal belongs to the host (the resumed Android Activity, the topmost iOS view controller, the macOS key window). The present module therefore serves it imperatively: present::show_alert answers a Presentation future resolving exactly one AlertOutcome, one presentation is live per process (a second request resolves PresentError::Busy), and present::dismiss takes one down. It needs no frust-api feature — a plain core::future::Future pollable from any executor. All three arms are built: iOS/iPadOS presents a UIAlertController (an iPad action sheet anchored as a popover on AnchorRect); macOS presents an NSAlert window sheet; Android presents a framework android.app.AlertDialog over the resumed Activity (one more Kotlin object, FrustNativePresenter, in the same Gradle module, tracking the resumed Activity from a manifest-declared init provider) — every other target answers PresentError::Unsupported. A sheet (present::show_sheet: a SheetSpec’s constrained native content — title, message, image, up to three action rows — resolving one SheetOutcome, with detent changes streamed to a callback) is built on iOS/iPadOS only, a page sheet under UISheetPresentationController (an iPad in regular width shows a form sheet and ignores detents); every other target answers PresentError::Unsupported. Alerts and sheets share the one-at-a-time slot. With frust-api on, show_native_alert / show_native_sheet (the awaitable forms) and show_native_alert_into / show_native_sheet_into (writing the outcome into an RwSignal, spawned on frust::spawn_local) are the app-facing front door, re-exported at the crate root like the builders.

§Charter: a platform plugin

Like frust-camera and frust-secure-storage, this is a platform plugin (see docs/ARCHITECTURE.md’s Module Structure): it depends on frust-plugin plus FFI crates only, and carries no other frust-* framework dependency. An app adds this crate to its own Cargo.toml alongside frust; the facade does not depend on or re-export it.

§Placement doctrine

Placement is static-first: app bars, bottom bars, and full-body surfaces have no desync by construction, while a scroll-hosted native widget is a documented, degraded tier (an earlier device measurement of a scrolled native camera preview found cross-pipeline desync only while a slot’s rect moves relative to frust content). Nothing in v1 may require hosting a native widget inside a scroller; the headline use case is a high-rate native surface (a live chart, a streaming dashboard) updating at panel rate while frust’s own frame loop idles.

§Native-widget events bypass RenderRoot::event

A native control’s interaction is entirely platform-owned: a tap fires the platform’s own listener (Android’s View.OnClickListener, iOS/macOS target-action), which this crate’s plugin-private JNI/ObjC exports deliver straight into a registered Rust callback — never through frust-core’s EventCtx. That means every frust-core/frust-widgets interaction convention (docs/CODE_STANDARDS.md’s Interaction Semantics: capture, focus, fire-on-up-inside, Cancel-never-mutates-state, reduce_motion/state-layer conventions) simply does not apply to a native control — this is a defining property of hosting real platform widgets, not a gap to close. The app-facing API wraps a native event straight into a signal write, which is what wakes exactly one frust frame.

Re-exports§

pub use component::ComponentCtx;
pub use component::ListenerHandle;
pub use component::ListenerKinds;
pub use component::NativeChild;
pub use component::NativeComponent;
pub use component::NativeEvent;
pub use component::NativeRoot;
pub use component::register_component;
pub use present::ActionRole;
pub use present::AlertAction;
pub use present::AlertOutcome;
pub use present::AlertSpec;
pub use present::AlertStyle;
pub use present::AnchorRect;
pub use present::Detent;
pub use present::DismissReason;
pub use present::PresentError;
pub use present::Presentation;
pub use present::PresentationHandle;
pub use present::SheetAction;
pub use present::SheetContent;
pub use present::SheetHandle;
pub use present::SheetOutcome;
pub use present::SheetSpec;
pub use api::*;

Modules§

api
The app-facing api feature: builders returning impl View<State> compositions over the platform-agnostic controls/runtime/events machinery crate already carries — the facade-glue half of this crate that sanctions depending on frust.
component
NativeComponent — the public trait a plugin author writes a native component against, from pure Rust, with no per-component Kotlin or Swift.
present
Native presentations: platform-owned modal UI (an alert, and on iOS/iPadOS a sheet) requested imperatively from Rust and resolved to exactly one terminal outcome.

Structs§

Registry
A retained-handle registry keyed by SlotId, generic over the concrete platform handle type H so its core contract is host-testable with no FFI dependency (see the module doc, and this module’s tests).

Enums§

NativeWidgetError
Errors from a native-widgets operation.

Type Aliases§

SlotId
A platform_view slot id (frust_shell_common::platform_view’s differ-assigned identity, stamped from next_slot_id()’s process-wide counter — docs/ARCHITECTURE.md’s frust-core row) — the registry’s key.