agg_gui/widgets/rich_text/
layout.rs1use std::sync::Arc;
26
27use super::model::{Block, InlineStyle, ListKind, RichDoc};
28use crate::text::{measure_advance, Font};
29use crate::widgets::text_area::TextHAlign;
30
31pub const INDENT_PX: f64 = 24.0;
33pub const LIST_GUTTER_PX: f64 = 24.0;
35pub const MARKER_GAP_PX: f64 = 6.0;
37pub const LINE_SPACING: f64 = 1.35;
39
40pub type FontResolver<'a> = dyn Fn(&InlineStyle) -> Arc<Font> + 'a;
42
43#[derive(Clone)]
47pub struct LineFragment {
48 pub text: String,
50 pub style: InlineStyle,
52 pub font: Arc<Font>,
54 pub font_size: f64,
56 pub x: f64,
58 pub width: f64,
60 pub ascent: f64,
62 pub descent: f64,
64 pub start_byte: usize,
71}
72
73#[derive(Clone)]
75pub struct LineLayout {
76 pub fragments: Vec<LineFragment>,
78 pub width: f64,
80 pub height: f64,
82 pub ascent: f64,
84 pub descent: f64,
86 pub align_dx: f64,
88 pub baseline_from_top: f64,
90 pub start_byte: usize,
96 pub end_byte: usize,
97}
98
99#[derive(Clone)]
101pub struct BlockLayout {
102 pub lines: Vec<LineLayout>,
104 pub text_left: f64,
106 pub marker: Option<String>,
108 pub marker_font: Option<Arc<Font>>,
110 pub marker_font_size: f64,
112 pub marker_x: f64,
114 pub height: f64,
116 pub width: f64,
118}
119
120#[derive(Clone)]
122pub struct DocLayout {
123 pub blocks: Vec<BlockLayout>,
125 pub width: f64,
127 pub height: f64,
129}
130
131fn run_size(style: &InlineStyle, default_size: f64) -> f64 {
133 style.font_size.unwrap_or(default_size)
134}
135
136pub fn layout_doc(
139 doc: &RichDoc,
140 width: f64,
141 default_font_size: f64,
142 resolver: &FontResolver,
143) -> DocLayout {
144 let numbers = ordered_numbers(&doc.blocks);
145 let mut blocks = Vec::with_capacity(doc.blocks.len());
146 let mut y = 0.0f64;
147 for (bi, block) in doc.blocks.iter().enumerate() {
148 let bl = layout_block(block, numbers[bi], width, default_font_size, resolver);
149 y += bl.height;
150 blocks.push(bl);
151 }
152 let max_w = blocks.iter().map(|b| b.width).fold(0.0f64, f64::max);
153 DocLayout {
154 blocks,
155 width: width.max(max_w),
156 height: y,
157 }
158}
159
160fn ordered_numbers(blocks: &[Block]) -> Vec<usize> {
165 let mut out = vec![0usize; blocks.len()];
166 let mut counters = [0usize; (MAX_LEVELS)];
167 let mut active = [false; MAX_LEVELS];
168 for (i, block) in blocks.iter().enumerate() {
169 let d = (block.indent as usize).min(MAX_LEVELS - 1);
170 match block.list {
171 ListKind::Ordered => {
172 counters[d] = if active[d] { counters[d] + 1 } else { 1 };
173 out[i] = counters[d];
174 for (k, a) in active.iter_mut().enumerate() {
176 *a = k == d;
177 }
178 }
179 _ => {
181 for a in active.iter_mut() {
182 *a = false;
183 }
184 }
185 }
186 }
187 out
188}
189
190const MAX_LEVELS: usize = 16;
191
192fn layout_block(
195 block: &Block,
196 ordinal: usize,
197 width: f64,
198 default_font_size: f64,
199 resolver: &FontResolver,
200) -> BlockLayout {
201 let base_indent = block.indent as f64 * INDENT_PX;
202 let is_list = block.list != ListKind::None;
203 let text_left = base_indent + if is_list { LIST_GUTTER_PX } else { 0.0 };
204 let avail = (width - text_left).max(1.0);
205
206 let lead_style = block
208 .runs
209 .first()
210 .map(|r| r.style.clone())
211 .unwrap_or_default();
212 let marker_font = resolver(&lead_style);
213 let marker_font_size = run_size(&lead_style, default_font_size);
214 let marker = match block.list {
215 ListKind::None => None,
216 ListKind::Bullet => Some("\u{2022}".to_string()),
217 ListKind::Ordered => Some(format!("{ordinal}.")),
218 };
219 let marker_x = if let Some(m) = &marker {
220 let mw = measure_advance(&marker_font, m, marker_font_size);
221 (text_left - MARKER_GAP_PX - mw).max(base_indent)
222 } else {
223 base_indent
224 };
225
226 let pieces = tokenize(block, default_font_size, resolver);
227 let mut lines = wrap(pieces, avail);
228 if lines.is_empty() {
229 let font = resolver(&lead_style);
231 let size = run_size(&lead_style, default_font_size);
232 lines.push(LineLayout {
233 fragments: Vec::new(),
234 width: 0.0,
235 height: (font.ascender_px(size) + font.descender_px(size)) * LINE_SPACING,
236 ascent: font.ascender_px(size),
237 descent: font.descender_px(size),
238 align_dx: 0.0,
239 baseline_from_top: 0.0,
240 start_byte: 0,
241 end_byte: 0,
242 });
243 }
244
245 let mut height = 0.0f64;
247 let mut max_line_w = 0.0f64;
248 for line in &mut lines {
249 finalize_line(line, avail, block.align);
250 height += line.height;
251 max_line_w = max_line_w.max(line.width);
252 }
253
254 BlockLayout {
255 lines,
256 text_left,
257 marker,
258 marker_font: Some(marker_font),
259 marker_font_size,
260 marker_x,
261 height,
262 width: text_left + max_line_w,
263 }
264}
265
266struct Piece {
269 text: String,
270 style: InlineStyle,
271 font: Arc<Font>,
272 font_size: f64,
273 width: f64,
274 ascent: f64,
275 descent: f64,
276 is_ws: bool,
277 start_byte: usize,
279}
280
281fn tokenize(block: &Block, default_font_size: f64, resolver: &FontResolver) -> Vec<Piece> {
285 let mut pieces = Vec::new();
286 let mut run_start = 0usize;
288 for run in &block.runs {
289 if run.text.is_empty() {
290 continue;
291 }
292 let font = resolver(&run.style);
293 let size = run_size(&run.style, default_font_size);
294 let ascent = font.ascender_px(size);
295 let descent = font.descender_px(size);
296 let mut chunk_off = 0usize;
299 for chunk in split_ws(&run.text) {
300 let is_ws = chunk.chars().next().map(|c| c.is_whitespace()).unwrap_or(false);
301 let width = measure_advance(&font, chunk, size);
302 pieces.push(Piece {
303 text: chunk.to_string(),
304 style: run.style.clone(),
305 font: Arc::clone(&font),
306 font_size: size,
307 width,
308 ascent,
309 descent,
310 is_ws,
311 start_byte: run_start + chunk_off,
312 });
313 chunk_off += chunk.len();
314 }
315 run_start += run.text.len();
316 }
317 pieces
318}
319
320fn split_ws(s: &str) -> Vec<&str> {
322 let mut out = Vec::new();
323 let mut start = 0usize;
324 let mut prev_ws: Option<bool> = None;
325 for (i, c) in s.char_indices() {
326 let ws = c.is_whitespace();
327 match prev_ws {
328 Some(p) if p != ws => {
329 out.push(&s[start..i]);
330 start = i;
331 }
332 _ => {}
333 }
334 prev_ws = Some(ws);
335 }
336 if start < s.len() {
337 out.push(&s[start..]);
338 }
339 out
340}
341
342fn wrap(pieces: Vec<Piece>, avail: f64) -> Vec<LineLayout> {
347 let mut lines: Vec<LineLayout> = Vec::new();
348 let mut cur: Vec<Piece> = Vec::new();
349 let mut cur_w = 0.0f64;
350 let mut pending_space: Option<Piece> = None;
351
352 let mut iter = pieces.into_iter().peekable();
354 while let Some(piece) = iter.next() {
355 if piece.is_ws {
356 if !cur.is_empty() {
357 pending_space = Some(piece);
358 }
359 continue;
360 }
361 let mut word = vec![piece];
363 while let Some(next) = iter.peek() {
364 if next.is_ws {
365 break;
366 }
367 word.push(iter.next().unwrap());
368 }
369 let word_w: f64 = word.iter().map(|p| p.width).sum();
370 let space_w = pending_space.as_ref().map(|p| p.width).unwrap_or(0.0);
371
372 if !cur.is_empty() && cur_w + space_w + word_w > avail {
373 lines.push(finish_pieces(std::mem::take(&mut cur)));
374 cur_w = 0.0;
375 pending_space = None;
376 } else if let Some(sp) = pending_space.take() {
377 cur_w += sp.width;
378 cur.push(sp);
379 }
380 cur_w += word_w;
381 cur.extend(word);
382 }
383 if !cur.is_empty() {
384 lines.push(finish_pieces(cur));
385 }
386 lines
387}
388
389fn finish_pieces(pieces: Vec<Piece>) -> LineLayout {
392 let mut fragments: Vec<LineFragment> = Vec::new();
393 let mut x = 0.0f64;
394 let mut ascent = 0.0f64;
395 let mut descent = 0.0f64;
396 for p in pieces {
397 ascent = ascent.max(p.ascent);
398 descent = descent.max(p.descent);
399 if let Some(last) = fragments.last_mut() {
400 if last.style == p.style
401 && Arc::ptr_eq(&last.font, &p.font)
402 && (last.font_size - p.font_size).abs() < 1e-9
403 {
404 last.text.push_str(&p.text);
405 last.width += p.width;
406 x += p.width;
407 continue;
408 }
409 }
410 fragments.push(LineFragment {
411 text: p.text,
412 style: p.style,
413 font: p.font,
414 font_size: p.font_size,
415 x,
416 width: p.width,
417 ascent: p.ascent,
418 descent: p.descent,
419 start_byte: p.start_byte,
420 });
421 x += p.width;
422 }
423 let (start_byte, end_byte) = match (fragments.first(), fragments.last()) {
426 (Some(first), Some(last)) => (first.start_byte, last.start_byte + last.text.len()),
427 _ => (0, 0),
428 };
429 LineLayout {
430 fragments,
431 width: x,
432 height: 0.0,
433 ascent,
434 descent,
435 align_dx: 0.0,
436 baseline_from_top: 0.0,
437 start_byte,
438 end_byte,
439 }
440}
441
442fn finalize_line(line: &mut LineLayout, avail: f64, align: TextHAlign) {
445 let content = line.ascent + line.descent;
446 line.height = content * LINE_SPACING;
447 let leading = line.height - content;
448 line.baseline_from_top = leading * 0.5 + line.ascent;
449 line.align_dx = match align {
450 TextHAlign::Left => 0.0,
451 TextHAlign::Center => ((avail - line.width) * 0.5).max(0.0),
452 TextHAlign::Right => (avail - line.width).max(0.0),
453 };
454}
455
456#[cfg(test)]
457#[path = "layout_tests.rs"]
458mod layout_tests;