noesis_bevy 0.15.1

Bevy plugin that drives the Noesis GUI Native SDK and renders its UIs into a Bevy frame via wgpu compositing.
Documentation

noesis_bevy

CI crates.io docs.rs

A Bevy 0.19 plugin that renders Noesis GUI XAML-driven UI into your frame. Noesis draws the scene on Bevy's own GPU; the plugin composites the result onto a camera.

It builds on the FFI crate noesis_runtime, which wraps the C++ SDK. All unsafe lives there. This crate has none of its own and sets #![forbid(unsafe_code)].

Built for Dead Money's own games and mostly written by AI agents under human direction.

You need a Noesis license

This crate links against the Noesis Native SDK, closed-source commercial software we don't redistribute. Buy it separately (Indie tier or higher) and point NOESIS_SDK_DIR at your install; the build links against it from there.

This release targets Noesis Native SDK 3.2.13 and is compiled against that version's headers, so a different SDK version may not link. Match it unless you've verified a newer one.

Supported targets are Linux (x86_64, aarch64) and Windows (x86_64-pc-windows-msvc). Linux is the primary target; Windows support is newer.

Set NOESIS_LICENSE_NAME and NOESIS_LICENSE_KEY to apply your license. Without them the UI runs for a while, then blanks the view with a "Trial expired" message.

Quick start

[dependencies]
bevy = "0.19"
noesis_bevy = "0.13"

It links the Noesis SDK at build time, so you need NOESIS_SDK_DIR set (see above) to compile.

use std::sync::Arc;
use bevy::prelude::*;
use noesis_bevy::{NoesisCamera, NoesisPlugin, NoesisView, XamlRegistry};

const MENU_XAML: &str = r#"<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
  <TextBlock Text="Hello, Noesis!" Foreground="White"
             HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>"#;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(NoesisPlugin::default())
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands, mut xaml: ResMut<XamlRegistry>) {
    // Register XAML by URI. For real projects, load .xaml files through the
    // asset server instead; the loader feeds the same registry.
    xaml.insert("menu.xaml", Arc::new(MENU_XAML.as_bytes().to_vec()));

    // A view is a `NoesisView` component on a 2D camera tagged `NoesisCamera`.
    // Noesis renders the scene offscreen and composites it onto that camera.
    commands.spawn((
        Camera2d,
        NoesisCamera,
        NoesisView {
            xaml_uri: "menu.xaml".to_string(),
            size: UVec2::new(1920, 1080),
            ..default()
        },
    ));
}

NoesisPlugin::default() reads NOESIS_LICENSE_NAME and NOESIS_LICENSE_KEY from the environment. Pass NoesisLicense { name, key } to set them explicitly.

How the UI fits Bevy

The mental model is closer to "Bevy hosts an embedded retained-mode GUI runtime and renders its output" than "Bevy UI describes the widgets." XAML is authoritative for structure and layout; the ECS supplies data and intent across a small typed bridge surface.

  • The XAML tree lives inside Noesis, not the ECS. Noesis parses your XAML and owns the live tree of controls (buttons, grids, text blocks), along with the visual and logical trees, the dependency-property system, styles, triggers, and animations. None of those controls are Bevy entities; there is no entity per <Button>.
  • One entity per view. The only ECS-visible part is the NoesisView camera entity. It renders one XAML document into the frame, and the whole tree behind it is opaque to the ECS.
  • You reach the controls through bridges, not entities. Instead of querying widget entities, you attach bridge components to the view entity, and reconcile systems in NoesisSet::Apply push values into and pull them out of the live scene: NoesisText for text, NoesisDp for dependency properties, NoesisVm for view models, NoesisCommands for commands, and so on. Read-backs arrive as Message { view, .. } events, not as changed components on a widget. Data binding, commands, and view models are the seam, the idiomatic XAML/WPF approach rather than immediate-mode or an entity per widget.
  • The tradeoff. You don't get ECS-native features like per-widget Transform, picking, or change detection for free. Anything you want to drive from gameplay goes through the bridge layer, or means adding to it.

On top of this base, the entity-driven API below raises whole panels and list rows to first-class entities when you want plain ECS ergonomics; the bridges stay the lower-level seam beneath it.

The entity-driven UI API

When you'd rather drive the UI as plain ECS, three primitives make a Bevy entity the unit of UI. You still author the XAML; you spawn entities and write ordinary systems instead of string-keyed bridges. examples/ecs_ui.rs runs all three end to end.

Panel = entity. A UiPanel mounts a sub-XAML fragment into a host slot, and the entity's bound components are its DataContext:

#[derive(Component, NoesisViewModel, Clone, Copy)]
struct Health(f32);   // binds {Binding Health} inside the fragment

commands.spawn((UiPanel::new("hud.xaml").mount_into(view, "Slot"), Health(100.0)));

fn regen(mut q: Query<&mut Health, With<UiPanel>>) {
    for mut h in &mut q { h.0 = (h.0 + 1.0).min(100.0); }
}

Two panels of the same type bind independently. Show and hide is a String field bound to Visibility (visibility::{VISIBLE, COLLAPSED, HIDDEN}), with no converter.

List = query. A list is its own entity naming the view it renders into; its rows are entities too. Tag an entity into a list and it appears as a row; the bound ObservableCollection is reconciled by Entity, so mutating one component updates only its row and selection survives a reorder. One view can own any number of lists:

#[derive(Component, NoesisViewModel, Clone)]
struct Item { name: String, qty: i32 }

let list = commands.spawn(UiList::new(view, "Inventory")).id();
commands.spawn((Item { name: "Potion".into(), qty: 3 }, ListedIn(list)));

The selected row carries a Selected marker, read back with Query<&Item, With<Selected>>.

Events = observers. UI events arrive as EntityEvents targeting the entity they came from, a panel or a row:

fn use_item(on: On<UiClicked>, items: Query<&Item>) {
    if let Ok(item) = items.get(on.event_target()) {
        // the row the click came from
    }
}

Driving the UI from systems

Each piece of UI state (text, visibility, list contents, and more) is a bridge component on the view entity. Spawning a NoesisView auto-attaches every per-view bridge as a Bevy required component, each defaulting to empty at no cost, so you write to them without spawning them by hand. A write set from Startup or OnEnter, before the scene exists, lands once the scene builds. The two binding bridges NoesisVm and NoesisCommands are the exception: add those yourself, since they need a class or command name.

For an app with a single view, NoesisUi finds it for you so a system doesn't spell out the query:

use bevy::prelude::*;
use noesis_bevy::{NoesisText, NoesisUi};

fn update_score(score: Res<Score>, mut ui: NoesisUi<&mut NoesisText>) {
    if !score.is_changed() { return; }
    let Some(mut text) = ui.get_mut() else { return };
    text.write("Score", score.0.to_string());
}

NoesisUi<&mut T> reads or writes a bridge component T on the single view; plain NoesisUi yields the view entity via ui.entity(), which matches the view: Entity that read-back messages carry. Its accessors return None (rather than skipping the system) when there isn't exactly one view, so a multi-view app routes by that entity instead.

Custom controls and markup extensions

Write a control or a {Binding}-style markup extension in Rust, register it from a Startup system, and XAML can use it by name:

use bevy::prelude::*;
use noesis_bevy::classes::{
    ClassBase, ClassBuilder, NoesisClassRegistry, PropType,
    PropertyChangeHandler, PropertyValue, Instance,
};

struct NineSlicerHandler { source_idx: u32 /* ... */ }
impl PropertyChangeHandler for NineSlicerHandler {
    fn on_changed(&mut self, instance: Instance, idx: u32, value: PropertyValue<'_>) {
        // Recompute derived properties and write them back via instance.set_*().
    }
}

fn register(mut registry: ResMut<NoesisClassRegistry>) {
    let mut b = ClassBuilder::new("MyNs.NineSlicer", ClassBase::ContentControl,
                                  NineSlicerHandler { source_idx: 0 });
    b.add_property("Source", PropType::ImageSource);
    b.add_property("SliceThickness", PropType::Thickness);
    if let Some(reg) = b.register() { registry.add(reg); }
}

MarkupExtensionRegistration works the same way via NoesisMarkupExtensionRegistry. See the noesis_runtime README for the FFI-level details.

Data binding

Bind a plain Bevy Resource to XAML {Binding field_name}: derive NoesisViewModel, register it, and each field is reflected to the binding engine by name, two-way.

use bevy::prelude::*;
use noesis_bevy::{NoesisPlugin, NoesisViewModel, NoesisViewModelAppExt};

#[derive(Resource, NoesisViewModel)]
struct SettingsVm {
    volume: f32,   // <Slider Value="{Binding volume, Mode=TwoWay}"/>
    muted: bool,   // <CheckBox IsChecked="{Binding muted}"/>
    quality: i32,  // <ComboBox SelectedIndex="{Binding quality, Mode=TwoWay}"/>
}

App::new()
    .add_plugins((DefaultPlugins, NoesisPlugin::default()))
    .insert_resource(SettingsVm { volume: 0.8, muted: false, quality: 2 })
    .add_noesis_view_model::<SettingsVm>(); // attach as the view-root DataContext

Mutating the resource updates the bound controls (Bevy change detection drives INotifyPropertyChanged); a control edit writes back into the resource. Supported field types are f32/f64, i32/u32, bool, and String; mark other fields #[noesis(skip)].

For finer control, three lower-level bridges sit underneath: NoesisVm (a view model built one property at a time), NoesisItems (fill a list or dropdown from a Rust collection), and NoesisDp (get, set, or watch any property on a named element directly, no binding required).

Version compatibility

Bevy noesis_bevy
0.19 0.13
0.18 0.10 – 0.12

noesis_bevy tracks Bevy: each Bevy minor gets a fresh noesis_bevy minor, and patch releases stay on the row's Bevy version. Pin wgpu to the same version Bevy's renderer uses (the crate does this for you) so render-device types stay interchangeable.

Setup

unzip NoesisGUI-NativeSDK-linux-3.2.13-Indie.zip -d ~/sdks/noesis-3.2.13
export NOESIS_SDK_DIR=~/sdks/noesis-3.2.13
export LD_LIBRARY_PATH=$NOESIS_SDK_DIR/Bin/linux_x86_64:$LD_LIBRARY_PATH

Symlink the SDK's font and data directories so the examples and the NOESIS_VIEWER_THEME loader can find them (these assets/ paths are gitignored):

ln -sfn $NOESIS_SDK_DIR/Data/Fonts assets/Fonts
ln -sfn $NOESIS_SDK_DIR/Data        assets/Data

Apply your license credentials so the runtime runs licensed:

export NOESIS_LICENSE_NAME=...
export NOESIS_LICENSE_KEY=...

Then build and run:

cargo test
cargo run --example xaml_viewer

Licensing

Source in this repository is © 2026 Dead Money under the MIT License. Everything under src/, tests/, examples/, and assets/ is original work; no Noesis SDK code is vendored.

The Noesis Native SDK is not redistributed here. You obtain it from Noesis Technologies under their EULA, and noesis_runtime's build.rs links it from NOESIS_SDK_DIR at compile time. Use and distribution of binaries you build that link the SDK are governed by the Noesis EULA, not by the MIT License above.

Acknowledgements

Built on Bevy and the Noesis Native SDK. The upstream docs at docs.noesisengine.com are the source of truth for XAML, control templates, and binding behavior. Report SDK bugs there; report integration bugs here.