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