1#![forbid(unsafe_code)]
2
3pub mod cursor;
58pub mod editor;
59pub mod rope;
60pub mod segment;
61pub mod text;
62pub mod view;
63pub mod width_cache;
64pub mod wrap;
65
66pub mod hyphenation;
67pub mod incremental_break;
68
69#[cfg(feature = "markup")]
70pub mod markup;
71
72#[cfg(feature = "bidi")]
73pub mod bidi;
74
75#[cfg(feature = "normalization")]
76pub mod normalization;
77
78pub mod cluster_map;
79pub mod justification;
80pub mod layout_policy;
81pub mod script_segmentation;
82pub mod search;
83pub mod shaped_render;
84pub mod shaping;
85pub mod shaping_fallback;
86pub mod tier_budget;
87pub mod vertical_metrics;
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
91pub struct TextMeasurement {
92 pub minimum: usize,
94 pub maximum: usize,
96}
97
98impl TextMeasurement {
99 pub const ZERO: Self = Self {
101 minimum: 0,
102 maximum: 0,
103 };
104
105 pub fn union(self, other: Self) -> Self {
107 Self {
108 minimum: self.minimum.max(other.minimum),
109 maximum: self.maximum.max(other.maximum),
110 }
111 }
112
113 pub fn stack(self, other: Self) -> Self {
115 Self {
116 minimum: self.minimum.saturating_add(other.minimum),
117 maximum: self.maximum.saturating_add(other.maximum),
118 }
119 }
120
121 pub fn clamp(self, min_width: Option<usize>, max_width: Option<usize>) -> Self {
123 let mut result = self;
124 if let Some(min_w) = min_width {
125 result.minimum = result.minimum.max(min_w);
126 result.maximum = result.maximum.max(min_w);
127 }
128 if let Some(max_w) = max_width {
129 result.minimum = result.minimum.min(max_w);
130 result.maximum = result.maximum.min(max_w);
131 }
132 result
133 }
134}
135
136pub use cluster_map::{ClusterEntry, ClusterMap};
137pub use cursor::{CursorNavigator, CursorPosition};
138pub use editor::{Editor, Selection};
139pub use hyphenation::{
140 HyphenBreakPoint, HyphenationDict, HyphenationPattern, PatternTrie, break_penalties,
141 compile_pattern, english_dict_mini,
142};
143pub use incremental_break::{BreakerSnapshot, EditEvent, IncrementalBreaker, ReflowResult};
144pub use justification::{
145 GlueSpec, JustificationControl, JustifyMode, SUBCELL_SCALE, SpaceCategory, SpacePenalty,
146};
147pub use layout_policy::{LayoutPolicy, LayoutTier, PolicyError, ResolvedPolicy, RuntimeCapability};
148pub use rope::Rope;
149pub use script_segmentation::{
150 RunCacheKey, RunDirection, Script, ScriptRun, TextRun, partition_by_script, partition_text_runs,
151};
152pub use segment::{ControlCode, Segment, SegmentLine, SegmentLines, join_lines, split_into_lines};
153pub use shaped_render::{CellPlacement, RenderHint, ShapedLineLayout, SpacingDelta};
154pub use shaping::{
155 FontFeature, FontFeatures, FontId, NoopShaper, ShapedGlyph, ShapedRun, ShapingCache,
156 ShapingCacheStats, ShapingKey, TextShaper,
157};
158pub use shaping_fallback::{FallbackEvent, FallbackStats, ShapingFallback};
159pub use text::{Line, Span, Text};
160pub use tier_budget::{
161 FrameBudget, MemoryBudget, QueueBudget, SafetyInvariant, TierBudget, TierFeatures, TierLadder,
162};
163pub use vertical_metrics::{
164 BaselineGrid, LeadingSpec, ParagraphSpacing, VerticalMetrics, VerticalPolicy,
165};
166pub use view::{TextView, ViewLine, Viewport};
167pub use width_cache::{
168 CacheStats, CountMinSketch, DEFAULT_CACHE_CAPACITY, Doorkeeper, S3FifoWidthCache,
169 TinyLfuWidthCache, WidthCache,
170};
171pub use wrap::{
172 KpBreakResult, WrapMode, WrapOptions, ascii_width, display_width, grapheme_count,
173 grapheme_width, graphemes, has_wide_chars, is_ascii_only, truncate_to_width,
174 truncate_to_width_with_info, truncate_with_ellipsis, word_boundaries, word_segments,
175 wrap_optimal, wrap_text, wrap_text_optimal, wrap_with_options,
176};
177
178#[cfg(feature = "markup")]
179pub use markup::{MarkupError, MarkupParser, parse_markup};
180
181#[cfg(feature = "normalization")]
182pub use normalization::{NormForm, eq_normalized, is_normalized, normalize, normalize_for_search};
183
184#[cfg(feature = "normalization")]
185pub use search::{
186 PolicySearchResult, SearchPolicy, search_case_insensitive, search_normalized,
187 search_with_policy,
188};
189pub use search::{
190 SearchResult, WidthMode, display_col_at, search_ascii_case_insensitive, search_exact,
191 search_exact_overlapping,
192};
193
194#[cfg(test)]
195mod measurement_tests {
196 use super::TextMeasurement;
197
198 #[test]
199 fn union_uses_max_bounds() {
200 let a = TextMeasurement {
201 minimum: 2,
202 maximum: 8,
203 };
204 let b = TextMeasurement {
205 minimum: 4,
206 maximum: 6,
207 };
208 let merged = a.union(b);
209 assert_eq!(
210 merged,
211 TextMeasurement {
212 minimum: 4,
213 maximum: 8
214 }
215 );
216 }
217
218 #[test]
219 fn stack_adds_bounds() {
220 let a = TextMeasurement {
221 minimum: 1,
222 maximum: 5,
223 };
224 let b = TextMeasurement {
225 minimum: 2,
226 maximum: 7,
227 };
228 let stacked = a.stack(b);
229 assert_eq!(
230 stacked,
231 TextMeasurement {
232 minimum: 3,
233 maximum: 12
234 }
235 );
236 }
237
238 #[test]
239 fn clamp_enforces_min() {
240 let measurement = TextMeasurement {
241 minimum: 2,
242 maximum: 6,
243 };
244 let clamped = measurement.clamp(Some(5), None);
245 assert_eq!(
246 clamped,
247 TextMeasurement {
248 minimum: 5,
249 maximum: 6
250 }
251 );
252 }
253
254 #[test]
255 fn clamp_enforces_max() {
256 let measurement = TextMeasurement {
257 minimum: 4,
258 maximum: 10,
259 };
260 let clamped = measurement.clamp(None, Some(6));
261 assert_eq!(
262 clamped,
263 TextMeasurement {
264 minimum: 4,
265 maximum: 6
266 }
267 );
268 }
269
270 #[test]
271 fn clamp_preserves_ordering() {
272 let measurement = TextMeasurement {
273 minimum: 3,
274 maximum: 5,
275 };
276 let clamped = measurement.clamp(Some(7), Some(4));
277 assert!(clamped.minimum <= clamped.maximum);
278 assert_eq!(clamped.minimum, 4);
279 assert_eq!(clamped.maximum, 4);
280 }
281
282 #[test]
283 fn zero_constant() {
284 assert_eq!(TextMeasurement::ZERO.minimum, 0);
285 assert_eq!(TextMeasurement::ZERO.maximum, 0);
286 }
287
288 #[test]
289 fn default_is_zero() {
290 let m = TextMeasurement::default();
291 assert_eq!(m, TextMeasurement::ZERO);
292 }
293
294 #[test]
295 fn union_with_zero_is_identity() {
296 let m = TextMeasurement {
297 minimum: 5,
298 maximum: 10,
299 };
300 assert_eq!(m.union(TextMeasurement::ZERO), m);
301 assert_eq!(TextMeasurement::ZERO.union(m), m);
302 }
303
304 #[test]
305 fn stack_with_zero_is_identity() {
306 let m = TextMeasurement {
307 minimum: 5,
308 maximum: 10,
309 };
310 assert_eq!(m.stack(TextMeasurement::ZERO), m);
311 assert_eq!(TextMeasurement::ZERO.stack(m), m);
312 }
313
314 #[test]
315 fn stack_saturates_on_overflow() {
316 let big = TextMeasurement {
317 minimum: usize::MAX - 1,
318 maximum: usize::MAX,
319 };
320 let one = TextMeasurement {
321 minimum: 5,
322 maximum: 5,
323 };
324 let stacked = big.stack(one);
325 assert_eq!(stacked.maximum, usize::MAX);
327 }
328
329 #[test]
330 fn clamp_no_constraints() {
331 let m = TextMeasurement {
332 minimum: 3,
333 maximum: 7,
334 };
335 let clamped = m.clamp(None, None);
336 assert_eq!(clamped, m);
337 }
338
339 #[test]
340 fn clamp_min_raises_both_bounds() {
341 let m = TextMeasurement {
342 minimum: 1,
343 maximum: 2,
344 };
345 let clamped = m.clamp(Some(5), None);
347 assert_eq!(clamped.minimum, 5);
348 assert_eq!(clamped.maximum, 5);
349 }
350
351 #[test]
352 fn union_is_commutative() {
353 let a = TextMeasurement {
354 minimum: 2,
355 maximum: 8,
356 };
357 let b = TextMeasurement {
358 minimum: 4,
359 maximum: 6,
360 };
361 assert_eq!(a.union(b), b.union(a));
362 }
363
364 #[test]
365 fn stack_is_commutative() {
366 let a = TextMeasurement {
367 minimum: 2,
368 maximum: 8,
369 };
370 let b = TextMeasurement {
371 minimum: 4,
372 maximum: 6,
373 };
374 assert_eq!(a.stack(b), b.stack(a));
375 }
376}