跪拜 Guibai
← All articles
Frontend · Backend · Rust

Rust Terminal Novel Reader Gets Bell-Ringing Mouse Support: Hit Testing, Pointer Capture, and the Art of Not Swallowing Events

By 红尘散仙 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Terminal applications no longer need to feel keyboard-only. A reusable Rust component now gives TUI apps the same click/drag/hover semantics as GUI toolkits, without porting a full DOM event model, and it exposes these behaviors through simple hooks that any ratatui-based app can adopt.

Summary

Mouse support in a terminal app is more than reading coordinates. TRNovel, a Rust-based novel reader, now lets users click books, scroll chapters, and drag scrollbars with the mouse, thanks to the underlying ratatui-kit framework. The article walks through the raw escape sequences crossterm parses, the "Stamp" records that track each component's visible region per draw frame, and the priority rules that decide which nested component gets the event first.

Two problems get particular attention: a popup button that was drawn but unclickable because a parent keyboard handler swallowed all events with `Consumed`, and the need for pointer capture so a drag continues even when the cursor leaves the scrollbar track. TRNovel also makes reader-specific decisions—such as capping wheel scroll at chapter boundaries rather than auto-turning pages, and reusing the Enter key's business logic for mouse clicks on list items.

The framework exposes four hooks—`use_click`, `use_hover`, `use_drag`, and `use_pointer`—so component authors don't reimplement common interactions. A key design choice is using draw order to decide event priority, making event dispatch mirror what the user sees.

Takeaways
— Raw mouse input from the terminal is an escape sequence like `\x1b[<0;6;5M`, which crossterm parses into zero-based coordinates such as `(5, 4)`.
— The app must explicitly enable mouse event reporting; otherwise, the terminal handles selection and scrolling itself.
— Each component gets a per-frame `Stamp` recording its visible screen area, coordinate transform, draw frame number, and draw order, used for hit testing.
— Hit testing only considers the portion of a component actually visible in the current frame; scrolled-out or clipped content is not clickable.
— Event dispatch order is: input layer, handler priority, then draw order, with registration order as a fallback.
— A parent handler that returns `Consumed` for all events can block child components; the fix is to return `Ignored` for events it doesn't recognize.
— Pointer capture establishes an owner on `Down` and routes all later `Drag` and `Up` events to that owner, tracked via a stable `PointerToken`.
— `use_click` only generates a click if the `Down` and `Up` pair both land within the component's hit region.
— TRNovel's wheel scroll is capped at the chapter's start and end; it does not auto-turn pages, and manual scrolling pauses auto-follow during text-to-speech.
— List item clicks use `hit_test` to find the actual item and then reuse the same business callback as the Enter key.
— The framework provides four hooks: `use_click`, `use_hover`, `use_drag`, and `use_pointer`.
— Hover state only changes on enter/leave; moving within the same element does not trigger a redraw.
Conclusions

Using draw order as the primary signal for event priority is a clever mapping between visual stacking and event dispatch, which avoids maintaining a separate event-routing graph.

The decision to stop wheel scroll at chapter boundaries respects the reader's intent, acknowledging that a continuous stream of scroll events shouldn't be conflated with a deliberate page turn.

The framework's separation of input layers (which level is active) from in-layer priority (who within that level handles first) is a clean decomposition that scales to complex TUI layouts.

The bug where a keyboard handler swallowed mouse events shows how easy it is to conflate different event types in a unified event system; the fix is a principle: consume only what you own.

The emphasis on visible-region hit testing addresses a TUI-specific trap where internal coordinates don't map to screen space after scrolling or resize.

Concepts & terms
Hit testing
The process of determining which UI component a mouse coordinate falls into, typically by checking each component's visible screen region. In TUI apps, this requires converting content coordinates to screen coordinates and intersecting with scroll viewports.
Pointer capture
A mechanism that routes all subsequent mouse events (move, release) for a drag or similar operation to the component that received the initial press, even if the cursor leaves that component's bounds, until the operation completes.
Input layer
A stacking system in ratatui-kit that determines which set of components currently receives interactive events. An exclusive layer can block background components, as when a modal popup is open.
EventResult::Consumed / Ignored
The two possible outcomes when a component processes a mouse event: `Consumed` stops further dispatch to other handlers, while `Ignored` lets the event continue to the next handler in priority order.
crossterm
A Rust library that provides cross-platform terminal handling, including parsing raw mouse escape sequences into structured events and enabling mouse capture.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗