noesis_runtime 0.11.0

Rust bindings for the Noesis GUI Native SDK: load XAML UI, drive the view and renderer, and write custom controls in Rust. Renderer-agnostic; Bevy integration lives in noesis_bevy.
Documentation
//! Supply image pixels to Noesis from Rust. Implement [`TextureProvider`]
//! to resolve `Image.Source` / `ImageBrush.ImageSource` URIs into RGBA8
//! textures, then register it with [`set_texture_provider`] (or one of the
//! scheme/assembly-scoped variants). Your boxed impl is handed to a C++
//! `RustTextureProvider` subclass through a vtable of trampolines, and the
//! returned [`Registered`] guard owns both the boxed impl and the C++ handle.
//!
//! This parallels [`crate::xaml_provider`] and [`crate::font_provider`] if you
//! have already used those.
//!
//! # How it works
//!
//! Noesis's [`TextureProvider`](https://www.noesisengine.com/docs/) base
//! class has two virtuals we override:
//!
//! - `GetTextureInfo(uri)`: return width / height (and optional atlas
//!   rect + dpi scale) for the image at `uri`. Lets Noesis size an
//!   `Image` element before the pixels are decoded. Return `None` to
//!   signal "not found"; Noesis then falls back to the load path.
//!
//! - `LoadTexture(uri, device)`: return the image as tightly-packed
//!   RGBA8 bytes. The C++ shim immediately hands the bytes to
//!   `device->CreateTexture(...)` on the same `RenderDevice` Noesis passed
//!   in (our `RustRenderDevice`), so the resulting `Noesis::Texture` is
//!   backed by a real wgpu texture and plugs into `Batch.pattern` /
//!   `Batch.image` through the existing `*_handle()` path. The byte
//!   buffer only needs to live for the duration of the `load` call.
//!
//! # Lifetime
//!
//! The [`Registered`] guard must outlive every Noesis-internal reference
//! that might call back into the provider. Keep it alive until after
//! [`crate::shutdown`] returns.

#![allow(unsafe_op_in_unsafe_fn)] // thin FFI surface; explicit blocks add noise

use core::ptr::NonNull;
use std::borrow::Cow;
use std::ffi::{CStr, c_void};
use std::os::raw::c_char;

use crate::ffi::{
    TextureInfoFfi, TextureProviderVTable, noesis_set_texture_provider,
    noesis_set_texture_provider_assembly, noesis_set_texture_provider_scheme,
    noesis_set_texture_provider_scheme_assembly, noesis_texture_provider_create,
    noesis_texture_provider_destroy,
};

/// Metadata a [`TextureProvider`] can report for a URI without decoding
/// pixels. [`new`](Self::new) leaves `x` / `y` at `0` (set them only for atlas
/// sub-rects) and `dpi_scale` at `1.0` (96dpi).
#[derive(Copy, Clone, Debug, Default)]
pub struct TextureInfo {
    pub width: u32,
    pub height: u32,
    pub x: u32,
    pub y: u32,
    pub dpi_scale: f32,
}

impl TextureInfo {
    /// Metadata for a whole-image texture of the given size, at 96dpi
    /// (`dpi_scale` 1.0) with no atlas offset.
    #[must_use]
    pub fn new(width: u32, height: u32) -> Self {
        Self {
            width,
            height,
            x: 0,
            y: 0,
            dpi_scale: 1.0,
        }
    }
}

/// Decoded RGBA8 image payload returned by [`TextureProvider::load`].
/// `bytes` must be exactly `width * height * 4` tightly-packed RGBA8.
pub struct ImageData<'a> {
    pub width: u32,
    pub height: u32,
    pub bytes: &'a [u8],
}

/// Resolves image URIs to pixels for Noesis. Implement [`info`](Self::info)
/// to report a texture's size during layout and [`load`](Self::load) to hand
/// back the decoded RGBA8 bytes. Both are keyed by the URI string Noesis takes
/// verbatim from `ImageBrush.ImageSource` / `Image.Source`.
///
/// The `Send + Sync` supertraits let the resulting [`Registered`] guard live in
/// a Bevy `Resource`.
pub trait TextureProvider: Send + Sync + 'static {
    /// Downcast escape hatch used by [`Registered::provider_mut`].
    fn as_any_mut(&mut self) -> &mut dyn std::any::Any;

    /// Return metadata for `uri` without decoding pixels. Return `None`
    /// when the URI is unknown. Called by Noesis during layout so it can
    /// size an `Image` before deciding whether to render it.
    fn info(&mut self, uri: &str) -> Option<TextureInfo>;

    /// Return the decoded RGBA8 bytes for `uri`. The returned slice must
    /// stay valid for the duration of the call (the C++ shim copies into
    /// the GPU texture synchronously, so returning a borrow into an
    /// internally-owned `Vec<u8>` is fine).
    ///
    /// Return `None` to signal "not found".
    fn load(&mut self, uri: &str) -> Option<ImageData<'_>>;
}

/// SAFETY: `userdata` must be a pointer produced by [`set_texture_provider`]
/// and still alive.
unsafe fn provider<'a>(userdata: *mut c_void) -> &'a mut Box<dyn TextureProvider> {
    &mut *userdata.cast::<Box<dyn TextureProvider>>()
}

/// Decode a Noesis-supplied URI lossily. Odd/non-UTF-8 engine input must not
/// panic across the C ABI, so invalid bytes become U+FFFD rather than aborting.
fn cstr_to_str<'a>(p: *const c_char) -> Cow<'a, str> {
    if p.is_null() {
        Cow::Borrowed("")
    } else {
        unsafe { CStr::from_ptr(p) }.to_string_lossy()
    }
}

unsafe extern "C" fn t_get_info(
    userdata: *mut c_void,
    uri: *const c_char,
    out: *mut TextureInfoFfi,
) -> bool {
    crate::panic_guard::guard(|| {
        let uri = cstr_to_str(uri);
        let Some(info) = provider(userdata).info(&uri) else {
            return false;
        };
        out.write(TextureInfoFfi {
            width: info.width,
            height: info.height,
            x: info.x,
            y: info.y,
            dpi_scale: info.dpi_scale,
        });
        true
    })
}

unsafe extern "C" fn t_load_texture(
    userdata: *mut c_void,
    uri: *const c_char,
    out_width: *mut u32,
    out_height: *mut u32,
    out_data: *mut *const u8,
    out_len: *mut u32,
) -> bool {
    crate::panic_guard::guard(|| {
        let uri = cstr_to_str(uri);
        let Some(img) = provider(userdata).load(&uri) else {
            return false;
        };
        // Enforce `len == w * h * 4` here so the C++ shim can trust it at CreateTexture.
        let expected = img.width.saturating_mul(img.height).saturating_mul(4) as usize;
        if img.bytes.len() != expected {
            return false;
        }
        // A >4 GiB buffer can't be represented to the shim, so treat it as a
        // failure rather than panicking inside the trampoline.
        let Ok(len) = u32::try_from(img.bytes.len()) else {
            return false;
        };
        out_width.write(img.width);
        out_height.write(img.height);
        out_data.write(img.bytes.as_ptr());
        out_len.write(len);
        true
    })
}

static VTABLE: TextureProviderVTable = TextureProviderVTable {
    get_info: t_get_info,
    load_texture: t_load_texture,
};

/// Owns a Rust [`TextureProvider`] impl together with its C++
/// `RustTextureProvider` instance. Parallel to
/// [`crate::xaml_provider::Registered`].
#[must_use = "dropping the guard immediately clears the registration"]
pub struct Registered {
    handle: NonNull<c_void>,
    userdata: NonNull<Box<dyn TextureProvider>>,
}

// SAFETY: Send-only (NOT Sync); see the crate-level "Thread affinity" docs.
unsafe impl Send for Registered {}

impl Registered {
    /// Raw `Noesis::TextureProvider*`.
    #[must_use]
    pub fn raw(&self) -> *mut c_void {
        self.handle.as_ptr()
    }

    /// Mutable access to the concrete [`TextureProvider`] impl behind
    /// this guard, via `TypeId`-checked downcast.
    ///
    /// # Panics
    ///
    /// Panics if `P` is not the concrete type passed to
    /// [`set_texture_provider`].
    pub fn provider_mut<P: TextureProvider>(&mut self) -> &mut P {
        let boxed: &mut Box<dyn TextureProvider> = unsafe { self.userdata.as_mut() };
        (**boxed)
            .as_any_mut()
            .downcast_mut::<P>()
            .expect("Registered::provider_mut: type does not match set_texture_provider")
    }
}

impl Drop for Registered {
    fn drop(&mut self) {
        // SAFETY: handle + userdata produced together by set_texture_provider;
        // both freed exactly once here.
        unsafe {
            noesis_texture_provider_destroy(self.handle.as_ptr());
            drop(Box::from_raw(self.userdata.as_ptr()));
        }
    }
}

/// Install `provider` as the global Noesis texture provider. Returns a
/// [`Registered`] guard that owns the boxed trait object and the C++
/// wrapper; drop it to tear everything down (after [`crate::shutdown`]).
///
/// # Panics
///
/// Panics if the C++ factory returns null.
pub fn set_texture_provider<P: TextureProvider>(provider: P) -> Registered {
    // SAFETY: install globally; Noesis retains its own +1.
    register_with(provider, |handle| unsafe {
        noesis_set_texture_provider(handle)
    })
}

/// Build the C++ `RustTextureProvider` wrapping `provider`, hand its handle to
/// `install` (the only thing that differs between the global / scheme /
/// assembly variants), and return the owning [`Registered`] guard.
fn register_with<P: TextureProvider>(provider: P, install: impl FnOnce(*mut c_void)) -> Registered {
    let outer: Box<Box<dyn TextureProvider>> = Box::new(Box::new(provider));
    let userdata = Box::into_raw(outer);
    // SAFETY: VTABLE is 'static; userdata is freshly leaked.
    let handle = unsafe { noesis_texture_provider_create(&raw const VTABLE, userdata.cast()) };
    let handle = NonNull::new(handle).expect("noesis_texture_provider_create returned null");
    install(handle.as_ptr());

    Registered {
        handle,
        userdata: NonNull::new(userdata).expect("Box::into_raw returned null"),
    }
}

/// Install `provider` as the texture provider for the URI `scheme` (the part
/// before `://`). Noesis consults the scheme-scoped provider for matching
/// texture URIs in preference to the global one. Reuses
/// [`set_texture_provider`]'s trampoline + [`Registered`] machinery; only the
/// install call differs.
///
/// # Panics
///
/// Panics if the C++ factory returns null, or `scheme` contains an interior
/// NUL byte.
pub fn set_scheme_texture_provider<P: TextureProvider>(scheme: &str, provider: P) -> Registered {
    let scheme = std::ffi::CString::new(scheme).expect("scheme contained interior NUL");
    register_with(provider, move |handle| {
        // SAFETY: handle is a live RustTextureProvider*; `scheme` outlives the call.
        unsafe { noesis_set_texture_provider_scheme(scheme.as_ptr(), handle) }
    })
}

/// Install `provider` as the texture provider for `assembly` (the assembly name
/// in a pack URI). Reuses [`set_texture_provider`]'s machinery; only the
/// install call differs.
///
/// # Panics
///
/// Panics if the C++ factory returns null, or `assembly` contains an interior
/// NUL byte.
pub fn set_assembly_texture_provider<P: TextureProvider>(
    assembly: &str,
    provider: P,
) -> Registered {
    let assembly = std::ffi::CString::new(assembly).expect("assembly contained interior NUL");
    register_with(provider, move |handle| {
        // SAFETY: handle is a live RustTextureProvider*; `assembly` outlives the call.
        unsafe { noesis_set_texture_provider_assembly(assembly.as_ptr(), handle) }
    })
}

/// Install `provider` as the texture provider scoped to both a `scheme` and an
/// `assembly`. Reuses [`set_texture_provider`]'s machinery; only the install
/// call differs.
///
/// # Panics
///
/// Panics if the C++ factory returns null, or `scheme` / `assembly` contain an
/// interior NUL byte.
pub fn set_scheme_assembly_texture_provider<P: TextureProvider>(
    scheme: &str,
    assembly: &str,
    provider: P,
) -> Registered {
    let scheme = std::ffi::CString::new(scheme).expect("scheme contained interior NUL");
    let assembly = std::ffi::CString::new(assembly).expect("assembly contained interior NUL");
    register_with(provider, move |handle| {
        // SAFETY: handle is a live RustTextureProvider*; both CStrings outlive the call.
        unsafe {
            noesis_set_texture_provider_scheme_assembly(scheme.as_ptr(), assembly.as_ptr(), handle)
        }
    })
}