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: bool,
80 pub overline: bool,
81 pub strikeout: bool,
82 pub is_link: bool,
83 pub letter_spacing: f32,
85 pub word_spacing: f32,
87}
88
89pub fn layout_block(
91 registry: &FontRegistry,
92 params: &BlockLayoutParams,
93 available_width: f32,
94) -> BlockLayout {
95 let effective_left_margin = params.left_margin + params.list_indent;
96 let content_width = (available_width - effective_left_margin - params.right_margin).max(0.0);
97
98 let mut shaped_runs = Vec::new();
100 let mut default_metrics: Option<FontMetricsPx> = None;
101
102 for frag in ¶ms.fragments {
103 let resolved = resolve_font(
104 registry,
105 frag.font_family.as_deref(),
106 frag.font_weight,
107 frag.font_bold,
108 frag.font_italic,
109 frag.font_point_size,
110 );
111
112 if let Some(resolved) = resolved {
113 if default_metrics.is_none() {
115 default_metrics = font_metrics_px(registry, &resolved);
116 }
117
118 if let Some(mut run) = shape_text(registry, &resolved, &frag.text, frag.offset) {
119 run.underline = frag.underline;
120 run.overline = frag.overline;
121 run.strikeout = frag.strikeout;
122 run.is_link = frag.is_link;
123
124 if frag.letter_spacing != 0.0 || frag.word_spacing != 0.0 {
126 apply_spacing(&mut run, &frag.text, frag.letter_spacing, frag.word_spacing);
127 }
128
129 if !params.tab_positions.is_empty() {
131 apply_tab_stops(&mut run, &frag.text, ¶ms.tab_positions);
132 }
133
134 shaped_runs.push(run);
135 }
136 }
137 }
138
139 let metrics = default_metrics.unwrap_or_else(|| get_default_metrics(registry));
141
142 let wrap_width = if params.non_breakable_lines {
144 f32::INFINITY
145 } else {
146 content_width
147 };
148
149 let mut lines = break_into_lines(
151 shaped_runs,
152 ¶ms.text,
153 wrap_width,
154 params.alignment,
155 params.text_indent,
156 &metrics,
157 );
158
159 let line_height_mul = params.line_height_multiplier.unwrap_or(1.0).max(0.1);
161
162 let mut y = 0.0f32;
164 for line in &mut lines {
165 if line_height_mul != 1.0 {
166 line.line_height *= line_height_mul;
167 }
168 line.y = y + line.ascent; y += line.line_height;
170 }
171
172 let content_height = y;
173 let total_height = params.top_margin + content_height + params.bottom_margin;
174
175 let list_marker = if params.checkbox.is_some() {
177 shape_checkbox_marker(registry, &metrics, params)
178 } else if !params.list_marker.is_empty() {
179 shape_list_marker(registry, &metrics, params)
180 } else {
181 None
182 };
183
184 BlockLayout {
185 block_id: params.block_id,
186 position: params.position,
187 lines,
188 y: 0.0, height: total_height,
190 top_margin: params.top_margin,
191 bottom_margin: params.bottom_margin,
192 left_margin: effective_left_margin,
193 right_margin: params.right_margin,
194 list_marker,
195 background_color: params.background_color,
196 }
197}
198
199fn apply_spacing(run: &mut ShapedRun, text: &str, letter_spacing: f32, word_spacing: f32) {
201 let mut extra_advance = 0.0f32;
202 for glyph in &mut run.glyphs {
203 glyph.x_advance += letter_spacing;
204 extra_advance += letter_spacing;
205
206 if word_spacing != 0.0 {
209 let byte_offset = glyph.cluster as usize;
210 if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
211 && ch == ' '
212 {
213 glyph.x_advance += word_spacing;
214 extra_advance += word_spacing;
215 }
216 }
217 }
218 run.advance_width += extra_advance;
219}
220
221fn shape_list_marker(
223 registry: &FontRegistry,
224 _metrics: &FontMetricsPx,
225 params: &BlockLayoutParams,
226) -> Option<ShapedListMarker> {
227 let resolved = resolve_font(registry, None, None, None, None, None)?;
229 let run = shape_text(registry, &resolved, ¶ms.list_marker, 0)?;
230
231 let gap = 4.0; let marker_x = params.left_margin + params.list_indent - run.advance_width - gap;
234 let marker_x = marker_x.max(params.left_margin);
235
236 Some(ShapedListMarker { run, x: marker_x })
237}
238
239fn apply_tab_stops(run: &mut ShapedRun, text: &str, tab_positions: &[f32]) {
241 let default_tab = 48.0; let mut pen_x = 0.0f32;
243
244 for glyph in &mut run.glyphs {
245 let byte_offset = glyph.cluster as usize;
246 if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
247 && ch == '\t'
248 {
249 let next_stop = tab_positions
251 .iter()
252 .find(|&&stop| stop > pen_x + 1.0)
253 .copied()
254 .unwrap_or_else(|| {
255 let last = tab_positions.last().copied().unwrap_or(0.0);
257 let increment = if tab_positions.len() >= 2 {
258 tab_positions[1] - tab_positions[0]
259 } else {
260 default_tab
261 };
262 let mut stop = last + increment;
263 while stop <= pen_x + 1.0 {
264 stop += increment;
265 }
266 stop
267 });
268
269 let tab_advance = next_stop - pen_x;
270 let delta = tab_advance - glyph.x_advance;
271 glyph.x_advance = tab_advance;
272 run.advance_width += delta;
273 }
274 pen_x += glyph.x_advance;
275 }
276}
277
278fn shape_checkbox_marker(
280 registry: &FontRegistry,
281 _metrics: &FontMetricsPx,
282 params: &BlockLayoutParams,
283) -> Option<ShapedListMarker> {
284 let checked = params.checkbox?;
285 let marker_text = if checked { "\u{2611}" } else { "\u{2610}" }; let resolved = resolve_font(registry, None, None, None, None, None)?;
288 let run = shape_text(registry, &resolved, marker_text, 0)?;
289
290 let run = if run.glyphs.iter().any(|g| g.glyph_id == 0) {
292 let fallback_text = if checked { "[x]" } else { "[ ]" };
293 shape_text(registry, &resolved, fallback_text, 0)?
294 } else {
295 run
296 };
297
298 let gap = 4.0;
299 let marker_x = params.left_margin + params.list_indent - run.advance_width - gap;
300 let marker_x = marker_x.max(params.left_margin);
301
302 Some(ShapedListMarker { run, x: marker_x })
303}
304
305fn get_default_metrics(registry: &FontRegistry) -> FontMetricsPx {
306 if let Some(default_id) = registry.default_font() {
307 let resolved = ResolvedFont {
308 font_face_id: default_id,
309 size_px: registry.default_size_px(),
310 face_index: registry.get(default_id).map(|e| e.face_index).unwrap_or(0),
311 swash_cache_key: registry
312 .get(default_id)
313 .map(|e| e.swash_cache_key)
314 .unwrap_or_default(),
315 };
316 if let Some(m) = font_metrics_px(registry, &resolved) {
317 return m;
318 }
319 }
320 FontMetricsPx {
322 ascent: 14.0,
323 descent: 4.0,
324 leading: 0.0,
325 underline_offset: -2.0,
326 strikeout_offset: 5.0,
327 stroke_size: 1.0,
328 }
329}