lgui-core 0.2.0

Platform-neutral runtime and UI primitives for LGUI
Documentation
use std::{
    future::Future,
    sync::{
        atomic::{AtomicUsize, Ordering},
        Arc, Mutex,
    },
    task::{Context, Poll, Waker},
};

use crate::{
    application::RenderErrorStage,
    command::Command,
    core::{Size, UiRect},
};

use super::*;

struct RecordingBackend(Arc<Mutex<Option<WindowOptions>>>);

struct ReportingBackend;

struct BuilderCommand;

impl Command for BuilderCommand {
    type Args = usize;
    type Output = usize;
    type Error = ();

    const NAME: &'static str = "test.builder";
}

struct InvokingBackend(Arc<Mutex<Option<usize>>>);

impl ApplicationBackend for RecordingBackend {
    type Error = ();

    fn run(
        self,
        options: WindowOptions,
        _view: AppView,
        _context: ApplicationContext,
    ) -> Result<(), Self::Error> {
        *self.0.lock().expect("window options lock poisoned") = Some(options);
        Ok(())
    }
}

impl ApplicationBackend for ReportingBackend {
    type Error = ();

    fn run(
        self,
        _options: WindowOptions,
        _view: AppView,
        context: ApplicationContext,
    ) -> Result<(), Self::Error> {
        context.report_render_error(RenderError::new(
            WindowId::new("reporting-window"),
            "test",
            RenderErrorStage::Present,
            "present_test_frame",
            0x80004005_u32 as i32,
            "synthetic present failure",
        ));
        Ok(())
    }
}

impl ApplicationBackend for InvokingBackend {
    type Error = ();

    fn run(
        self,
        _options: WindowOptions,
        _view: AppView,
        context: ApplicationContext,
    ) -> Result<(), Self::Error> {
        let mut future = Box::pin(context.invoke::<BuilderCommand>(41));
        let waker = Waker::noop();
        let mut task_context = Context::from_waker(waker);
        let Poll::Ready(result) = future.as_mut().poll(&mut task_context) else {
            panic!("test command unexpectedly pending");
        };
        *self.0.lock().expect("command result lock poisoned") = Some(result?);
        Ok(())
    }
}

#[test]
fn application_builder_registers_commands_on_the_runtime_context() {
    let result = Arc::new(Mutex::new(None));

    Application::with_backend(InvokingBackend(Arc::clone(&result)))
        .memory_options(crate::memory::test_memory_options())
        .command::<BuilderCommand>(|_, value| async move { Ok(value + 1) })
        .run(|cx| crate::core::group(cx.viewport()))
        .expect("invoking backend should run");

    assert_eq!(
        *result.lock().expect("command result lock poisoned"),
        Some(42)
    );
}

#[test]
fn application_memory_options_are_owned_by_the_runtime_context() {
    struct MemoryBackend(Arc<Mutex<Option<crate::memory::MemoryOptions>>>);

    impl ApplicationBackend for MemoryBackend {
        type Error = ();

        fn run(
            self,
            _options: WindowOptions,
            _view: AppView,
            context: ApplicationContext,
        ) -> Result<(), Self::Error> {
            *self.0.lock().expect("memory options lock poisoned") =
                Some(context.memory().options());
            Ok(())
        }
    }

    let captured = Arc::new(Mutex::new(None));
    let options = crate::memory::test_memory_options();
    Application::with_backend(MemoryBackend(Arc::clone(&captured)))
        .memory_options(options)
        .run(|cx| crate::core::group(cx.viewport()))
        .unwrap();

    assert_eq!(
        *captured.lock().expect("memory options lock poisoned"),
        Some(options)
    );
}

#[test]
fn render_errors_are_delivered_with_structured_context() {
    let errors = Arc::new(Mutex::new(Vec::new()));
    let captured = Arc::clone(&errors);

    Application::with_backend(ReportingBackend)
        .memory_options(crate::memory::test_memory_options())
        .on_render_error(move |error| {
            captured
                .lock()
                .expect("render error capture poisoned")
                .push(error.clone());
        })
        .run(|cx| crate::core::group(cx.viewport()))
        .expect("reporting backend should run");

    let errors = errors.lock().expect("render error capture poisoned");
    assert_eq!(errors.len(), 1);
    assert_eq!(errors[0].window().as_str(), "reporting-window");
    assert_eq!(errors[0].renderer(), "test");
    assert_eq!(errors[0].stage(), RenderErrorStage::Present);
    assert_eq!(errors[0].operation(), "present_test_frame");
    assert_eq!(errors[0].code(), 0x80004005_u32 as i32);
    assert_eq!(errors[0].message(), "synthetic present failure");
}

#[test]
fn application_passes_window_options_to_the_backend() {
    let recorded = Arc::new(Mutex::new(None));
    let options = WindowOptions::new("counter")
        .size(Size::new(640.0, 480.0))
        .minimum_size(Size::new(320.0, 240.0))
        .maximum_size(Size::new(1280.0, 960.0))
        .resizable(false);

    Application::with_backend(RecordingBackend(Arc::clone(&recorded)))
        .memory_options(crate::memory::test_memory_options())
        .window_options(options.clone())
        .run(|_| crate::core::group(UiRect::new(0.0, 0.0, 1.0, 1.0)))
        .expect("mock backend should run");

    let recorded = recorded
        .lock()
        .expect("window options lock poisoned")
        .clone()
        .expect("backend should receive window options");
    assert_eq!(recorded.id, options.id);
    assert_eq!(recorded.size, options.size);
    assert_eq!(recorded.minimum_size, options.minimum_size);
    assert_eq!(recorded.maximum_size, options.maximum_size);
    assert_eq!(recorded.resizable, options.resizable);
}

#[test]
fn application_handle_posts_tasks_and_requests_frames() {
    let posted = Arc::new(AtomicUsize::new(0));
    let frames = Arc::new(AtomicUsize::new(0));
    let handle = ApplicationHandle::new(
        {
            let posted = Arc::clone(&posted);
            move |task| {
                task();
                posted.fetch_add(1, Ordering::SeqCst);
            }
        },
        {
            let frames = Arc::clone(&frames);
            move || {
                frames.fetch_add(1, Ordering::SeqCst);
            }
        },
    );

    handle.post(|| {});
    handle.request_frame();

    assert_eq!(posted.load(Ordering::SeqCst), 1);
    assert_eq!(frames.load(Ordering::SeqCst), 1);
}

struct RenderingBackend(Arc<AtomicUsize>);

impl ApplicationBackend for RenderingBackend {
    type Error = ();

    fn run(
        self,
        _options: WindowOptions,
        view: AppView,
        _context: ApplicationContext,
    ) -> Result<(), Self::Error> {
        let mut session = crate::session::UiSession::new();
        let _ = session.render_view(
            &view,
            UiRect::new(0.0, 0.0, 320.0, 200.0),
            crate::core::UiScale::ONE,
        );
        self.0.fetch_add(1, Ordering::SeqCst);
        Ok(())
    }
}

#[test]
fn application_run_mounts_and_renders_the_root_component() {
    let backend_frames = Arc::new(AtomicUsize::new(0));
    let root_executions = Arc::new(AtomicUsize::new(0));
    let executions = Arc::clone(&root_executions);

    Application::with_backend(RenderingBackend(Arc::clone(&backend_frames)))
        .memory_options(crate::memory::test_memory_options())
        .run(move |cx| {
            let _application = cx.application();
            executions.fetch_add(1, Ordering::SeqCst);
            crate::core::content_text("root")
        })
        .expect("recording backend should render");

    assert_eq!(backend_frames.load(Ordering::SeqCst), 1);
    assert_eq!(root_executions.load(Ordering::SeqCst), 1);
}