1use std::cell::OnceCell;
2use std::rc::Rc;
3
4use crate::pretext_layout_analysis::{analyze_tokens, contains_cjk, AnalysisToken};
5use crate::pretext_layout_bidi::compute_segment_levels;
6use crate::pretext_layout_measurement::{
7 build_base_cjk_units, get_engine_profile, merge_keep_all_text_units, EngineProfile, FontState,
8 MeasuredSegment,
9};
10use crate::{
11 PrepareOptions, PrepareProfile, PreparedLineChunk, PreparedText, PreparedTextWithSegments,
12 PretextError, PretextLayout, SegmentBreakKind, WordBreakMode,
13};
14
15struct PreparedContent {
16 base: PreparedText,
17 segments: Vec<String>,
18 segment_levels: Option<Vec<i8>>,
19}
20
21impl PretextLayout {
22 pub fn prepare(
24 &self,
25 text: &str,
26 font: &str,
27 options: PrepareOptions,
28 ) -> Result<PreparedText, PretextError> {
29 Ok(self.prepare_core(text, font, options, false)?.base)
30 }
31
32 pub fn prepare_with_segments(
34 &self,
35 text: &str,
36 font: &str,
37 options: PrepareOptions,
38 ) -> Result<Rc<PreparedTextWithSegments>, PretextError> {
39 let content = self.prepare_core(text, font, options, true)?;
40 let grapheme_offsets = (0..content.segments.len())
41 .map(|_| OnceCell::new())
42 .collect();
43 Ok(Rc::new(PreparedTextWithSegments {
44 base: content.base,
45 segments: content.segments,
46 segment_levels: content.segment_levels,
47 grapheme_offsets,
48 }))
49 }
50
51 pub fn profile_prepare(
55 &self,
56 text: &str,
57 font: &str,
58 options: PrepareOptions,
59 mut now_ms: impl FnMut() -> f64,
60 ) -> Result<PrepareProfile, PretextError> {
61 let start = now_ms();
62 let locale = self.locale();
63 let tokens = analyze_tokens(
64 text,
65 options.white_space,
66 options.word_break,
67 locale.as_deref(),
68 );
69 let analysis_end = now_ms();
70 let state = self.get_font_state(font)?;
71 let mut prepared_segments = 0;
72 let mut breakable_segments = 0;
73 for token in &tokens {
74 visit_prepared_segments(
75 token,
76 &state,
77 get_engine_profile(),
78 options.word_break,
79 |segment| {
80 prepared_segments += 1;
81 if segment.is_breakable_run
82 && segment
83 .breakable_widths
84 .as_ref()
85 .is_some_and(|widths| widths.len() > 1)
86 {
87 breakable_segments += 1;
88 }
89 },
90 )?;
91 }
92 let end = now_ms();
93 if !start.is_finite()
94 || !analysis_end.is_finite()
95 || !end.is_finite()
96 || analysis_end < start
97 || end < analysis_end
98 {
99 return Err(PretextError::InvalidInput(
100 "profiling clock must return finite, monotonic milliseconds".into(),
101 ));
102 }
103 Ok(PrepareProfile {
104 analysis_ms: analysis_end - start,
105 measure_ms: end - analysis_end,
106 total_ms: end - start,
107 analysis_segments: tokens.len(),
108 prepared_segments,
109 breakable_segments,
110 })
111 }
112
113 fn prepare_core(
114 &self,
115 text: &str,
116 font: &str,
117 options: PrepareOptions,
118 include_segments: bool,
119 ) -> Result<PreparedContent, PretextError> {
120 let state = self.get_font_state(font)?;
121 let locale = self.locale();
122 let tokens = analyze_tokens(
123 text,
124 options.white_space,
125 options.word_break,
126 locale.as_deref(),
127 );
128 let capacity = tokens.len();
129 let mut base = PreparedText {
130 font: font.to_owned(),
131 white_space: options.white_space,
132 widths: Vec::with_capacity(capacity),
133 line_end_fit_advances: Vec::with_capacity(capacity),
134 line_end_paint_advances: Vec::with_capacity(capacity),
135 kinds: Vec::with_capacity(capacity),
136 breakable_widths: Vec::with_capacity(capacity),
137 breakable_prefix_widths: Vec::with_capacity(capacity),
138 discretionary_hyphen_width: state.hyphen_width,
139 tab_stop_advance: state.tab_stop_advance,
140 chunks: Vec::new(),
141 simple_line_walk_fast_path: true,
142 };
143 let mut segments = if include_segments {
144 Vec::with_capacity(capacity)
145 } else {
146 Vec::new()
147 };
148 let mut normalized = String::new();
149 let mut starts = Vec::new();
150 let mut utf16_length = 0;
151 for token in &tokens {
152 visit_prepared_segments(
153 token,
154 &state,
155 get_engine_profile(),
156 options.word_break,
157 |segment| {
158 base.simple_line_walk_fast_path &= matches!(
159 segment.kind,
160 SegmentBreakKind::Text
161 | SegmentBreakKind::Space
162 | SegmentBreakKind::ZeroWidthBreak
163 );
164 base.widths.push(segment.width);
165 base.line_end_fit_advances
166 .push(get_line_end_fit_advance(&segment, state.hyphen_width));
167 base.line_end_paint_advances
168 .push(get_line_end_paint_advance(&segment, state.hyphen_width));
169 base.kinds.push(segment.kind);
170 base.breakable_widths.push(segment.breakable_widths);
171 base.breakable_prefix_widths
172 .push(segment.breakable_prefix_widths);
173 if include_segments {
174 starts.push(utf16_length);
175 utf16_length += segment.text.encode_utf16().count();
176 normalized.push_str(&segment.text);
177 segments.push(segment.text);
178 }
179 },
180 )?;
181 }
182 base.chunks = build_chunks(&base.kinds);
183 base.simple_line_walk_fast_path &= base.chunks.len() <= 1;
184 let segment_levels = if include_segments {
185 compute_segment_levels(&normalized, &starts)
186 } else {
187 None
188 };
189 Ok(PreparedContent {
190 base,
191 segments,
192 segment_levels,
193 })
194 }
195
196 pub(crate) fn get_font_state(&self, font: &str) -> Result<Rc<FontState>, PretextError> {
197 let cached = self.font_states.borrow().get(font).cloned();
198 if let Some(cached) = cached {
199 return Ok(cached);
200 }
201 let generation = self.cache_generation.get();
202 let factory = self.text_measurer_factory();
203 if !factory.is_supported() {
204 return Err(PretextError::Backend(format!(
205 "text measurement backend '{}' is unsupported",
206 factory.name()
207 )));
208 }
209 let state = Rc::new(FontState::new(factory.create(font)?)?);
210 if self.cache_generation.get() == generation {
214 let displaced = self
215 .font_states
216 .borrow_mut()
217 .insert(font.to_owned(), state.clone());
218 drop(displaced);
221 }
222 Ok(state)
223 }
224}
225
226fn visit_prepared_segments(
227 token: &AnalysisToken,
228 state: &FontState,
229 profile: EngineProfile,
230 word_break: WordBreakMode,
231 mut on_segment: impl FnMut(MeasuredSegment),
232) -> Result<(), PretextError> {
233 if matches!(
234 token.kind,
235 SegmentBreakKind::HardBreak
236 | SegmentBreakKind::ZeroWidthBreak
237 | SegmentBreakKind::SoftHyphen
238 | SegmentBreakKind::Tab
239 ) {
240 on_segment(MeasuredSegment {
241 text: token.text.clone(),
242 kind: token.kind,
243 is_breakable_run: false,
244 width: 0.0,
245 breakable_widths: None,
246 breakable_prefix_widths: None,
247 });
248 return Ok(());
249 }
250 if token.kind == SegmentBreakKind::Text && contains_cjk(&token.text) {
251 let units = build_base_cjk_units(&token.text, profile.carry_cjk_after_closing_quote);
252 let units = if word_break == WordBreakMode::KeepAll {
253 merge_keep_all_text_units(units)
254 } else {
255 units
256 };
257 for unit in units {
258 let is_breakable_run = word_break == WordBreakMode::KeepAll || !contains_cjk(&unit);
259 on_segment(state.measure_segment(&unit, token.kind, is_breakable_run)?);
260 }
261 } else {
262 let is_breakable_run = token.kind == SegmentBreakKind::Text
263 && token.is_word_like
264 && token.text.encode_utf16().count() > 1;
265 on_segment(state.measure_segment(&token.text, token.kind, is_breakable_run)?);
266 }
267 Ok(())
268}
269
270fn get_line_end_fit_advance(segment: &MeasuredSegment, hyphen_width: f64) -> f64 {
271 match segment.kind {
272 SegmentBreakKind::Space
273 | SegmentBreakKind::PreservedSpace
274 | SegmentBreakKind::ZeroWidthBreak
275 | SegmentBreakKind::Tab
276 | SegmentBreakKind::HardBreak => 0.0,
277 SegmentBreakKind::SoftHyphen => hyphen_width,
278 _ => segment.width,
279 }
280}
281
282fn get_line_end_paint_advance(segment: &MeasuredSegment, hyphen_width: f64) -> f64 {
283 match segment.kind {
284 SegmentBreakKind::Space
285 | SegmentBreakKind::ZeroWidthBreak
286 | SegmentBreakKind::Tab
287 | SegmentBreakKind::HardBreak => 0.0,
288 SegmentBreakKind::SoftHyphen => hyphen_width,
289 _ => segment.width,
290 }
291}
292
293fn build_chunks(kinds: &[SegmentBreakKind]) -> Vec<PreparedLineChunk> {
294 let mut chunks = Vec::new();
295 let mut start = 0;
296 for (index, &kind) in kinds.iter().enumerate() {
297 if kind == SegmentBreakKind::HardBreak {
298 chunks.push(PreparedLineChunk {
299 start_segment_index: start,
300 end_segment_index: index,
301 consumed_end_segment_index: index + 1,
302 });
303 start = index + 1;
304 }
305 }
306 if start < kinds.len() {
307 chunks.push(PreparedLineChunk {
308 start_segment_index: start,
309 end_segment_index: kinds.len(),
310 consumed_end_segment_index: kinds.len(),
311 });
312 }
313 chunks
314}