1use crate::font::registry::FontRegistry;
2use crate::font::resolve::{ResolvedFont, resolve_font};
3use crate::layout::line::LayoutLine;
4use crate::layout::paragraph::{Alignment, break_into_lines};
5use crate::shaping::run::ShapedRun;
6use crate::shaping::shaper::{FontMetricsPx, font_metrics_px, shape_text};
7
8#[derive(Clone)]
10pub struct BlockLayout {
11 pub block_id: usize,
12 pub position: usize,
14 pub lines: Vec<LayoutLine>,
16 pub y: f32,
18 pub height: f32,
20 pub top_margin: f32,
21 pub bottom_margin: f32,
22 pub left_margin: f32,
23 pub right_margin: f32,
24 pub list_marker: Option<ShapedListMarker>,
27 pub background_color: Option<[f32; 4]>,
29}
30
31#[derive(Clone)]
33pub struct ShapedListMarker {
34 pub run: ShapedRun,
35 pub x: f32,
37}
38
39#[derive(Clone)]
42pub struct BlockLayoutParams {
43 pub block_id: usize,
44 pub position: usize,
45 pub text: String,
46 pub fragments: Vec<FragmentParams>,
47 pub alignment: Alignment,
48 pub top_margin: f32,
49 pub bottom_margin: f32,
50 pub left_margin: f32,
51 pub right_margin: f32,
52 pub text_indent: f32,
53 pub list_marker: String,
55 pub list_indent: f32,
57 pub tab_positions: Vec<f32>,
59 pub line_height_multiplier: Option<f32>,
62 pub non_breakable_lines: bool,
64 pub checkbox: Option<bool>,
66 pub background_color: Option<[f32; 4]>,
68}
69
70#[derive(Clone)]
72pub struct FragmentParams {
73 pub text: String,
74 pub offset: usize,
84 pub length: usize,
85 pub font_family: Option<String>,
86 pub font_weight: Option<u32>,
87 pub font_bold: Option<bool>,
88 pub font_italic: Option<bool>,
89 pub font_point_size: Option<u32>,
90 pub underline_style: crate::types::UnderlineStyle,
91 pub overline: bool,
92 pub strikeout: bool,
93 pub is_link: bool,
94 pub letter_spacing: f32,
96 pub word_spacing: f32,
98 pub foreground_color: Option<[f32; 4]>,
100 pub underline_color: Option<[f32; 4]>,
102 pub background_color: Option<[f32; 4]>,
104 pub anchor_href: Option<String>,
106 pub tooltip: Option<String>,
108 pub vertical_alignment: crate::types::VerticalAlignment,
110 pub image_name: Option<String>,
112 pub image_width: f32,
114 pub image_height: f32,
116}
117
118pub fn layout_block(
123 registry: &FontRegistry,
124 params: &BlockLayoutParams,
125 available_width: f32,
126 scale_factor: f32,
127) -> BlockLayout {
128 let effective_left_margin = params.left_margin + params.list_indent;
129 let content_width = (available_width - effective_left_margin - params.right_margin).max(0.0);
130
131 let mut shaped_runs = Vec::new();
133 let mut default_metrics: Option<FontMetricsPx> = None;
134
135 for frag in ¶ms.fragments {
136 if let Some(ref image_name) = frag.image_name {
138 use crate::shaping::run::{ShapedGlyph, ShapedRun};
139 let image_glyph = ShapedGlyph {
140 glyph_id: 0,
141 cluster: 0,
142 x_advance: frag.image_width,
143 y_advance: 0.0,
144 x_offset: 0.0,
145 y_offset: 0.0,
146 font_face_id: crate::types::FontFaceId(0),
147 };
148 let run = ShapedRun {
149 font_face_id: crate::types::FontFaceId(0),
150 size_px: 0.0,
151 weight: 400,
152 glyphs: vec![image_glyph],
153 advance_width: frag.image_width,
154 text_range: frag.offset..frag.offset + frag.text.len(),
155 underline_style: frag.underline_style,
156 overline: false,
157 strikeout: false,
158 is_link: frag.is_link,
159 foreground_color: None,
160 underline_color: None,
161 background_color: None,
162 anchor_href: frag.anchor_href.clone(),
163 tooltip: frag.tooltip.clone(),
164 vertical_alignment: crate::types::VerticalAlignment::Normal,
165 image_name: Some(image_name.clone()),
166 image_height: frag.image_height,
167 };
168 shaped_runs.push(run);
169 continue;
170 }
171
172 let font_point_size = match frag.vertical_alignment {
174 crate::types::VerticalAlignment::SuperScript
175 | crate::types::VerticalAlignment::SubScript => frag
176 .font_point_size
177 .map(|s| ((s as f32 * 0.65) as u32).max(1)),
178 crate::types::VerticalAlignment::Normal => frag.font_point_size,
179 };
180
181 let resolved = resolve_font(
182 registry,
183 frag.font_family.as_deref(),
184 frag.font_weight,
185 frag.font_bold,
186 frag.font_italic,
187 font_point_size,
188 scale_factor,
189 );
190
191 if let Some(resolved) = resolved {
192 if default_metrics.is_none() {
194 default_metrics = font_metrics_px(registry, &resolved);
195 }
196
197 if let Some(mut run) = shape_text(registry, &resolved, &frag.text, frag.offset) {
198 run.underline_style = frag.underline_style;
199 run.overline = frag.overline;
200 run.strikeout = frag.strikeout;
201 run.is_link = frag.is_link;
202 run.foreground_color = frag.foreground_color;
203 run.underline_color = frag.underline_color;
204 run.background_color = frag.background_color;
205 run.anchor_href = frag.anchor_href.clone();
206 run.tooltip = frag.tooltip.clone();
207 run.vertical_alignment = frag.vertical_alignment;
208
209 if frag.letter_spacing != 0.0 || frag.word_spacing != 0.0 {
211 apply_spacing(&mut run, &frag.text, frag.letter_spacing, frag.word_spacing);
212 }
213
214 if !params.tab_positions.is_empty() {
216 apply_tab_stops(&mut run, &frag.text, ¶ms.tab_positions);
217 }
218
219 shaped_runs.push(run);
220 }
221 }
222 }
223
224 let metrics = default_metrics.unwrap_or_else(|| get_default_metrics(registry, scale_factor));
226
227 let wrap_width = if params.non_breakable_lines {
229 f32::INFINITY
230 } else {
231 content_width
232 };
233
234 let mut lines = break_into_lines(
236 shaped_runs,
237 ¶ms.text,
238 wrap_width,
239 params.alignment,
240 params.text_indent,
241 &metrics,
242 );
243
244 let line_height_mul = params.line_height_multiplier.unwrap_or(1.0).max(0.1);
246
247 let mut y = 0.0f32;
249 for line in &mut lines {
250 if line_height_mul != 1.0 {
251 line.line_height *= line_height_mul;
252 }
253 line.y = y + line.ascent; y += line.line_height;
255 }
256
257 let content_height = y;
258 let total_height = params.top_margin + content_height + params.bottom_margin;
259
260 let list_marker = if params.checkbox.is_some() {
262 shape_checkbox_marker(registry, &metrics, params, scale_factor)
263 } else if !params.list_marker.is_empty() {
264 shape_list_marker(registry, &metrics, params, scale_factor)
265 } else {
266 None
267 };
268
269 BlockLayout {
270 block_id: params.block_id,
271 position: params.position,
272 lines,
273 y: 0.0, height: total_height,
275 top_margin: params.top_margin,
276 bottom_margin: params.bottom_margin,
277 left_margin: effective_left_margin,
278 right_margin: params.right_margin,
279 list_marker,
280 background_color: params.background_color,
281 }
282}
283
284#[derive(Clone, Debug, Default, PartialEq)]
293pub struct PaintSpan {
294 pub char_start: usize,
295 pub char_end: usize,
296 pub foreground_color: Option<[f32; 4]>,
297 pub underline_color: Option<[f32; 4]>,
298 pub background_color: Option<[f32; 4]>,
299 pub underline_style: Option<crate::types::UnderlineStyle>,
300 pub overline: Option<bool>,
301 pub strikeout: Option<bool>,
302}
303
304#[derive(Clone, Default, PartialEq)]
307struct PaintOverride {
308 foreground_color: Option<[f32; 4]>,
309 underline_color: Option<[f32; 4]>,
310 background_color: Option<[f32; 4]>,
311 underline_style: Option<crate::types::UnderlineStyle>,
312 overline: Option<bool>,
313 strikeout: Option<bool>,
314}
315
316impl PaintOverride {
317 fn is_noop(&self) -> bool {
318 *self == PaintOverride::default()
319 }
320
321 fn for_char(char_off: usize, spans: &[PaintSpan]) -> Self {
325 let mut o = PaintOverride::default();
326 for s in spans {
327 if s.char_start <= char_off && char_off < s.char_end {
328 if s.foreground_color.is_some() {
329 o.foreground_color = s.foreground_color;
330 }
331 if s.underline_color.is_some() {
332 o.underline_color = s.underline_color;
333 }
334 if s.background_color.is_some() {
335 o.background_color = s.background_color;
336 }
337 if s.underline_style.is_some() {
338 o.underline_style = s.underline_style;
339 }
340 if s.overline.is_some() {
341 o.overline = s.overline;
342 }
343 if s.strikeout.is_some() {
344 o.strikeout = s.strikeout;
345 }
346 }
347 }
348 o
349 }
350
351 fn apply(&self, run: &mut crate::layout::line::PositionedRun) {
356 if let Some(c) = self.foreground_color {
357 run.shaped_run.foreground_color = Some(c);
358 run.decorations.foreground_color = Some(c);
359 }
360 if let Some(c) = self.underline_color {
361 run.shaped_run.underline_color = Some(c);
362 run.decorations.underline_color = Some(c);
363 }
364 if let Some(c) = self.background_color {
365 run.shaped_run.background_color = Some(c);
366 run.decorations.background_color = Some(c);
367 }
368 if let Some(s) = self.underline_style {
369 run.shaped_run.underline_style = s;
370 run.decorations.underline_style = s;
371 }
372 if let Some(b) = self.overline {
373 run.shaped_run.overline = b;
374 run.decorations.overline = b;
375 }
376 if let Some(b) = self.strikeout {
377 run.shaped_run.strikeout = b;
378 run.decorations.strikeout = b;
379 }
380 }
381}
382
383pub fn apply_paint_spans(base: &BlockLayout, spans: &[PaintSpan]) -> BlockLayout {
395 let mut out = base.clone();
396 if spans.is_empty() {
397 return out;
398 }
399 for line in &mut out.lines {
400 let mut new_runs: Vec<crate::layout::line::PositionedRun> =
401 Vec::with_capacity(line.runs.len());
402 for run in line.runs.drain(..) {
403 recolor_run_into(run, spans, &mut new_runs);
404 }
405 line.runs = new_runs;
406 }
407 out
408}
409
410fn recolor_run_into(
414 run: crate::layout::line::PositionedRun,
415 spans: &[PaintSpan],
416 out: &mut Vec<crate::layout::line::PositionedRun>,
417) {
418 if run.shaped_run.glyphs.is_empty() || run.shaped_run.image_name.is_some() {
419 out.push(run);
420 return;
421 }
422
423 let overrides: Vec<PaintOverride> = run
426 .shaped_run
427 .glyphs
428 .iter()
429 .map(|g| PaintOverride::for_char(g.cluster as usize, spans))
430 .collect();
431
432 if overrides.iter().all(|o| *o == overrides[0]) {
436 let mut seg = run;
437 overrides[0].apply(&mut seg);
438 out.push(seg);
439 return;
440 }
441
442 let glyphs = run.shaped_run.glyphs.clone();
444 let mut seg_x = run.x;
445 let mut start = 0usize;
446 while start < glyphs.len() {
447 let ov = &overrides[start];
448 let mut end = start + 1;
449 while end < glyphs.len() && overrides[end] == *ov {
450 end += 1;
451 }
452 let seg_glyphs: Vec<crate::shaping::run::ShapedGlyph> = glyphs[start..end].to_vec();
453 let seg_advance: f32 = seg_glyphs.iter().map(|g| g.x_advance).sum();
454 let mut shaped = run.shaped_run.clone();
455 shaped.glyphs = seg_glyphs;
456 shaped.advance_width = seg_advance;
457 let mut seg = crate::layout::line::PositionedRun {
458 shaped_run: shaped,
459 x: seg_x,
460 decorations: run.decorations.clone(),
461 };
462 if !ov.is_noop() {
463 ov.apply(&mut seg);
464 }
465 out.push(seg);
466 seg_x += seg_advance;
467 start = end;
468 }
469}
470
471fn apply_spacing(run: &mut ShapedRun, text: &str, letter_spacing: f32, word_spacing: f32) {
473 let mut extra_advance = 0.0f32;
474 for glyph in &mut run.glyphs {
475 glyph.x_advance += letter_spacing;
476 extra_advance += letter_spacing;
477
478 if word_spacing != 0.0 {
481 let byte_offset = glyph.cluster as usize;
482 if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
483 && ch == ' '
484 {
485 glyph.x_advance += word_spacing;
486 extra_advance += word_spacing;
487 }
488 }
489 }
490 run.advance_width += extra_advance;
491}
492
493fn shape_list_marker(
495 registry: &FontRegistry,
496 _metrics: &FontMetricsPx,
497 params: &BlockLayoutParams,
498 scale_factor: f32,
499) -> Option<ShapedListMarker> {
500 let resolved = resolve_font(registry, None, None, None, None, None, scale_factor)?;
502 let run = shape_text(registry, &resolved, ¶ms.list_marker, 0)?;
503
504 let gap = 4.0; let marker_x = params.left_margin + params.list_indent - run.advance_width - gap;
507 let marker_x = marker_x.max(params.left_margin);
508
509 Some(ShapedListMarker { run, x: marker_x })
510}
511
512fn apply_tab_stops(run: &mut ShapedRun, text: &str, tab_positions: &[f32]) {
514 let default_tab = 48.0; let mut pen_x = 0.0f32;
516
517 for glyph in &mut run.glyphs {
518 let byte_offset = glyph.cluster as usize;
519 if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
520 && ch == '\t'
521 {
522 let next_stop = tab_positions
524 .iter()
525 .find(|&&stop| stop > pen_x + 1.0)
526 .copied()
527 .unwrap_or_else(|| {
528 let last = tab_positions.last().copied().unwrap_or(0.0);
530 let increment = if tab_positions.len() >= 2 {
531 tab_positions[1] - tab_positions[0]
532 } else {
533 default_tab
534 };
535 let mut stop = last + increment;
536 while stop <= pen_x + 1.0 {
537 stop += increment;
538 }
539 stop
540 });
541
542 let tab_advance = next_stop - pen_x;
543 let delta = tab_advance - glyph.x_advance;
544 glyph.x_advance = tab_advance;
545 run.advance_width += delta;
546 }
547 pen_x += glyph.x_advance;
548 }
549}
550
551fn shape_checkbox_marker(
553 registry: &FontRegistry,
554 _metrics: &FontMetricsPx,
555 params: &BlockLayoutParams,
556 scale_factor: f32,
557) -> Option<ShapedListMarker> {
558 let checked = params.checkbox?;
559 let marker_text = if checked { "\u{2611}" } else { "\u{2610}" }; let resolved = resolve_font(registry, None, None, None, None, None, scale_factor)?;
562 let run = shape_text(registry, &resolved, marker_text, 0)?;
563
564 let run = if run.glyphs.iter().any(|g| g.glyph_id == 0) {
566 let fallback_text = if checked { "[x]" } else { "[ ]" };
567 shape_text(registry, &resolved, fallback_text, 0)?
568 } else {
569 run
570 };
571
572 let gap = 4.0;
573 let marker_x = params.left_margin + params.list_indent - run.advance_width - gap;
574 let marker_x = marker_x.max(params.left_margin);
575
576 Some(ShapedListMarker { run, x: marker_x })
577}
578
579fn get_default_metrics(registry: &FontRegistry, scale_factor: f32) -> FontMetricsPx {
580 if let Some(default_id) = registry.default_font() {
581 let resolved = ResolvedFont {
582 font_face_id: default_id,
583 size_px: registry.default_size_px(),
584 face_index: registry.get(default_id).map(|e| e.face_index).unwrap_or(0),
585 swash_cache_key: registry
586 .get(default_id)
587 .map(|e| e.swash_cache_key)
588 .unwrap_or_default(),
589 scale_factor,
590 weight: 400,
591 };
592 if let Some(m) = font_metrics_px(registry, &resolved) {
593 return m;
594 }
595 }
596 FontMetricsPx {
598 ascent: 14.0,
599 descent: 4.0,
600 leading: 0.0,
601 underline_offset: -2.0,
602 strikeout_offset: 5.0,
603 stroke_size: 1.0,
604 }
605}