i_slint_core/textlayout/sharedparley/
shaping.rs1use super::*;
13
14const INLINE_CODE_FONT_SCALE: f32 = 0.85;
19
20std::thread_local! {
21 static LAYOUT_CONTEXT: RefCell<parley::LayoutContext<Brush>> = Default::default();
22}
23
24#[derive(Debug, Default, PartialEq, Clone, Copy)]
25pub(super) struct Brush {
26 pub(super) override_fill_color: Option<Color>,
28 pub(super) stroke: Option<TextStrokeStyle>,
29 pub(super) link_color: Option<Color>,
30}
31
32pub(super) struct LayoutWithoutLineBreaksBuilder {
33 font_request: Option<FontRequest>,
34 pub(super) text_wrap: TextWrap,
35 stroke: Option<TextStrokeStyle>,
36 pub(super) scale_factor: ScaleFactor,
37 pub(super) pixel_size: LogicalLength,
38 overflow_wrap_anywhere: bool,
41}
42
43impl LayoutWithoutLineBreaksBuilder {
44 pub(super) fn new(
45 font_request: Option<FontRequest>,
46 text_wrap: TextWrap,
47 stroke: Option<TextStrokeStyle>,
48 scale_factor: ScaleFactor,
49 ) -> Self {
50 let pixel_size = font_request
51 .as_ref()
52 .and_then(|font_request| font_request.pixel_size)
53 .unwrap_or(DEFAULT_FONT_SIZE);
54
55 Self {
56 font_request,
57 text_wrap,
58 stroke,
59 scale_factor,
60 pixel_size,
61 overflow_wrap_anywhere: true,
62 }
63 }
64
65 fn ranged_builder<'a>(
66 &self,
67 layout_ctx: &'a mut parley::LayoutContext<Brush>,
68 font_ctx: &'a mut parley::FontContext,
69 text: &'a str,
70 ) -> parley::RangedBuilder<'a, Brush> {
71 let line_height_ratio =
75 self.font_request.as_ref().and_then(|fr| line_height_ratio(font_ctx, fr));
76
77 let mut builder = layout_ctx.ranged_builder(font_ctx, text, self.scale_factor.get(), false);
78
79 if let Some(ratio) = line_height_ratio {
80 builder.push_default(parley::StyleProperty::LineHeight(
81 parley::style::LineHeight::FontSizeRelative(ratio),
82 ));
83 }
84
85 if let Some(ref font_request) = self.font_request {
86 let mut fallback_family_iter = sharedfontique::FALLBACK_FAMILIES
87 .into_iter()
88 .map(parley::style::FontFamilyName::Generic);
89
90 let font_families: &[parley::style::FontFamilyName] = if let Some(family) =
91 &font_request.family
92 {
93 let mut iter =
94 core::iter::once(parley::style::FontFamilyName::named(family.as_str()))
95 .chain(fallback_family_iter);
96 &core::array::from_fn::<
97 _,
98 { sharedfontique::FALLBACK_FAMILIES.as_slice().len() + 1 },
99 _,
100 >(|_| iter.next().unwrap())
101 } else {
102 &core::array::from_fn::<_, { sharedfontique::FALLBACK_FAMILIES.as_slice().len() }, _>(
103 |_| fallback_family_iter.next().unwrap(),
104 )
105 };
106
107 builder.push_default(parley::style::FontFamily::List(std::borrow::Cow::Borrowed(
108 font_families,
109 )));
110
111 if let Some(weight) = font_request.weight {
112 builder.push_default(parley::StyleProperty::FontWeight(
113 parley::style::FontWeight::new(weight as f32),
114 ));
115 }
116 if let Some(letter_spacing) = font_request.letter_spacing {
117 builder.push_default(parley::StyleProperty::LetterSpacing(letter_spacing.get()));
118 }
119 builder.push_default(parley::StyleProperty::FontStyle(if font_request.italic {
120 parley::style::FontStyle::Italic
121 } else {
122 parley::style::FontStyle::Normal
123 }));
124 }
125 builder.push_default(parley::StyleProperty::FontSize(self.pixel_size.get()));
126 builder.push_default(parley::StyleProperty::WordBreak(match self.text_wrap {
127 TextWrap::NoWrap => parley::style::WordBreak::KeepAll,
128 TextWrap::WordWrap => parley::style::WordBreak::Normal,
129 TextWrap::CharWrap => parley::style::WordBreak::BreakAll,
130 }));
131 builder.push_default(parley::StyleProperty::OverflowWrap(
132 match (self.text_wrap, self.overflow_wrap_anywhere) {
133 (TextWrap::NoWrap, _) | (_, false) => parley::style::OverflowWrap::Normal,
134 (TextWrap::WordWrap | TextWrap::CharWrap, true) => {
135 parley::style::OverflowWrap::Anywhere
136 }
137 },
138 ));
139 if self.text_wrap == TextWrap::NoWrap {
140 builder.push_default(parley::StyleProperty::TextWrapMode(
145 parley::style::TextWrapMode::NoWrap,
146 ));
147 }
148
149 builder.push_default(parley::StyleProperty::Brush(Brush {
150 override_fill_color: None,
151 stroke: self.stroke,
152 link_color: None,
153 }));
154
155 builder
156 }
157
158 pub(super) fn build(
163 &self,
164 font_context: &mut parley::FontContext,
165 text: &str,
166 formatting: impl IntoIterator<Item = i_slint_common::styled_text::FormattedSpan>,
167 link_color: Option<Color>,
168 ) -> parley::Layout<Brush> {
169 use i_slint_common::styled_text::Style;
170
171 LAYOUT_CONTEXT.with_borrow_mut(|layout_ctx| {
172 let mut builder = self.ranged_builder(layout_ctx, font_context, text);
173
174 for span in formatting.into_iter().filter(|s| !s.range.is_empty()) {
176 match span.style {
177 Style::Emphasis => {
178 builder.push(
179 parley::StyleProperty::FontStyle(parley::style::FontStyle::Italic),
180 span.range,
181 );
182 }
183 Style::Strikethrough => {
184 builder.push(parley::StyleProperty::Strikethrough(true), span.range);
185 }
186 Style::Strong => {
187 builder.push(
188 parley::StyleProperty::FontWeight(parley::style::FontWeight::BOLD),
189 span.range,
190 );
191 }
192 Style::Code => {
193 builder.push(
194 parley::StyleProperty::FontFamily(parley::style::FontFamily::Single(
195 parley::style::FontFamilyName::Generic(
196 parley::style::GenericFamily::Monospace,
197 ),
198 )),
199 span.range.clone(),
200 );
201 builder.push(
205 parley::StyleProperty::FontSize(
206 self.pixel_size.get() * INLINE_CODE_FONT_SCALE,
207 ),
208 span.range,
209 );
210 }
211 Style::Underline => {
212 builder.push(parley::StyleProperty::Underline(true), span.range);
213 }
214 Style::Link => {
215 builder.push(parley::StyleProperty::Underline(true), span.range.clone());
216 builder.push(
217 parley::StyleProperty::Brush(Brush {
218 override_fill_color: None,
219 stroke: self.stroke,
220 link_color,
221 }),
222 span.range,
223 );
224 }
225 Style::Color(color) => {
226 builder.push(
227 parley::StyleProperty::Brush(Brush {
228 override_fill_color: Some(crate::Color::from_argb_encoded(color)),
229 stroke: self.stroke,
230 link_color: None,
231 }),
232 span.range,
233 );
234 }
235 }
236 }
237
238 builder.build(text)
239 })
240 }
241}
242
243pub(super) fn line_height_ratio(
245 font_ctx: &mut parley::FontContext,
246 font_request: &FontRequest,
247) -> Option<f32> {
248 let font = font_request.query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)?;
249 let face = skrifa::FontRef::from_index(font.blob.data(), font.index).ok()?;
250 let location = face.axes().location(font.synthesis.variation_settings());
251 let metrics = face.metrics(skrifa::instance::Size::unscaled(), &location);
252 let units_per_em = metrics.units_per_em as f32;
253 (units_per_em > 0.0)
254 .then(|| (metrics.ascent - metrics.descent + metrics.leading) / units_per_em)
255 .map(|natural_ratio| {
256 font_request.line_height_for_natural_height(natural_ratio).unwrap_or(natural_ratio)
257 })
258}
259
260pub(super) fn paragraph_ranges(text: &str) -> impl Iterator<Item = Range<usize>> + '_ {
264 let mut start = 0;
265 text.split('\n').map(move |paragraph| {
266 let end = start + paragraph.len();
267 let range = if paragraph.ends_with('\r') { start..end - 1 } else { start..end };
268 start = end + 1;
269 range
270 })
271}
272
273pub(super) fn create_text_paragraphs(
274 layout_builder: &LayoutWithoutLineBreaksBuilder,
275 font_context: &mut parley::FontContext,
276 text: PlainOrStyledText,
277 link_color: Color,
278) -> Vec<TextParagraph> {
279 let paragraph_from_text =
280 |font_context: &mut parley::FontContext,
281 text: &str,
282 range: std::ops::Range<usize>,
283 formatting: Vec<i_slint_common::styled_text::FormattedSpan>,
284 links: Vec<(std::ops::Range<usize>, std::string::String)>| {
285 let code_ranges: alloc::vec::Vec<Range<usize>> = formatting
286 .iter()
287 .filter(|s| matches!(s.style, i_slint_common::styled_text::Style::Code))
288 .map(|s| s.range.clone())
289 .collect();
290
291 let layout = layout_builder.build(font_context, text, formatting, Some(link_color));
292
293 TextParagraph { range, y: PhysicalLength::default(), layout, links, code_ranges }
294 };
295
296 let mut paragraphs = Vec::with_capacity(1);
297
298 match text {
299 PlainOrStyledText::Plain(ref text) => {
300 for range in paragraph_ranges(text) {
301 paragraphs.push(paragraph_from_text(
302 font_context,
303 &text[range.clone()],
304 range,
305 Default::default(),
306 Default::default(),
307 ));
308 }
309 }
310 PlainOrStyledText::Styled(rich_text) => {
311 for paragraph in rich_text.paragraphs {
312 paragraphs.push(paragraph_from_text(
313 font_context,
314 ¶graph.text,
315 0..0,
316 paragraph.formatting,
317 paragraph.links,
318 ));
319 }
320 }
321 };
322
323 paragraphs
324}
325
326pub(super) fn shaping_builder(
330 text: Pin<&dyn crate::item_rendering::RenderString>,
331 item_rc: Option<&crate::item_tree::ItemRc>,
332 text_wrap: TextWrap,
333 scale_factor: ScaleFactor,
334) -> LayoutWithoutLineBreaksBuilder {
335 let (stroke_brush, _, stroke_style) = text.stroke();
336 LayoutWithoutLineBreaksBuilder::new(
337 item_rc.map(|irc| text.font_request(irc)),
338 text_wrap,
339 (!stroke_brush.is_transparent()).then_some(stroke_style),
340 scale_factor,
341 )
342}
343
344pub(super) fn content_widths_builder(
352 font_request: FontRequest,
353 scale_factor: ScaleFactor,
354) -> LayoutWithoutLineBreaksBuilder {
355 let mut builder = LayoutWithoutLineBreaksBuilder::new(
356 Some(font_request),
357 TextWrap::WordWrap,
358 None,
359 scale_factor,
360 );
361 builder.overflow_wrap_anywhere = false;
362 builder
363}
364
365#[cfg(test)]
369pub(super) fn plain_builder_for_tests() -> LayoutWithoutLineBreaksBuilder {
370 LayoutWithoutLineBreaksBuilder::new(None, TextWrap::NoWrap, None, ScaleFactor::new(1.0))
371}
372
373#[cfg(test)]
374pub(super) fn wrap_builder_for_tests() -> LayoutWithoutLineBreaksBuilder {
375 LayoutWithoutLineBreaksBuilder::new(None, TextWrap::WordWrap, None, ScaleFactor::new(1.0))
376}
377
378pub(super) fn shape_paragraphs(
382 text: Pin<&dyn crate::item_rendering::RenderString>,
383 item_rc: Option<&crate::item_tree::ItemRc>,
384 text_wrap: TextWrap,
385 scale_factor: ScaleFactor,
386 font_context: &mut parley::FontContext,
387) -> Vec<TextParagraph> {
388 let builder = shaping_builder(text, item_rc, text_wrap, scale_factor);
389 create_text_paragraphs(&builder, font_context, text.text(), text.link_color())
390}
391
392pub(super) struct TextParagraph {
393 pub(super) range: Range<usize>,
394 pub(super) y: PhysicalLength,
395 pub(super) layout: parley::Layout<Brush>,
396 pub(super) links: std::vec::Vec<(Range<usize>, std::string::String)>,
397 pub(super) code_ranges: std::vec::Vec<Range<usize>>,
401}