htui 0.2.0

A terminal HTML/CSS renderer with an embedded JavaScript engine — build interactive TUIs from HTML, CSS and JS
//! JS-driven event system on top of the terminal HTML renderer.
//!
//! This module is a *side channel*: it does not participate in the
//! `html → tdom → layout → tui` rendering pipeline. Instead it records where
//! elements landed after a layout pass and lets the input loop translate
//! terminal mouse events into DOM-style events with bubbling.
//!
//! ## Architecture
//!
//! 1. The render thread publishes an immutable [`Geometry`] snapshot (DOM
//!    arena + laid-out boxes) after every redraw.
//! 2. A dedicated hit-test thread consumes the snapshot, walks the layout
//!    forest and builds a [`HitMap`] of [`HitRegion`]s — one rectangle per
//!    laid-out element, together with each element's DOM ancestor chain.
//! 3. The input loop hit-tests mouse coordinates against the [`HitMap`] and
//!    forwards the event to the [`ScriptEngine`](crate::script::ScriptEngine),
//!    which fires the JS listeners registered on the bubbling path
//!    (`addEventListener` or compiled `on*` attribute bodies).
//!
//! The pure interaction logic (enter / leave detection, click dispatch) lives
//! in [`interaction`] so it can be tested without a terminal.
//!
//! ## Event handler semantics (browser-aligned)
//!
//! Elements declare behaviour either with the familiar HTML event attributes
//!
//! ```html
//! <div onclick="open()" onmouseover="highlight()">…</div>
//! ```
//!
//! or programmatically from a `<script>` block:
//!
//! ```html
//! <script>
//!   document.getElementById('btn').addEventListener('click', function () { … });
//! </script>
//! ```
//!
//! Like a browser, an attribute value is *code*: it is compiled to
//! `function (event) { <body> }` and registered as a listener. A bare
//! identifier (`onclick="open"`) is evaluated but never invoked, matching the
//! browser's handler semantics. Scripts run in the Boa engine with the DOM
//! bindings described in [`crate::script`]; Rust application callbacks are
//! registered as **host functions** (see [`EventBindings::host`]) and become
//! JS globals callable from scripts.

use std::collections::HashMap;

use crate::{script::HostFn, tdom::node::NodeId};

mod geometry;
mod hit_map;
pub mod interaction;

pub use geometry::{Geometry, SharedGeometry};
pub use hit_map::{HitMap, HitRegion, ScrollbarRegion, SharedHitMap};
pub use interaction::{HoverState, on_click, on_move};

/// DOM-like event types supported by the input layer.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EventType {
    /// Fired when the left mouse button is pressed and released on the same
    /// cell of an interactive element (`onclick`).
    Click,
    /// Fired when the pointer enters an interactive element (`onmouseover`).
    MouseOver,
    /// Fired when the pointer leaves an interactive element (`onmouseout`).
    MouseOut,
    /// Fired when an element gains keyboard focus (`onfocus`). Only `<input>`
    /// fields are focusable today.
    Focus,
    /// Fired when an element loses keyboard focus (`onblur`).
    Blur,
    /// Fired whenever an `<input>` field's value changes while editing
    /// (`oninput`).
    Input,
    /// Fired when a field's value is committed (on blur / Enter) and differs
    /// from the value it had when it gained focus (`onchange`).
    Change,
    /// Fired when a key is pressed (`onkeydown`). Dispatched to the focused
    /// `<input>` (or the document `<body>`, falling back to the root, when
    /// none) and bubbles along the DOM ancestor chain. Page-level built-in
    /// shortcuts (scrolling / zoom / quit) are gated on this event: the input
    /// loop dispatches `keydown` to the script layer first and runs the
    /// built-in only when no listener called `preventDefault()` (see
    /// [`KeyGate`](crate::tui::graph_api::KeyGate)). Input-editing keys,
    /// `<select>` navigation and `Tab` focus cycling are input-control
    /// behaviour and are never gated.
    KeyDown,
    /// Fired when a key is released (`onkeyup`). Only emitted by terminals
    /// that report key releases (kitty keyboard protocol / Windows console
    /// mode); the dispatch target matches [`EventType::KeyDown`].
    KeyUp,
    /// Fired on a `<form>` when a `type=submit` button is clicked or the
    /// form is otherwise submitted (`onsubmit`). The engine performs no
    /// default navigation — pages handle it in JS.
    Submit,
    /// Fired on a `<form>` when a `type=reset` button is clicked
    /// (`onreset`). The engine does not restore control defaults itself;
    /// pages handle the reset in JS.
    Reset,
    /// Fired on a `<details>` element after its `open` attribute changes
    /// (`ontoggle`). Dispatched by the engine's native disclosure-widget
    /// default action when its `<summary>` is clicked.
    Toggle,
    /// Fired on a `<dialog>` when it is closed (`onclose`). Dispatched by the
    /// engine's native dialog default action when a `<form method="dialog">`
    /// submit button removes the `open` attribute.
    Close,
    /// Fired when the terminal viewport changes size (`onresize`).
    /// Dispatched to the window listeners (`window.addEventListener`) and
    /// along the `<body>` ancestor chain; the event target is `<body>` (htui
    /// has no separate window node).
    Resize,
    /// Fired when a mouse button is pressed on an element (`onmousedown`).
    MouseDown,
    /// Fired when a held mouse button is released (`onmouseup`).
    MouseUp,
    /// Fired when the pointer moves (`onmousemove`). Fires on every move
    /// (subject to the input loop's in-flight cap), not just hover changes.
    MouseMove,
    /// Fired after the page scroll offset changed (`onscroll`). Same dispatch
    /// shape as [`EventType::Resize`] (window listeners + `<body>` chain);
    /// the event carries no payload — handlers read `window.scrollY`.
    Scroll,
    /// Fired when a page or field selection changes (`onselectionchange`).
    /// The event has no payload; it is a change notification, matching the
    /// browser's document-level event shape.
    SelectionChange,
    /// Second click of a double click (`ondblclick`): a click on the same
    /// cell within the double-click window. Dispatched to the click target
    /// and bubbles like [`EventType::Click`].
    DblClick,
    /// A wheel tick over the document (`onwheel`). Only dispatched when the
    /// page registered a `wheel` listener (the input loop checks the shared
    /// flag); when a listener calls `preventDefault()` the built-in
    /// container / page scroll for that tick is cancelled (same gate the
    /// `keydown` shortcuts use).
    Wheel,
    /// Right-button release without a drag (`oncontextmenu`). No native
    /// context menu exists — the event is purely for page JS.
    ContextMenu,
    /// The pointer entered an element (`onmouseenter`). Unlike
    /// [`EventType::MouseOver`] it does not bubble: only the element the
    /// pointer moved onto receives it.
    MouseEnter,
    /// The pointer left an element (`onmouseleave`). Non-bubbling, dispatched
    /// only to the element the pointer left.
    MouseLeave,
    /// The DOM is ready: fired once after the document parsed and every
    /// inline / session script ran (`DOMContentLoaded`, window + `<body>`
    /// listeners). A whole-document reload fires it again (browser refresh
    /// semantics).
    DomContentLoaded,
    /// Fired right after [`EventType::DomContentLoaded`] (window `load`).
    Load,
}

/// Terminal-agnostic keyboard payload carried by [`EventType::KeyDown`] /
/// [`EventType::KeyUp`], converted from a crossterm `KeyEvent` on the input
/// loop so the script layer stays independent of the terminal event source.
/// Field values mirror the browser `KeyboardEvent` shape (`key` / `code` /
/// `keyCode` + modifier flags), so JS listeners can branch on the same strings
/// they would in a real browser.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct KeyEventInfo {
    /// `event.key` — the printed character (`"a"`, `" "`, `"A"`) or a named
    /// key (`"Enter"`, `"ArrowUp"`, `"F5"`, `"Escape"`, …).
    pub key: String,
    /// `event.code` — the physical key (`"KeyA"`, `"Space"`, `"ArrowUp"`,
    /// `"Enter"`, `"F5"`, …); independent of the active layout / modifiers.
    pub code: String,
    /// Browser-style numeric key code (`13` for Enter, `32` for Space,
    /// `112..123` for F1..F12, …). `0` when no DOM equivalent exists.
    pub key_code: u32,
    /// `event.shiftKey`.
    pub shift: bool,
    /// `event.ctrlKey`.
    pub ctrl: bool,
    /// `event.altKey`.
    pub alt: bool,
    /// `event.metaKey` (Super / Command / Windows key).
    pub meta: bool,
    /// `event.repeat` — whether this is an auto-repeated press.
    pub repeat: bool,
}

/// One element on a bubbling path (target first, then its ancestors).
///
/// `id` is the stable cross-session identity; `node_id` is the session-local
/// arena key used by the script engine to look up JS listeners.
#[derive(Debug, Clone)]
pub struct ElementRef {
    pub id: Option<String>,
    pub node_id: NodeId,
}

/// Event context delivered to a Rust host function, mirroring the DOM `Event`
/// shape. `target` is the deepest element under the pointer; `current_target`
/// is the element whose listener is being invoked (they differ when an
/// ancestor handles a bubbled event).
#[derive(Debug, Clone)]
pub struct HtmlEvent {
    /// The event being dispatched.
    pub event_type: EventType,
    /// `id` of the deepest element under the pointer.
    pub target: Option<String>,
    /// `id` of the element whose listener is currently running.
    pub current_target: Option<String>,
    /// Canvas coordinates (column / row in the virtual canvas, already offset
    /// by `scroll_y`).
    pub x: u32,
    pub y: u32,
    /// Always `true` — bubbling is the only supported propagation model.
    pub bubbles: bool,
    /// Keyboard payload for [`EventType::KeyDown`] / [`EventType::KeyUp`];
    /// `None` for pointer / form events.
    pub key: Option<KeyEventInfo>,
}

/// Application bindings for the script engine.
///
/// `host` maps JS global names to Rust callbacks (see [`HostFn`]). Every entry
/// is exposed to the document's scripts as a callable global, so JS code can
/// hand logic back to the application — e.g. `sample()` for a system monitor.
#[derive(Default)]
pub struct EventBindings {
    /// JS-callable host functions.
    pub host: HashMap<String, HostFn>,
}

impl EventBindings {
    /// Register one host function under a JS global name.
    ///
    /// The value is usually produced by `#[htui::command]` (which generates
    /// `<fn>_command()` returning a [`HostFn`]):
    ///
    /// ```ignore
    /// bindings.command("sample", sample_command());
    /// ```
    pub fn command(&mut self, name: impl Into<String>, host: HostFn) {
        self.host.insert(name.into(), host);
    }
}