oxiforge 0.4.0

YAML-to-Rust code generator for oxivgl LVGL UIs
Documentation
// SPDX-License-Identifier: GPL-3.0-only
//! Shared LVGL test infrastructure — mirrors oxivgl's tests/common/mod.rs.
//!
//! Provides:
//! - Single-thread enforcement (panics if tests run in parallel)
//! - LVGL driver init (shared instance, headless display backend)
//! - `fresh_screen()` for test isolation
//! - `pump()` for layout/render pass
//! - `get_label_text()` helper (lv_label_get_text has no oxivgl wrapper)
//! - `child_count_raw()` helper for Screen child counts

// LVGL objects are opaque — use c_void to avoid a direct oxivgl_sys dependency.
use core::ffi::{c_char, c_void};
use std::sync::{
    Once,
    atomic::{AtomicUsize, Ordering},
};

use oxiforge::{
    driver::LvglDriver,
    prelude::{AsLvHandle, Screen},
};

/// LVGL raw object pointer, aliased to c_void for FFI without importing
/// oxivgl_sys.
pub type LvRawObj = c_void;

// Declare the raw LVGL C functions we need that lack safe wrappers in oxivgl.
unsafe extern "C" {
    fn lv_obj_create(parent: *mut LvRawObj) -> *mut LvRawObj;
    fn lv_screen_load(scr: *mut LvRawObj);
    fn lv_refr_now(disp: *mut LvRawObj);
    fn lv_obj_get_child_count(obj: *const LvRawObj) -> u32;
    fn lv_obj_get_child(obj: *const LvRawObj, id: i32) -> *mut LvRawObj;
    fn lv_label_get_text(obj: *mut LvRawObj) -> *const c_char;
}

static INIT: Once = Once::new();
static mut DRIVER: Option<LvglDriver> = None;

/// Thread ID of the first caller — used to enforce single-threaded access.
static INIT_THREAD: AtomicUsize = AtomicUsize::new(0);

/// Initialise LVGL once. Panics if:
/// - `SDL_VIDEODRIVER` is not set
/// - Called from a different thread than the first caller (detects parallel
///   test runs)
pub fn ensure_init() {
    let tid = thread_id();
    let prev = INIT_THREAD.compare_exchange(0, tid, Ordering::SeqCst, Ordering::SeqCst);
    match prev {
        Ok(_) => {}
        Err(first_tid) => {
            assert_eq!(
                first_tid, tid,
                "LVGL tests must run single-threaded (--test-threads=1). \
                 First init on thread {first_tid}, now called from {tid}."
            );
        }
    }

    INIT.call_once(|| {
        assert!(std::env::var("SDL_VIDEODRIVER").is_ok(), "SDL_VIDEODRIVER not set — run via: ./run_runtime_tests.sh");
        // SAFETY: single-threaded (enforced above).
        unsafe { DRIVER = Some(LvglDriver::init(320, 240)) };
    });
}

/// Get a fresh screen, clearing all widgets from the previous test.
pub fn fresh_screen() -> Screen {
    ensure_init();
    // SAFETY: LVGL is initialised via ensure_init(); single-threaded.
    // Creating a new screen object and loading it replaces the previous active
    // screen.
    unsafe {
        let new = lv_obj_create(core::ptr::null_mut());
        lv_screen_load(new);
    }
    Screen::active().expect("no active screen after init")
}

/// Pump LVGL timer and force a full layout + refresh pass.
pub fn pump() {
    // SAFETY: DRIVER is set by ensure_init() before this is called.
    let driver = unsafe { (*core::ptr::addr_of!(DRIVER)).as_ref() }.expect("pump() called before ensure_init()");
    driver.timer_handler();
    // SAFETY: LVGL is initialised; null pointer means "all displays".
    unsafe { lv_refr_now(core::ptr::null_mut()) };
}

/// Get the child count of a widget by its raw handle (as `*mut c_void`).
///
/// Used for `Screen` which does not expose `get_child_count()` directly via
/// oxivgl.
///
/// # Safety
/// The `handle` returned by `screen.handle()` (cast to `*mut c_void`) must be
/// a valid, non-null `lv_obj_t` pointer for the duration of this call.
pub unsafe fn child_count_raw(handle: *mut LvRawObj) -> u32 {
    // SAFETY: caller guarantees handle is a valid non-null lv_obj_t pointer.
    unsafe { lv_obj_get_child_count(handle) }
}

/// Get a child widget by index from a raw handle (as `*mut c_void`).
///
/// Returns null if the index is out of range.
///
/// # Safety
/// `handle` must be a valid, non-null `lv_obj_t` pointer.
pub unsafe fn get_child_raw(handle: *mut LvRawObj, idx: i32) -> *mut LvRawObj {
    // SAFETY: caller guarantees handle is a valid non-null lv_obj_t pointer.
    unsafe { lv_obj_get_child(handle, idx) }
}

/// Read the text from an LVGL label widget via the raw C API.
///
/// oxivgl does not wrap `lv_label_get_text`, so we declare the extern fn here.
///
/// # Safety
/// `handle` must be a valid, non-null `lv_obj_t` pointer to a label widget.
pub unsafe fn get_label_text(handle: *mut LvRawObj) -> String {
    // SAFETY: caller guarantees handle is a valid label pointer.
    let ptr = unsafe { lv_label_get_text(handle) };
    if ptr.is_null() {
        return String::new();
    }
    // SAFETY: LVGL returns a null-terminated C string.
    unsafe { std::ffi::CStr::from_ptr(ptr) }.to_str().expect("label text is valid UTF-8").to_owned()
}

/// Cast an `AsLvHandle` widget pointer to the opaque `*mut LvRawObj` used by
/// our inline FFI declarations.
///
/// This is a pointer cast only — no data is moved or copied.
pub fn as_raw<W: AsLvHandle>(widget: &W) -> *mut LvRawObj {
    widget.lv_handle().cast::<LvRawObj>()
}

fn thread_id() -> usize {
    // Use a thread-local variable's address as a unique thread identifier.
    thread_local! { static ANCHOR: u8 = const { 0 }; }
    ANCHOR.with(|a| a as *const u8 as usize)
}