Skip to main content

noesis_bevy/
lib.rs

1//! Bevy plugin for the Noesis GUI SDK.
2//!
3//! Drives `libNoesis.so` through the [`noesis_runtime`] FFI crate and implements
4//! `Noesis::RenderDevice` on top of Bevy's wgpu device. UIs render into an
5//! offscreen wgpu texture and composite into the Bevy frame.
6//!
7//! Add [`NoesisPlugin`] to initialize the runtime, then host XAML through a
8//! [`NoesisView`] camera.
9#![warn(missing_docs)]
10
11use bevy::prelude::*;
12
13pub mod animation;
14pub mod bake;
15pub mod binding;
16pub mod brushes;
17pub mod classes;
18pub mod clip;
19pub mod commands;
20pub mod diagnostics;
21pub mod dp;
22pub mod events;
23pub mod focus;
24pub mod focus_input;
25pub mod font;
26pub mod geometry;
27pub mod headless;
28#[cfg(feature = "hot_reload")]
29pub mod hot_reload;
30pub mod image;
31pub mod imaging;
32pub mod inlines;
33pub mod input;
34pub mod integration;
35pub mod items;
36pub mod layout;
37pub mod list;
38pub mod markup;
39pub mod panel;
40pub mod plain_vm;
41pub mod reconcile;
42pub mod render;
43pub mod render_device;
44pub mod resources;
45pub mod routed_events;
46pub mod shapes;
47pub mod styles;
48pub mod svg;
49pub mod text;
50pub mod theme;
51pub mod transforms;
52pub mod transforms3d;
53pub mod typography;
54pub mod ui;
55pub mod viewmodel;
56pub mod visibility;
57pub mod visual_state;
58pub mod window_compat;
59pub mod xaml;
60
61pub use animation::{AnimationSpec, NoesisAnimation, NoesisAnimationPlugin};
62pub use bake::{NoesisLabelBaker, NoesisLabelBakerPlugin};
63pub use binding::{
64    BindingMode, ConvertArg, Converted, MultiValueConverter, NoesisBinding, NoesisBindingPlugin,
65    SourceSpec, ValueConverter,
66};
67pub use brushes::{
68    BrushReadback, BrushSpec, BrushTarget, GradientStop, NoesisBrushChanged, NoesisBrushes,
69    NoesisBrushesPlugin,
70};
71pub use classes::{NoesisClassPlugin, NoesisClassRegistry};
72pub use clip::{NoesisClip, NoesisClipPlugin};
73pub use commands::{
74    CommandForwarder, CommandsDef, NoesisCommandInvoked, NoesisCommands, NoesisCommandsPlugin,
75    SharedCommandQueue,
76};
77pub use diagnostics::{NoesisDiagnostics, NoesisDiagnosticsPlugin};
78pub use dp::{DpKind, DpValue, DpWatch, NoesisDp, NoesisDpChanged, NoesisDpPlugin};
79pub use events::{
80    ClickWatchEntry, Key, KeyDownWatchEntry, NoesisClickWatch, NoesisClicked, NoesisEventsPlugin,
81    NoesisKeyDown, NoesisKeyDownWatch, SharedClickQueue, SharedKeyDownQueue, UiClicked, UiKeyDown,
82};
83pub use focus::{NoesisFocus, NoesisFocusPlugin};
84pub use focus_input::{
85    FocusMove, FocusNavigationDirection, FocusPredict, KeyBindingSpec, ModifierKeys,
86    NoesisFocusBindingFired, NoesisFocusControl, NoesisFocusControlPlugin, NoesisFocusPredicted,
87};
88pub use font::{BevyFontProvider, FontAsset, FontAssetLoader, FontAssetPlugin, FontRegistry};
89pub use geometry::{NoesisGeometry, NoesisGeometryPlugin};
90pub use headless::NoesisHeadlessPlugin;
91#[cfg(feature = "hot_reload")]
92pub use hot_reload::{NoesisHotReload, NoesisHotReloadPlugin};
93pub use image::{
94    BevyTextureProvider, ImageAsset, ImageAssetLoader, ImageAssetPlugin, ImageRegistry,
95};
96pub use imaging::{
97    ImageBitmap, ImageReadback, NoesisImageChanged, NoesisImaging, NoesisImagingPlugin,
98};
99pub use inlines::{
100    InlineSpec, InlinesReadback, NoesisInlines, NoesisInlinesChanged, NoesisInlinesPlugin,
101    TextDecorations,
102};
103pub use input::{
104    NoesisInputEvent, NoesisInputPlugin, NoesisInputQueue, NoesisPointerOverUi, TargetedInput,
105};
106pub use integration::{
107    CursorType, NoesisCursorRequested, NoesisIntegrationPlugin, NoesisOpenUrl, NoesisPlayAudio,
108    get_culture, open_url, play_audio, set_culture,
109};
110pub use items::{
111    CollectionViewOp, ItemValue, ItemsBinding, NoesisItems, NoesisItemsCurrent, NoesisItemsPlugin,
112    ObjectRow, ObjectSource,
113};
114pub use layout::{Margin, NoesisLayout, NoesisLayoutPlugin};
115pub use list::{
116    ListRows, ListSort, ListedIn, NoesisListAppExt, NoesisListOps, NoesisListPlugin,
117    NoesisListSelection, NoesisListSet, NoesisRowSelected, Selected, UiList,
118};
119pub use markup::{NoesisMarkupExtensionPlugin, NoesisMarkupExtensionRegistry};
120/// Derive macro for [`NoesisViewModel`]: binds a plain struct's fields by name.
121pub use noesis_bevy_derive::NoesisViewModel;
122pub use panel::{
123    NoesisPanelAppExt, NoesisPanelPlugin, NoesisPanelSet, NoesisPanelText, NoesisPanelTextChanged,
124    SealPanel, UiPanel,
125};
126pub use plain_vm::{NoesisViewModel, NoesisViewModelAppExt, PlainType, PlainValue, PlainValueRef};
127pub use render::{NoesisCamera, NoesisIntermediate, NoesisRenderPlugin, NoesisSet, NoesisView};
128pub use resources::{
129    NoesisResources, NoesisResourcesInstalled, NoesisResourcesPlugin, ResourceEntry,
130};
131pub use routed_events::{
132    EventWatchEntry, MouseButton, NoesisEventWatch, NoesisRoutedEvent, NoesisRoutedEventsPlugin,
133    RoutedEvent, RoutedEventSnapshot, SharedRoutedEventQueue, UiRoutedEvent,
134};
135pub use shapes::{NoesisShapes, NoesisShapesPlugin, ShapeKind, ShapeSpec};
136pub use styles::{
137    DataTriggerSpec, MultiTriggerSpec, NoesisStyles, NoesisStylesPlugin, PropertyTrigger, StyleSpec,
138};
139pub use svg::{NoesisSvg, NoesisSvgChanged, NoesisSvgPlugin};
140pub use text::{NoesisText, NoesisTextChanged, NoesisTextPlugin};
141pub use theme::NoesisDefaultThemePlugin;
142pub use transforms::{
143    NoesisTransform, NoesisTransformChanged, NoesisTransformPlugin, TransformSpec,
144};
145pub use transforms3d::{
146    Matrix3DSpec, NoesisMatrixTransform3DChanged, NoesisTransform3D, NoesisTransform3DChanged,
147    NoesisTransform3DPlugin, Transform3DSpec,
148};
149pub use typography::{
150    FontStretch, FontStyle, FontStyling, FontWeight, NoesisTypography, NoesisTypographyChanged,
151    NoesisTypographyPlugin, TypographyField, TypographyValue, TypographyWatch,
152};
153pub use ui::NoesisUi;
154pub use viewmodel::{
155    NoesisViewModelChanged, NoesisViewModelPlugin, NoesisVm, SharedVmChangedQueue,
156    ViewModelChangeForwarder, ViewModelDef, VmValue,
157};
158pub use visibility::{NoesisVisibility, NoesisVisibilityPlugin};
159pub use visual_state::{NoesisVisualState, NoesisVisualStatePlugin, StateRequest};
160pub use window_compat::{NoesisWindowCompatPlugin, WINDOW_CLASS};
161pub use xaml::{BevyXamlProvider, XamlAsset, XamlAssetLoader, XamlAssetPlugin, XamlRegistry};
162
163/// Per-developer Indie license credentials.
164#[derive(Clone, Debug)]
165pub struct NoesisLicense {
166    /// Licensee name, as issued with the Indie license.
167    pub name: String,
168    /// License key paired with [`name`](Self::name).
169    pub key: String,
170}
171
172impl NoesisLicense {
173    /// Read `NOESIS_LICENSE_NAME` and `NOESIS_LICENSE_KEY` from the environment.
174    /// Returns `None` if either is unset.
175    #[must_use]
176    pub fn from_env() -> Option<Self> {
177        let name = std::env::var("NOESIS_LICENSE_NAME").ok()?;
178        let key = std::env::var("NOESIS_LICENSE_KEY").ok()?;
179        Some(Self { name, key })
180    }
181}
182
183/// Bevy plugin that initializes Noesis at app startup and shuts it down when
184/// the [`App`] is dropped.
185///
186/// Falls back to `NoesisLicense::from_env()` when [`license`](Self::license)
187/// is `None`. Without a license, Noesis runs in trial mode (visible watermark).
188#[derive(Default)]
189pub struct NoesisPlugin {
190    /// License to activate. Leave `None` to fall back to
191    /// [`NoesisLicense::from_env`], or to run in trial mode if the environment
192    /// has no credentials either.
193    pub license: Option<NoesisLicense>,
194}
195
196impl NoesisPlugin {
197    /// Activate the license (explicit, else `NOESIS_LICENSE_*` env) and start the
198    /// process-global Noesis runtime. Both the real plugin and the headless test
199    /// harness ([`NoesisHeadlessPlugin`]) call this once at `build` time, before
200    /// any `NoesisRenderState` registers a device/provider with the runtime.
201    pub(crate) fn init_runtime(&self) {
202        if let Some(lic) = self.license.clone().or_else(NoesisLicense::from_env) {
203            noesis_runtime::set_license(&lic.name, &lic.key);
204        }
205        noesis_runtime::init();
206
207        info!("Noesis runtime version {}", noesis_runtime::version());
208    }
209
210    /// Add every asset loader, input, integration, and per-element/data bridge
211    /// plugin, but **not** the render pipeline plugin. This is the bridge set
212    /// both the real [`NoesisPlugin`] (which pairs it with
213    /// [`render::NoesisRenderPlugin`]) and the headless test harness (which pairs
214    /// it with [`NoesisHeadlessPlugin`]) drive, so both exercise identical bridges.
215    ///
216    /// Exposed so a custom harness can run the bridges against its own render
217    /// wiring (or none); app code should add [`NoesisPlugin`] instead.
218    pub fn add_bridge_plugins(app: &mut App) {
219        // Tuples are split to stay under Bevy's 15-element `Plugins` impl limit.
220        app.add_plugins((
221            xaml::XamlAssetPlugin,
222            font::FontAssetPlugin,
223            image::ImageAssetPlugin,
224            input::NoesisInputPlugin,
225            integration::NoesisIntegrationPlugin,
226        ));
227        // Group A: foundational per-element bridges.
228        app.add_plugins((
229            events::NoesisEventsPlugin,
230            routed_events::NoesisRoutedEventsPlugin,
231            classes::NoesisClassPlugin,
232            markup::NoesisMarkupExtensionPlugin,
233            visibility::NoesisVisibilityPlugin,
234            layout::NoesisLayoutPlugin,
235            text::NoesisTextPlugin,
236            inlines::NoesisInlinesPlugin,
237            geometry::NoesisGeometryPlugin,
238            clip::NoesisClipPlugin,
239        ));
240        // Group B: interaction + data bridges. New bridges append here.
241        app.add_plugins((
242            focus::NoesisFocusPlugin,
243            visual_state::NoesisVisualStatePlugin,
244            focus_input::NoesisFocusControlPlugin,
245            viewmodel::NoesisViewModelPlugin,
246            commands::NoesisCommandsPlugin,
247            items::NoesisItemsPlugin,
248            dp::NoesisDpPlugin,
249            (
250                transforms::NoesisTransformPlugin,
251                transforms3d::NoesisTransform3DPlugin,
252            ),
253            brushes::NoesisBrushesPlugin,
254            animation::NoesisAnimationPlugin,
255            typography::NoesisTypographyPlugin,
256            binding::NoesisBindingPlugin,
257            imaging::NoesisImagingPlugin,
258            svg::NoesisSvgPlugin,
259            diagnostics::NoesisDiagnosticsPlugin::default(),
260        ));
261        // Group C: past group B's 15-element `Plugins` limit.
262        app.add_plugins((
263            styles::NoesisStylesPlugin,
264            shapes::NoesisShapesPlugin,
265            resources::NoesisResourcesPlugin,
266            panel::NoesisPanelPlugin,
267            list::NoesisListPlugin,
268        ));
269    }
270}
271
272impl Plugin for NoesisPlugin {
273    fn build(&self, app: &mut App) {
274        self.init_runtime();
275
276        // Global `shutdown()` is owned by `NoesisRenderState::drop`: it releases every
277        // live Noesis handle then shuts the engine down, last, on the main thread.
278        // A separate guard can't guarantee it runs after the state (Bevy gives no
279        // drop order between two main-world resources).
280
281        Self::add_bridge_plugins(app);
282
283        // Opt-in live XAML editing. Kept out of `add_bridge_plugins` (the set
284        // the headless test harness shares) so tests never spin a filesystem
285        // watcher thread; only real apps building with `hot_reload` get it.
286        #[cfg(feature = "hot_reload")]
287        app.add_plugins(hot_reload::NoesisHotReloadPlugin);
288
289        // NoesisRenderPlugin no-ops its render-sub-app half if RenderApp is
290        // absent (headless), but its main-world driving pipeline still runs.
291        app.add_plugins(render::NoesisRenderPlugin);
292    }
293}