1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
//! 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.
//!
//! ```text
//! 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::Span`]s.
//! 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::HeightIndex`] gives O(log n)
//! pixel↔row in both directions over genuinely variable heights.
//! - **Anchor, not offset.** [`anchor::ScrollAnchor`] pins 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_lines` re-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.
pub use ;
pub use ;
pub use HeightIndex;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;