altui 0.2.0

A state-driven TUI runtime built on top of altui-core
Documentation
use altui_core::{buffer::Buffer, layout::Rect, widgets::Widget};
use crossterm::event::Event;

use crate::{context::ViewCtx, ctxstore::AreaCache};

/// A state-aware UI component.
///
/// `View` is the central abstraction of `altui`.
///
/// Each view:
///
/// - Executes frame logic
/// - Receives events
/// - Renders into a terminal buffer
///
/// Views are stored by [`ViewFactory`](crate::ViewFactory) and reused across
/// frames.
///
/// # Associated Type
///
/// ## `State`
///
/// The global application state shared across all views.
///
/// This enables a fully explicit state model without requiring
/// `Rc<RefCell<_>>` or global variables.
///
/// ## Examples
///
/// Minimal custom view:
/// ```
/// use altui::{buffer::Buffer, AreaCache, View, ViewCtx};
/// use altui_core::{layout::Rect, widgets::{Block, Widget}};
///
/// struct BlockView(Block<'static>);
/// struct AppState;
///
/// impl View for BlockView {
///     type State = AppState;
///
///     fn logic(&mut self, _ctx: &mut ViewCtx, _state: &mut AppState) {}
///
///     fn render(&mut self, area: Rect, _cache: &mut AreaCache, buf: &mut Buffer) {
///         self.0.render(area, buf);
///     }
/// }
/// ```
///
/// Minimal event-aware view:
/// ```
/// use altui::{buffer::Buffer, widgets::Widget, AreaCache, Input, TextArea, View, ViewCtx};
/// use altui_core::layout::Rect;
/// use crossterm::event::Event;
///
/// struct EditorView<'a>(TextArea<'a>);
/// struct AppState;
///
/// impl View for EditorView<'_> {
///     type State = AppState;
///
///     fn on_event(&mut self, event: Event, _ctx: &mut ViewCtx, _state: &mut AppState) {
///         if let Event::Key(key) = event {
///             let _ = self.0.input(Input::from(key));
///         }
///     }
///
///     fn render(&mut self, area: Rect, _cache: &mut AreaCache, buf: &mut Buffer) {
///         self.0.render(area, buf);
///     }
/// }
/// ```
///
/// Full scenarios:
///
/// - `scroll_paragraph`: <https://altlinux.space/writers/altui/src/branch/main/examples/scroll_paragraph>
/// - `popup`: <https://altlinux.space/writers/altui/src/branch/main/examples/popup>
#[allow(unused_variables, reason = "Default impls don't use method arguments")]
pub trait View {
    type State;

    /// Executes per-frame logic before rendering.
    ///
    /// Use this for:
    ///
    /// - Updating internal state
    /// - Reacting to layout context
    /// - Synchronizing with global application state
    ///
    /// `logic` execution timing:
    ///
    /// - For active or hovered view: executed immediately after event dispatch.
    /// - For all other views: executed later in the frame.
    ///
    fn logic(&mut self, ctx: &mut ViewCtx, state: &mut Self::State) {}

    /// Handles an incoming event.
    ///
    /// Called when the event loop receives input.
    ///
    /// Override this if the view needs keyboard or mouse interaction.
    fn on_event(&mut self, event: Event, ctx: &mut ViewCtx, state: &mut Self::State) {}

    /// Renders the view into the provided buffer.
    ///
    /// # Parameters
    ///
    /// - `area`: the rectangular region assigned to this view
    /// - `cache`: geometry cache for resize-aware behavior
    /// - `buf`: terminal buffer
    ///
    /// This method must be implemented.
    fn render(&mut self, area: Rect, cache: &mut AreaCache, buf: &mut Buffer);
}

impl<T> View for Box<T>
where
    T: View,
{
    type State = T::State;

    fn render(&mut self, area: Rect, cache: &mut AreaCache, buf: &mut Buffer) {
        self.as_mut().render(area, cache, buf);
    }

    fn logic(&mut self, ctx: &mut ViewCtx, state: &mut Self::State) {
        self.as_mut().logic(ctx, state);
    }

    fn on_event(&mut self, event: Event, ctx: &mut ViewCtx, state: &mut Self::State) {
        self.as_mut().on_event(event, ctx, state);
    }
}

/// A generic adapter that turns any `Widget` into a [`View`].
///
/// This is the recommended choice when:
///
/// - The widget does not need event handling
/// - The widget has minimal logic
/// - You want a quick integration with the view system
///
/// For interactive components, implement [`View`] directly.
pub struct AnyView<State, W> {
    logic: Box<dyn Fn(&mut W, &mut ViewCtx, &mut State)>,
    widget: W,
    phantom: std::marker::PhantomData<State>,
}

impl<State, W> AnyView<State, W>
where
    State: 'static,
    W: Widget + 'static,
{
    /// Creates an `AnyView` with custom per-frame logic.
    ///
    /// The provided closure runs inside `View::logic`.
    pub fn new(widget: W, logic: impl Fn(&mut W, &mut ViewCtx, &mut State) + 'static) -> Self {
        AnyView {
            logic: Box::new(logic),
            widget,
            phantom: std::marker::PhantomData,
        }
    }

    /// Creates a simple view without additional logic.
    ///
    /// Ideal for static widgets.
    pub fn simple(widget: W) -> Self {
        let logic: fn(&mut W, &mut ViewCtx, &mut State) = |_, _, _| {};
        AnyView {
            logic: Box::new(logic),
            widget,
            phantom: std::marker::PhantomData,
        }
    }
}

impl<State, W> View for AnyView<State, W>
where
    W: Widget,
{
    type State = State;

    fn logic(&mut self, ctx: &mut ViewCtx, state: &mut State) {
        (self.logic)(&mut self.widget, ctx, state)
    }

    fn on_event(&mut self, _event: Event, _ctx: &mut ViewCtx, _state: &mut State) {}

    fn render(&mut self, area: Rect, _: &mut AreaCache, buf: &mut Buffer) {
        Widget::render(&mut self.widget, area, buf);
    }
}