altui 0.2.0

A state-driven TUI runtime built on top of altui-core
Documentation
use std::sync::{
    atomic::{AtomicUsize, Ordering},
    LazyLock,
};

use crate::context::{ACTIVE, HOVER};

// 1 - running
// 11 - running and hover
pub(crate) static RUNNING: LazyLock<AtomicUsize> = LazyLock::new(|| AtomicUsize::new(1));

/// Trait implemented by the global application state.
///
/// `AppHandler` gives `altui` access to the shared runtime state that lives
/// above individual views:
///
/// - whether the application should keep running
/// - which view is currently hovered
/// - which view is currently active
///
/// The default implementation stores this data in process-wide atomics. That is
/// why `ViewLoop` can work with a very small application trait while
/// [`ViewCtx`](crate::ViewCtx) still exposes per-view helpers.
///
/// Conceptually:
///
/// - `hover` means "the view currently selected by navigation"
/// - `active` means "the view currently handling the active action or owning input"
///
/// This is the state-level side of the same model used by
/// [`ViewCtx::is_hover`](crate::ViewCtx::is_hover) and
/// [`ViewCtx::is_active`](crate::ViewCtx::is_active).
pub trait AppHandler: 'static {
    /// Returns `true` while the application should continue running.
    fn running(&self) -> bool {
        let running = RUNNING.swap(1, std::sync::atomic::Ordering::Acquire);
        running == 1 || running == 11
    }

    /// Requests application shutdown.
    ///
    /// Because `altui` uses a two-step running state internally, calling
    /// `stop()` immediately after hover-resetting navigation may require one
    /// more frame before `running()` turns `false`. In normal usage this detail
    /// is invisible, and examples simply call `state.stop()` from
    /// `event_handler`.
    fn stop(&self) {
        let running = RUNNING.load(std::sync::atomic::Ordering::Acquire);
        match running {
            1 => RUNNING.store(0, std::sync::atomic::Ordering::Release),
            11 => RUNNING.store(1, std::sync::atomic::Ordering::Release),
            _ => panic!("Altui: Wrong 'RUNNING' value"),
        }
    }

    /// Returns the id of the currently hovered view, if any.
    fn hover(&self) -> Option<usize> {
        let hover = HOVER.load(Ordering::Acquire);
        match hover == 0 {
            true => None,
            false => Some(hover),
        }
    }

    /// Returns the id of the currently active view, if any.
    fn active(&self) -> Option<usize> {
        let active = ACTIVE.load(Ordering::Acquire);
        match active == 0 {
            true => None,
            false => Some(active),
        }
    }

    /// Clears hover state.
    ///
    /// This only succeeds when no view is active. If a view currently owns the
    /// active state, resetting hover would leave the runtime in an inconsistent
    /// state, so the method returns an error instead.
    fn reset_hover(&self) -> std::io::Result<usize> {
        match self.active().is_some() {
            true => return Err(std::io::Error::other("Failed to reset hover")),
            false => {
                RUNNING.store(11, std::sync::atomic::Ordering::Release);
                Ok(HOVER.swap(0, Ordering::Release))
            }
        }
    }

    /// Clears active state and returns the previous active id, if any.
    fn reset_active(&self) -> Option<usize> {
        let prev = ACTIVE.swap(0, Ordering::Release);
        match prev != 0 {
            true => Some(prev),
            false => None,
        }
    }

    /// Sets the currently hovered view.
    ///
    /// This also clears the previous active view, if any. It is the state-level
    /// counterpart of [`WidgetId::set_hover`](crate::WidgetId::set_hover).
    fn set_hover(&self, id: usize) {
        ACTIVE.store(0, Ordering::Release);
        HOVER.store(id, Ordering::Release);
    }

    /// Activates the currently hovered view.
    ///
    /// This is the transition used by built-in navigation when the user presses
    /// `Enter`, `i`, `Space`, or clicks a hovered view with the mouse.
    fn set_active(&self) -> std::io::Result<()> {
        if let Some(id) = self.hover() {
            ACTIVE.store(id, Ordering::Release);
            Ok(())
        } else {
            return Err(std::io::Error::other("Failed to set active"));
        }
    }
}