lgui-core 0.2.0

Platform-neutral runtime and UI primitives for LGUI
Documentation
use super::*;
use crate::{
    application::ApplicationContext,
    core::{
        component, content_text, context_provider, ComponentTree, HostTree, HostTreeBuilder,
        RootComponent, UiRect, UiRuntime, UiScale, UiTask,
    },
    events::{Event, EventKey},
};
use std::{
    sync::atomic::{AtomicUsize, Ordering},
    task::{Context, Poll, Waker},
};

#[derive(Clone)]
struct HookEvent;

const HOOK_EVENT: EventKey<HookEvent> = EventKey::new("test.hook");

impl Event for HookEvent {
    const NAME: &'static str = "test.hook";
}

#[allow(dead_code)]
fn async_event_hook_is_part_of_the_portable_api(cx: &mut RenderCx<'_, '_>) {
    cx.use_event_async_once::<HookEvent>(|_, _| async {});
    cx.listen_async(HOOK_EVENT.clone(), |_| async {});
}

struct EventRoot {
    application: ApplicationContext,
    mounted: bool,
    deliveries: Arc<AtomicUsize>,
}

impl RootComponent for EventRoot {
    fn render_root(self, _cx: &mut RenderCx<'_, '_>) -> crate::core::Element {
        let content = if self.mounted {
            let deliveries = Arc::clone(&self.deliveries);
            component((), move |_cx, _| {
                let deliveries = Arc::clone(&deliveries);
                crate::events::listen(HOOK_EVENT.clone(), move |_| {
                    deliveries.fetch_add(1, Ordering::SeqCst);
                });
                content_text("mounted")
            })
        } else {
            content_text("unmounted")
        };
        context_provider(self.application, content)
    }
}

fn mount_event_root(ui: &UiRuntime, tree: HostTree, root: impl RootComponent) -> HostTree {
    let viewport = UiRect::new(0.0, 0.0, 10.0, 10.0);
    let interaction = ui.interaction_state();
    let mut builder = HostTreeBuilder::from_retained(tree);
    builder.mount(
        root,
        viewport,
        &interaction,
        ui.animations(),
        ui.component_states(),
        ui.component_tree(),
        ui.contexts(),
        ui.hook_states(),
        ui.hook_updates(),
        ui.task_spawner(),
        ui.effects(),
        UiScale::ONE,
    );
    builder.finish()
}

const ASYNC_SOURCE: EventKey<usize> = EventKey::new("test.async.source");
const ASYNC_RESULT: EventKey<usize> = EventKey::new("test.async.result");

struct AsyncEventRoot {
    application: ApplicationContext,
    total: Arc<AtomicUsize>,
}

impl RootComponent for AsyncEventRoot {
    fn render_root(self, _cx: &mut RenderCx<'_, '_>) -> crate::core::Element {
        let total = Arc::clone(&self.total);
        context_provider(
            self.application,
            component((), move |_cx, _| {
                crate::events::listen_async(ASYNC_SOURCE.clone(), |value| async move {
                    crate::events::emit(ASYNC_RESULT.clone(), value).await;
                });
                let total = Arc::clone(&total);
                crate::events::listen(ASYNC_RESULT.clone(), move |value| {
                    total.fetch_add(value, Ordering::SeqCst);
                });
                content_text("async listeners")
            }),
        )
    }
}

#[test]
fn equality_setter_is_directly_callable_and_try_update_enqueues_once() {
    let value = Arc::new(Mutex::new(1_u32));
    let writes = Arc::new(AtomicUsize::new(0));
    let set_writes = Arc::clone(&writes);
    let state = State {
        value: Arc::clone(&value),
        invalidate: Arc::new(move || {
            set_writes.fetch_add(1, Ordering::SeqCst);
        }),
    };
    let setter = StateSetter::new(state).with_equality();

    setter(1);
    assert_eq!(writes.load(Ordering::SeqCst), 0);
    assert!(setter.try_update(|value| {
        *value += 1;
        true
    }));
    assert_eq!(writes.load(Ordering::SeqCst), 1);
    assert_eq!(*value.lock().expect("test state value poisoned"), 2);
}

#[test]
fn stale_state_handle_cannot_dirty_a_remounted_component_generation() {
    let components = ComponentTree::new();
    let updates = Arc::new(super::super::UiUpdateQueue::new());
    let store = super::super::HookStateStore::new();

    components.begin_render();
    let old_owner = components.root(UiId::owned("state-owner"), "state owner");
    components.begin_component_execution(old_owner);
    components.finish_component(old_owner);
    components.end_render();

    let state_updates = Arc::clone(&updates);
    let state = State {
        value: Arc::new(Mutex::new(1_u32)),
        invalidate: Arc::new(move || {
            state_updates.invalidate(old_owner, UiId::owned("state-owner"));
        }),
    };

    components.begin_render();
    components.end_render();
    components.begin_render();
    let new_owner = components.root(UiId::owned("state-owner"), "state owner");
    components.begin_component_execution(new_owner);
    components.finish_component(new_owner);
    components.end_render();
    assert_ne!(old_owner, new_owner);

    state.set(2);

    assert!(updates.apply(&store, &components).is_empty());
    assert!(!components.is_dirty(new_owner));
}

#[test]
fn event_hook_unsubscribes_when_its_component_unmounts() {
    let ui = UiRuntime::new();
    let application = ApplicationContext::empty(crate::memory::test_memory_options());
    let deliveries = Arc::new(AtomicUsize::new(0));

    let tree = mount_event_root(
        &ui,
        HostTree::new(),
        EventRoot {
            application: application.clone(),
            mounted: true,
            deliveries: Arc::clone(&deliveries),
        },
    );
    ui.run_effects();
    assert_eq!(application.emit_keyed(&HOOK_EVENT, HookEvent), 1);
    assert_eq!(deliveries.load(Ordering::SeqCst), 1);

    ui.component_tree().mark_all_dirty();
    let _tree = mount_event_root(
        &ui,
        tree,
        EventRoot {
            application: application.clone(),
            mounted: false,
            deliveries: Arc::clone(&deliveries),
        },
    );
    ui.run_effects();

    assert_eq!(application.emit_keyed(&HOOK_EVENT, HookEvent), 0);
    assert_eq!(deliveries.load(Ordering::SeqCst), 1);
}

#[test]
fn async_listener_tasks_inherit_their_application_scope() {
    let ui = UiRuntime::new();
    let application = ApplicationContext::empty(crate::memory::test_memory_options());
    application.set_executor(Arc::new(|mut task: UiTask| {
        let waker = Waker::noop();
        let mut context = Context::from_waker(waker);
        assert!(matches!(task.as_mut().poll(&mut context), Poll::Ready(())));
    }));
    let total = Arc::new(AtomicUsize::new(0));

    let _tree = mount_event_root(
        &ui,
        HostTree::new(),
        AsyncEventRoot {
            application: application.clone(),
            total: Arc::clone(&total),
        },
    );
    ui.run_effects();

    assert_eq!(application.emit_keyed(&ASYNC_SOURCE, 4), 1);
    assert_eq!(total.load(Ordering::SeqCst), 4);
}