quvyta-framework 0.1.29

A Rust framework for building terminal applications
Documentation
//! What a painted frame leaves behind for routing input, and the interaction state the runtime
//! keeps between frames.

use std::time::Duration;

use crate::geometry::{Rect, Size};
use crate::keymap::KeyChord;
use crate::widget::{IdMap, PointerShape, WidgetId};

/// What was painted in a frame; used to route the next events.
#[derive(Debug, Default)]
pub(crate) struct Frame {
    pub(crate) rects: IdMap<WidgetId, Rect>,
    pub(crate) parents: IdMap<WidgetId, WidgetId>,
    pub(crate) names: IdMap<WidgetId, String>,
    pub(crate) scopes: IdMap<WidgetId, WidgetId>,
    pub(crate) hits: Vec<(Rect, WidgetId)>,
    pub(crate) focusable: Vec<WidgetId>,
    pub(crate) overlays: Vec<(WidgetId, Rect)>,
    pub(crate) next_frame: Option<Duration>,
    /// Chords widgets listen to without focus; see [`PaintCx::listen_key`](crate::widget::PaintCx::listen_key).
    pub(crate) listeners: Vec<(KeyChord, WidgetId)>,
    /// Widgets that take typed text, see [`PaintCx::takes_text`](super::PaintCx::takes_text).
    pub(crate) text_takers: Vec<WidgetId>,
    /// Areas where a mouse press never starts a text selection.
    pub(crate) unselectable: Vec<Rect>,
    /// Visible areas where a mouse drag selects text, with the widget each belongs to, in paint
    /// order; see [`PaintCx::selectable`](crate::widget::PaintCx::selectable).
    pub(crate) selectable: Vec<(Rect, WidgetId)>,
    /// Cells that decorate rather than hold content (pillars, scrollbars); clean copies of a
    /// text selection leave them out. See [`PaintCx::decoration`](crate::widget::PaintCx::decoration).
    pub(crate) decorations: Vec<Rect>,
    /// Widgets that asked for plain pointer moves; see
    /// [`PaintCx::track_pointer_moves`](crate::widget::PaintCx::track_pointer_moves).
    pub(crate) pointer_moves: Vec<WidgetId>,
    /// Widgets that see a press before the widgets inside them; see
    /// [`PaintCx::preview_presses`](crate::widget::PaintCx::preview_presses).
    pub(crate) press_previews: Vec<WidgetId>,
    /// Every layer painted this frame, in paint order (the last one is on top): dismissable
    /// layers such as popovers and modal layers such as dialogs share one stack.
    pub(crate) layers: Vec<LayerEntry>,
    /// Where keyboard focus should move once the frame is painted.
    pub(crate) focus_request: Option<FocusRequest>,
    /// Sizes measured while painting this frame; see [`MeasureKey`].
    pub(crate) measures: IdMap<MeasureKey, Size>,
    /// Areas widgets asked to bring into view, with the widget that asked; the nearest scroll
    /// view around each takes it. See [`PaintCx::reveal`](crate::widget::PaintCx::reveal).
    pub(crate) reveals: Vec<(WidgetId, Rect)>,
    /// Pointer shapes widgets asked for over their areas, with the widget that asked, in paint
    /// order; see [`PaintCx::pointer_shape`](crate::widget::PaintCx::pointer_shape).
    pub(crate) pointer_shapes: Vec<(Rect, PointerShape, WidgetId)>,
    /// Pictures the terminal is asked to draw itself, in paint order, as their images recorded
    /// them before anything was painted over them.
    #[cfg(feature = "image")]
    pub(crate) pictures: Vec<crate::widgets::image::Picture>,
    /// Blends painted over pictures, in paint order; see
    /// [`PaintCx::tint`](crate::widget::PaintCx::tint).
    #[cfg(feature = "image")]
    pub(crate) dims: Vec<crate::widgets::image::Dim>,
    /// Where the terminal shows those pictures once the frame is painted: only the parts nothing
    /// was painted over. See [`resolve`](crate::widgets::image::resolve).
    #[cfg(feature = "image")]
    pub(crate) placements: Vec<crate::widgets::image::PicturePlacement>,
}

/// A node measured in a frame: its id, its address in the view tree and the space it was
/// offered. Measuring depends only on the node, the environment and that space, none of which
/// change while a frame is painted, so containers that measure their children again (a flex
/// row before painting, a scroll view for its content height) reuse the first answer instead of
/// measuring whole subtrees once per level of nesting. The address tells apart nodes that share
/// an id by mistake.
pub(crate) type MeasureKey = (WidgetId, usize, Size);

/// One layer of the frame's layer stack.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LayerEntry {
    pub(crate) id: WidgetId,
    /// Modal layers trap focus and input; see [`PaintCx::open_layer`](crate::widget::PaintCx::open_layer). Other layers only close
    /// on presses outside them and unused Esc; see [`PaintCx::register_dismissable`](crate::widget::PaintCx::register_dismissable).
    pub(crate) modal: bool,
    /// Where a modal layer's surface sits, once the layer said so; toasts keep clear of it.
    /// `None` counts as the whole screen.
    pub(crate) surface: Option<Rect>,
}

/// A focus change asked for while painting, applied after the frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FocusRequest {
    /// The first focusable widget inside this one, unless focus is already inside.
    Within(WidgetId),
    /// Exactly this widget, when it is focusable.
    Exact(WidgetId),
}

impl Frame {
    /// Empties the frame for painting the next one, keeping the memory its maps and lists
    /// already grew, so a steady view allocates nothing for them.
    pub(crate) fn clear(&mut self) {
        // Naming every field makes a new field impossible to forget here.
        let Self {
            rects,
            parents,
            names,
            scopes,
            hits,
            focusable,
            overlays,
            next_frame,
            listeners,
            text_takers,
            unselectable,
            selectable,
            decorations,
            pointer_moves,
            press_previews,
            layers,
            focus_request,
            measures,
            reveals,
            pointer_shapes,
            #[cfg(feature = "image")]
            pictures,
            #[cfg(feature = "image")]
            dims,
            #[cfg(feature = "image")]
            placements,
        } = self;
        rects.clear();
        parents.clear();
        names.clear();
        scopes.clear();
        hits.clear();
        focusable.clear();
        overlays.clear();
        *next_frame = None;
        listeners.clear();
        text_takers.clear();
        unselectable.clear();
        selectable.clear();
        decorations.clear();
        pointer_moves.clear();
        press_previews.clear();
        layers.clear();
        *focus_request = None;
        measures.clear();
        reveals.clear();
        pointer_shapes.clear();
        #[cfg(feature = "image")]
        {
            pictures.clear();
            dims.clear();
            placements.clear();
        }
    }

    /// The topmost widget whose hit area contains the cell.
    pub(crate) fn hit(&self, x: i32, y: i32) -> Option<WidgetId> {
        self.hits.iter().rev().find(|(rect, _)| rect.contains(x, y)).map(|(_, id)| *id)
    }

    /// The pointer's shape at `pointer`: the last shape asked for over that cell by the widget
    /// under the pointer or one around it, so a window on top hides the edges of the windows
    /// beneath it and a dialog hides them all. While `capture` holds the pointer the widget
    /// under it is the capturing one, and when the pointer has left every area that widget asked
    /// for, the first shape it asked for, its shape for the whole of itself, still holds: a drag
    /// keeps the arrow it started with.
    pub(crate) fn pointer_shape(&self, pointer: Option<(i32, i32)>, capture: Option<WidgetId>) -> PointerShape {
        let Some((x, y)) = pointer else {
            return PointerShape::Default;
        };
        let Some(under) = capture.or_else(|| self.hit(x, y)) else {
            return PointerShape::Default;
        };
        let chain = self.ancestry(under);
        let asked = || self.pointer_shapes.iter().rev();
        let over = asked().find(|(rect, _, owner)| rect.contains(x, y) && chain.contains(owner));
        let held = || capture.and_then(|holder| self.pointer_shapes.iter().find(|(_, _, owner)| *owner == holder));
        over.or_else(held).map_or(PointerShape::Default, |(_, shape, _)| *shape)
    }

    /// `id` and its ancestors, innermost first.
    pub(crate) fn ancestry(&self, id: WidgetId) -> Vec<WidgetId> {
        let mut chain = vec![id];
        let mut current = id;
        while let Some(parent) = self.parents.get(&current) {
            chain.push(*parent);
            current = *parent;
        }
        chain
    }

    /// The topmost modal layer, if one is open.
    pub(crate) fn top_layer(&self) -> Option<WidgetId> {
        self.layers.iter().rev().find(|layer| layer.modal).map(|layer| layer.id)
    }

    /// Whether `id` may receive input while modal layers are open: it lies inside the topmost
    /// modal layer, or there is none.
    pub(crate) fn reachable(&self, id: WidgetId) -> bool {
        self.top_layer().is_none_or(|layer| self.is_within(id, layer))
    }

    /// `id` and its ancestors up to and including the topmost modal layer, so input inside a
    /// layer never bubbles to the widgets beneath it. Without a layer this is the whole ancestry.
    pub(crate) fn routed_ancestry(&self, id: WidgetId) -> Vec<WidgetId> {
        let mut chain = self.ancestry(id);
        if let Some(layer) = self.top_layer() {
            match chain.iter().position(|ancestor| *ancestor == layer) {
                Some(index) => chain.truncate(index + 1),
                None => chain = vec![layer],
            }
        }
        chain
    }

    /// Whether `descendant` is `ancestor` or lies inside it.
    pub(crate) fn is_within(&self, descendant: WidgetId, ancestor: WidgetId) -> bool {
        self.ancestry(descendant).contains(&ancestor)
    }

    /// Asks for a frame at `at`, keeping an earlier request.
    pub(super) fn schedule(&mut self, at: Duration) {
        self.next_frame = Some(self.next_frame.map_or(at, |current| current.min(at)));
    }
}

/// Pointer and keyboard state the runtime tracks for widgets.
#[derive(Debug, Default, Clone)]
pub(crate) struct Interaction {
    pub(crate) pointer: Option<(i32, i32)>,
    pub(crate) hovered: Option<WidgetId>,
    /// The hovered widget and its ancestors, innermost first.
    pub(crate) hovered_chain: Vec<WidgetId>,
    pub(crate) focused: Option<WidgetId>,
    pub(crate) pressed: Option<(WidgetId, Duration)>,
    pub(crate) pointer_capture: Option<WidgetId>,
    pub(crate) key_capture: Option<WidgetId>,
    /// Whether the last input was a mouse press, so focus came from the pointer; pressables
    /// then keep a quiet focus look and breathe only when reached with the keyboard.
    pub(crate) focus_by_pointer: bool,
    /// Open modal layers, bottom first.
    pub(crate) layers: Vec<LayerRecord>,
    /// Whether the clipboard had text when it was read last, or the application copied since:
    /// a Paste menu entry is enabled only then.
    pub(crate) can_paste: bool,
}

/// A modal layer the runtime has seen open.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LayerRecord {
    pub(crate) id: WidgetId,
    /// The widget focused before the layer opened; focus returns there when it closes.
    pub(crate) previous_focus: Option<WidgetId>,
    /// When the layer first appeared.
    pub(crate) opened: Duration,
}