Expand description
The layout engine behind the Rotulus chat view.
Everything between “a message arrives” and “the view knows what
pixels to put where”, with no GTK, no GLib and no Pango, so all of it
runs under cargo test on display-less CI. The widget that consumes
it is rotulus; see docs/design.md.
Message ──parse──▶ ParsedText (text + Spans)
│ │
│ layout_message ──▶ LayoutCache (height, LineBoxes)
▼ │
ChatBuffer ◀────────────────┘
│ rows + HeightIndex + ScrollAnchor
▼
view: "which rows are visible, and how tall is everything"§What this replaces, and why
HexChat’s xtext widget lays out on a uniform grid: every row is
fontsize × subline_count, the scroll adjustment’s unit is fractional
text lines, and hit-testing is y / fontsize. That assumption is
wired through the whole widget, and every feature added to it late
had to work around it — inline media reserves
ceil(image_height / fontsize) blank text rows and paints a texture
across them, which is why selection drags through empty space and the
marker line draws above an image rather than across it.
Four things here are deliberate departures:
- Parse once. Styling is resolved at append into
span::Spans. xtext re-ran its escape state machine over every visible byte on every render pass and separately built a run list at append that the render path then ignored. - Pixels, not lines.
index::HeightIndexgives O(log n) pixel↔row in both directions over genuinely variable heights. - Anchor, not offset.
anchor::ScrollAnchorpins the viewport to a row. Resize, zoom, history backfill, scrollback trim and a late-arriving image decode then all preserve reading position for free, rather than each needing its own compensation. - Lazy layout. A width or font change invalidates without
recomputing, so resize costs O(visible). xtext’s
gtk_xtext_calc_linesre-wraps the entire scrollback.
§Measurement
measure::TextMeasure is the seam. The view supplies a Pango
implementation; tests supply measure::FixedMeasure, where every
character is N pixels wide, so wrap assertions are exact rather than
font-dependent. The trait’s unit of work is a run, never a
character — xtext measured per character through a full Pango round
trip each time, and the trait shape makes repeating that impossible.
§Input vocabularies
Style arrives as runs from the application, already resolved, so the
engine decodes no in-band escapes and nothing a remote user sends can
restyle a row. markdown is the one vocabulary it parses itself: an
inline subset, rendered on display and transmitted literally.
linkify finds URLs, against a scheme list the application owns.
Re-exports§
pub use anchor::Gravity;pub use anchor::ScrollAnchor;pub use buffer::ChatBuffer;pub use buffer::MIN_INDENT;pub use index::HeightIndex;pub use linkify::Linkifier;pub use linkify::DEFAULT_SCHEMES;pub use markdown::scan_delims;pub use markdown::SourceSpan;pub use measure::FixedMeasure;pub use measure::FontMetrics;pub use measure::TextMeasure;pub use message::Block;pub use message::Blocks;pub use message::GroupKey;pub use message::ImageSize;pub use message::LoadMoreDirection;pub use message::Message;pub use message::MessageFlags;pub use message::MessageId;pub use message::MessageKind;pub use message::Speaker;pub use search::find_all;pub use search::Match;pub use search::SearchState;pub use select::Caret;pub use select::RowSelection;pub use select::Selection;pub use span::Attrs;pub use span::ColorRef;pub use span::Link;pub use span::LinkId;pub use span::ParsedText;pub use span::Span;pub use span::Style;pub use wrap::AvatarBox;pub use wrap::LayoutCache;pub use wrap::LayoutGeneration;pub use wrap::LayoutParams;pub use wrap::LineBox;pub use wrap::LineSource;
Modules§
- anchor
- Scroll position, expressed as a row rather than a pixel.
- buffer
- The buffer: rows, marks, lazy layout, trim.
- index
- The height index: pixel position ↔ row, in O(log n).
- linkify
- Finding links in plain text.
- markdown
- The markdown inline scanner.
- measure
- Text measurement, abstracted.
- message
- The structured message model.
- search
- In-buffer search: find every occurrence of a needle, and a cursor over the results.
- select
- Selection: hit-testing, the selection range, and text extraction.
- span
- Styled text runs.
- wrap
- Line breaking and per-message layout.