Rust · 32657 bytes Raw Blame History
1 //! Math layout engine
2 //!
3 //! Computes bounding boxes and positions for mathematical typesetting.
4 //! Uses baseline-aligned layout with proper ascent/descent metrics.
5
6 use crate::mathbox::{LimitDirection, MathBox, Operator};
7 use cairo::Context;
8
9 /// Layout metrics for a rendered element
10 #[derive(Debug, Clone, Default)]
11 pub struct LayoutBox {
12 /// Total width of the element
13 pub width: f64,
14 /// Height above the baseline
15 pub ascent: f64,
16 /// Depth below the baseline
17 pub descent: f64,
18 /// Positioned children: (x_offset, y_offset, child_layout)
19 pub children: Vec<(f64, f64, LayoutBox)>,
20 }
21
22 impl LayoutBox {
23 /// Total height (ascent + descent)
24 pub fn height(&self) -> f64 {
25 self.ascent + self.descent
26 }
27
28 /// Create an empty layout box
29 pub fn empty() -> Self {
30 Self::default()
31 }
32 }
33
34 /// Layout engine for mathematical typesetting
35 pub struct MathLayoutEngine {
36 /// Base font size in points
37 pub base_font_size: f64,
38 /// Font family name
39 pub font_family: String,
40 }
41
42 impl Default for MathLayoutEngine {
43 fn default() -> Self {
44 Self {
45 base_font_size: 16.0,
46 font_family: "serif".to_string(),
47 }
48 }
49 }
50
51 impl MathLayoutEngine {
52 /// Create a new layout engine with given font settings
53 pub fn new(font_family: &str, font_size: f64) -> Self {
54 Self {
55 base_font_size: font_size,
56 font_family: font_family.to_string(),
57 }
58 }
59
60 /// Compute layout for a MathBox tree
61 pub fn layout(&self, mathbox: &MathBox, ctx: &Context) -> LayoutBox {
62 self.layout_at_depth(mathbox, ctx, 0)
63 }
64
65 /// Compute layout from an explicit starting depth
66 pub fn layout_with_depth(&self, mathbox: &MathBox, ctx: &Context, depth: u32) -> LayoutBox {
67 self.layout_at_depth(mathbox, ctx, depth)
68 }
69
70 /// Compute layout at a specific nesting depth
71 fn layout_at_depth(&self, mathbox: &MathBox, ctx: &Context, depth: u32) -> LayoutBox {
72 let scale = self.scale_for_depth(depth);
73 let font_size = self.base_font_size * scale;
74
75 match mathbox {
76 MathBox::Number(s) => self.layout_text(s, ctx, font_size),
77 MathBox::Symbol(s) => self.layout_symbol(s, ctx, font_size),
78 MathBox::Operator(op) => self.layout_operator(*op, ctx, font_size),
79 MathBox::Slot => self.layout_slot(ctx, font_size),
80 MathBox::Fraction { num, den } => self.layout_fraction(num, den, ctx, depth),
81 MathBox::Power { base, exp } => self.layout_power(base, exp, ctx, depth),
82 MathBox::Subscript { base, sub } => self.layout_subscript(base, sub, ctx, depth),
83 MathBox::Root { index, radicand } => {
84 self.layout_root(index.as_deref(), radicand, ctx, depth)
85 }
86 MathBox::Func { name, args } => self.layout_func(name, args, ctx, depth),
87 MathBox::Abs(inner) => self.layout_abs(inner, ctx, depth),
88 MathBox::Parens(inner) => self.layout_parens(inner, ctx, depth),
89 MathBox::Integral {
90 lower,
91 upper,
92 body,
93 var,
94 } => self.layout_integral(lower.as_deref(), upper.as_deref(), body, var, ctx, depth),
95 MathBox::Derivative { order, var, body } => {
96 self.layout_derivative(*order, var, body, ctx, depth)
97 }
98 MathBox::Limit {
99 var,
100 to,
101 direction,
102 body,
103 } => self.layout_limit(var, to, *direction, body, ctx, depth),
104 MathBox::Sum {
105 var,
106 lower,
107 upper,
108 body,
109 } => self.layout_bigop("∑", var, lower, upper, body, ctx, depth),
110 MathBox::Product {
111 var,
112 lower,
113 upper,
114 body,
115 } => self.layout_bigop("∏", var, lower, upper, body, ctx, depth),
116 MathBox::Matrix { rows } => self.layout_matrix(rows, ctx, depth),
117 MathBox::Row(items) => self.layout_row(items, ctx, depth),
118 }
119 }
120
121 /// Scale factor for nested elements
122 pub fn scale_for_depth(&self, depth: u32) -> f64 {
123 match depth {
124 0 => 1.0,
125 1 => 0.8,
126 _ => 0.65,
127 }
128 }
129
130 /// Layout plain text
131 fn layout_text(&self, text: &str, ctx: &Context, font_size: f64) -> LayoutBox {
132 ctx.set_font_size(font_size);
133 let extents = ctx.text_extents(text).unwrap();
134 let font_extents = ctx.font_extents().unwrap();
135
136 LayoutBox {
137 width: extents.x_advance(),
138 ascent: font_extents.ascent(),
139 descent: font_extents.descent(),
140 children: vec![],
141 }
142 }
143
144 /// Layout a symbol (may use italic)
145 fn layout_symbol(&self, symbol: &str, ctx: &Context, font_size: f64) -> LayoutBox {
146 ctx.select_font_face(
147 &self.font_family,
148 cairo::FontSlant::Italic,
149 cairo::FontWeight::Normal,
150 );
151 let layout = self.layout_text(symbol, ctx, font_size);
152 ctx.select_font_face(
153 &self.font_family,
154 cairo::FontSlant::Normal,
155 cairo::FontWeight::Normal,
156 );
157 layout
158 }
159
160 /// Layout an operator
161 fn layout_operator(&self, op: Operator, ctx: &Context, font_size: f64) -> LayoutBox {
162 let ch = op.as_char().to_string();
163 let mut layout = self.layout_text(&ch, ctx, font_size);
164 // Add padding around operators
165 layout.width += font_size * 0.3;
166 layout
167 }
168
169 /// Layout an empty slot (placeholder box)
170 fn layout_slot(&self, _ctx: &Context, font_size: f64) -> LayoutBox {
171 // Keep slots readable at nested depths by enforcing a minimum visual size.
172 let slot_width = (font_size * 0.86).max(10.0);
173 let slot_height = (font_size * 0.9).max(11.0);
174
175 LayoutBox {
176 width: slot_width,
177 ascent: slot_height * 0.62,
178 descent: slot_height * 0.38,
179 children: vec![],
180 }
181 }
182
183 /// Layout a fraction
184 fn layout_fraction(
185 &self,
186 num: &MathBox,
187 den: &MathBox,
188 ctx: &Context,
189 depth: u32,
190 ) -> LayoutBox {
191 let num_layout = self.layout_at_depth(num, ctx, depth + 1);
192 let den_layout = self.layout_at_depth(den, ctx, depth + 1);
193
194 let scale = self.scale_for_depth(depth);
195 let bar_thickness = 1.0 * scale;
196 let gap = self.base_font_size * 0.1 * scale;
197
198 let width = num_layout.width.max(den_layout.width) + self.base_font_size * 0.2;
199 let num_x = (width - num_layout.width) / 2.0;
200 let den_x = (width - den_layout.width) / 2.0;
201
202 // Position numerator above bar, denominator below
203 let num_y = -(gap + bar_thickness / 2.0 + num_layout.descent);
204 let den_y = gap + bar_thickness / 2.0 + den_layout.ascent;
205
206 LayoutBox {
207 width,
208 ascent: gap + bar_thickness / 2.0 + num_layout.height(),
209 descent: gap + bar_thickness / 2.0 + den_layout.height(),
210 children: vec![(num_x, num_y, num_layout), (den_x, den_y, den_layout)],
211 }
212 }
213
214 /// Layout a power (superscript)
215 fn layout_power(&self, base: &MathBox, exp: &MathBox, ctx: &Context, depth: u32) -> LayoutBox {
216 let base_layout = self.layout_at_depth(base, ctx, depth);
217 let exp_layout = self.layout_at_depth(exp, ctx, depth + 1);
218 let scale = self.scale_for_depth(depth);
219 let font_size = self.base_font_size * scale;
220
221 // Exponent is raised above the baseline and slightly kerned to the right.
222 let exp_raise = base_layout.ascent * 0.58 + exp_layout.descent * 0.1;
223 let exp_kern = (font_size * 0.06).max(0.8);
224
225 LayoutBox {
226 width: base_layout.width + exp_kern + exp_layout.width,
227 ascent: base_layout.ascent.max(exp_raise + exp_layout.ascent),
228 descent: base_layout.descent,
229 children: vec![
230 (0.0, 0.0, base_layout.clone()),
231 (base_layout.width + exp_kern, -exp_raise, exp_layout),
232 ],
233 }
234 }
235
236 /// Layout a subscript
237 fn layout_subscript(
238 &self,
239 base: &MathBox,
240 sub: &MathBox,
241 ctx: &Context,
242 depth: u32,
243 ) -> LayoutBox {
244 let base_layout = self.layout_at_depth(base, ctx, depth);
245 let sub_layout = self.layout_at_depth(sub, ctx, depth + 1);
246 let scale = self.scale_for_depth(depth);
247 let font_size = self.base_font_size * scale;
248
249 // Subscript is lowered with a small right kern to avoid touching the base.
250 let sub_lower =
251 (base_layout.descent * 0.6 + sub_layout.ascent * 0.9).max(sub_layout.ascent * 0.75);
252 let sub_kern = (font_size * 0.05).max(0.6);
253
254 LayoutBox {
255 width: base_layout.width + sub_kern + sub_layout.width,
256 ascent: base_layout
257 .ascent
258 .max((sub_layout.ascent - sub_lower).max(0.0)),
259 descent: base_layout.descent.max(sub_lower + sub_layout.descent),
260 children: vec![
261 (0.0, 0.0, base_layout.clone()),
262 (base_layout.width + sub_kern, sub_lower, sub_layout),
263 ],
264 }
265 }
266
267 /// Layout a root (square or nth)
268 fn layout_root(
269 &self,
270 index: Option<&MathBox>,
271 radicand: &MathBox,
272 ctx: &Context,
273 depth: u32,
274 ) -> LayoutBox {
275 let radicand_layout = self.layout_at_depth(radicand, ctx, depth);
276 let scale = self.scale_for_depth(depth);
277 let font_size = self.base_font_size * scale;
278
279 // Radical symbol width
280 let radical_width = font_size * 0.6;
281 let bar_overhang = font_size * 0.1;
282 let gap = font_size * 0.1;
283
284 let mut total_width = radical_width + radicand_layout.width + bar_overhang;
285 let mut children = vec![];
286
287 // Handle nth root index
288 let index_width = if let Some(idx) = index {
289 let idx_layout = self.layout_at_depth(idx, ctx, depth + 2);
290 let idx_x = 0.0;
291 let idx_y = -(radicand_layout.ascent * 0.5);
292 children.push((idx_x, idx_y, idx_layout.clone()));
293 idx_layout.width
294 } else {
295 0.0
296 };
297
298 total_width += index_width;
299
300 // Radicand position
301 let radicand_x = index_width + radical_width;
302 children.push((radicand_x, 0.0, radicand_layout.clone()));
303
304 LayoutBox {
305 width: total_width,
306 ascent: radicand_layout.ascent + gap + 1.0, // +1 for bar
307 descent: radicand_layout.descent,
308 children,
309 }
310 }
311
312 /// Layout a function call
313 fn layout_func(&self, name: &str, args: &[MathBox], ctx: &Context, depth: u32) -> LayoutBox {
314 if name == "factorial" && args.len() == 1 {
315 return self.layout_factorial(&args[0], ctx, depth);
316 }
317
318 let scale = self.scale_for_depth(depth);
319 let font_size = self.base_font_size * scale;
320
321 // Function name
322 let name_layout = self.layout_text(name, ctx, font_size);
323 let paren_width = font_size * 0.3;
324
325 let mut width = name_layout.width + paren_width; // opening paren
326 let mut max_ascent = name_layout.ascent;
327 let mut max_descent = name_layout.descent;
328 let mut children = vec![(0.0, 0.0, name_layout.clone())];
329
330 let mut x = name_layout.width + paren_width;
331
332 for (i, arg) in args.iter().enumerate() {
333 if i > 0 {
334 // Comma separator
335 let comma_layout = self.layout_text(",", ctx, font_size);
336 x += comma_layout.width;
337 width += comma_layout.width;
338 }
339
340 let arg_layout = self.layout_at_depth(arg, ctx, depth);
341 max_ascent = max_ascent.max(arg_layout.ascent);
342 max_descent = max_descent.max(arg_layout.descent);
343 children.push((x, 0.0, arg_layout.clone()));
344 x += arg_layout.width;
345 width += arg_layout.width;
346 }
347
348 width += paren_width; // closing paren
349
350 LayoutBox {
351 width,
352 ascent: max_ascent,
353 descent: max_descent,
354 children,
355 }
356 }
357
358 fn layout_factorial(&self, arg: &MathBox, ctx: &Context, depth: u32) -> LayoutBox {
359 let scale = self.scale_for_depth(depth);
360 let font_size = self.base_font_size * scale;
361 let gap = (font_size * 0.06).max(0.6);
362
363 let arg_layout = self.layout_at_depth(arg, ctx, depth);
364 let arg_width = arg_layout.width;
365 let bang_layout = self.layout_text("!", ctx, font_size);
366
367 LayoutBox {
368 width: arg_width + gap + bang_layout.width,
369 ascent: arg_layout.ascent.max(bang_layout.ascent),
370 descent: arg_layout.descent.max(bang_layout.descent),
371 children: vec![(0.0, 0.0, arg_layout), (arg_width + gap, 0.0, bang_layout)],
372 }
373 }
374
375 /// Layout absolute value
376 fn layout_abs(&self, inner: &MathBox, ctx: &Context, depth: u32) -> LayoutBox {
377 let inner_layout = self.layout_at_depth(inner, ctx, depth);
378 let scale = self.scale_for_depth(depth);
379 let bar_width = self.base_font_size * 0.15 * scale;
380
381 LayoutBox {
382 width: inner_layout.width + 2.0 * bar_width,
383 ascent: inner_layout.ascent,
384 descent: inner_layout.descent,
385 children: vec![(bar_width, 0.0, inner_layout)],
386 }
387 }
388
389 /// Layout parenthesized expression
390 fn layout_parens(&self, inner: &MathBox, ctx: &Context, depth: u32) -> LayoutBox {
391 let inner_layout = self.layout_at_depth(inner, ctx, depth);
392 let scale = self.scale_for_depth(depth);
393 let paren_width = self.base_font_size * 0.25 * scale;
394
395 LayoutBox {
396 width: inner_layout.width + 2.0 * paren_width,
397 ascent: inner_layout.ascent + 2.0,
398 descent: inner_layout.descent + 2.0,
399 children: vec![(paren_width, 0.0, inner_layout)],
400 }
401 }
402
403 /// Layout an integral
404 fn layout_integral(
405 &self,
406 lower: Option<&MathBox>,
407 upper: Option<&MathBox>,
408 body: &MathBox,
409 var: &MathBox,
410 ctx: &Context,
411 depth: u32,
412 ) -> LayoutBox {
413 let scale = self.scale_for_depth(depth);
414 let font_size = self.base_font_size * scale;
415
416 let body_layout = self.layout_at_depth(body, ctx, depth);
417 let var_layout = self.layout_at_depth(var, ctx, depth);
418 let d_layout = self.layout_text("d", ctx, font_size);
419
420 // Integral symbol sizing and metrics
421 let symbol_size = body_layout.height().max(font_size * 1.8);
422 ctx.set_font_size(symbol_size);
423 let int_extents = ctx.text_extents("∫").unwrap();
424 let int_font_extents = ctx.font_extents().unwrap();
425 let symbol_width = int_extents.x_advance();
426 let symbol_ascent = int_font_extents.ascent();
427 let symbol_descent = int_font_extents.descent();
428
429 let lo_layout = lower.map(|lo| self.layout_at_depth(lo, ctx, depth + 1));
430 let hi_layout = upper.map(|hi| self.layout_at_depth(hi, ctx, depth + 1));
431
432 let bound_gap = font_size * 0.14;
433 let bounds_width = symbol_width
434 .max(lo_layout.as_ref().map(|l| l.width).unwrap_or(0.0))
435 .max(hi_layout.as_ref().map(|l| l.width).unwrap_or(0.0));
436 let body_gap = font_size * 0.22;
437 let dx_gap = font_size * 0.14;
438
439 let mut children = vec![];
440 let body_x = bounds_width + body_gap;
441 children.push((body_x, 0.0, body_layout.clone()));
442
443 let d_x = body_x + body_layout.width + dx_gap;
444 children.push((d_x, 0.0, d_layout.clone()));
445 let var_x = d_x + d_layout.width;
446 children.push((var_x, 0.0, var_layout.clone()));
447
448 let ascent =
449 symbol_ascent + bound_gap + hi_layout.as_ref().map(|l| l.height()).unwrap_or(0.0);
450 let descent =
451 symbol_descent + bound_gap + lo_layout.as_ref().map(|l| l.height()).unwrap_or(0.0);
452
453 LayoutBox {
454 width: var_x + var_layout.width,
455 ascent: ascent.max(body_layout.ascent),
456 descent: descent.max(body_layout.descent),
457 children,
458 }
459 }
460
461 /// Layout a derivative
462 fn layout_derivative(
463 &self,
464 order: u32,
465 var: &MathBox,
466 body: &MathBox,
467 ctx: &Context,
468 depth: u32,
469 ) -> LayoutBox {
470 let scale = self.scale_for_depth(depth);
471 let font_size = self.base_font_size * scale;
472 let frac_font_size = font_size * 0.8;
473
474 let var_layout = self.layout_at_depth(var, ctx, depth + 1);
475 let body_layout = self.layout_at_depth(body, ctx, depth);
476
477 // Build "d/dx" or "d²/dx²"
478 let d_str = if order > 1 {
479 format!("d{}", superscript_digits(order))
480 } else {
481 "d".to_string()
482 };
483
484 let dx_str = if order > 1 {
485 format!("d{}", superscript_digits(order))
486 } else {
487 "d".to_string()
488 };
489
490 let d_layout = self.layout_text(&d_str, ctx, frac_font_size);
491 let dx_layout = self.layout_text(&dx_str, ctx, frac_font_size);
492 let den_sep = (frac_font_size * 0.08).max(0.6);
493 let denom_width = dx_layout.width + den_sep + var_layout.width;
494 let denom_height = dx_layout.height().max(var_layout.height());
495 let frac_width = d_layout.width.max(denom_width) + font_size * 0.18;
496 let bar_gap = font_size * 0.14;
497 let body_gap = font_size * 0.3;
498
499 LayoutBox {
500 width: frac_width + body_gap + body_layout.width,
501 ascent: (bar_gap + d_layout.height()).max(body_layout.ascent),
502 descent: (bar_gap + denom_height).max(body_layout.descent),
503 children: vec![(frac_width + body_gap, 0.0, body_layout)],
504 }
505 }
506
507 /// Layout a limit
508 fn layout_limit(
509 &self,
510 var: &MathBox,
511 to: &MathBox,
512 direction: Option<LimitDirection>,
513 body: &MathBox,
514 ctx: &Context,
515 depth: u32,
516 ) -> LayoutBox {
517 let scale = self.scale_for_depth(depth);
518 let font_size = self.base_font_size * scale;
519
520 let body_layout = self.layout_at_depth(body, ctx, depth);
521 let var_layout = self.layout_at_depth(var, ctx, depth + 1);
522 let to_layout = self.layout_at_depth(to, ctx, depth + 1);
523
524 // "lim" text
525 let lim_layout = self.layout_text("lim", ctx, font_size);
526 let lim_width = lim_layout.width;
527
528 // Build subscript: "x→a" or "x→a⁺" or "x→a⁻"
529 let sub_font_size = font_size * 0.7;
530 let arrow_layout = self.layout_text("→", ctx, sub_font_size);
531 let dir_str = match direction {
532 Some(LimitDirection::FromRight) => "⁺",
533 Some(LimitDirection::FromLeft) => "⁻",
534 None => "",
535 };
536 let dir_layout = if !dir_str.is_empty() {
537 Some(self.layout_text(dir_str, ctx, sub_font_size * 0.6))
538 } else {
539 None
540 };
541
542 let sub_sep = (sub_font_size * 0.08).max(0.5);
543 let subscript_width = var_layout.width
544 + sub_sep
545 + arrow_layout.width
546 + sub_sep
547 + to_layout.width
548 + dir_layout.as_ref().map(|l| l.width).unwrap_or(0.0);
549 let subscript_height = var_layout
550 .height()
551 .max(arrow_layout.height())
552 .max(to_layout.height());
553
554 let lim_col_width = lim_width.max(subscript_width);
555 let body_gap = font_size * 0.3;
556 let subscript_drop = font_size * 0.2 + subscript_height;
557
558 LayoutBox {
559 width: lim_col_width + body_gap + body_layout.width,
560 ascent: lim_layout.ascent.max(body_layout.ascent),
561 descent: subscript_drop.max(body_layout.descent),
562 children: vec![(lim_col_width + body_gap, 0.0, body_layout)],
563 }
564 }
565
566 /// Layout a big operator (sum, product)
567 fn layout_bigop(
568 &self,
569 symbol: &str,
570 var: &MathBox,
571 lower: &MathBox,
572 upper: &MathBox,
573 body: &MathBox,
574 ctx: &Context,
575 depth: u32,
576 ) -> LayoutBox {
577 let scale = self.scale_for_depth(depth);
578 let font_size = self.base_font_size * scale;
579
580 let body_layout = self.layout_at_depth(body, ctx, depth);
581 let var_layout = self.layout_at_depth(var, ctx, depth + 1);
582 let lower_layout = self.layout_at_depth(lower, ctx, depth + 1);
583 let upper_layout = self.layout_at_depth(upper, ctx, depth + 1);
584
585 // Big operator symbol
586 let symbol_size = font_size * 1.5;
587 ctx.set_font_size(symbol_size);
588 let symbol_extents = ctx.text_extents(symbol).unwrap();
589 let symbol_width = symbol_extents.x_advance();
590 let symbol_font_extents = ctx.font_extents().unwrap();
591 let symbol_ascent = symbol_font_extents.ascent();
592 let symbol_descent = symbol_font_extents.descent();
593
594 let bound_font_size = self.base_font_size * self.scale_for_depth(depth + 1);
595 let eq_layout = self.layout_text("=", ctx, bound_font_size);
596 let lower_sep = (bound_font_size * 0.08).max(0.6);
597 let lower_block_width =
598 var_layout.width + lower_sep + eq_layout.width + lower_sep + lower_layout.width;
599
600 let op_width = symbol_width.max(lower_block_width).max(upper_layout.width);
601 let bounds_gap = font_size * 0.16;
602 let body_gap = font_size * 0.32;
603 // Rendering centers big-op symbols around the expression baseline.
604 // Reserve half of total glyph height above and below for consistent placement.
605 let symbol_half_height = (symbol_ascent + symbol_descent) * 0.5;
606
607 let ascent =
608 (symbol_half_height + bounds_gap + upper_layout.height()).max(body_layout.ascent);
609 let descent =
610 (symbol_half_height + bounds_gap + lower_layout.height().max(var_layout.height()))
611 .max(body_layout.descent);
612
613 LayoutBox {
614 width: op_width + body_gap + body_layout.width,
615 ascent,
616 descent,
617 children: vec![(op_width + body_gap, 0.0, body_layout)],
618 }
619 }
620
621 /// Layout a matrix
622 fn layout_matrix(&self, rows: &[Vec<MathBox>], ctx: &Context, depth: u32) -> LayoutBox {
623 let scale = self.scale_for_depth(depth);
624 let font_size = self.base_font_size * scale;
625 let cell_padding = font_size * 0.3;
626
627 if rows.is_empty() {
628 return LayoutBox::empty();
629 }
630
631 let num_cols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
632 if num_cols == 0 {
633 return LayoutBox::empty();
634 }
635
636 // Compute layouts for all cells
637 let cell_layouts: Vec<Vec<LayoutBox>> = rows
638 .iter()
639 .map(|row| {
640 row.iter()
641 .map(|cell| self.layout_at_depth(cell, ctx, depth))
642 .collect()
643 })
644 .collect();
645
646 // Find max width per column and max height per row
647 let mut col_widths = vec![0.0f64; num_cols];
648 let mut row_heights = vec![0.0f64; rows.len()];
649
650 for (r, row) in cell_layouts.iter().enumerate() {
651 for (c, cell) in row.iter().enumerate() {
652 col_widths[c] = col_widths[c].max(cell.width);
653 row_heights[r] = row_heights[r].max(cell.height());
654 }
655 }
656
657 // Total dimensions
658 let bracket_width = font_size * 0.2;
659 let total_width: f64 = col_widths.iter().sum::<f64>()
660 + cell_padding * (num_cols as f64 - 1.0)
661 + 2.0 * bracket_width;
662 let total_height: f64 =
663 row_heights.iter().sum::<f64>() + cell_padding * (rows.len() as f64 - 1.0);
664
665 // Position cells
666 let mut children = vec![];
667 let mut y = -total_height / 2.0;
668
669 for (r, row) in cell_layouts.iter().enumerate() {
670 let mut x = bracket_width;
671 let row_h = row_heights[r];
672
673 for (c, cell) in row.iter().enumerate() {
674 let col_w = col_widths[c];
675 // Center cell in its column
676 let cell_x = x + (col_w - cell.width) / 2.0;
677 let cell_y = y + row_h / 2.0;
678 children.push((cell_x, cell_y, cell.clone()));
679 x += col_w + cell_padding;
680 }
681
682 y += row_h + cell_padding;
683 }
684
685 LayoutBox {
686 width: total_width,
687 ascent: total_height / 2.0 + cell_padding,
688 descent: total_height / 2.0 + cell_padding,
689 children,
690 }
691 }
692
693 /// Layout a horizontal row of elements
694 fn layout_row(&self, items: &[MathBox], ctx: &Context, depth: u32) -> LayoutBox {
695 let scale = self.scale_for_depth(depth);
696 let _font_size = self.base_font_size * scale;
697
698 let mut width: f64 = 0.0;
699 let mut max_ascent: f64 = 0.0;
700 let mut max_descent: f64 = 0.0;
701 let mut children = vec![];
702
703 for item in items {
704 let item_layout = self.layout_at_depth(item, ctx, depth);
705 children.push((width, 0.0, item_layout.clone()));
706 width += item_layout.width;
707 max_ascent = max_ascent.max(item_layout.ascent);
708 max_descent = max_descent.max(item_layout.descent);
709 }
710
711 LayoutBox {
712 width,
713 ascent: max_ascent,
714 descent: max_descent,
715 children,
716 }
717 }
718 }
719
720 /// Convert a number to superscript Unicode digits
721 fn superscript_digits(n: u32) -> String {
722 n.to_string()
723 .chars()
724 .map(|c| match c {
725 '0' => '⁰',
726 '1' => '¹',
727 '2' => '²',
728 '3' => '³',
729 '4' => '⁴',
730 '5' => '⁵',
731 '6' => '⁶',
732 '7' => '⁷',
733 '8' => '⁸',
734 '9' => '⁹',
735 _ => c,
736 })
737 .collect()
738 }
739
740 #[cfg(test)]
741 mod tests {
742 use super::*;
743 use cairo::{Context, Format, ImageSurface};
744
745 fn test_context() -> Context {
746 let surface = ImageSurface::create(Format::ARgb32, 256, 256).unwrap();
747 Context::new(&surface).unwrap()
748 }
749
750 #[test]
751 fn test_scale_for_depth() {
752 let engine = MathLayoutEngine::default();
753 assert_eq!(engine.scale_for_depth(0), 1.0);
754 assert_eq!(engine.scale_for_depth(1), 0.8);
755 assert_eq!(engine.scale_for_depth(2), 0.65);
756 assert_eq!(engine.scale_for_depth(5), 0.65);
757 }
758
759 #[test]
760 fn test_superscript_digits() {
761 assert_eq!(superscript_digits(2), "²");
762 assert_eq!(superscript_digits(123), "¹²³");
763 }
764
765 #[test]
766 fn test_slot_min_size_at_nested_depth() {
767 let engine = MathLayoutEngine::default();
768 let ctx = test_context();
769 let slot = engine.layout_with_depth(&MathBox::Slot, &ctx, 2);
770 assert!(slot.width >= 10.0);
771 assert!(slot.height() >= 11.0);
772 }
773
774 #[test]
775 fn test_power_layout_raises_exponent() {
776 let engine = MathLayoutEngine::default();
777 let ctx = test_context();
778 let power = MathBox::Power {
779 base: Box::new(MathBox::Number("2".to_string())),
780 exp: Box::new(MathBox::Slot),
781 };
782 let layout = engine.layout(&power, &ctx);
783 let base_layout = engine.layout(&MathBox::Number("2".to_string()), &ctx);
784
785 assert!(layout.width > base_layout.width);
786 assert!(layout.ascent > base_layout.ascent);
787 assert_eq!(layout.children.len(), 2);
788 assert!(layout.children[1].1 < 0.0);
789 }
790
791 #[test]
792 fn test_subscript_layout_lowers_and_extends_descent() {
793 let engine = MathLayoutEngine::default();
794 let ctx = test_context();
795 let sub = MathBox::Subscript {
796 base: Box::new(MathBox::Number("x".to_string())),
797 sub: Box::new(MathBox::Slot),
798 };
799 let layout = engine.layout(&sub, &ctx);
800 let base_layout = engine.layout(&MathBox::Number("x".to_string()), &ctx);
801
802 assert!(layout.descent > base_layout.descent);
803 assert_eq!(layout.children.len(), 2);
804 assert!(layout.children[1].1 > 0.0);
805 }
806
807 #[test]
808 fn test_bigop_layout_reserves_space_for_var_equals_lower() {
809 let engine = MathLayoutEngine::default();
810 let ctx = test_context();
811
812 let short = MathBox::Sum {
813 var: Box::new(MathBox::Symbol("i".to_string())),
814 lower: Box::new(MathBox::Number("1".to_string())),
815 upper: Box::new(MathBox::Number("5".to_string())),
816 body: Box::new(MathBox::Slot),
817 };
818 let wide = MathBox::Sum {
819 var: Box::new(MathBox::Symbol("index".to_string())),
820 lower: Box::new(MathBox::Number("123456".to_string())),
821 upper: Box::new(MathBox::Number("5".to_string())),
822 body: Box::new(MathBox::Slot),
823 };
824
825 let short_layout = engine.layout(&short, &ctx);
826 let wide_layout = engine.layout(&wide, &ctx);
827 assert!(wide_layout.width > short_layout.width);
828 }
829
830 #[test]
831 fn test_bigop_layout_expands_ascent_descent_for_bounds() {
832 let engine = MathLayoutEngine::default();
833 let ctx = test_context();
834
835 let sum = MathBox::Sum {
836 var: Box::new(MathBox::Symbol("i".to_string())),
837 lower: Box::new(MathBox::Slot),
838 upper: Box::new(MathBox::Slot),
839 body: Box::new(MathBox::Number("x".to_string())),
840 };
841 let body_layout = engine.layout(&MathBox::Number("x".to_string()), &ctx);
842 let sum_layout = engine.layout(&sum, &ctx);
843
844 assert!(sum_layout.ascent > body_layout.ascent);
845 assert!(sum_layout.descent > body_layout.descent);
846 }
847
848 #[test]
849 fn test_derivative_layout_widens_for_long_variable() {
850 let engine = MathLayoutEngine::default();
851 let ctx = test_context();
852
853 let short = MathBox::Derivative {
854 order: 1,
855 var: Box::new(MathBox::Symbol("x".to_string())),
856 body: Box::new(MathBox::Slot),
857 };
858 let long = MathBox::Derivative {
859 order: 1,
860 var: Box::new(MathBox::Symbol("variable".to_string())),
861 body: Box::new(MathBox::Slot),
862 };
863
864 let short_layout = engine.layout(&short, &ctx);
865 let long_layout = engine.layout(&long, &ctx);
866 assert!(long_layout.width > short_layout.width);
867 }
868
869 #[test]
870 fn test_integral_layout_bounds_expand_vertical_space() {
871 let engine = MathLayoutEngine::default();
872 let ctx = test_context();
873
874 let plain = MathBox::Integral {
875 lower: None,
876 upper: None,
877 body: Box::new(MathBox::Slot),
878 var: Box::new(MathBox::Symbol("x".to_string())),
879 };
880 let bounded = MathBox::Integral {
881 lower: Some(Box::new(MathBox::Slot)),
882 upper: Some(Box::new(MathBox::Slot)),
883 body: Box::new(MathBox::Slot),
884 var: Box::new(MathBox::Symbol("x".to_string())),
885 };
886
887 let plain_layout = engine.layout(&plain, &ctx);
888 let bounded_layout = engine.layout(&bounded, &ctx);
889 assert!(bounded_layout.ascent > plain_layout.ascent);
890 assert!(bounded_layout.descent > plain_layout.descent);
891 }
892
893 #[test]
894 fn test_limit_layout_widens_for_wide_subscript_and_extends_descent() {
895 let engine = MathLayoutEngine::default();
896 let ctx = test_context();
897
898 let narrow = MathBox::Limit {
899 var: Box::new(MathBox::Symbol("x".to_string())),
900 to: Box::new(MathBox::Number("1".to_string())),
901 direction: None,
902 body: Box::new(MathBox::Slot),
903 };
904 let wide = MathBox::Limit {
905 var: Box::new(MathBox::Symbol("veryLongVariable".to_string())),
906 to: Box::new(MathBox::Number("123456".to_string())),
907 direction: Some(LimitDirection::FromRight),
908 body: Box::new(MathBox::Slot),
909 };
910
911 let narrow_layout = engine.layout(&narrow, &ctx);
912 let wide_layout = engine.layout(&wide, &ctx);
913 let body_layout = engine.layout(&MathBox::Slot, &ctx);
914
915 assert!(wide_layout.width > narrow_layout.width);
916 assert!(wide_layout.descent > body_layout.descent);
917 }
918 }
919