1use std::collections::HashMap;
4
5use crate::command::Point;
6use crate::export::SpanMap;
7use crate::host::{Metrics, Rect, RenderOutput};
8use crate::menu::{Menu, MenuItem, MenuView};
9use crate::model::{Cursor, Kind, NodeId, Selection, SeqId, Tree};
10
11use mathtex_ir::{Fragment, LayoutNode, LayoutNodeKind, NodeId as IrId, Point as IrPoint};
12
13#[derive(Debug, Clone, Default)]
15pub struct BoxMap {
16 pub node: HashMap<NodeId, Rect>,
18 pub seq: HashMap<SeqId, Rect>,
20}
21
22pub(crate) fn render(
24 tree: &Tree,
25 cursor: Cursor,
26 sel: Option<Selection>,
27 spans: &SpanMap,
28 ir: Fragment,
29 menu: Option<&Menu>,
30) -> RenderOutput {
31 let boxes = match_boxes(spans, &ir);
32 let caret = caret_rect(&boxes, tree, cursor);
33 let selection = sel
34 .map(|s| selection_rects(&boxes, tree, s))
35 .unwrap_or_default();
36 let placeholders: Vec<Rect> = spans
38 .seq
39 .keys()
40 .filter(|&&s| tree.is_empty(s))
41 .filter_map(|s| boxes.seq.get(s).copied())
42 .collect();
43 let metrics = Metrics {
44 width: pt(ir.surface.width),
45 height: pt(ir.surface.height),
46 baseline: pt(ir.surface.baseline),
47 };
48 let menu = menu.map(|m| MenuView {
50 anchor: boxes.node.get(&m.anchor).copied().unwrap_or(ZERO),
51 items: m
52 .visible()
53 .iter()
54 .map(|row| MenuItem { label: row.label() })
55 .collect(),
56 selected: m.selected,
57 query: m.query.clone(),
58 });
59 RenderOutput {
60 ir,
61 caret,
62 selection,
63 placeholders,
64 metrics,
65 menu,
66 }
67}
68
69pub fn match_boxes(spans: &SpanMap, fragment: &Fragment) -> BoxMap {
71 let abs = absolute_origins(fragment);
72 let mut parent: HashMap<IrId, IrId> = HashMap::new();
74 for n in &fragment.nodes {
75 for c in children_of(n) {
76 parent.insert(c, n.id);
77 }
78 }
79 let mut box_metrics: Vec<(usize, usize, f64, f64)> = Vec::new();
81 for n in &fragment.nodes {
82 let LayoutNodeKind::Box(bx) = &n.kind else { continue };
83 let Some(s) = n.primary_source else { continue };
84 let (a, b) = (s.span.start as usize, s.span.end as usize);
85 box_metrics.push((a, b, pt(bx.metrics.height), pt(bx.metrics.depth)));
86 }
87 let own_box_split = |a: usize, b: usize, glyph_total: f64| -> Option<(f64, f64)> {
89 box_metrics
90 .iter()
91 .filter(|(ba, bb, _, _)| *ba == a && *bb == b)
92 .min_by(|(_, _, h1, d1), (_, _, h2, d2)| {
93 let e1 = (h1 + d1 - glyph_total).abs();
94 let e2 = (h2 + d2 - glyph_total).abs();
95 e1.total_cmp(&e2)
96 })
97 .map(|(_, _, h, d)| (*h, *d))
98 };
99 let mut leaves: Vec<(usize, usize, Rect)> = Vec::new();
100 let mut containers: Vec<(IrId, usize, usize, Rect)> = Vec::new();
101 for n in &fragment.nodes {
102 let Some(s) = n.primary_source else { continue };
103 let (a, b) = (s.span.start as usize, s.span.end as usize);
104 if a >= b {
105 continue;
106 }
107 if let LayoutNodeKind::Rule(r) = &n.kind {
109 if r.size.width.0 == 0 || r.size.height.0 == 0 {
110 continue;
111 }
112 }
113 let mut rect = rect_of(fragment, &abs, n.id);
114 if matches!(
115 n.kind,
116 LayoutNodeKind::GlyphRun(_) | LayoutNodeKind::Rule(_) | LayoutNodeKind::Drawing(_)
117 ) {
118 if let LayoutNodeKind::GlyphRun(_) = &n.kind {
120 let glyph_total = pt(n.bounds.size.height);
121 if let (Some((h, d)), Some(o)) = (own_box_split(a, b, glyph_total), abs.get(&n.id))
122 {
123 rect.y = pt(o.y) - h;
124 rect.height = h + d;
125 }
126 }
127 leaves.push((a, b, rect));
128 } else {
129 if let LayoutNodeKind::Box(bx) = &n.kind {
131 if let Some(o) = abs.get(&n.id) {
132 rect.y = pt(o.y) - pt(bx.metrics.height);
133 rect.height = pt(bx.metrics.height) + pt(bx.metrics.depth);
134 }
135 }
136 containers.push((n.id, a, b, rect));
137 }
138 }
139 let is_leafless =
141 |ca: usize, cb: usize| !leaves.iter().any(|(la, lb, _)| *la >= ca && *lb <= cb);
142 let union_in = |range: &std::ops::Range<usize>| -> Option<Rect> {
143 let leaf_rects: Vec<Rect> = leaves
144 .iter()
145 .filter(|(a, b, _)| *a >= range.start && *b <= range.end)
146 .map(|(_, _, r)| *r)
147 .collect();
148 let leafless: Vec<(IrId, Rect)> = containers
150 .iter()
151 .filter(|(_, a, b, _)| *a >= range.start && *b <= range.end)
152 .filter(|(_, a, b, _)| is_leafless(*a, *b))
153 .map(|(id, _, _, r)| (*id, *r))
154 .collect();
155 let mut has_leafless_descendant: std::collections::HashSet<IrId> =
156 std::collections::HashSet::new();
157 for (id, _) in &leafless {
158 let mut cur = *id;
159 while let Some(&p) = parent.get(&cur) {
160 if leafless.iter().any(|(cid, _)| *cid == p) {
161 has_leafless_descendant.insert(p);
162 }
163 cur = p;
164 }
165 }
166 let leafless_deepest: Vec<Rect> = leafless
167 .iter()
168 .filter(|(id, _)| !has_leafless_descendant.contains(id))
169 .map(|(_, r)| *r)
170 .collect();
171 union(&[leaf_rects, leafless_deepest].concat())
172 };
173 let mut map = BoxMap::default();
174 for (&node, range) in &spans.node {
175 if let Some(r) = union_in(range) {
176 map.node.insert(node, r);
177 }
178 }
179 for (&seq, range) in &spans.seq {
180 if let Some(r) = union_in(range) {
181 map.seq.insert(seq, r);
182 }
183 }
184 map
185}
186
187fn absolute_origins(fragment: &Fragment) -> HashMap<IrId, IrPoint> {
189 let mut map = HashMap::new();
190 if let Some(root) = select_root(fragment) {
191 let base = IrPoint {
193 x: mathtex_ir::Length(0),
194 y: fragment.surface.baseline,
195 };
196 walk_origins(fragment, root, base, &mut map);
197 }
198 for n in &fragment.nodes {
200 map.entry(n.id).or_insert(n.origin);
201 }
202 map
203}
204
205fn walk_origins(fragment: &Fragment, id: IrId, parent_abs: IrPoint, map: &mut HashMap<IrId, IrPoint>) {
206 let Some(node) = fragment.node(id) else { return };
207 let abs = IrPoint {
208 x: mathtex_ir::Length(parent_abs.x.0.saturating_add(node.origin.x.0)),
209 y: mathtex_ir::Length(parent_abs.y.0.saturating_add(node.origin.y.0)),
210 };
211 map.insert(id, abs);
212 for child in children_of(node) {
213 walk_origins(fragment, child, abs, map);
214 }
215}
216
217fn children_of(node: &LayoutNode) -> Vec<IrId> {
218 match &node.kind {
219 LayoutNodeKind::Box(b) => b.children.clone(),
220 LayoutNodeKind::List(l) => l.children.clone(),
221 LayoutNodeKind::Group { children } => children.clone(),
222 _ => Vec::new(),
223 }
224}
225
226fn select_root(fragment: &Fragment) -> Option<IrId> {
228 let mut is_child = std::collections::HashSet::new();
229 for n in &fragment.nodes {
230 for c in children_of(n) {
231 is_child.insert(c);
232 }
233 }
234 fragment.nodes.iter().find_map(|n| match n.kind {
235 LayoutNodeKind::Box(_) if !is_child.contains(&n.id) => Some(n.id),
236 _ => None,
237 })
238}
239
240fn rect_of(fragment: &Fragment, abs: &HashMap<IrId, IrPoint>, id: IrId) -> Rect {
241 let Some(n) = fragment.node(id) else {
242 return ZERO;
243 };
244 let o = abs.get(&id).copied().unwrap_or(n.origin);
245 Rect {
247 x: pt(mathtex_ir::Length(o.x.0.saturating_add(n.bounds.origin.x.0))),
248 y: pt(mathtex_ir::Length(
249 o.y.0
250 .saturating_sub(n.bounds.origin.y.0)
251 .saturating_sub(n.bounds.size.height.0),
252 )),
253 width: pt(n.bounds.size.width),
254 height: pt(n.bounds.size.height),
255 }
256}
257
258pub fn caret_rect(boxes: &BoxMap, tree: &Tree, cursor: Cursor) -> Rect {
260 if tree.is_empty(cursor.seq) {
262 return boxes
263 .seq
264 .get(&cursor.seq)
265 .map(|v| caret_at(v.x, v))
266 .unwrap_or(ZERO);
267 }
268 let items = tree.items(cursor.seq);
269 let placement = if cursor.index > 0 {
271 Some((items[cursor.index - 1], true))
272 } else if cursor.index < items.len() {
273 Some((items[cursor.index], false))
274 } else {
275 None
276 };
277 if let Some((node, right_edge)) = placement {
278 if let Some(v) = boxes.node.get(&node) {
279 return caret_at(if right_edge { v.x + v.width } else { v.x }, v);
280 }
281 }
282 boxes
283 .seq
284 .get(&cursor.seq)
285 .map(|v| caret_at(v.x, v))
286 .unwrap_or(ZERO)
287}
288
289fn caret_at(x: f64, v: &Rect) -> Rect {
291 Rect {
292 x,
293 y: v.y,
294 width: 0.0,
295 height: v.height,
296 }
297}
298
299pub fn selection_rects(boxes: &BoxMap, tree: &Tree, sel: Selection) -> Vec<Rect> {
301 let lo = sel.anchor.min(sel.focus);
302 let hi = sel.anchor.max(sel.focus).min(tree.len(sel.seq));
303 let rects: Vec<Rect> = tree.items(sel.seq)[lo..hi]
304 .iter()
305 .filter_map(|n| boxes.node.get(n).copied())
306 .collect();
307 union(&rects).into_iter().collect()
308}
309
310pub fn hit_test(fragment: &Fragment, spans: &SpanMap, tree: &Tree, point: Point) -> Cursor {
312 hit_test_boxes(&match_boxes(spans, fragment), spans, tree, point)
313}
314
315#[derive(Clone, Copy)]
317enum Target {
318 Node(NodeId),
319 EmptySeq(SeqId),
320}
321
322fn hit_test_boxes(boxes: &BoxMap, spans: &SpanMap, tree: &Tree, point: Point) -> Cursor {
324 let mut best: Option<(Target, Rect, usize)> = None;
325 for (&node, &rect) in &boxes.node {
326 if contains(&rect, point) {
327 let span = spans.node.get(&node).map_or(usize::MAX, |r| r.end - r.start);
328 if best.as_ref().is_none_or(|(_, _, s)| span < *s) {
329 best = Some((Target::Node(node), rect, span));
330 }
331 }
332 }
333 for (&seq, &rect) in &boxes.seq {
335 if tree.is_empty(seq) && contains(&rect, point) {
336 let span = spans.seq.get(&seq).map_or(usize::MAX, |r| r.end - r.start);
337 if best.as_ref().is_none_or(|(_, _, s)| span < *s) {
338 best = Some((Target::EmptySeq(seq), rect, span));
339 }
340 }
341 }
342 let chosen = best
344 .map(|(t, r, _)| (t, r))
345 .or_else(|| nearest_target(boxes, tree, point));
346 if let Some((target, rect)) = chosen {
347 if let Some(c) = resolve_target(tree, target, rect, point) {
348 return c;
349 }
350 }
351 Cursor {
352 seq: tree.root(),
353 index: 0,
354 }
355}
356
357fn resolve_target(tree: &Tree, target: Target, rect: Rect, point: Point) -> Option<Cursor> {
358 match target {
359 Target::Node(node) => {
360 let (seq, idx) = tree.index_in_parent(node)?;
361 let mid = rect.x + rect.width / 2.0;
362 let index = if point.x > mid { idx + 1 } else { idx };
363 Some(Cursor { seq, index })
364 }
365 Target::EmptySeq(seq) => Some(Cursor { seq, index: 0 }),
367 }
368}
369
370fn nearest_target(boxes: &BoxMap, tree: &Tree, point: Point) -> Option<(Target, Rect)> {
372 let nodes = boxes
373 .node
374 .iter()
375 .map(|(&n, &r)| (Target::Node(n), r, rect_dist2(&r, point)));
376 let empty_seqs = boxes
377 .seq
378 .iter()
379 .filter(|&(&s, _)| tree.is_empty(s))
380 .map(|(&s, &r)| (Target::EmptySeq(s), r, rect_dist2(&r, point)));
381 nodes
382 .chain(empty_seqs)
383 .min_by(|(_, _, a), (_, _, b)| a.total_cmp(b))
384 .map(|(t, r, _)| (t, r))
385}
386
387fn rect_dist2(r: &Rect, p: Point) -> f64 {
389 let cx = p.x.clamp(r.x, r.x + r.width);
390 let cy = p.y.clamp(r.y, r.y + r.height);
391 let (dx, dy) = (p.x - cx, p.y - cy);
392 dx * dx + dy * dy
393}
394
395const ZERO: Rect = Rect {
396 x: 0.0,
397 y: 0.0,
398 width: 0.0,
399 height: 0.0,
400};
401
402fn contains(r: &Rect, p: Point) -> bool {
403 p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height
404}
405fn union(rects: &[Rect]) -> Option<Rect> {
406 let first = rects.first()?;
407 let (mut x0, mut y0) = (first.x, first.y);
408 let (mut x1, mut y1) = (first.x + first.width, first.y + first.height);
409 for r in &rects[1..] {
410 x0 = x0.min(r.x);
411 y0 = y0.min(r.y);
412 x1 = x1.max(r.x + r.width);
413 y1 = y1.max(r.y + r.height);
414 }
415 Some(Rect {
416 x: x0,
417 y: y0,
418 width: x1 - x0,
419 height: y1 - y0,
420 })
421}
422
423fn pt(len: mathtex_ir::Length) -> f64 {
425 len.0 as f64 / 65536.0
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431 use crate::model::{MathClass, Symbol};
432
433 fn atom(c: &str) -> Symbol {
434 Symbol { latex: c.into(), class: MathClass::Ord }
435 }
436
437 #[test]
439 fn hit_test_falls_back_to_nearest_box_on_a_miss() {
440 let mut t = Tree::new();
441 let root = t.root();
442 t.insert_atom(Cursor { seq: root, index: 0 }, atom("a"));
443 t.insert_atom(Cursor { seq: root, index: 1 }, atom("b"));
444 let a = t.items(root)[0];
445 let b = t.items(root)[1];
446
447 let mut boxes = BoxMap::default();
448 boxes.node.insert(a, Rect { x: 0.0, y: 0.0, width: 1.0, height: 1.0 });
449 boxes.node.insert(b, Rect { x: 5.0, y: 0.0, width: 1.0, height: 1.0 });
450 let spans = SpanMap::default();
451
452 let c = hit_test_boxes(&boxes, &spans, &t, Point { x: 1.5, y: 0.5 });
454 assert_eq!(c, Cursor { seq: root, index: 1 });
455
456 let c = hit_test_boxes(&boxes, &spans, &t, Point { x: 4.0, y: 0.5 });
458 assert_eq!(c, Cursor { seq: root, index: 1 });
459
460 let c = hit_test_boxes(&boxes, &spans, &t, Point { x: 0.8, y: -5.0 });
462 assert_eq!(c, Cursor { seq: root, index: 1 });
463 }
464
465 #[test]
466 fn hit_test_on_a_truly_empty_box_map_defaults_to_document_start() {
467 let t = Tree::new();
468 let root = t.root();
469 let c = hit_test_boxes(&BoxMap::default(), &SpanMap::default(), &t, Point { x: 3.0, y: 3.0 });
470 assert_eq!(c, Cursor { seq: root, index: 0 });
471 }
472
473 #[test]
475 fn hit_test_lands_inside_empty_matrix_cells() {
476 let mut t = Tree::new();
477 let root = t.root();
478 let c = t.insert_matrix(Cursor { seq: root, index: 0 }, crate::model::MatrixEnv::Pmatrix, 2, 2);
479 let matrix = t.items(root)[0];
480 let cells = t.child_seqs(matrix); assert_eq!(cells.len(), 4);
482 assert_eq!(c.seq, cells[0]); let mut boxes = BoxMap::default();
485 boxes.node.insert(matrix, Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
486 boxes.seq.insert(cells[0], Rect { x: 1.0, y: 1.0, width: 3.0, height: 3.0 }); boxes.seq.insert(cells[1], Rect { x: 6.0, y: 1.0, width: 3.0, height: 3.0 }); boxes.seq.insert(cells[2], Rect { x: 1.0, y: 6.0, width: 3.0, height: 3.0 }); boxes.seq.insert(cells[3], Rect { x: 6.0, y: 6.0, width: 3.0, height: 3.0 }); let mut spans = SpanMap::default();
493 spans.node.insert(matrix, 0..40);
494 for (i, &cell) in cells.iter().enumerate() {
495 spans.seq.insert(cell, i * 11..i * 11 + 11);
496 }
497
498 let hit = |x: f64, y: f64| hit_test_boxes(&boxes, &spans, &t, Point { x, y });
499 assert_eq!(hit(2.5, 2.5), Cursor { seq: cells[0], index: 0 });
500 assert_eq!(hit(7.5, 2.5), Cursor { seq: cells[1], index: 0 });
501 assert_eq!(hit(2.5, 7.5), Cursor { seq: cells[2], index: 0 });
502 assert_eq!(hit(7.5, 7.5), Cursor { seq: cells[3], index: 0 });
503 }
504
505 #[test]
507 fn hit_test_prefers_node_over_a_non_empty_seqs_own_box() {
508 let mut t = Tree::new();
509 let root = t.root();
510 t.insert_atom(Cursor { seq: root, index: 0 }, atom("a"));
511 let a = t.items(root)[0];
512
513 let mut boxes = BoxMap::default();
514 boxes.node.insert(a, Rect { x: 0.0, y: 0.0, width: 2.0, height: 2.0 });
515 boxes.seq.insert(root, Rect { x: 0.0, y: 0.0, width: 2.0, height: 2.0 });
516 let spans = SpanMap::default();
517
518 let c = hit_test_boxes(&boxes, &spans, &t, Point { x: 0.5, y: 0.5 });
520 assert_eq!(c, Cursor { seq: root, index: 0 });
521 }
522}