1use super::*;
4
5impl Text {
6 pub fn fit_lines(
12 &self,
13 max_lines: usize,
14 fonts: &FontLibrary,
15 ) -> Result<(Self, bool), ValidationError> {
16 use unicode_segmentation::UnicodeSegmentation;
17 if !(1..=512).contains(&max_lines) {
18 return Err(invalid("max_lines", "Text line limit requires 1..=512"));
19 }
20 let full = self.layout(fonts)?;
21 let fits = |layout: &ParagraphLayout| {
22 layout.lines.len() <= max_lines
23 && self.width.is_none_or(|width| {
24 layout.bounds.width() <= width
25 && layout.lines.iter().all(|line| line.width() <= width)
26 })
27 };
28 if fits(&full) {
29 return Ok((self.clone().fonts(fonts), false));
30 }
31 let end = full
32 .line_sources
33 .iter()
34 .take(max_lines)
35 .map(|range| range.end)
36 .max()
37 .unwrap_or(0);
38 let mut cuts = full
39 .text
40 .grapheme_indices(true)
41 .map(|(index, _)| index)
42 .filter(|index| *index <= end)
43 .collect::<Vec<_>>();
44 if end == full.text.len() && !cuts.contains(&end) {
45 cuts.push(end);
46 }
47 cuts.sort_unstable();
48 for end in cuts.into_iter().rev() {
49 let mut spans = Vec::new();
50 let mut consumed = 0;
51 for source in self.spans.iter() {
52 let count = end.saturating_sub(consumed).min(source.text.len());
53 if count > 0 {
54 let mut span = source.clone();
55 span.text = Arc::from(&source.text[..count]);
56 spans.push(span);
57 }
58 consumed += source.text.len();
59 if consumed >= end {
60 break;
61 }
62 }
63 if let Some(last) = spans.last_mut() {
64 last.text = Arc::from(format!("{}\u{2026}", last.text));
65 } else {
66 let mut span = self
67 .spans
68 .first()
69 .cloned()
70 .unwrap_or_else(|| TextSpan::new(""));
71 span.text = Arc::from("\u{2026}");
72 spans.push(span);
73 }
74 let mut candidate = self.clone();
75 candidate.spans = spans.into();
76 candidate.fonts = Some(fonts.clone());
77 candidate.invalidate_layout_cache();
78 let layout = candidate.layout(fonts)?;
79 if fits(&layout) {
80 return Ok((candidate, true));
81 }
82 }
83 Err(invalid(
84 "max_width",
85 "No grapheme-safe ellipsis fits the requested paragraph",
86 ))
87 }
88
89 pub fn layout(&self, fonts: &FontLibrary) -> Result<Arc<ParagraphLayout>, ValidationError> {
91 self.validate_layout()?;
92 if let Some(result) = self
93 .layout_cache
94 .lock()
95 .map_err(|_| invalid("cache", "Layout cache is poisoned"))?
96 .get(&fonts.snapshot_id())
97 {
98 return result.clone();
99 }
100 let key = self.layout_key();
101 let result = if let Some(layout) = fonts
102 .layout_cache
103 .lock()
104 .map_err(|_| invalid("cache", "Shared paragraph cache is poisoned"))?
105 .get(&key)
106 .cloned()
107 {
108 Ok(layout)
109 } else {
110 let result = self.shape(fonts).map(Arc::new);
111 if let Ok(layout) = &result {
112 let mut shared = fonts
113 .layout_cache
114 .lock()
115 .map_err(|_| invalid("cache", "Shared paragraph cache is poisoned"))?;
116 if shared.len() >= 64 {
117 shared.clear();
118 }
119 shared.insert(key, layout.clone());
120 }
121 result
122 };
123 let mut cache = self
124 .layout_cache
125 .lock()
126 .map_err(|_| invalid("cache", "Layout cache is poisoned"))?;
127 if cache.len() >= 8 {
128 cache.clear();
129 }
130 cache.insert(fonts.snapshot_id(), result.clone());
131 result
132 }
133
134 fn validate_layout(&self) -> Result<(), ValidationError> {
135 self.fill.validate()?;
136 if let Some((width, paint)) = &self.stroke {
137 if !width.is_finite() || *width < 0.0 {
138 return Err(invalid(
139 "stroke",
140 "Text stroke requires finite nonnegative width",
141 ));
142 }
143 paint.validate()?;
144 }
145 for span in self.spans.iter() {
146 if let Some(paint) = &span.fill {
147 paint.validate()?;
148 }
149 }
150 Ok(())
151 }
152
153 fn layout_key(&self) -> LayoutKey {
154 LayoutKey {
155 spans: self
156 .spans
157 .iter()
158 .map(|span| {
159 (
160 span.text.clone(),
161 span.family.clone(),
162 span.size.map(f64::to_bits),
163 span.weight.map(|weight| weight.0),
164 span.italic,
165 )
166 })
167 .collect(),
168 family: self.family.clone(),
169 size: self.size.to_bits(),
170 weight: self.weight.0,
171 line_height: self.line_height.to_bits(),
172 spacing: self.spacing.to_bits(),
173 width: self.width.map(f64::to_bits),
174 alignment: self.alignment,
175 }
176 }
177
178 pub(super) fn resolve(
179 &self,
180 context: &DrawContext,
181 ) -> Result<Arc<ParagraphLayout>, ValidationError> {
182 let fonts = self
183 .fonts
184 .as_ref()
185 .or(context
186 .fonts()
187 .and_then(|environment| environment.downcast_ref::<FontLibrary>()))
188 .ok_or_else(|| invalid("fonts", "paragraphs require an explicit FontLibrary"))?;
189 if context.layout_cache().is_none() {
190 return self.layout(fonts);
191 }
192 let fonts = context
193 .configured_render_cache()
194 .map_or_else(|| fonts.clone(), |cache| fonts.with_render_cache(cache));
195 self.validate_layout()?;
196 let key = self.layout_key();
197 let key_bytes = std::mem::size_of::<LayoutKey>()
198 + std::mem::size_of_val(key.spans.as_slice())
199 + key.family.len()
200 + key
201 .spans
202 .iter()
203 .map(|span| span.0.len() + span.1.as_ref().map_or(0, |family| family.len()))
204 .sum::<usize>();
205 context
206 .cache_layout(
207 (fonts.snapshot_id(), key),
208 || self.shape(&fonts).map(Arc::new),
209 |result| {
210 key_bytes.saturating_add(result.as_ref().map_or_else(
211 |error| error.message.len(),
212 |layout| {
213 std::mem::size_of::<ParagraphLayout>()
214 + std::mem::size_of_val(layout.glyphs.as_ref())
215 + std::mem::size_of_val(layout.lines.as_ref())
216 + std::mem::size_of_val(layout.line_sources.as_ref())
217 + layout.text.len()
218 },
219 ))
220 },
221 )
222 .as_ref()
223 .clone()
224 }
225
226 fn paragraph_text(&self) -> Result<String, ValidationError> {
227 if self.spans.len() > 256 {
228 return Err(limited("Text exceeds 256 rich spans"));
229 }
230 if !(1..=1000).contains(&self.weight.0)
231 || self.spans.iter().any(|span| {
232 span.weight
233 .is_some_and(|weight| !(1..=1000).contains(&weight.0))
234 })
235 {
236 return Err(invalid("weight", "Font weight must be in 1..=1000"));
237 }
238 let length = self
239 .spans
240 .iter()
241 .try_fold(0usize, |length, span| length.checked_add(span.text.len()))
242 .ok_or_else(|| limited("Paragraph size overflow"))?;
243 if length > 16_384 {
244 return Err(limited("Paragraph exceeds 16384 UTF-8 bytes"));
245 }
246 let text = self
247 .spans
248 .iter()
249 .map(|span| span.text.as_ref())
250 .collect::<String>();
251 if text.len() > 16_384
252 || text
253 .chars()
254 .filter(|value| matches!(value, '\n' | '\r'))
255 .count()
256 > 4096
257 {
258 return Err(limited("Paragraph exceeds its text/line allowance"));
259 }
260 if !self.size.is_finite()
261 || self.size <= 0.0
262 || self.size > 4096.0
263 || self.size as f32 == 0.0
264 || !self.line_height.is_finite()
265 || !(0.1..=16.0).contains(&self.line_height)
266 || !self.spacing.is_finite()
267 || self.spacing.abs() > 4096.0
268 || !(self.spacing / self.size).is_finite()
269 || !((self.spacing / self.size) as f32).is_finite()
270 || self.width.is_some_and(|width| {
271 !width.is_finite() || width <= 0.0 || width > 1e7 || width as f32 == 0.0
272 })
273 {
274 return Err(invalid(
275 "style",
276 "Invalid paragraph size, line-height, tracking or width",
277 ));
278 }
279 self.fill.validate()?;
280 if let Some((width, paint)) = &self.stroke {
281 if !width.is_finite() || *width < 0.0 {
282 return Err(invalid(
283 "stroke",
284 "Text stroke requires finite nonnegative width",
285 ));
286 }
287 paint.validate()?;
288 }
289 Ok(text)
290 }
291
292 fn shape(&self, fonts: &FontLibrary) -> Result<ParagraphLayout, ValidationError> {
293 let text = self.paragraph_text()?;
294 let mut features = cosmic_text::FontFeatures::new();
295 features
296 .disable(cosmic_text::FeatureTag::STANDARD_LIGATURES)
297 .disable(cosmic_text::FeatureTag::CONTEXTUAL_LIGATURES);
298
299 fn family(name: &str) -> cosmic_text::Family<'_> {
300 if name.is_empty() {
301 cosmic_text::Family::SansSerif
302 } else {
303 cosmic_text::Family::Name(name)
304 }
305 }
306 let defaults = cosmic_text::Attrs::new()
307 .family(family(&self.family))
308 .weight(self.weight)
309 .letter_spacing((self.spacing / self.size) as f32)
310 .font_features(features.clone());
311 let mut styled = Vec::new();
312 for (index, span) in self.spans.iter().enumerate() {
313 let size = span.size.unwrap_or(self.size);
314 if !size.is_finite()
315 || size <= 0.0
316 || size > 4096.0
317 || size as f32 == 0.0
318 || !((self.spacing / size) as f32).is_finite()
319 || (size * self.line_height) as f32 == 0.0
320 {
321 return Err(invalid("span", "Invalid rich-span font size or tracking"));
322 }
323 if let Some(paint) = &span.fill {
324 paint.validate()?;
325 }
326 styled.push((
327 span.text.as_ref(),
328 cosmic_text::Attrs::new()
329 .family(family(span.family.as_deref().unwrap_or(&self.family)))
330 .weight(span.weight.unwrap_or(self.weight))
331 .style(if span.italic {
332 cosmic_text::Style::Italic
333 } else {
334 cosmic_text::Style::Normal
335 })
336 .metadata(index)
337 .metrics(cosmic_text::Metrics::new(
338 size as f32,
339 (size * self.line_height) as f32,
340 ))
341 .letter_spacing((self.spacing / size) as f32)
342 .font_features(features.clone()),
343 ));
344 }
345 if text.is_empty() {
346 return Ok(ParagraphLayout {
347 glyphs: Arc::from([]),
348 bounds: Rect::default(),
349 baseline: 0.0,
350 lines: Arc::from([]),
351 line_sources: Arc::from([]),
352 text: text.into(),
353 });
354 }
355 if fonts.is_empty() {
356 return Err(invalid("fonts", "The font library is empty"));
357 }
358 let mut system = fonts.lock_font_system()?;
359 let mut buffer = cosmic_text::Buffer::new(
360 &mut system,
361 cosmic_text::Metrics::new(self.size as f32, (self.size * self.line_height) as f32),
362 );
363 buffer.set_size(self.width.map(|width| width as f32), None);
364 buffer.set_wrap(cosmic_text::Wrap::WordOrGlyph);
365 let alignment = match self.alignment {
366 TextAlignment::Start => None,
367 TextAlignment::End => Some(cosmic_text::Align::End),
368 TextAlignment::Center => Some(cosmic_text::Align::Center),
369 TextAlignment::Justified => Some(cosmic_text::Align::Justified),
370 };
371 buffer.set_rich_text(styled, &defaults, cosmic_text::Shaping::Advanced, alignment);
372 buffer.shape_until_scroll(&mut system, false);
373 let mut offset = 0;
374 let mut offsets = Vec::with_capacity(buffer.lines.len());
375 for line in &buffer.lines {
376 if !text[offset..].starts_with(line.text()) {
377 return Err(invalid(
378 "source",
379 "Shaped line no longer matches original UTF-8 text",
380 ));
381 }
382 offsets.push(offset);
383 offset += line.text().len();
384 let remaining = &text[offset..];
385 if remaining.starts_with("\r\n") {
386 offset += 2;
387 } else if let Some(separator) = remaining
388 .chars()
389 .next()
390 .filter(|value| matches!(value, '\r' | '\n' | '\u{0085}' | '\u{2028}' | '\u{2029}'))
391 {
392 offset += separator.len_utf8();
393 }
394 }
395 self.collect_layout(fonts, &mut system, &buffer, text, offsets)
396 }
397
398 fn collect_layout(
399 &self,
400 fonts: &FontLibrary,
401 system: &mut cosmic_text::FontSystem,
402 buffer: &cosmic_text::Buffer,
403 text: String,
404 offsets: Vec<usize>,
405 ) -> Result<ParagraphLayout, ValidationError> {
406 let mut glyphs = Vec::new();
407 let mut lines = Vec::new();
408 let mut line_sources = Vec::new();
409 let mut bounds = Rect::default();
410 let mut baseline = 0.0;
411 for run in buffer.layout_runs() {
412 if lines.len() >= 4096 {
413 return Err(limited("Paragraph exceeds 4096 visual lines"));
414 }
415 if lines.is_empty() {
416 baseline = f64::from(run.line_y);
417 }
418 let line = Rect::new(
419 0.0,
420 f64::from(run.line_top),
421 self.width.unwrap_or(f64::from(run.line_w)),
422 f64::from(run.line_top + run.line_height),
423 );
424 bounds = bounds.union(line);
425 let line_index = lines.len();
426 lines.push(line);
427 let start = run
428 .glyphs
429 .iter()
430 .map(|glyph| glyph.start)
431 .min()
432 .unwrap_or(0);
433 let end = run
434 .glyphs
435 .iter()
436 .map(|glyph| glyph.end)
437 .max()
438 .unwrap_or(start);
439 line_sources.push(offsets[run.line_i] + start..offsets[run.line_i] + end);
440 for glyph in run.glyphs {
441 if glyphs.len() >= 4096 {
442 return Err(limited("Paragraph exceeds 4096 shaped glyphs"));
443 }
444 if glyph.glyph_id == 0 {
445 return Err(ValidationError::unsupported(
446 "text",
447 "missing_glyph",
448 "Registered fonts do not cover a shaped text cluster",
449 ));
450 }
451 let artwork = glyph::Artwork::load(
452 fonts,
453 system,
454 glyph.font_id,
455 glyph.font_weight,
456 glyph.glyph_id,
457 )?;
458 let scale = f64::from(glyph.font_size) / artwork.units;
459 let horizontal =
460 f64::from(glyph.x) + f64::from(glyph.font_size) * f64::from(glyph.x_offset);
461 let vertical = f64::from(run.line_y + glyph.y)
462 - f64::from(glyph.font_size) * f64::from(glyph.y_offset);
463 let transform = Transform::scale_non_uniform(scale, -scale)
464 .then(Transform::translate(horizontal, vertical));
465 let hit = Rect::new(
466 f64::from(glyph.x),
467 f64::from(run.line_top),
468 f64::from(glyph.x + glyph.w),
469 f64::from(run.line_top + run.line_height),
470 );
471 let ink = artwork
472 .bounds(transform)?
473 .map_or(hit, |bounds| hit.union(bounds));
474 bounds = bounds.union(ink);
475 let family = system
476 .db()
477 .face(glyph.font_id)
478 .and_then(|face| face.families.first())
479 .map(|family| Arc::from(family.0.as_str()))
480 .unwrap_or_else(|| Arc::from("<font>"));
481 glyphs.push(TextGlyph {
482 source: offsets[run.line_i] + glyph.start..offsets[run.line_i] + glyph.end,
483 line: line_index,
484 family,
485 bounds: ink,
486 artwork,
487 transform,
488 span: glyph.metadata,
489 });
490 }
491 }
492 Ok(ParagraphLayout {
493 glyphs: glyphs.into(),
494 bounds,
495 baseline,
496 lines: lines.into(),
497 line_sources: line_sources.into(),
498 text: text.into(),
499 })
500 }
501}