altui 0.2.0

A state-driven TUI runtime built on top of altui-core
Documentation
use std::{
    fmt::Debug,
    hash::Hash,
    ops::Deref,
    sync::{
        atomic::{AtomicUsize, Ordering},
        Arc, LazyLock,
    },
};

use altui_core::layout::Rect;

use crate::ids::WidgetId;

// 0 if no hover on views
pub(crate) static HOVER: LazyLock<AtomicUsize> = LazyLock::new(|| AtomicUsize::new(0));
// 0 if the is no active views
pub(crate) static ACTIVE: LazyLock<AtomicUsize> = LazyLock::new(|| AtomicUsize::new(0));

/// Per-view runtime state owned by `altui`.
///
/// `ViewCtx` is passed to [`View::logic`](crate::View::logic) and
/// [`View::on_event`](crate::View::on_event). It holds the state that the
/// runtime tracks for each inserted view:
///
/// - widget identity
/// - assigned area
/// - visibility
/// - participation in navigation
/// - hover/active state
/// - optional tags
///
/// The four constructor families describe the intended interaction model:
///
/// - [`silent`](Self::silent): rendered and updated, but not part of navigation
/// - [`button`](Self::button): one-shot action, active for a single frame
/// - [`selectable`](Self::selectable): navigable and event-aware without taking
///   exclusive control
/// - [`interactive`](Self::interactive): navigable and able to keep input focus
///   while active
///
/// Typical patterns:
///
/// - `simple_pages` uses `set_visible(false)` to hide views belonging to other pages
/// - `popup` keeps the popup visible and hovered while disabling the background
///   button with `set_interactive(false)`
///
/// See:
///
/// - <https://altlinux.space/writers/altui/src/branch/main/examples/simple_pages>
/// - <https://altlinux.space/writers/altui/src/branch/main/examples/popup>
pub struct ViewCtx {
    id: WidgetId,
    area: Rect,
    tags: Vec<Arc<str>>,
    visible: bool,
    interactive: bool,
    button: bool,
    selectable: bool,
}

impl Clone for ViewCtx {
    fn clone(&self) -> Self {
        Self {
            id: self.id,
            area: self.area,
            tags: self.tags.clone(),
            visible: self.visible,
            interactive: self.interactive,
            button: self.button,
            selectable: self.button,
        }
    }
}

impl PartialEq for ViewCtx {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

impl Hash for ViewCtx {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.id.hash(state);
    }
}

impl Eq for ViewCtx {}

impl Debug for ViewCtx {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("NodeState")
            .field("id", &self.id)
            .field("area", &self.area)
            .field("description", &self.tags)
            .field("visible", &self.visible)
            .field("interactive", &self.interactive)
            .field("button", &self.button)
            .field("selectable", &self.selectable)
            .finish()
    }
}

impl Default for ViewCtx {
    fn default() -> Self {
        Self::interactive()
    }
}

impl Deref for ViewCtx {
    type Target = Rect;

    fn deref(&self) -> &Self::Target {
        &self.area
    }
}

impl ViewCtx {
    /// Returns the internal numeric id used by the runtime.
    pub fn get_id(&self) -> usize {
        *self.id
    }

    /// Returns the stable [`WidgetId`] of this view.
    pub fn get_widget_id(&self) -> WidgetId {
        self.id
    }

    /// Returns the area assigned to this view for the current frame.
    pub fn get_area(&self) -> Rect {
        self.area
    }

    /// Returns a shared reference to the current area.
    pub fn get_area_ref(&self) -> &Rect {
        &self.area
    }

    /// Creates a context for an interactive view.
    ///
    /// Interactive views participate in navigation and may keep control of the
    /// event stream while active.
    pub fn interactive() -> Self {
        Self::interactive_with_tags(&[""])
    }

    /// Same as [`interactive`](Self::interactive), but initializes custom tags.
    pub fn interactive_with_tags(tags: &[impl Into<Arc<str>> + Clone]) -> Self {
        let tags = tags.iter().map(|tag| tag.to_owned().into()).collect();
        Self {
            id: WidgetId::next(),
            area: Rect::default(),
            tags,
            visible: true,
            interactive: true,
            button: false,
            selectable: false,
        }
    }

    /// Creates a context for a button-style view.
    ///
    /// Button views participate in navigation, become active on activation, and
    /// then automatically reset their active state after one frame.
    pub fn button() -> Self {
        Self::button_with_tags(&[""])
    }

    /// Same as [`button`](Self::button), but initializes custom tags.
    pub fn button_with_tags(tags: &[impl Into<Arc<str>> + Clone]) -> Self {
        let tags = tags.iter().map(|tag| tag.to_owned().into()).collect();
        Self {
            id: WidgetId::next(),
            area: Rect::default(),
            tags,
            visible: true,
            interactive: true,
            button: true,
            selectable: false,
        }
    }

    /// Creates a context for a selectable view.
    ///
    /// Selectable views participate in navigation and may react in
    /// [`View::on_event`](crate::View::on_event), but they are intended for
    /// extra behavior on top of navigation rather than for owning the input
    /// stream the way interactive views do.
    pub fn selectable() -> Self {
        Self::selectable_with_tags(&[""])
    }

    /// Same as [`selectable`](Self::selectable), but initializes custom tags.
    pub fn selectable_with_tags(tags: &[impl Into<Arc<str>> + Clone]) -> Self {
        let tags = tags.iter().map(|tag| tag.to_owned().into()).collect();
        Self {
            id: WidgetId::next(),
            area: Rect::default(),
            tags,
            visible: true,
            interactive: true,
            button: false,
            selectable: true,
        }
    }

    /// Creates a context for a non-interactive view.
    pub fn silent() -> Self {
        Self::silent_with_tags(&[""])
    }

    /// Same as [`silent`](Self::silent), but initializes custom tags.
    pub fn silent_with_tags(tags: &[impl Into<Arc<str>> + Clone]) -> Self {
        let tags = tags.iter().map(|tag| tag.to_owned().into()).collect();
        Self {
            id: WidgetId::next(),
            area: Rect::default(),
            tags,
            visible: true,
            interactive: false,
            button: false,
            selectable: false,
        }
    }

    /// Returns whether this view currently participates in navigation.
    pub fn is_interactive(&self) -> bool {
        self.interactive
    }

    /// Enables or disables navigation and event dispatch for this view.
    ///
    /// Disabling interactivity also clears hover for this view if it currently
    /// owns it. This is the mechanism used by layered UIs such as `popup`,
    /// where the background button remains visible but is removed from the
    /// navigation graph while the popup is open.
    pub fn set_interactive(&mut self, interactive: bool) {
        if !interactive {
            self.reset_hover();
        }
        self.interactive = interactive
    }

    /// Returns whether this view is a button-style one-shot action.
    pub fn is_button(&self) -> bool {
        self.button
    }

    /// Marks this view as button-style or not.
    ///
    /// Setting `button = true` clears `selectable`, because these two modes are
    /// mutually exclusive in the current runtime model.
    pub fn set_button(&mut self, button: bool) {
        if button {
            self.selectable = false;
        }
        self.button = button
    }

    /// Returns whether this view is selectable.
    pub fn is_selectable(&self) -> bool {
        self.selectable
    }

    /// Marks this view as selectable or not.
    ///
    /// Setting `selectable = true` clears `button`, because these two modes are
    /// mutually exclusive in the current runtime model.
    pub fn set_selectable(&mut self, selectable: bool) {
        if selectable {
            self.button = false;
        }
        self.selectable = selectable
    }

    /// Returns whether this view is visible.
    pub fn is_visible(&self) -> bool {
        self.visible
    }

    /// Shows or hides this view.
    ///
    /// Hiding a view also clears hover for it if necessary. Hidden views still
    /// exist in the runtime and still require matching rectangles in the `areas`
    /// closure, which is why page-style UIs usually combine `set_visible(false)`
    /// with `CtxStore::skip_areas(...)`.
    pub fn set_visible(&mut self, visible: bool) {
        if !visible {
            self.reset_hover();
        }
        self.visible = visible
    }

    /// Returns the current tag list.
    pub fn tags(&self) -> &[Arc<str>] {
        &self.tags
    }

    /// Appends one tag.
    pub fn set_tag(&mut self, tag: impl Into<Arc<str>>) {
        self.tags.push(tag.into());
    }

    /// Replaces all tags.
    pub fn set_tags(&mut self, tags: &[impl Into<Arc<str>> + Clone]) {
        let tags = tags.iter().map(|tag| tag.to_owned().into()).collect();
        self.tags = tags;
    }

    /// Removes one tag if it exists.
    pub fn remove_tag(&mut self, tag: impl Into<Arc<str>>) {
        let tag = tag.into();
        self.tags.retain(|old_tag| old_tag != &tag);
    }

    /// Removes all tags.
    pub fn clear_tags(&mut self) {
        self.tags.clear();
    }

    /// Sets the area assigned to this view for the current frame.
    pub fn set_area(&mut self, area: Rect) {
        self.area = area;
    }

    /// Marks this view as hovered.
    ///
    /// Only interactive views can own hover. This is mostly managed by the
    /// runtime, but views may also do it manually. The `popup` example uses
    /// this to keep the popup selected while its layer is open.
    pub fn set_hover(&self) {
        if self.is_interactive() {
            HOVER.store(self.get_id(), Ordering::Release);
            // ACTIVE.store(0, Ordering::Release);
        }
    }

    /// Clears hover if this view currently owns it.
    ///
    /// If the same view is also active, active is cleared as well.
    #[inline(always)]
    pub fn reset_hover(&self) {
        if self.get_id() == HOVER.load(Ordering::Acquire) {
            ACTIVE.store(0, Ordering::Release);
            HOVER.store(0, Ordering::Release);
        }
    }

    /// Clears active if this view currently owns it.
    #[inline(always)]
    pub fn reset_active(&self) {
        if self.get_id() == ACTIVE.load(Ordering::Acquire) {
            ACTIVE.store(0, Ordering::Release)
        }
    }

    /// Returns whether this view is the currently hovered one.
    pub fn is_hover(&self) -> bool {
        match self.is_interactive() {
            true => self.get_id() == HOVER.load(Ordering::Acquire),
            false => false,
        }
    }

    /// Marks this view as active.
    ///
    /// Only interactive and visible views can become active. Becoming active
    /// does not automatically set hover; the runtime usually activates the
    /// current hovered view through [`AppHandler::set_active`](crate::AppHandler::set_active).
    pub fn set_active(&self) {
        if self.is_interactive() && self.is_visible() {
            // HOVER.store(self.get_id(), Ordering::Release);
            ACTIVE.store(self.get_id(), Ordering::Release);
        }
    }

    /// Returns whether this view is the currently active one.
    pub fn is_active(&self) -> bool {
        match self.is_interactive() {
            true => self.get_id() == ACTIVE.load(Ordering::Acquire),
            false => false,
        }
    }
}