cranpose-ui 0.1.140

UI primitives for Cranpose
Documentation
use std::sync::{Arc, mpsc};

use super::*;
use crate::text::{AnnotatedString, TextLayoutResult, TextMeasurer, TextMetrics, TextStyle};

#[test]
fn a_draw_closure_can_name_its_own_node_for_the_next_frame() {
    let _scope = app_context_test_scope();
    let _ = take_draw_repass_nodes();
    let _ = take_render_invalidation();

    observe_draw_reads(DrawObservationScope::new(33, 0), || {
        request_current_draw_redraw();
    });
    assert_eq!(
        take_draw_repass_nodes(),
        vec![33],
        "the recording node must be scheduled for re-record"
    );
    assert!(
        take_render_invalidation(),
        "the next frame must be requested"
    );

    request_current_draw_redraw();
    assert!(take_draw_repass_nodes().is_empty());
    assert!(take_render_invalidation());
}

struct TestTextMeasurer;

impl TextMeasurer for TestTextMeasurer {
    fn measure(&self, text: &AnnotatedString, _style: &TextStyle) -> TextMetrics {
        TextMetrics {
            width: text.text.len() as f32,
            height: 1.0,
            line_height: 1.0,
            line_count: 1,
        }
    }

    fn get_offset_for_position(
        &self,
        text: &AnnotatedString,
        _style: &TextStyle,
        x: f32,
        _y: f32,
    ) -> usize {
        x.round().max(0.0) as usize % text.text.len().max(1)
    }

    fn get_cursor_x_for_offset(
        &self,
        _text: &AnnotatedString,
        _style: &TextStyle,
        offset: usize,
    ) -> f32 {
        offset as f32
    }

    fn layout(&self, text: &AnnotatedString, _style: &TextStyle) -> TextLayoutResult {
        TextLayoutResult::monospaced(&text.text, 1.0, 1.0)
    }
}

#[test]
fn app_context_ids_do_not_use_process_global_counter() {
    let source = include_str!("../render_state.rs");
    assert!(!source.contains(concat!("NEXT_", "APP_CONTEXT_ID: Atomic")));
}

#[test]
fn app_context_ids_are_unique_within_thread_registry() {
    let first = AppContext::new();
    let second = AppContext::new();

    assert_ne!(first.id, second.id);
    assert!(app_context_by_id(first.id).is_some());
    assert!(app_context_by_id(second.id).is_some());
}

#[test]
fn set_text_measurer_requires_active_app_context() {
    let result = std::panic::catch_unwind(|| {
        crate::text::set_text_measurer(TestTextMeasurer);
    });
    assert!(result.is_err());

    let context = AppContext::new();
    context.enter(|| {
        crate::text::set_text_measurer(TestTextMeasurer);
    });
}

#[test]
fn the_font_scale_starts_at_one_and_invalidates_layout_when_it_moves() {
    let context = AppContext::new();
    context.enter(|| {
        assert_eq!(current_font_scale(), 1.0);
        let _ = take_layout_invalidation();

        set_font_scale(1.3);
        assert_eq!(current_font_scale(), 1.3);
        assert!(
            take_layout_invalidation(),
            "every Sp on screen just changed size"
        );

        set_font_scale(1.3);
        assert!(!take_layout_invalidation());
    });
}

#[test]
fn a_font_scale_no_platform_reports_is_refused() {
    let context = AppContext::new();
    context.enter(|| {
        for nonsense in [0.0, -1.0, f32::NAN, f32::INFINITY] {
            set_font_scale(1.0);
            set_font_scale(nonsense);
            assert_eq!(current_font_scale(), 1.0, "{nonsense} was let through");
        }
        set_font_scale(99.0);
        assert_eq!(current_font_scale(), MAX_FONT_SCALE);
        set_font_scale(0.01);
        assert_eq!(current_font_scale(), MIN_FONT_SCALE);
    });
}

#[test]
fn the_font_scale_is_per_app_context() {
    let first = AppContext::new();
    let second = AppContext::new();
    first.enter(|| set_font_scale(1.5));
    first.enter(|| assert_eq!(current_font_scale(), 1.5));
    second.enter(|| assert_eq!(current_font_scale(), 1.0));
}

#[test]
fn invalidation_flags_are_shared_across_threads() {
    let state = Arc::new(RenderState::new_with_density(1.0));
    let (tx, rx) = mpsc::channel();
    let worker_state = Arc::clone(&state);

    let handle = std::thread::spawn(move || {
        worker_state
            .render_invalidated
            .store(true, Ordering::Relaxed);
        worker_state
            .pointer_invalidated
            .store(true, Ordering::Relaxed);
        worker_state
            .focus_invalidated
            .store(true, Ordering::Relaxed);
        worker_state
            .layout_invalidated
            .store(true, Ordering::Relaxed);
        worker_state
            .density_bits
            .store(f32::to_bits(2.0), Ordering::Relaxed);
        tx.send(()).expect("signal invalidation setup");

        f32::from_bits(worker_state.density_bits.load(Ordering::Relaxed))
    });

    rx.recv().expect("wait for worker invalidation setup");
    assert!(state.render_invalidated.load(Ordering::Relaxed));
    assert!(state.pointer_invalidated.load(Ordering::Relaxed));
    assert!(state.focus_invalidated.load(Ordering::Relaxed));
    assert!(state.layout_invalidated.load(Ordering::Relaxed));
    assert_eq!(
        f32::from_bits(state.density_bits.load(Ordering::Relaxed)),
        2.0
    );
    assert!(state.render_invalidated.swap(false, Ordering::Relaxed));
    assert!(state.pointer_invalidated.swap(false, Ordering::Relaxed));
    assert!(state.focus_invalidated.swap(false, Ordering::Relaxed));
    assert!(state.layout_invalidated.swap(false, Ordering::Relaxed));

    let density = handle.join().expect("worker invalidation snapshot");
    assert_eq!(density, 2.0);
    assert!(!state.render_invalidated.load(Ordering::Relaxed));
    assert!(!state.pointer_invalidated.load(Ordering::Relaxed));
    assert!(!state.focus_invalidated.load(Ordering::Relaxed));
    assert!(!state.layout_invalidated.load(Ordering::Relaxed));
}

#[test]
fn app_contexts_keep_density_and_invalidations_isolated() {
    let first = AppContext::new_with_density(1.0);
    let second = AppContext::new_with_density(1.0);

    first.enter(|| {
        set_density(2.0);
        request_render_invalidation();
        request_pointer_invalidation();
        schedule_layout_repass(11);
        schedule_draw_repass(12);
    });

    second.enter(|| {
        assert_eq!(current_density(), 1.0);
        assert!(!peek_render_invalidation());
        assert!(!peek_pointer_invalidation());
        assert!(!peek_layout_invalidation());
        assert!(!has_pending_layout_repasses());
        assert!(!has_pending_draw_repasses());
    });

    first.enter(|| {
        assert_eq!(current_density(), 2.0);
        assert!(peek_render_invalidation());
        assert!(peek_pointer_invalidation());
        assert!(peek_layout_invalidation());
        assert!(has_pending_layout_repasses());
        assert!(has_pending_draw_repasses());
        assert_eq!(take_layout_repass_nodes(), vec![11]);
        assert_eq!(take_draw_repass_nodes(), vec![12]);
        assert!(take_render_invalidation());
        assert!(take_pointer_invalidation());
        assert!(take_layout_invalidation());
    });
}

#[test]
fn app_contexts_keep_fling_velocity_diagnostics_isolated() {
    let first = AppContext::new_with_density(1.0);
    let second = AppContext::new_with_density(1.0);

    first.enter(|| {
        record_last_fling_velocity(1200.0);
        assert_eq!(debug_last_fling_velocity(), 1200.0);
    });

    second.enter(|| {
        assert_eq!(debug_last_fling_velocity(), 0.0);
        record_last_fling_velocity(-450.0);
        assert_eq!(debug_last_fling_velocity(), -450.0);
    });

    first.enter(|| {
        assert_eq!(debug_last_fling_velocity(), 1200.0);
        debug_reset_last_fling_velocity();
        assert_eq!(debug_last_fling_velocity(), 0.0);
    });

    second.enter(|| {
        assert_eq!(debug_last_fling_velocity(), -450.0);
    });
}

#[test]
fn app_context_new_uses_independent_density() {
    let outer = AppContext::new_with_density(2.0);
    let context = AppContext::new();
    context.enter(|| {
        assert_eq!(current_density(), 1.0);
    });
    outer.enter(|| {
        assert_eq!(current_density(), 2.0);
    });
}

#[test]
fn runtime_state_access_requires_explicit_app_context_even_in_tests() {
    let result = std::panic::catch_unwind(|| {
        request_render_invalidation();
    });
    assert!(result.is_err());
}

#[test]
fn app_contexts_keep_layout_frame_arenas_isolated() {
    let first = AppContext::new_with_density(1.0);
    let second = AppContext::new_with_density(1.0);

    first.enter(|| {
        assert_eq!(layout_frame_arena_placement_scratch_count(), 0);
        let mut arena = take_layout_frame_arena();
        arena.seed_placement_scratch_for_test();
        replace_layout_frame_arena(arena);
        assert_eq!(layout_frame_arena_placement_scratch_count(), 1);
    });

    second.enter(|| {
        assert_eq!(layout_frame_arena_placement_scratch_count(), 0);
    });

    first.enter(|| {
        assert_eq!(layout_frame_arena_placement_scratch_count(), 1);
    });
}

#[test]
fn current_app_context_scope_does_not_extend_context_lifetime() {
    let weak = {
        let context = AppContext::new_with_density(1.0);
        let weak = Rc::downgrade(&context);
        context.enter(|| {
            assert!(current_app_context().is_some());
        });
        weak
    };

    assert!(weak.upgrade().is_none());
    assert!(current_app_context().is_none());
}

#[test]
fn dropped_app_context_unregisters_from_thread_lookup_registry() {
    let start_count = app_context_registry_entry_count();

    let id = {
        let context = AppContext::new_with_density(1.0);
        let id = context.id;
        assert!(app_context_by_id(id).is_some());
        id
    };

    assert_eq!(
        app_context_registry_entry_count(),
        start_count,
        "dropped AppContexts must remove their weak registry entry"
    );
    assert!(app_context_by_id(id).is_none());
}