noesis_runtime 0.12.1

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
//! `ICollectionView` current-item navigation: every navigation method is driven
//! and asserted via live reads; pointer identity and a `CurrentChanged` counter
//! prove the FFI crossing.

use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};

use noesis_runtime::binding::ObservableCollection;
use noesis_runtime::collection_view::CollectionViewSource;

#[test]
fn collection_view_current_item_navigation() {
    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();

    {
        let mut list = ObservableCollection::new();
        list.push_string("alpha");
        list.push_string("beta");
        list.push_string("gamma");

        let mut cvs = CollectionViewSource::new();
        assert!(
            cvs.set_source(&list),
            "set_source on a CollectionViewSource"
        );

        let view = cvs
            .view()
            .expect("GetView returns a CollectionView once Source is set");
        assert_eq!(view.count(), 3, "view sees all 3 source records");

        // CurrentChanged counter to prove the event delegate crossed the FFI.
        let counter = Arc::new(AtomicU32::new(0));
        let counter_cb = Arc::clone(&counter);
        let _sub = view
            .subscribe_current_changed(move || {
                counter_cb.fetch_add(1, Ordering::SeqCst);
            })
            .expect("subscribe to CurrentChanged");

        assert!(view.move_current_to_first(), "first is a valid record");
        assert_eq!(view.current_position(), 0);
        assert_eq!(
            view.current_item().and_then(|i| i.as_string()).as_deref(),
            Some("alpha"),
            "current item is alpha"
        );

        assert!(view.move_current_to_next());
        assert_eq!(view.current_position(), 1);
        assert_eq!(
            view.current_item().and_then(|i| i.as_string()).as_deref(),
            Some("beta"),
        );

        assert!(view.move_current_to_last());
        assert_eq!(view.current_position(), 2);
        assert_eq!(
            view.current_item().and_then(|i| i.as_string()).as_deref(),
            Some("gamma"),
        );

        // Past the end, the cursor lands in the well-defined "after last"
        // state (position == count, no current item).
        let _ = view.move_current_to_next();
        assert!(view.is_current_after_last(), "cursor is after the last");
        assert!(
            view.current_item().is_none(),
            "no current item past the end"
        );
        assert_eq!(view.current_position(), 3, "after-last position is count");

        assert!(view.move_current_to_position(1));
        assert_eq!(view.current_position(), 1);
        assert_eq!(
            view.current_item().and_then(|i| i.as_string()).as_deref(),
            Some("beta"),
        );

        let _ = view.move_current_to_previous();
        assert_eq!(view.current_position(), 0);
        let _ = view.move_current_to_previous();
        assert!(view.is_current_before_first(), "cursor is before the first");
        assert!(
            view.current_item().is_none(),
            "no current item before the start"
        );
        assert_eq!(view.current_position(), -1, "before-first position is -1");

        // Pointer identity: the current item is the very object stored in the
        // source collection, not a copy.
        assert!(view.move_current_to_first());
        let item = view.current_item().expect("current item");
        let src0 = list.get(0).expect("source[0]");
        assert_eq!(
            item.raw(),
            src0.as_ptr(),
            "current item is the same boxed object as source[0]"
        );

        view.refresh();

        assert!(
            counter.load(Ordering::SeqCst) > 0,
            "CurrentChanged fired at least once during navigation"
        );
    }

    // Typed items navigate and unbox through the view's current item.
    {
        let mut list = ObservableCollection::new();
        list.push_bool(true);
        list.push_i32(42);
        list.push_f64(3.5);

        let mut cvs = CollectionViewSource::new();
        assert!(cvs.set_source(&list), "set_source on a typed collection");
        let view = cvs.view().expect("GetView once Source is set");
        assert_eq!(view.count(), 3);

        assert!(view.move_current_to_first());
        let item = view.current_item().expect("bool current item");
        assert_eq!(item.as_bool(), Some(true), "bool item round-trips");
        assert_eq!(item.as_i32(), None, "bool item is not an i32");
        assert_eq!(item.as_string(), None, "bool item is not a string");

        assert!(view.move_current_to_next());
        let item = view.current_item().expect("i32 current item");
        assert_eq!(item.as_i32(), Some(42), "i32 item round-trips");
        assert_eq!(item.as_string(), None, "i32 item is not a string");

        assert!(view.move_current_to_next());
        let item = view.current_item().expect("f64 current item");
        assert_eq!(item.as_f64(), Some(3.5), "f64 item round-trips");
        assert_eq!(item.as_bool(), None, "f64 item is not a bool");
    }

    noesis_runtime::shutdown();
}