fmd_font/text_run.rs
1//! Shared owned text/font/run contract (FCB-016.A).
2//!
3//! Implements Plan §13.1, §13.2, §13.6, and §27.5:
4//! - Immutable font backing and unique font identity.
5//! - Bidirectional mapping between Byte (UTF-8), Native (UTF-16), and Visual domains.
6//! - Cluster and source associations preserving exact logical source ranges.
7//! - Caret affinity (`Leading` vs `Trailing`) and CPU line-level hit testing.
8//! - Discontiguous visual selection rectangles for logical source ranges.
9//! - Capability distinction between bundled deterministic faces and system fallback faces.
10
11use crate::shaping::{Direction, ShapedRun};
12use std::ops::Range;
13
14mod interaction;
15mod shaped;
16
17/// Unique identifier for an immutable font face.
18///
19/// Plan §13.1: "Cache font data once per font identity, not per file or pane."
20#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub struct FontId(pub u64);
22
23impl FontId {
24 /// Construct a font identifier from a raw numeric id.
25 #[must_use]
26 pub const fn new(id: u64) -> Self {
27 Self(id)
28 }
29
30 /// Compute a stable FNV-1a content hash from immutable font binary bytes.
31 #[must_use]
32 pub fn from_font_data(data: &[u8]) -> Self {
33 let mut hash = 0xcbf29ce484222325u64;
34 for &b in data {
35 hash = (hash ^ u64::from(b)).wrapping_mul(0x100000001b3);
36 }
37 Self(hash)
38 }
39}
40
41/// Capability origin of the font face used for shaping.
42///
43/// Plan §13.2: "The capability record distinguishes deterministic bundled-face
44/// runs from system-shaped fallback runs."
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub enum FontOrigin {
47 /// Deterministic bundled font face (e.g. OFL IBM Plex Sans, Computer Modern).
48 BundledFace,
49 /// Platform/system fallback font face (e.g. CoreText on macOS).
50 SystemFallbackFace,
51}
52
53impl FontOrigin {
54 /// Whether this font origin provides cross-machine pixel determinism.
55 #[must_use]
56 pub const fn is_deterministic(self) -> bool {
57 matches!(self, Self::BundledFace)
58 }
59}
60
61/// Explicit caret affinity for cursor positioning at cluster and direction boundaries.
62///
63/// Plan §13.6: "Retain cluster advance arrays and a line-level hit-test index."
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
65pub enum CaretAffinity {
66 /// Associated with the leading edge / preceding character.
67 Leading,
68 /// Associated with the trailing edge / subsequent character.
69 Trailing,
70}
71
72/// Caret position resolved across all three coordinate domains simultaneously.
73#[derive(Clone, Copy, Debug, PartialEq)]
74pub struct CaretPosition {
75 /// Offset in the original UTF-8 source bytes.
76 pub byte_offset: usize,
77 /// Offset in native UTF-16 code units (for Apple AppKit/CoreText interop).
78 pub utf16_offset: usize,
79 /// Visual horizontal pixel position from the start of the text run.
80 pub visual_x: f32,
81 /// Caret affinity at this boundary.
82 pub affinity: CaretAffinity,
83}
84
85/// Result of hit-testing a visual horizontal coordinate against a text run.
86#[derive(Clone, Copy, Debug, PartialEq)]
87pub struct HitTestResult {
88 /// Index of the hit text cluster.
89 pub cluster_index: usize,
90 /// Exact caret location and affinity.
91 pub caret: CaretPosition,
92 /// Whether the visual coordinate fell strictly inside the run's bounds.
93 pub is_exact: bool,
94}
95
96/// A visual selection rectangle covering a contiguous visual portion of a selection.
97///
98/// Plan §13.6: "Selection rectangles may cover discontiguous visual runs for one logical range."
99#[derive(Clone, Copy, Debug, PartialEq)]
100pub struct SelectionRect {
101 /// Visual X coordinate of the left edge.
102 pub x: f32,
103 /// Visual Y coordinate of the top edge.
104 pub y: f32,
105 /// Width of this selection slice.
106 pub width: f32,
107 /// Height of this selection slice.
108 pub height: f32,
109}
110
111/// Context identity for an owned text run.
112#[derive(Clone, Debug, PartialEq)]
113pub struct TextRunContext {
114 /// Font identity.
115 pub font_id: FontId,
116 /// Font point size in user/layout space.
117 pub font_size: f32,
118 /// OpenType script tag (e.g. `*b"latn"`).
119 pub script: [u8; 4],
120 /// OpenType language tag (e.g. `*b"dflt"`).
121 pub language: [u8; 4],
122 /// Text direction (LTR or RTL).
123 pub direction: Direction,
124 /// Font origin and capability tier.
125 pub font_origin: FontOrigin,
126}
127
128/// A positioned glyph within an owned text run.
129#[derive(Clone, Copy, Debug, PartialEq)]
130pub struct RunGlyph {
131 /// OpenType glyph identifier.
132 pub glyph_id: u16,
133 /// Producing font identity (Plan §13.7: glyph ID has meaning only with actual font).
134 pub font_id: FontId,
135 /// Index of the parent cluster in the run's cluster array.
136 pub cluster_index: usize,
137 /// Horizontal advance in user units.
138 pub x_advance: f32,
139 /// Vertical advance in user units.
140 pub y_advance: f32,
141 /// Horizontal placement offset.
142 pub x_offset: f32,
143 /// Vertical placement offset.
144 pub y_offset: f32,
145}
146
147/// An atomic text cluster associating source code points with shaped glyphs.
148#[derive(Clone, Debug, PartialEq)]
149pub struct TextCluster {
150 /// 0-based index of this cluster within the run.
151 pub cluster_index: usize,
152 /// Byte range in the original UTF-8 logical source text.
153 pub byte_range: Range<usize>,
154 /// UTF-16 code unit range in native representation.
155 pub utf16_range: Range<usize>,
156 /// Range of glyph indices in the run's `glyphs` array belonging to this cluster.
157 pub glyph_range: Range<usize>,
158 /// Visual horizontal start coordinate (inclusive).
159 pub x_start: f32,
160 /// Visual horizontal end coordinate (inclusive).
161 pub x_end: f32,
162 /// Producing font identity for this cluster.
163 pub font_id: FontId,
164}
165
166impl TextCluster {
167 /// Visual advance width of this cluster.
168 #[must_use]
169 pub fn advance(&self) -> f32 {
170 (self.x_end - self.x_start).abs()
171 }
172}
173
174/// A fully measured, shaped, and hit-testable text run owning all metrics.
175///
176/// Provides the shared text-run representation across source code views,
177/// Markdown documents, labels, and search excerpts (Plan §13.1).
178#[derive(Clone, Debug, PartialEq)]
179pub struct OwnedTextRun {
180 /// Run styling, font, and direction context.
181 pub context: TextRunContext,
182 /// Preserved exact original logical text.
183 pub logical_text: String,
184 /// Clustered source-to-glyph associations in logical order.
185 pub clusters: Vec<TextCluster>,
186 /// Positioned glyphs in visual presentation order.
187 pub glyphs: Vec<RunGlyph>,
188 /// Total visual horizontal advance width of the run.
189 pub total_advance: f32,
190}
191
192impl OwnedTextRun {
193 /// Construct an `OwnedTextRun` from a shaped run and layout scale.
194 ///
195 /// The `scale` converts font design units into user coordinates
196 /// (`font_size / units_per_em`). Glyphs retain the shaper's visual order;
197 /// clusters are stored in logical source order in both directions. In RTL
198 /// runs their visual positions therefore descend through the cluster array.
199 ///
200 /// # Errors
201 /// Rejects a direction mismatch, non-positive/non-finite size or scale,
202 /// incomplete or non-monotone source coverage, invalid scalar boundaries,
203 /// missing glyphs, negative horizontal advances, or non-finite positioning.
204 /// Repeated cluster ranges must be consecutive, as for a base and its marks.
205 pub fn from_shaped_run(
206 context: TextRunContext,
207 shaped: &ShapedRun,
208 scale: f32,
209 ) -> Result<Self, String> {
210 shaped::build(context, shaped, scale)
211 }
212
213 /// Perform CPU line-level hit testing against visual X position.
214 ///
215 /// Resolves to an actual atomic cluster edge. Exterior coordinates and
216 /// gaps produce inexact nearest-edge hits. NaN produces an inexact logical
217 /// start; infinities clamp to the corresponding visual extreme. Empty runs
218 /// have an inexact zero caret. Glyph positions and cluster geometry must
219 /// come from the same layout snapshot.
220 #[must_use]
221 pub fn hit_test(&self, visual_x: f32) -> HitTestResult {
222 interaction::hit_test(self, visual_x)
223 }
224
225 /// Resolve a logical UTF-8 position to an atomic source-cluster caret.
226 ///
227 /// At an interior scalar boundary of a ligature or combining cluster,
228 /// `Leading` snaps to the cluster's logical start and `Trailing` to its
229 /// logical end. The returned byte and UTF-16 offsets identify that real
230 /// edge, not the requested interior position. No glyph-internal caret is
231 /// invented. Mid-scalar and out-of-range offsets return `None`.
232 ///
233 /// At a shared boundary, `Leading` chooses the following cluster's leading
234 /// edge; `Trailing` chooses the preceding cluster's trailing edge. At the
235 /// document endpoints only the existing inward cluster edge is available.
236 #[must_use]
237 pub fn caret_at_byte(
238 &self,
239 byte_offset: usize,
240 affinity: CaretAffinity,
241 ) -> Option<CaretPosition> {
242 interaction::caret_at_byte(self, byte_offset, affinity)
243 }
244
245 /// Resolve a native UTF-16 position using the same cluster-snapping policy.
246 /// Surrogate-pair interiors and out-of-range offsets return `None`.
247 #[must_use]
248 pub fn caret_at_utf16(
249 &self,
250 utf16_offset: usize,
251 affinity: CaretAffinity,
252 ) -> Option<CaretPosition> {
253 let byte_offset = utf16_to_byte(&self.logical_text, utf16_offset)?;
254 self.caret_at_byte(byte_offset, affinity)
255 }
256
257 /// Compute visual-order selection rectangles for a logical source range.
258 ///
259 /// Partial cluster selections cover the complete cluster. Adjacent and
260 /// overlapping visual intervals are unioned in either direction; separated
261 /// intervals remain separate. Ranges must be in bounds at UTF-8 scalar
262 /// boundaries. Invalid ranges, non-finite coordinates, or non-positive heights return
263 /// no rectangles, rather than publishing a partial selection.
264 #[must_use]
265 pub fn selection_rects(&self, range: Range<usize>, y: f32, height: f32) -> Vec<SelectionRect> {
266 interaction::selection_rects(self, range, y, height)
267 }
268}
269
270/// Convert a UTF-8 byte offset to a native UTF-16 code unit offset.
271#[must_use]
272pub fn byte_to_utf16(text: &str, byte_offset: usize) -> Option<usize> {
273 if byte_offset > text.len() || !text.is_char_boundary(byte_offset) {
274 return None;
275 }
276 let mut u16_count = 0;
277 for ch in text[..byte_offset].chars() {
278 u16_count += ch.len_utf16();
279 }
280 Some(u16_count)
281}
282
283/// Convert a native UTF-16 code unit offset to a UTF-8 byte offset.
284#[must_use]
285pub fn utf16_to_byte(text: &str, utf16_offset: usize) -> Option<usize> {
286 let mut cur_u16 = 0;
287 let mut cur_byte = 0;
288
289 for ch in text.chars() {
290 if cur_u16 == utf16_offset {
291 return Some(cur_byte);
292 }
293 if cur_u16 > utf16_offset {
294 // Offset landed inside a surrogate pair interior
295 return None;
296 }
297 cur_u16 += ch.len_utf16();
298 cur_byte += ch.len_utf8();
299 }
300
301 if cur_u16 == utf16_offset {
302 Some(cur_byte)
303 } else {
304 None
305 }
306}