1use crate::font::registry::FontRegistry;
2use crate::font::resolve::{ResolvedFont, resolve_font};
3use crate::layout::line::LayoutLine;
4use crate::layout::paragraph::{Alignment, Hyphenator, break_into_lines};
5use crate::shaping::run::{ShapedGlyph, ShapedRun};
6use crate::shaping::shaper::{
7 FontMetricsPx, TextDirection, font_metrics_px, shape_text, shape_text_with_fallback,
8 to_harfrust_features,
9};
10
11#[derive(Clone)]
13pub struct BlockLayout {
14 pub block_id: usize,
15 pub position: usize,
17 pub lines: Vec<LayoutLine>,
19 pub y: f32,
21 pub height: f32,
23 pub top_margin: f32,
24 pub bottom_margin: f32,
25 pub left_margin: f32,
26 pub right_margin: f32,
27 pub list_marker: Option<ShapedListMarker>,
30 pub background_color: Option<[f32; 4]>,
32}
33
34#[derive(Clone)]
36pub struct ShapedListMarker {
37 pub run: ShapedRun,
38 pub x: f32,
40}
41
42#[derive(Clone)]
45pub struct BlockLayoutParams {
46 pub block_id: usize,
47 pub position: usize,
48 pub text: String,
49 pub fragments: Vec<FragmentParams>,
50 pub alignment: Alignment,
51 pub top_margin: f32,
52 pub bottom_margin: f32,
53 pub left_margin: f32,
54 pub right_margin: f32,
55 pub text_indent: f32,
56 pub list_marker: String,
58 pub list_indent: f32,
60 pub tab_positions: Vec<f32>,
62 pub line_height_multiplier: Option<f32>,
65 pub non_breakable_lines: bool,
67 pub hyphenation: Option<crate::types::Hyphenation>,
70 pub checkbox: Option<bool>,
72 pub background_color: Option<[f32; 4]>,
74}
75
76#[derive(Clone)]
78pub struct FragmentParams {
79 pub text: String,
80 pub offset: usize,
90 pub length: usize,
91 pub font_family: Option<String>,
92 pub font_weight: Option<u32>,
93 pub font_bold: Option<bool>,
94 pub font_italic: Option<bool>,
95 pub font_point_size: Option<u32>,
96 pub underline_style: crate::types::UnderlineStyle,
97 pub overline: bool,
98 pub strikeout: bool,
99 pub is_link: bool,
100 pub letter_spacing: f32,
102 pub word_spacing: f32,
104 pub foreground_color: Option<[f32; 4]>,
106 pub underline_color: Option<[f32; 4]>,
108 pub background_color: Option<[f32; 4]>,
110 pub anchor_href: Option<String>,
112 pub tooltip: Option<String>,
114 pub vertical_alignment: crate::types::VerticalAlignment,
116 pub image_name: Option<String>,
118 pub image_width: f32,
120 pub image_height: f32,
122 pub features: Vec<crate::types::FontFeature>,
125}
126
127pub fn layout_block(
132 registry: &FontRegistry,
133 params: &BlockLayoutParams,
134 available_width: f32,
135 scale_factor: f32,
136 font_scale: f32,
137) -> BlockLayout {
138 let effective_left_margin = params.left_margin + params.list_indent;
139 let content_width = (available_width - effective_left_margin - params.right_margin).max(0.0);
140
141 let mut shaped_runs = Vec::new();
143 let mut default_metrics: Option<FontMetricsPx> = None;
144
145 for frag in ¶ms.fragments {
146 if let Some(ref image_name) = frag.image_name {
148 let image_glyph = ShapedGlyph {
149 glyph_id: 0,
150 cluster: 0,
151 x_advance: frag.image_width,
152 y_advance: 0.0,
153 x_offset: 0.0,
154 y_offset: 0.0,
155 font_face_id: crate::types::FontFaceId(0),
156 };
157 let run = ShapedRun {
158 font_face_id: crate::types::FontFaceId(0),
159 size_px: 0.0,
160 weight: 400,
161 glyphs: vec![image_glyph],
162 advance_width: frag.image_width,
163 text_range: frag.offset..frag.offset + frag.text.len(),
164 direction: TextDirection::LeftToRight,
165 underline_style: frag.underline_style,
166 overline: false,
167 strikeout: false,
168 is_link: frag.is_link,
169 foreground_color: None,
170 underline_color: None,
171 background_color: None,
172 anchor_href: frag.anchor_href.clone(),
173 tooltip: frag.tooltip.clone(),
174 vertical_alignment: crate::types::VerticalAlignment::Normal,
175 image_name: Some(image_name.clone()),
176 image_height: frag.image_height,
177 };
178 shaped_runs.push(run);
179 continue;
180 }
181
182 let font_point_size = match frag.vertical_alignment {
184 crate::types::VerticalAlignment::SuperScript
185 | crate::types::VerticalAlignment::SubScript => frag
186 .font_point_size
187 .map(|s| ((s as f32 * 0.65) as u32).max(1)),
188 crate::types::VerticalAlignment::Normal => frag.font_point_size,
189 };
190
191 let resolved = resolve_font(
192 registry,
193 frag.font_family.as_deref(),
194 frag.font_weight,
195 frag.font_bold,
196 frag.font_italic,
197 font_point_size,
198 scale_factor,
199 font_scale,
200 );
201
202 if let Some(resolved) = resolved {
203 if default_metrics.is_none() {
205 default_metrics = font_metrics_px(registry, &resolved);
206 }
207
208 let features = to_harfrust_features(&frag.features);
209 if let Some(mut run) = shape_text_with_fallback(
210 registry,
211 &resolved,
212 &frag.text,
213 frag.offset,
214 TextDirection::Auto,
215 &features,
216 ) {
217 run.underline_style = frag.underline_style;
218 run.overline = frag.overline;
219 run.strikeout = frag.strikeout;
220 run.is_link = frag.is_link;
221 run.foreground_color = frag.foreground_color;
222 run.underline_color = frag.underline_color;
223 run.background_color = frag.background_color;
224 run.anchor_href = frag.anchor_href.clone();
225 run.tooltip = frag.tooltip.clone();
226 run.vertical_alignment = frag.vertical_alignment;
227
228 if frag.letter_spacing != 0.0 || frag.word_spacing != 0.0 {
230 apply_spacing(&mut run, &frag.text, frag.letter_spacing, frag.word_spacing);
231 }
232
233 if !params.tab_positions.is_empty() {
235 apply_tab_stops(&mut run, &frag.text, ¶ms.tab_positions);
236 }
237
238 shaped_runs.push(run);
239 }
240 }
241 }
242
243 let metrics =
245 default_metrics.unwrap_or_else(|| get_default_metrics(registry, scale_factor, font_scale));
246
247 let wrap_width = if params.non_breakable_lines {
249 f32::INFINITY
250 } else {
251 content_width
252 };
253
254 let hyphenator = params
257 .hyphenation
258 .filter(|_| !params.non_breakable_lines)
259 .and_then(|h| {
260 shape_hyphen(registry, scale_factor, font_scale).map(|glyph| Hyphenator {
261 glyph,
262 language: h.language,
263 })
264 });
265
266 let mut lines = break_into_lines(
268 shaped_runs,
269 ¶ms.text,
270 wrap_width,
271 params.alignment,
272 params.text_indent,
273 &metrics,
274 hyphenator,
275 );
276
277 let line_height_mul = params.line_height_multiplier.unwrap_or(1.0).max(0.1);
279
280 let mut y = 0.0f32;
282 for line in &mut lines {
283 if line_height_mul != 1.0 {
284 line.line_height *= line_height_mul;
285 }
286 line.y = y + line.ascent; y += line.line_height;
288 }
289
290 let content_height = y;
291 let total_height = params.top_margin + content_height + params.bottom_margin;
292
293 let list_marker = if params.checkbox.is_some() {
295 shape_checkbox_marker(registry, &metrics, params, scale_factor, font_scale)
296 } else if !params.list_marker.is_empty() {
297 shape_list_marker(registry, &metrics, params, scale_factor, font_scale)
298 } else {
299 None
300 };
301
302 BlockLayout {
303 block_id: params.block_id,
304 position: params.position,
305 lines,
306 y: 0.0, height: total_height,
308 top_margin: params.top_margin,
309 bottom_margin: params.bottom_margin,
310 left_margin: effective_left_margin,
311 right_margin: params.right_margin,
312 list_marker,
313 background_color: params.background_color,
314 }
315}
316
317#[derive(Clone, Debug, Default, PartialEq)]
326pub struct PaintSpan {
327 pub char_start: usize,
328 pub char_end: usize,
329 pub foreground_color: Option<[f32; 4]>,
330 pub underline_color: Option<[f32; 4]>,
331 pub background_color: Option<[f32; 4]>,
332 pub underline_style: Option<crate::types::UnderlineStyle>,
333 pub overline: Option<bool>,
334 pub strikeout: Option<bool>,
335}
336
337#[derive(Clone, Default, PartialEq)]
340struct PaintOverride {
341 foreground_color: Option<[f32; 4]>,
342 underline_color: Option<[f32; 4]>,
343 background_color: Option<[f32; 4]>,
344 underline_style: Option<crate::types::UnderlineStyle>,
345 overline: Option<bool>,
346 strikeout: Option<bool>,
347}
348
349impl PaintOverride {
350 fn is_noop(&self) -> bool {
351 *self == PaintOverride::default()
352 }
353
354 fn for_char(char_off: usize, spans: &[PaintSpan]) -> Self {
358 let mut o = PaintOverride::default();
359 for s in spans {
360 if s.char_start <= char_off && char_off < s.char_end {
361 if s.foreground_color.is_some() {
362 o.foreground_color = s.foreground_color;
363 }
364 if s.underline_color.is_some() {
365 o.underline_color = s.underline_color;
366 }
367 if s.background_color.is_some() {
368 o.background_color = s.background_color;
369 }
370 if s.underline_style.is_some() {
371 o.underline_style = s.underline_style;
372 }
373 if s.overline.is_some() {
374 o.overline = s.overline;
375 }
376 if s.strikeout.is_some() {
377 o.strikeout = s.strikeout;
378 }
379 }
380 }
381 o
382 }
383
384 fn apply(&self, run: &mut crate::layout::line::PositionedRun) {
389 if let Some(c) = self.foreground_color {
390 run.shaped_run.foreground_color = Some(c);
391 run.decorations.foreground_color = Some(c);
392 }
393 if let Some(c) = self.underline_color {
394 run.shaped_run.underline_color = Some(c);
395 run.decorations.underline_color = Some(c);
396 }
397 if let Some(c) = self.background_color {
398 run.shaped_run.background_color = Some(c);
399 run.decorations.background_color = Some(c);
400 }
401 if let Some(s) = self.underline_style {
402 run.shaped_run.underline_style = s;
403 run.decorations.underline_style = s;
404 }
405 if let Some(b) = self.overline {
406 run.shaped_run.overline = b;
407 run.decorations.overline = b;
408 }
409 if let Some(b) = self.strikeout {
410 run.shaped_run.strikeout = b;
411 run.decorations.strikeout = b;
412 }
413 }
414}
415
416pub fn apply_paint_spans(base: &BlockLayout, spans: &[PaintSpan]) -> BlockLayout {
428 let mut out = base.clone();
429 if spans.is_empty() {
430 return out;
431 }
432 for line in &mut out.lines {
433 let mut new_runs: Vec<crate::layout::line::PositionedRun> =
434 Vec::with_capacity(line.runs.len());
435 for run in line.runs.drain(..) {
436 recolor_run_into(run, spans, &mut new_runs);
437 }
438 line.runs = new_runs;
439 }
440 out
441}
442
443fn recolor_run_into(
447 run: crate::layout::line::PositionedRun,
448 spans: &[PaintSpan],
449 out: &mut Vec<crate::layout::line::PositionedRun>,
450) {
451 if run.shaped_run.glyphs.is_empty() || run.shaped_run.image_name.is_some() {
452 out.push(run);
453 return;
454 }
455
456 let overrides: Vec<PaintOverride> = run
459 .shaped_run
460 .glyphs
461 .iter()
462 .map(|g| PaintOverride::for_char(g.cluster as usize, spans))
463 .collect();
464
465 if overrides.iter().all(|o| *o == overrides[0]) {
469 let mut seg = run;
470 overrides[0].apply(&mut seg);
471 out.push(seg);
472 return;
473 }
474
475 let glyphs = run.shaped_run.glyphs.clone();
477 let mut seg_x = run.x;
478 let mut start = 0usize;
479 while start < glyphs.len() {
480 let ov = &overrides[start];
481 let mut end = start + 1;
482 while end < glyphs.len() && overrides[end] == *ov {
483 end += 1;
484 }
485 let seg_glyphs: Vec<crate::shaping::run::ShapedGlyph> = glyphs[start..end].to_vec();
486 let seg_advance: f32 = seg_glyphs.iter().map(|g| g.x_advance).sum();
487 let mut shaped = run.shaped_run.clone();
488 shaped.glyphs = seg_glyphs;
489 shaped.advance_width = seg_advance;
490 let mut seg = crate::layout::line::PositionedRun {
491 shaped_run: shaped,
492 x: seg_x,
493 decorations: run.decorations.clone(),
494 };
495 if !ov.is_noop() {
496 ov.apply(&mut seg);
497 }
498 out.push(seg);
499 seg_x += seg_advance;
500 start = end;
501 }
502}
503
504fn apply_spacing(run: &mut ShapedRun, text: &str, letter_spacing: f32, word_spacing: f32) {
506 let mut extra_advance = 0.0f32;
507 for glyph in &mut run.glyphs {
508 glyph.x_advance += letter_spacing;
509 extra_advance += letter_spacing;
510
511 if word_spacing != 0.0 {
514 let byte_offset = glyph.cluster as usize;
515 if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
516 && ch == ' '
517 {
518 glyph.x_advance += word_spacing;
519 extra_advance += word_spacing;
520 }
521 }
522 }
523 run.advance_width += extra_advance;
524}
525
526pub(crate) fn shape_hyphen(
529 registry: &FontRegistry,
530 scale_factor: f32,
531 font_scale: f32,
532) -> Option<ShapedGlyph> {
533 let resolved = resolve_font(
534 registry,
535 None,
536 None,
537 None,
538 None,
539 None,
540 scale_factor,
541 font_scale,
542 )?;
543 let run = shape_text(registry, &resolved, "-", 0)?;
544 run.glyphs.into_iter().next()
545}
546
547fn shape_list_marker(
549 registry: &FontRegistry,
550 _metrics: &FontMetricsPx,
551 params: &BlockLayoutParams,
552 scale_factor: f32,
553 font_scale: f32,
554) -> Option<ShapedListMarker> {
555 let resolved = resolve_font(
557 registry,
558 None,
559 None,
560 None,
561 None,
562 None,
563 scale_factor,
564 font_scale,
565 )?;
566 let run = shape_text(registry, &resolved, ¶ms.list_marker, 0)?;
567
568 let gap = 4.0; let marker_x = params.left_margin + params.list_indent - run.advance_width - gap;
571 let marker_x = marker_x.max(params.left_margin);
572
573 Some(ShapedListMarker { run, x: marker_x })
574}
575
576fn apply_tab_stops(run: &mut ShapedRun, text: &str, tab_positions: &[f32]) {
578 let default_tab = 48.0; let mut pen_x = 0.0f32;
580
581 for glyph in &mut run.glyphs {
582 let byte_offset = glyph.cluster as usize;
583 if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
584 && ch == '\t'
585 {
586 let next_stop = tab_positions
588 .iter()
589 .find(|&&stop| stop > pen_x + 1.0)
590 .copied()
591 .unwrap_or_else(|| {
592 let last = tab_positions.last().copied().unwrap_or(0.0);
594 let increment = if tab_positions.len() >= 2 {
595 tab_positions[1] - tab_positions[0]
596 } else {
597 default_tab
598 };
599 let mut stop = last + increment;
600 while stop <= pen_x + 1.0 {
601 stop += increment;
602 }
603 stop
604 });
605
606 let tab_advance = next_stop - pen_x;
607 let delta = tab_advance - glyph.x_advance;
608 glyph.x_advance = tab_advance;
609 run.advance_width += delta;
610 }
611 pen_x += glyph.x_advance;
612 }
613}
614
615fn shape_checkbox_marker(
617 registry: &FontRegistry,
618 _metrics: &FontMetricsPx,
619 params: &BlockLayoutParams,
620 scale_factor: f32,
621 font_scale: f32,
622) -> Option<ShapedListMarker> {
623 let checked = params.checkbox?;
624 let marker_text = if checked { "\u{2611}" } else { "\u{2610}" }; let resolved = resolve_font(
627 registry,
628 None,
629 None,
630 None,
631 None,
632 None,
633 scale_factor,
634 font_scale,
635 )?;
636 let run = shape_text(registry, &resolved, marker_text, 0)?;
637
638 let run = if run.glyphs.iter().any(|g| g.glyph_id == 0) {
640 let fallback_text = if checked { "[x]" } else { "[ ]" };
641 shape_text(registry, &resolved, fallback_text, 0)?
642 } else {
643 run
644 };
645
646 let gap = 4.0;
647 let marker_x = params.left_margin + params.list_indent - run.advance_width - gap;
648 let marker_x = marker_x.max(params.left_margin);
649
650 Some(ShapedListMarker { run, x: marker_x })
651}
652
653fn get_default_metrics(
654 registry: &FontRegistry,
655 scale_factor: f32,
656 font_scale: f32,
657) -> FontMetricsPx {
658 if let Some(default_id) = registry.default_font() {
659 let resolved = ResolvedFont {
660 font_face_id: default_id,
661 size_px: registry.default_size_px() * font_scale,
662 face_index: registry.get(default_id).map(|e| e.face_index).unwrap_or(0),
663 swash_cache_key: registry
664 .get(default_id)
665 .map(|e| e.swash_cache_key)
666 .unwrap_or_default(),
667 scale_factor,
668 weight: 400,
669 };
670 if let Some(m) = font_metrics_px(registry, &resolved) {
671 return m;
672 }
673 }
674 FontMetricsPx {
676 ascent: 14.0,
677 descent: 4.0,
678 leading: 0.0,
679 underline_offset: -2.0,
680 strikeout_offset: 5.0,
681 stroke_size: 1.0,
682 }
683}