noesis_bevy 0.10.0

Bevy plugin that drives the Noesis GUI Native SDK and renders its UIs into a Bevy frame via wgpu compositing.
Documentation
//! Per-view code-built imaging bridge: drive a named `<Image>`'s pixels from a
//! Rust-provided bitmap (RGBA8 + size), no image file on disk required. The
//! imaging counterpart of the [`crate::brushes`] / [`crate::geometry`] write
//! bridges.
//!
//! Add a [`NoesisImaging`] component to the view's camera entity. Its `images`
//! map is the desired bitmap per `x:Name`: each entry carries the raw RGBA8
//! pixels, the bitmap's pixel size, and the `uri` the element's XAML `Source`
//! references. On change the bridge stages those pixels into the shared
//! [`crate::ImageRegistry`] under `uri`, so the live `Noesis::TextureProvider`
//! resolves `<Image Source="uri"/>` to the Rust bytes (the same path a `.png`
//! asset takes, but fed from memory).
//!
//! ```ignore
//! // XAML: <Image x:Name="Pic" Source="dm-bitmap://logo" Stretch="None"/>
//! let rgba = Arc::new(vec![255u8; 13 * 7 * 4]); // 13x7 opaque white
//! commands.entity(view).insert(
//!     NoesisImaging::new().set("Pic", "dm-bitmap://logo", 13, 7, rgba),
//! );
//! ```
//!
//! # Timing
//!
//! Noesis resolves a `<Image>`'s `BitmapImage` source from the texture provider
//! **once, when the scene is first laid out**, and does not retry a miss. So the
//! bytes must be staged *before* the view's scene is built. Attach
//! [`NoesisImaging`] (populated) at spawn time, alongside the
//! [`NoesisView`](crate::NoesisView), rather than filling it in a later frame.
//! The staging system runs before the per-frame registry→provider sync precisely
//! so a same-frame spawn lands the bitmap ahead of scene build.
//!
//! # Why URI registration rather than `Image.Source = bitmap`
//!
//! Assigning a Rust-built `ImageSource` straight onto an element
//! (`Image::SetSource`) is an `unsafe` raw-pointer call in `noesis_runtime`, and
//! this crate is `unsafe_code = forbid` with no safe typed setter for it in the
//! runtime (unlike brushes' `set_background` / transforms' `set_render_transform`).
//! The texture-provider URI path is the safe route, and it is the canonical one
//! for tiled / streamed bitmaps anyway.
//!
//! # Observable
//!
//! The bridge *polls back* the live element and emits [`NoesisImageChanged`] when
//! a watched `<Image>`'s resolved size changes. Noesis sizes an `Image` from its
//! source's pixel dimensions, which it obtains from our texture provider's
//! `GetTextureInfo` during the layout pass, so a Rust-registered `13x7` bitmap
//! drives the element's `ActualWidth`/`ActualHeight` to `13`/`7` (with the
//! element authored `Stretch="None"`), with **no GPU render pass required**. An
//! unresolvable `Source` (nothing registered for its `uri`) measures to `0`, the
//! built-in negative control: a no-op apply, a wrong `uri`, or a wrong size all
//! read back differently from the requested dimensions.
//!
//! Everything runs on the main thread (Noesis is thread-affine and lives there):
//! the reconcile system stages each view's bytes into the registry, polls the
//! element read-back, and emits messages directly. No cross-world queues.

use std::collections::HashMap;
use std::sync::Arc;

use bevy::prelude::*;

use crate::image::ImageRegistry;
use crate::render::{NoesisRenderState, NoesisSet};

/// A Rust-provided bitmap, declarative side: tightly-packed RGBA8 pixels plus
/// the `uri` the target element's `Source` references. Staged into the shared
/// [`ImageRegistry`] at apply time so the live texture provider resolves it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImageBitmap {
    /// The `Source` URI the element's XAML references (e.g. `dm-bitmap://logo`).
    pub uri: String,
    /// Bitmap width in pixels.
    pub width: u32,
    /// Bitmap height in pixels.
    pub height: u32,
    /// Tightly-packed RGBA8 pixels; `bytes.len()` must be `width * height * 4`.
    /// `Arc` so staging into the registry shares the allocation rather than
    /// copying every frame.
    pub bytes: Arc<Vec<u8>>,
}

/// Per-view imaging bridge. Attach to a [`NoesisView`](crate::NoesisView) entity.
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisImaging {
    /// Desired bitmap per `x:Name`. Staged into the [`ImageRegistry`] whenever
    /// this component changes; writes to the same name apply last-wins.
    pub images: HashMap<String, ImageBitmap>,
}

impl NoesisImaging {
    /// An empty bridge with no bitmaps. Chain [`set`](Self::set) to add the
    /// element bitmaps you want staged.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Builder: drive element `name`'s `<Image>` from `bytes` (tightly-packed
    /// RGBA8, `width * height * 4` long), staged under `uri`, which must match
    /// the element's authored `Source`.
    #[must_use]
    pub fn set(
        mut self,
        name: impl Into<String>,
        uri: impl Into<String>,
        width: u32,
        height: u32,
        bytes: Arc<Vec<u8>>,
    ) -> Self {
        self.images.insert(
            name.into(),
            ImageBitmap {
                uri: uri.into(),
                width,
                height,
                bytes,
            },
        );
        self
    }
}

/// What was read back from a watched `<Image>` element after layout: the
/// observable proof a Rust-provided bitmap actually reached the live element. A
/// `Source` that resolved to our registered bytes carries the bitmap's pixel
/// dimensions in [`actual_size`](Self::actual_size); an unresolvable source
/// measures to `[0.0, 0.0]`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ImageReadback {
    /// Whether the element currently has a non-null `Source` (`ImageSource`) DP.
    pub has_source: bool,
    /// The element's `[ActualWidth, ActualHeight]` after the last layout pass.
    /// Equals the registered bitmap's `[width, height]` once resolved (element
    /// authored `Stretch="None"`); `[0.0, 0.0]` for an unresolvable source.
    pub actual_size: [f32; 2],
}

/// Emitted when a watched `<Image>`'s read-back changes from the previous frame's
/// snapshot. Proves a [`ImageBitmap`] reached the element: once the texture
/// provider resolves the staged bytes, `readback.actual_size` becomes the
/// bitmap's pixel size. Read with `MessageReader<NoesisImageChanged>`.
#[derive(Message, Debug, Clone)]
pub struct NoesisImageChanged {
    /// The [`NoesisView`](crate::NoesisView) entity whose image changed.
    pub view: Entity,
    /// `x:Name` of the watched `<Image>` element.
    pub name: String,
    /// What was read back from the live element.
    pub readback: ImageReadback,
}

/// Stage every changed [`NoesisImaging`]'s bitmaps into the [`ImageRegistry`].
/// Runs before the registry→provider sync (and thus before scene build) so a
/// same-frame spawn lands the bytes ahead of Noesis's one-shot source
/// resolution. Independent of [`NoesisRenderState`]: a plain resource write.
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn stage_imaging_bitmaps(
    views: Query<Ref<NoesisImaging>>,
    mut registry: ResMut<ImageRegistry>,
) {
    for imaging in &views {
        if !imaging.is_changed() {
            continue;
        }
        for bitmap in imaging.images.values() {
            registry.insert(
                bitmap.uri.clone(),
                bitmap.width,
                bitmap.height,
                Arc::clone(&bitmap.bytes),
            );
        }
    }
}

/// Poll each view's watched `<Image>` elements and emit [`NoesisImageChanged`]
/// when a resolved size / source presence changes. Runs in
/// [`NoesisSet::Apply`], reading the layout the previous frame's drive produced.
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn poll_imaging_reads(
    views: Query<(Entity, &NoesisImaging)>,
    state: Option<NonSendMut<NoesisRenderState>>,
    mut changed: MessageWriter<NoesisImageChanged>,
) {
    let Some(mut state) = state else {
        return;
    };
    for (entity, imaging) in &views {
        for (name, readback) in state.poll_image_reads_for(entity, &imaging.images) {
            changed.write(NoesisImageChanged {
                view: entity,
                name,
                readback,
            });
        }
    }
}

/// Wires the per-view imaging bridge. Added transitively by [`crate::NoesisPlugin`].
pub struct NoesisImagingPlugin;

impl Plugin for NoesisImagingPlugin {
    fn build(&self, app: &mut App) {
        app.add_message::<NoesisImageChanged>().add_systems(
            PostUpdate,
            (
                stage_imaging_bitmaps.before(NoesisSet::Sync),
                poll_imaging_reads.in_set(NoesisSet::Apply),
            ),
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn builder_collects_images() {
        let bytes = Arc::new(vec![255u8; 13 * 7 * 4]);
        let i = NoesisImaging::new().set("Pic", "dm-bitmap://logo", 13, 7, Arc::clone(&bytes));
        let got = i.images.get("Pic").expect("Pic entry");
        assert_eq!(got.uri, "dm-bitmap://logo");
        assert_eq!((got.width, got.height), (13, 7));
        assert_eq!(got.bytes.len(), 13 * 7 * 4);
    }
}