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
8pub struct BlockLayout {
10 pub block_id: usize,
11 pub position: usize,
13 pub lines: Vec<LayoutLine>,
15 pub y: f32,
17 pub height: f32,
19 pub top_margin: f32,
20 pub bottom_margin: f32,
21 pub left_margin: f32,
22 pub right_margin: f32,
23 pub list_marker: Option<ShapedListMarker>,
26 pub background_color: Option<[f32; 4]>,
28}
29
30pub struct ShapedListMarker {
32 pub run: ShapedRun,
33 pub x: f32,
35}
36
37#[derive(Clone)]
40pub struct BlockLayoutParams {
41 pub block_id: usize,
42 pub position: usize,
43 pub text: String,
44 pub fragments: Vec<FragmentParams>,
45 pub alignment: Alignment,
46 pub top_margin: f32,
47 pub bottom_margin: f32,
48 pub left_margin: f32,
49 pub right_margin: f32,
50 pub text_indent: f32,
51 pub list_marker: String,
53 pub list_indent: f32,
55 pub tab_positions: Vec<f32>,
57 pub line_height_multiplier: Option<f32>,
60 pub non_breakable_lines: bool,
62 pub checkbox: Option<bool>,
64 pub background_color: Option<[f32; 4]>,
66}
67
68#[derive(Clone)]
70pub struct FragmentParams {
71 pub text: String,
72 pub offset: usize,
73 pub length: usize,
74 pub font_family: Option<String>,
75 pub font_weight: Option<u32>,
76 pub font_bold: Option<bool>,
77 pub font_italic: Option<bool>,
78 pub font_point_size: Option<u32>,
79 pub underline_style: crate::types::UnderlineStyle,
80 pub overline: bool,
81 pub strikeout: bool,
82 pub is_link: bool,
83 pub letter_spacing: f32,
85 pub word_spacing: f32,
87 pub foreground_color: Option<[f32; 4]>,
89 pub underline_color: Option<[f32; 4]>,
91 pub background_color: Option<[f32; 4]>,
93 pub anchor_href: Option<String>,
95 pub tooltip: Option<String>,
97 pub vertical_alignment: crate::types::VerticalAlignment,
99 pub image_name: Option<String>,
101 pub image_width: f32,
103 pub image_height: f32,
105}
106
107pub fn layout_block(
112 registry: &FontRegistry,
113 params: &BlockLayoutParams,
114 available_width: f32,
115 scale_factor: f32,
116) -> BlockLayout {
117 let effective_left_margin = params.left_margin + params.list_indent;
118 let content_width = (available_width - effective_left_margin - params.right_margin).max(0.0);
119
120 let mut shaped_runs = Vec::new();
122 let mut default_metrics: Option<FontMetricsPx> = None;
123
124 for frag in ¶ms.fragments {
125 if let Some(ref image_name) = frag.image_name {
127 use crate::shaping::run::{ShapedGlyph, ShapedRun};
128 let image_glyph = ShapedGlyph {
129 glyph_id: 0,
130 cluster: 0,
131 x_advance: frag.image_width,
132 y_advance: 0.0,
133 x_offset: 0.0,
134 y_offset: 0.0,
135 font_face_id: crate::types::FontFaceId(0),
136 };
137 let run = ShapedRun {
138 font_face_id: crate::types::FontFaceId(0),
139 size_px: 0.0,
140 weight: 400,
141 glyphs: vec![image_glyph],
142 advance_width: frag.image_width,
143 text_range: frag.offset..frag.offset + frag.text.len(),
144 underline_style: frag.underline_style,
145 overline: false,
146 strikeout: false,
147 is_link: frag.is_link,
148 foreground_color: None,
149 underline_color: None,
150 background_color: None,
151 anchor_href: frag.anchor_href.clone(),
152 tooltip: frag.tooltip.clone(),
153 vertical_alignment: crate::types::VerticalAlignment::Normal,
154 image_name: Some(image_name.clone()),
155 image_height: frag.image_height,
156 };
157 shaped_runs.push(run);
158 continue;
159 }
160
161 let font_point_size = match frag.vertical_alignment {
163 crate::types::VerticalAlignment::SuperScript
164 | crate::types::VerticalAlignment::SubScript => frag
165 .font_point_size
166 .map(|s| ((s as f32 * 0.65) as u32).max(1)),
167 crate::types::VerticalAlignment::Normal => frag.font_point_size,
168 };
169
170 let resolved = resolve_font(
171 registry,
172 frag.font_family.as_deref(),
173 frag.font_weight,
174 frag.font_bold,
175 frag.font_italic,
176 font_point_size,
177 scale_factor,
178 );
179
180 if let Some(resolved) = resolved {
181 if default_metrics.is_none() {
183 default_metrics = font_metrics_px(registry, &resolved);
184 }
185
186 if let Some(mut run) = shape_text(registry, &resolved, &frag.text, frag.offset) {
187 run.underline_style = frag.underline_style;
188 run.overline = frag.overline;
189 run.strikeout = frag.strikeout;
190 run.is_link = frag.is_link;
191 run.foreground_color = frag.foreground_color;
192 run.underline_color = frag.underline_color;
193 run.background_color = frag.background_color;
194 run.anchor_href = frag.anchor_href.clone();
195 run.tooltip = frag.tooltip.clone();
196 run.vertical_alignment = frag.vertical_alignment;
197
198 if frag.letter_spacing != 0.0 || frag.word_spacing != 0.0 {
200 apply_spacing(&mut run, &frag.text, frag.letter_spacing, frag.word_spacing);
201 }
202
203 if !params.tab_positions.is_empty() {
205 apply_tab_stops(&mut run, &frag.text, ¶ms.tab_positions);
206 }
207
208 shaped_runs.push(run);
209 }
210 }
211 }
212
213 let metrics = default_metrics.unwrap_or_else(|| get_default_metrics(registry, scale_factor));
215
216 let wrap_width = if params.non_breakable_lines {
218 f32::INFINITY
219 } else {
220 content_width
221 };
222
223 let mut lines = break_into_lines(
225 shaped_runs,
226 ¶ms.text,
227 wrap_width,
228 params.alignment,
229 params.text_indent,
230 &metrics,
231 );
232
233 let line_height_mul = params.line_height_multiplier.unwrap_or(1.0).max(0.1);
235
236 let mut y = 0.0f32;
238 for line in &mut lines {
239 if line_height_mul != 1.0 {
240 line.line_height *= line_height_mul;
241 }
242 line.y = y + line.ascent; y += line.line_height;
244 }
245
246 let content_height = y;
247 let total_height = params.top_margin + content_height + params.bottom_margin;
248
249 let list_marker = if params.checkbox.is_some() {
251 shape_checkbox_marker(registry, &metrics, params, scale_factor)
252 } else if !params.list_marker.is_empty() {
253 shape_list_marker(registry, &metrics, params, scale_factor)
254 } else {
255 None
256 };
257
258 BlockLayout {
259 block_id: params.block_id,
260 position: params.position,
261 lines,
262 y: 0.0, height: total_height,
264 top_margin: params.top_margin,
265 bottom_margin: params.bottom_margin,
266 left_margin: effective_left_margin,
267 right_margin: params.right_margin,
268 list_marker,
269 background_color: params.background_color,
270 }
271}
272
273fn apply_spacing(run: &mut ShapedRun, text: &str, letter_spacing: f32, word_spacing: f32) {
275 let mut extra_advance = 0.0f32;
276 for glyph in &mut run.glyphs {
277 glyph.x_advance += letter_spacing;
278 extra_advance += letter_spacing;
279
280 if word_spacing != 0.0 {
283 let byte_offset = glyph.cluster as usize;
284 if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
285 && ch == ' '
286 {
287 glyph.x_advance += word_spacing;
288 extra_advance += word_spacing;
289 }
290 }
291 }
292 run.advance_width += extra_advance;
293}
294
295fn shape_list_marker(
297 registry: &FontRegistry,
298 _metrics: &FontMetricsPx,
299 params: &BlockLayoutParams,
300 scale_factor: f32,
301) -> Option<ShapedListMarker> {
302 let resolved = resolve_font(registry, None, None, None, None, None, scale_factor)?;
304 let run = shape_text(registry, &resolved, ¶ms.list_marker, 0)?;
305
306 let gap = 4.0; let marker_x = params.left_margin + params.list_indent - run.advance_width - gap;
309 let marker_x = marker_x.max(params.left_margin);
310
311 Some(ShapedListMarker { run, x: marker_x })
312}
313
314fn apply_tab_stops(run: &mut ShapedRun, text: &str, tab_positions: &[f32]) {
316 let default_tab = 48.0; let mut pen_x = 0.0f32;
318
319 for glyph in &mut run.glyphs {
320 let byte_offset = glyph.cluster as usize;
321 if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
322 && ch == '\t'
323 {
324 let next_stop = tab_positions
326 .iter()
327 .find(|&&stop| stop > pen_x + 1.0)
328 .copied()
329 .unwrap_or_else(|| {
330 let last = tab_positions.last().copied().unwrap_or(0.0);
332 let increment = if tab_positions.len() >= 2 {
333 tab_positions[1] - tab_positions[0]
334 } else {
335 default_tab
336 };
337 let mut stop = last + increment;
338 while stop <= pen_x + 1.0 {
339 stop += increment;
340 }
341 stop
342 });
343
344 let tab_advance = next_stop - pen_x;
345 let delta = tab_advance - glyph.x_advance;
346 glyph.x_advance = tab_advance;
347 run.advance_width += delta;
348 }
349 pen_x += glyph.x_advance;
350 }
351}
352
353fn shape_checkbox_marker(
355 registry: &FontRegistry,
356 _metrics: &FontMetricsPx,
357 params: &BlockLayoutParams,
358 scale_factor: f32,
359) -> Option<ShapedListMarker> {
360 let checked = params.checkbox?;
361 let marker_text = if checked { "\u{2611}" } else { "\u{2610}" }; let resolved = resolve_font(registry, None, None, None, None, None, scale_factor)?;
364 let run = shape_text(registry, &resolved, marker_text, 0)?;
365
366 let run = if run.glyphs.iter().any(|g| g.glyph_id == 0) {
368 let fallback_text = if checked { "[x]" } else { "[ ]" };
369 shape_text(registry, &resolved, fallback_text, 0)?
370 } else {
371 run
372 };
373
374 let gap = 4.0;
375 let marker_x = params.left_margin + params.list_indent - run.advance_width - gap;
376 let marker_x = marker_x.max(params.left_margin);
377
378 Some(ShapedListMarker { run, x: marker_x })
379}
380
381fn get_default_metrics(registry: &FontRegistry, scale_factor: f32) -> FontMetricsPx {
382 if let Some(default_id) = registry.default_font() {
383 let resolved = ResolvedFont {
384 font_face_id: default_id,
385 size_px: registry.default_size_px(),
386 face_index: registry.get(default_id).map(|e| e.face_index).unwrap_or(0),
387 swash_cache_key: registry
388 .get(default_id)
389 .map(|e| e.swash_cache_key)
390 .unwrap_or_default(),
391 scale_factor,
392 weight: 400,
393 };
394 if let Some(m) = font_metrics_px(registry, &resolved) {
395 return m;
396 }
397 }
398 FontMetricsPx {
400 ascent: 14.0,
401 descent: 4.0,
402 leading: 0.0,
403 underline_offset: -2.0,
404 strikeout_offset: 5.0,
405 stroke_size: 1.0,
406 }
407}