lgui-core 0.2.0

Platform-neutral runtime and UI primitives for LGUI
Documentation
use super::*;
use crate::{
    application::AppView,
    core::{
        component, group, text, Color, Element, ElementRenderCx, InputEvent, InteractionRole,
        Point, PointerButton, PointerData, State, TextStyle, UiElement, UiId, VisualStyle,
    },
};
use std::sync::{
    atomic::{AtomicUsize, Ordering},
    Arc, Mutex,
};

#[test]
fn full_invalidation_marks_retained_components_dirty() {
    let mut session = UiSession::new();
    let components = session.runtime().component_tree();
    components.begin_render();
    let root = components.root(UiId::owned("session-root"), "session-root");
    components.begin_component_execution(root);
    components.finish_component(root);
    components.end_render();
    assert!(!components.is_dirty(root));

    session.invalidate_all();

    assert!(session.runtime().component_tree().is_dirty(root));
}

#[test]
fn suspending_rendering_releases_the_host_and_preserves_component_state() {
    let mut session = UiSession::new();
    let components = session.runtime().component_tree();
    components.begin_render();
    let root = components.root(UiId::owned("session-root"), "session-root");
    components.begin_component_execution(root);
    components.finish_component(root);
    components.end_render();

    session.suspend_rendering();

    assert!(!session.has_tree());
    assert!(session.runtime().component_tree().is_alive(root));
    assert!(session.runtime().component_tree().is_dirty(root));
}

#[test]
fn trimmed_component_output_rebuilds_without_losing_state() {
    let executions = Arc::new(AtomicUsize::new(0));
    let captured = Arc::new(Mutex::new(None::<State<i32>>));
    let view: AppView = Arc::new({
        let executions = Arc::clone(&executions);
        let captured = Arc::clone(&captured);
        move |_cx| {
            component((), {
                let executions = Arc::clone(&executions);
                let captured = Arc::clone(&captured);
                move |cx, _| {
                    executions.fetch_add(1, Ordering::SeqCst);
                    let value = cx.state(1_i32);
                    *captured.lock().expect("captured state poisoned") = Some(value.clone());
                    text(
                        UiRect::new(0.0, 0.0, 120.0, 40.0),
                        format!("value={}", value.get()),
                        TextStyle::new(Color::WHITE, 14.0, 400),
                    )
                }
            })
        }
    });
    let viewport = UiRect::new(0.0, 0.0, 120.0, 40.0);
    let mut session = UiSession::new();

    session.render_view(&view, viewport, UiScale::ONE);
    captured
        .lock()
        .expect("captured state poisoned")
        .as_ref()
        .expect("state handle missing")
        .set(7);
    session.apply_pending_updates();
    session.render_view(&view, viewport, UiScale::ONE);
    let before = session.memory_usage().0.rebuildable_bytes;

    assert!(before > 0);
    assert_eq!(session.trim_component_outputs(0), before);
    session.render_view(&view, viewport, UiScale::ONE);

    assert_eq!(executions.load(Ordering::SeqCst), 3);
    assert_eq!(
        captured
            .lock()
            .expect("captured state poisoned")
            .as_ref()
            .expect("state handle missing")
            .get(),
        7
    );
}

#[test]
fn state_update_uses_host_diff_damage_instead_of_invalidating_the_component_bounds() {
    let state = Arc::new(Mutex::new(None::<State<i32>>));
    let captured = Arc::clone(&state);
    let view: AppView = Arc::new(move |cx| {
        let count = cx.state(0_i32);
        *captured.lock().expect("captured state poisoned") = Some(count.clone());
        group(UiRect::new(0.0, 0.0, 400.0, 300.0)).content((
            text(
                UiRect::new(20.0, 20.0, 180.0, 60.0),
                "unchanged",
                TextStyle::new(Color::WHITE, 18.0, 400),
            ),
            text(
                UiRect::new(20.0, 80.0, 180.0, 120.0),
                format!("count={}", count.get()),
                TextStyle::new(Color::WHITE, 18.0, 400),
            ),
        ))
    });
    let viewport = UiRect::new(0.0, 0.0, 400.0, 300.0);
    let mut session = UiSession::new();

    let first = session.render_view(&view, viewport, UiScale::ONE);
    assert!(first.damage.dirty.is_full());

    state
        .lock()
        .expect("captured state poisoned")
        .as_ref()
        .expect("state handle missing")
        .update(|count| *count += 1);
    let pending = session.apply_pending_updates();
    assert_eq!(pending.dirty_ids.len(), 1);

    let second = session.render_view(&view, viewport, UiScale::ONE);

    assert!(!second.damage.dirty.is_full());
    assert!(!second.damage.dirty.is_empty());
    assert!(second.damage.dirty.dirty_area() < second.damage.dirty.viewport_area() / 2.0);
    assert!(second.damage.dirty.effective_rects().iter().all(|rect| rect
        .intersect(UiRect::new(20.0, 20.0, 180.0, 60.0))
        .is_none()));
}

#[test]
fn pointer_focus_reexecutes_components_losing_and_gaining_focus() {
    let first_executions = Arc::new(AtomicUsize::new(0));
    let second_executions = Arc::new(AtomicUsize::new(0));
    let first_observed_focus = Arc::new(Mutex::new(Vec::new()));
    let second_observed_focus = Arc::new(Mutex::new(Vec::new()));
    let first_id = Arc::new(Mutex::new(None::<UiId>));
    let second_id = Arc::new(Mutex::new(None::<UiId>));
    let view: AppView = Arc::new({
        let first_executions = Arc::clone(&first_executions);
        let second_executions = Arc::clone(&second_executions);
        let first_observed_focus = Arc::clone(&first_observed_focus);
        let second_observed_focus = Arc::clone(&second_observed_focus);
        let first_id = Arc::clone(&first_id);
        let second_id = Arc::clone(&second_id);
        move |_cx| {
            let field = |rect: UiRect,
                         executions: Arc<AtomicUsize>,
                         observed_focus: Arc<Mutex<Vec<bool>>>,
                         field_id: Arc<Mutex<Option<UiId>>>| {
                component((), move |_cx, _props| {
                    executions.fetch_add(1, Ordering::SeqCst);
                    Element::new(move |cx: ElementRenderCx<'_, '_, '_>| {
                        let focused = cx.context.interaction_flags(&cx.id).focused;
                        observed_focus
                            .lock()
                            .expect("observed focus poisoned")
                            .push(focused);
                        *field_id.lock().expect("field id poisoned") = Some(cx.id.clone());
                        UiElement::panel(
                            cx.id,
                            rect,
                            VisualStyle::filled(if focused {
                                Color(0x00FF00)
                            } else {
                                Color(0xFF0000)
                            }),
                        )
                        .interaction(InteractionRole::Button)
                    })
                })
            };
            group(UiRect::new(0.0, 0.0, 240.0, 120.0)).content((
                field(
                    UiRect::new(0.0, 0.0, 120.0, 40.0),
                    Arc::clone(&first_executions),
                    Arc::clone(&first_observed_focus),
                    Arc::clone(&first_id),
                ),
                field(
                    UiRect::new(0.0, 60.0, 120.0, 100.0),
                    Arc::clone(&second_executions),
                    Arc::clone(&second_observed_focus),
                    Arc::clone(&second_id),
                ),
            ))
        }
    });
    let viewport = UiRect::new(0.0, 0.0, 240.0, 120.0);
    let mut session = UiSession::new();

    session.render_view(&view, viewport, UiScale::ONE);
    let first_id = first_id
        .lock()
        .expect("first id poisoned")
        .clone()
        .expect("first id missing");
    let second_id = second_id
        .lock()
        .expect("second id poisoned")
        .clone()
        .expect("second id missing");

    click(&mut session, Point::new(10.0, 10.0));
    assert_eq!(
        session.runtime().interaction_state().focused,
        Some(first_id)
    );
    session.render_view(&view, viewport, UiScale::ONE);

    click(&mut session, Point::new(10.0, 70.0));
    assert_eq!(
        session.runtime().interaction_state().focused,
        Some(second_id)
    );
    session.render_view(&view, viewport, UiScale::ONE);

    assert_eq!(first_executions.load(Ordering::SeqCst), 3);
    assert_eq!(second_executions.load(Ordering::SeqCst), 2);
    assert_eq!(
        *first_observed_focus
            .lock()
            .expect("first observed focus poisoned"),
        vec![false, true, false]
    );
    assert_eq!(
        *second_observed_focus
            .lock()
            .expect("second observed focus poisoned"),
        vec![false, true]
    );
}

fn click(session: &mut UiSession, point: Point) {
    for input in [
        InputEvent::PointerDown {
            pointer: PointerData::mouse(point),
            button: PointerButton::Left,
        },
        InputEvent::PointerUp {
            pointer: PointerData::mouse(point),
            button: PointerButton::Left,
        },
    ] {
        let output = session.handle_input(input);
        if let Some(bounds) = output.dirty_bounds {
            session.invalidations_mut().invalidate_rect(bounds);
        }
    }
}