noesis_runtime 0.12.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
//! Input gestures + bindings end-to-end: gesture → binding → Rust command
//! across the FFI. Covers `KeyBinding` (Ctrl+Enter) and `InputBinding`/`KeyGesture` (F5).
//!
//! Run with `NOESIS_SDK_DIR` set:
//!   `cargo test -p noesis_runtime --test input_bindings -- --nocapture`

use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

use noesis_runtime::commands::Command;
use noesis_runtime::input::{InputBinding, KeyBinding, KeyGesture, ModifierKeys};
use noesis_runtime::view::{FrameworkElement, Key, View};
use noesis_runtime::xaml_provider::XamlProvider;

const SCENE: &str = r##"<?xml version="1.0" encoding="utf-8"?>
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      Background="#FF202020" Width="200" Height="200">
  <Button x:Name="Target" Content="Hit me" Width="160" Height="40"
          HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>"##;

struct InMem {
    bytes: HashMap<String, Vec<u8>>,
}

impl XamlProvider for InMem {
    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
        self
    }
    fn load_xaml(&mut self, uri: &str) -> Option<&[u8]> {
        self.bytes.get(uri).map(Vec::as_slice)
    }
}

#[test]
fn input_bindings_fire_bound_commands() {
    if let (Ok(name), Ok(key)) = (
        std::env::var("NOESIS_LICENSE_NAME"),
        std::env::var("NOESIS_LICENSE_KEY"),
    ) {
        noesis_runtime::set_license(&name, &key);
    }
    noesis_runtime::init();

    // ── Case 1: Ctrl+Enter KeyBinding ───────────────────────────────────────
    {
        let mut bytes = HashMap::new();
        bytes.insert("scene.xaml".to_string(), SCENE.as_bytes().to_vec());
        let _registered = noesis_runtime::xaml_provider::set_xaml_provider(InMem { bytes });

        let root = FrameworkElement::load("scene.xaml").expect("load scene");
        let mut target = root.find_name("Target").expect("find Target");

        let counter = Arc::new(AtomicUsize::new(0));
        let c2 = Arc::clone(&counter);
        let command = Command::new(move |_param| {
            c2.fetch_add(1, Ordering::SeqCst);
        });

        let binding =
            KeyBinding::new(&command, Key::Return, ModifierKeys::CONTROL).expect("key binding");
        assert!(binding.add_to(&target), "add binding to InputBindings");

        let mut view = View::create(root);
        view.set_size(200, 200);
        view.activate();
        assert!(view.update(0.0), "first update builds tree");

        assert!(target.focus(), "Button accepts focus");
        let _ = view.update(0.016);
        assert!(target.is_keyboard_focused(), "target focused");

        // Sanity: a bare Enter (no Ctrl) must NOT match the Ctrl+Enter gesture.
        let _ = view.key_down(Key::Return);
        let _ = view.update(0.024);
        let _ = view.key_up(Key::Return);
        let _ = view.update(0.032);
        assert_eq!(
            counter.load(Ordering::SeqCst),
            0,
            "plain Enter must not trigger Ctrl+Enter binding"
        );

        let _ = view.key_down(Key::LeftCtrl);
        let _ = view.update(0.04);
        let _ = view.key_down(Key::Return);
        let _ = view.update(0.048);
        let _ = view.key_up(Key::Return);
        let _ = view.key_up(Key::LeftCtrl);
        let _ = view.update(0.056);

        assert_eq!(
            counter.load(Ordering::SeqCst),
            1,
            "Ctrl+Enter must fire the bound command exactly once"
        );

        // Tearing the binding down must stop the chord from firing it again.
        assert!(
            binding.remove_from(&target),
            "remove binding from InputBindings"
        );

        let _ = view.key_down(Key::LeftCtrl);
        let _ = view.update(0.064);
        let _ = view.key_down(Key::Return);
        let _ = view.update(0.072);
        let _ = view.key_up(Key::Return);
        let _ = view.key_up(Key::LeftCtrl);
        let _ = view.update(0.08);

        assert_eq!(
            counter.load(Ordering::SeqCst),
            1,
            "removed binding must not fire on a later Ctrl+Enter"
        );

        drop(view);
        drop(binding);
        drop(command);
    }

    // ── Case 2: explicit KeyGesture wrapped in a generic InputBinding ────────
    {
        let mut bytes = HashMap::new();
        bytes.insert("scene.xaml".to_string(), SCENE.as_bytes().to_vec());
        let _registered = noesis_runtime::xaml_provider::set_xaml_provider(InMem { bytes });

        let root = FrameworkElement::load("scene.xaml").expect("load scene");
        let mut target = root.find_name("Target").expect("find Target");

        let counter = Arc::new(AtomicUsize::new(0));
        let c2 = Arc::clone(&counter);
        let command = Command::new(move |_p| {
            c2.fetch_add(1, Ordering::SeqCst);
        });

        // gesture can be dropped once the binding takes its own reference
        let binding = {
            let gesture = KeyGesture::new(Key::F5, ModifierKeys::NONE);
            InputBinding::with_gesture(&command, &gesture).expect("input binding")
        };
        assert!(binding.add_to(&target), "attach input binding");

        let mut view = View::create(root);
        view.set_size(200, 200);
        view.activate();
        assert!(view.update(0.0));
        assert!(target.focus());
        let _ = view.update(0.016);
        assert!(target.is_keyboard_focused());

        let _ = view.key_down(Key::F5);
        let _ = view.update(0.024);
        let _ = view.key_up(Key::F5);
        let _ = view.update(0.032);

        assert_eq!(
            counter.load(Ordering::SeqCst),
            1,
            "F5 gesture fires the bound command"
        );

        drop(view);
        drop(binding);
        drop(command);
    }

    noesis_runtime::shutdown();
}