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
apifeature: builders returningimpl View<State>compositions over the platform-agnosticcontrols/runtime/eventsmachinerycratealready carries — the facade-glue half of this crate that sanctions depending onfrust. - 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 typeHso its core contract is host-testable with no FFI dependency (see the module doc, and this module’stests).
Enums§
- Native
Widget Error - Errors from a native-widgets operation.
Type Aliases§
- SlotId
- A
platform_viewslot id (frust_shell_common::platform_view’s differ-assigned identity, stamped fromnext_slot_id()’s process-wide counter —docs/ARCHITECTURE.md’sfrust-corerow) — the registry’s key.