1use std::collections::HashMap;
4use std::rc::Rc;
5
6use crate::{
7 LayoutCursor, LayoutLineRange, PrepareOptions, PreparedTextWithSegments, PretextLayout,
8};
9use ferroui_pretext_contracts::PretextError;
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
12pub enum RichInlineBreakMode {
13 #[default]
14 Normal,
15 Never,
16}
17
18#[derive(Clone, Debug, PartialEq)]
19pub struct RichInlineItem {
20 pub text: String,
21 pub font: String,
22 pub break_mode: RichInlineBreakMode,
23 pub extra_width: f64,
24}
25
26impl RichInlineItem {
27 pub fn new(text: impl Into<String>, font: impl Into<String>) -> Self {
28 Self {
29 text: text.into(),
30 font: font.into(),
31 break_mode: RichInlineBreakMode::Normal,
32 extra_width: 0.0,
33 }
34 }
35
36 pub fn with_break_mode(mut self, break_mode: RichInlineBreakMode) -> Self {
37 self.break_mode = break_mode;
38 self
39 }
40
41 pub fn with_extra_width(mut self, extra_width: f64) -> Self {
42 self.extra_width = extra_width;
43 self
44 }
45}
46
47#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
50pub struct RichInlineCursor {
51 pub item_index: usize,
52 pub segment_index: usize,
53 pub grapheme_index: usize,
54}
55
56#[derive(Clone, Copy, Debug, Default, PartialEq)]
57pub struct RichInlineStats {
58 pub line_count: usize,
59 pub max_line_width: f64,
60}
61
62#[derive(Clone, Debug, PartialEq)]
63pub struct RichInlineFragment {
64 pub item_index: usize,
65 pub text: String,
66 pub gap_before: f64,
67 pub occupied_width: f64,
68 pub start: LayoutCursor,
69 pub end: LayoutCursor,
70}
71
72#[derive(Clone, Copy, Debug, PartialEq)]
73pub struct RichInlineFragmentRange {
74 pub item_index: usize,
75 pub gap_before: f64,
76 pub occupied_width: f64,
77 pub start: LayoutCursor,
78 pub end: LayoutCursor,
79}
80
81#[derive(Clone, Debug, PartialEq)]
82pub struct RichInlineLine {
83 pub fragments: Vec<RichInlineFragment>,
84 pub width: f64,
85 pub end: RichInlineCursor,
86}
87
88#[derive(Clone, Debug, PartialEq)]
89pub struct RichInlineLineRange {
90 pub fragments: Vec<RichInlineFragmentRange>,
91 pub width: f64,
92 pub end: RichInlineCursor,
93}
94
95#[derive(Debug)]
98pub struct PreparedRichInline {
99 pub(crate) items: Vec<PreparedRichInlineItem>,
100 pub(crate) items_by_source_item_index: Vec<Option<usize>>,
101}
102
103impl PreparedRichInline {
104 pub fn item_count(&self) -> usize {
105 self.items.len()
106 }
107
108 pub fn source_item_count(&self) -> usize {
109 self.items_by_source_item_index.len()
110 }
111
112 pub fn is_empty(&self) -> bool {
113 self.items.is_empty()
114 }
115
116 pub fn prepared_item(&self, source_item_index: usize) -> Option<&Rc<PreparedTextWithSegments>> {
119 self.items_by_source_item_index
120 .get(source_item_index)
121 .copied()
122 .flatten()
123 .map(|index| &self.items[index].prepared)
124 }
125}
126
127#[derive(Debug)]
128pub(crate) struct PreparedRichInlineItem {
129 pub(crate) break_mode: RichInlineBreakMode,
130 pub(crate) end_grapheme_index: usize,
131 pub(crate) end_segment_index: usize,
132 pub(crate) extra_width: f64,
133 pub(crate) gap_before: f64,
134 pub(crate) natural_width: f64,
135 pub(crate) prepared: Rc<PreparedTextWithSegments>,
136 pub(crate) source_item_index: usize,
137}
138
139impl PreparedRichInlineItem {
140 fn end(&self) -> LayoutCursor {
141 LayoutCursor {
142 segment_index: self.end_segment_index,
143 grapheme_index: self.end_grapheme_index,
144 }
145 }
146}
147
148const EMPTY_LAYOUT_CURSOR: LayoutCursor = LayoutCursor {
149 segment_index: 0,
150 grapheme_index: 0,
151};
152
153impl PretextLayout {
154 pub fn prepare_rich_inline(
155 &self,
156 items: &[RichInlineItem],
157 ) -> Result<Rc<PreparedRichInline>, PretextError> {
158 let mut prepared_items = Vec::with_capacity(items.len());
159 let mut items_by_source_item_index = vec![None; items.len()];
160 let mut space_width_cache = HashMap::new();
161 let mut pending_gap_width = 0.0;
162
163 for (index, item) in items.iter().enumerate() {
164 let trimmed = trim_collapsible_boundary_text(&item.text);
165 if trimmed.text.is_empty() {
166 if (trimmed.has_leading_whitespace || trimmed.has_trailing_whitespace)
167 && pending_gap_width == 0.0
168 {
169 pending_gap_width =
170 self.collapsed_space_width(&item.font, &mut space_width_cache)?;
171 }
172 continue;
173 }
174
175 let gap_before = if pending_gap_width > 0.0 {
176 pending_gap_width
177 } else if trimmed.has_leading_whitespace {
178 self.collapsed_space_width(&item.font, &mut space_width_cache)?
179 } else {
180 0.0
181 };
182 let prepared =
183 self.prepare_with_segments(trimmed.text, &item.font, PrepareOptions::default())?;
184 let whole_line =
185 Self::layout_next_line_range(&prepared, EMPTY_LAYOUT_CURSOR, f64::INFINITY);
186 if let Some(whole_line) = whole_line {
187 items_by_source_item_index[index] = Some(prepared_items.len());
188 prepared_items.push(PreparedRichInlineItem {
189 break_mode: item.break_mode,
190 end_grapheme_index: whole_line.end.grapheme_index,
191 end_segment_index: whole_line.end.segment_index,
192 extra_width: item.extra_width,
193 gap_before,
194 natural_width: whole_line.width,
195 prepared,
196 source_item_index: index,
197 });
198 }
199
200 pending_gap_width = if trimmed.has_trailing_whitespace {
201 self.collapsed_space_width(&item.font, &mut space_width_cache)?
202 } else {
203 0.0
204 };
205 }
206
207 Ok(Rc::new(PreparedRichInline {
208 items: prepared_items,
209 items_by_source_item_index,
210 }))
211 }
212
213 pub fn layout_next_rich_inline_line_range(
214 prepared: &PreparedRichInline,
215 max_width: f64,
216 start: Option<RichInlineCursor>,
217 ) -> Option<RichInlineLineRange> {
218 let mut end = start.unwrap_or_default();
219 let mut fragments = Vec::new();
220 let width = step_rich_inline_line(
221 prepared,
222 max_width,
223 &mut end,
224 |item, gap_before, occupied_width, start, end| {
225 fragments.push(RichInlineFragmentRange {
226 item_index: item.source_item_index,
227 gap_before,
228 occupied_width,
229 start,
230 end,
231 });
232 },
233 )?;
234 Some(RichInlineLineRange {
235 fragments,
236 width,
237 end,
238 })
239 }
240
241 pub fn materialize_rich_inline_line_range(
246 prepared: &PreparedRichInline,
247 line: &RichInlineLineRange,
248 ) -> RichInlineLine {
249 let fragments = line
250 .fragments
251 .iter()
252 .map(|fragment| {
253 let item = prepared
254 .prepared_item(fragment.item_index)
255 .expect("rich inline range references a missing source item");
256 let materialized = Self::materialize_line_range(
257 item,
258 &LayoutLineRange {
259 width: fragment.occupied_width,
260 start: fragment.start,
261 end: fragment.end,
262 },
263 );
264 RichInlineFragment {
265 item_index: fragment.item_index,
266 text: materialized.text,
267 gap_before: fragment.gap_before,
268 occupied_width: fragment.occupied_width,
269 start: fragment.start,
270 end: fragment.end,
271 }
272 })
273 .collect();
274 RichInlineLine {
275 fragments,
276 width: line.width,
277 end: line.end,
278 }
279 }
280
281 pub fn walk_rich_inline_line_ranges(
282 prepared: &PreparedRichInline,
283 max_width: f64,
284 mut on_line: impl FnMut(RichInlineLineRange),
285 ) -> usize {
286 let mut cursor = RichInlineCursor::default();
287 let mut line_count = 0;
288 while let Some(line) =
289 Self::layout_next_rich_inline_line_range(prepared, max_width, Some(cursor))
290 {
291 cursor = line.end;
292 on_line(line);
293 line_count += 1;
294 }
295 line_count
296 }
297
298 pub fn measure_rich_inline_stats(
300 prepared: &PreparedRichInline,
301 max_width: f64,
302 ) -> RichInlineStats {
303 let mut cursor = RichInlineCursor::default();
304 let mut stats = RichInlineStats::default();
305 while let Some(width) =
306 step_rich_inline_line(prepared, max_width, &mut cursor, |_, _, _, _, _| {})
307 {
308 stats.line_count += 1;
309 stats.max_line_width = stats.max_line_width.max(width);
310 }
311 stats
312 }
313
314 fn collapsed_space_width(
315 &self,
316 font: &str,
317 cache: &mut HashMap<String, f64>,
318 ) -> Result<f64, PretextError> {
319 if let Some(width) = cache.get(font) {
320 return Ok(*width);
321 }
322 let joined = self.prepare_with_segments("A A", font, PrepareOptions::default())?;
323 let compact = self.prepare_with_segments("AA", font, PrepareOptions::default())?;
324 let width =
325 (Self::measure_natural_width(&joined) - Self::measure_natural_width(&compact)).max(0.0);
326 cache.insert(font.to_owned(), width);
327 Ok(width)
328 }
329}
330
331struct BoundaryTrimResult<'a> {
332 text: &'a str,
333 has_leading_whitespace: bool,
334 has_trailing_whitespace: bool,
335}
336
337fn trim_collapsible_boundary_text(text: &str) -> BoundaryTrimResult<'_> {
338 let without_start = text.trim_start_matches(is_collapsible_boundary_char);
339 let trimmed = without_start.trim_end_matches(is_collapsible_boundary_char);
340 BoundaryTrimResult {
341 text: trimmed,
342 has_leading_whitespace: without_start.len() < text.len(),
343 has_trailing_whitespace: text.ends_with(is_collapsible_boundary_char),
345 }
346}
347
348fn is_collapsible_boundary_char(ch: char) -> bool {
349 matches!(ch, ' ' | '\t' | '\n' | '\u{000c}' | '\r')
350}
351
352fn step_rich_inline_line(
355 prepared: &PreparedRichInline,
356 max_width: f64,
357 cursor: &mut RichInlineCursor,
358 mut on_fragment: impl FnMut(&PreparedRichInlineItem, f64, f64, LayoutCursor, LayoutCursor),
359) -> Option<f64> {
360 let items = &prepared.items;
361 if cursor.item_index >= items.len() {
362 return None;
363 }
364 let safe_width = if max_width > 1.0 { max_width } else { 1.0 };
365 let mut line_width = 0.0;
366 let mut has_fragment = false;
367 let mut remaining_width = safe_width;
368 let mut item_index = cursor.item_index;
369 let mut text_cursor = LayoutCursor {
370 segment_index: cursor.segment_index,
371 grapheme_index: cursor.grapheme_index,
372 };
373
374 while let Some(item) = items.get(item_index) {
375 if text_cursor != EMPTY_LAYOUT_CURSOR && text_cursor == item.end() {
376 item_index += 1;
377 text_cursor = EMPTY_LAYOUT_CURSOR;
378 continue;
379 }
380
381 let gap_before = if line_width == 0.0 {
382 0.0
383 } else {
384 item.gap_before
385 };
386 let at_item_start = text_cursor == EMPTY_LAYOUT_CURSOR;
387 if item.break_mode == RichInlineBreakMode::Never {
388 if !at_item_start {
389 item_index += 1;
390 text_cursor = EMPTY_LAYOUT_CURSOR;
391 continue;
392 }
393 let occupied_width = item.natural_width + item.extra_width;
394 let total_width = gap_before + occupied_width;
395 if line_width > 0.0 && total_width > remaining_width {
396 break;
397 }
398 on_fragment(
399 item,
400 gap_before,
401 occupied_width,
402 EMPTY_LAYOUT_CURSOR,
403 item.end(),
404 );
405 has_fragment = true;
406 line_width += total_width;
407 remaining_width = (safe_width - line_width).max(0.0);
408 item_index += 1;
409 text_cursor = EMPTY_LAYOUT_CURSOR;
410 continue;
411 }
412
413 let reserved_width = gap_before + item.extra_width;
414 if line_width > 0.0 && reserved_width >= remaining_width {
415 break;
416 }
417
418 if at_item_start {
419 let total_width = reserved_width + item.natural_width;
420 if total_width <= remaining_width {
421 on_fragment(
422 item,
423 gap_before,
424 item.natural_width + item.extra_width,
425 EMPTY_LAYOUT_CURSOR,
426 item.end(),
427 );
428 has_fragment = true;
429 line_width += total_width;
430 remaining_width = (safe_width - line_width).max(0.0);
431 item_index += 1;
432 text_cursor = EMPTY_LAYOUT_CURSOR;
433 continue;
434 }
435 }
436
437 let available_width = (remaining_width - reserved_width).max(1.0);
438 let Some(line) =
439 PretextLayout::layout_next_line_range(&item.prepared, text_cursor, available_width)
440 else {
441 item_index += 1;
442 text_cursor = EMPTY_LAYOUT_CURSOR;
443 continue;
444 };
445 if text_cursor == line.end {
446 item_index += 1;
447 text_cursor = EMPTY_LAYOUT_CURSOR;
448 continue;
449 }
450
451 if line_width > 0.0
452 && at_item_start
453 && gap_before > 0.0
454 && line.end.segment_index == 0
455 && line.end.grapheme_index > 0
456 {
457 let fresh_line = PretextLayout::layout_next_line_range(
458 &item.prepared,
459 EMPTY_LAYOUT_CURSOR,
460 (safe_width - item.extra_width).max(1.0),
461 );
462 if fresh_line.is_some_and(|fresh| fresh.end > line.end) {
463 break;
464 }
465 }
466
467 on_fragment(
468 item,
469 gap_before,
470 line.width + item.extra_width,
471 text_cursor,
472 line.end,
473 );
474 has_fragment = true;
475 line_width += gap_before + line.width + item.extra_width;
476 remaining_width = (safe_width - line_width).max(0.0);
477 if line.end == item.end() {
478 item_index += 1;
479 text_cursor = EMPTY_LAYOUT_CURSOR;
480 continue;
481 }
482 text_cursor = line.end;
483 break;
484 }
485
486 if !has_fragment {
490 return None;
491 }
492 *cursor = RichInlineCursor {
493 item_index,
494 segment_index: text_cursor.segment_index,
495 grapheme_index: text_cursor.grapheme_index,
496 };
497 Some(line_width)
498}