use crate::model::{Cursor, Kind, NodeId, SeqId, Tree, Variant};
pub(crate) fn adjacent_structure_backward(tree: &Tree, at: Cursor) -> Option<NodeId> {
let prev = *tree.items(at.seq).get(at.index.checked_sub(1)?)?;
is_selectable_structure(tree, prev).then_some(prev)
}
pub(crate) fn adjacent_structure_forward(tree: &Tree, at: Cursor) -> Option<NodeId> {
let next = *tree.items(at.seq).get(at.index)?;
is_selectable_structure(tree, next).then_some(next)
}
fn is_selectable_structure(tree: &Tree, node: NodeId) -> bool {
!tree.child_seqs(node).is_empty()
&& !matches!(tree.kind(node), Some(Kind::Styled { variant: Variant::Text, .. }))
}
pub(crate) fn is_illegal(tree: &Tree, at: Cursor) -> bool {
at.index == 0 && !tree.is_empty(at.seq) && tree.script_base_node(at.seq).is_some()
}
pub(crate) fn normalize(tree: &Tree, mut at: Cursor) -> Cursor {
at.index = at.index.min(tree.len(at.seq));
while is_illegal(tree, at) {
match tree.before_parent(at.seq) {
Some(c) => at = c,
None => break,
}
}
at
}
pub(crate) fn move_right(tree: &Tree, at: Cursor) -> Option<Cursor> {
if at.index < tree.len(at.seq) {
let node = tree.items(at.seq)[at.index];
return match tree.child_seqs(node).first() {
None => Some(Cursor { seq: at.seq, index: at.index + 1 }),
Some(&first) if is_illegal(tree, Cursor { seq: first, index: 0 }) => {
move_right(tree, Cursor { seq: first, index: 0 })
}
Some(&first) => Some(Cursor { seq: first, index: 0 }),
};
}
let parent = tree.seq_parent(at.seq)?;
let slots = tree.child_seqs(parent);
let pos = slots.iter().position(|&s| s == at.seq)?;
if let Some(&next) = slots.get(pos + 1) {
return Some(Cursor { seq: next, index: 0 });
}
let (pseq, pi) = tree.index_in_parent(parent)?;
Some(Cursor { seq: pseq, index: pi + 1 })
}
pub(crate) fn move_left(tree: &Tree, at: Cursor) -> Option<Cursor> {
let candidate = if at.index > 0 {
let node = tree.items(at.seq)[at.index - 1];
match tree.child_seqs(node).last() {
None => Cursor { seq: at.seq, index: at.index - 1 },
Some(&last) => Cursor { seq: last, index: tree.len(last) },
}
} else {
let parent = tree.seq_parent(at.seq)?;
let slots = tree.child_seqs(parent);
let pos = slots.iter().position(|&s| s == at.seq)?;
match pos.checked_sub(1) {
Some(p) => Cursor { seq: slots[p], index: tree.len(slots[p]) },
None => tree.before_parent(at.seq)?,
}
};
Some(normalize(tree, candidate))
}
fn vertical_step(tree: &Tree, at: Cursor, up: bool) -> Option<SeqId> {
let parent = tree.seq_parent(at.seq)?;
let stack: Vec<SeqId> = match tree.kind(parent)? {
Kind::Frac { num, den, .. } => vec![*num, *den],
Kind::Script { base, sub, sup } => sup.iter().copied().chain([*base]).chain(sub.iter().copied()).collect(),
Kind::BigOp { lower, upper, .. } => vec![*upper, *lower],
Kind::Sqrt { index, radicand } => vec![*index, *radicand],
Kind::UnderOver { base, over, under, .. } => {
over.iter().copied().chain([*base]).chain(under.iter().copied()).collect()
}
Kind::Matrix { rows, .. } => {
let (r, c) = rows
.iter()
.enumerate()
.find_map(|(ri, row)| row.iter().position(|&s| s == at.seq).map(|ci| (ri, ci)))?;
let nr = if up { r.checked_sub(1)? } else { r + 1 };
return rows.get(nr).and_then(|row| row.get(c)).copied();
}
_ => return None,
};
let pos = stack.iter().position(|&s| s == at.seq)?;
let next = if up { pos.checked_sub(1)? } else { pos + 1 };
stack.get(next).copied()
}
pub(crate) fn vertical(tree: &Tree, at: Cursor, up: bool) -> Option<Cursor> {
let mut from = at;
loop {
if let Some(target) = vertical_step(tree, from, up) {
let index = from.index.min(tree.len(target));
let index = if tree.script_base_node(target).is_some() { tree.len(target) } else { index };
return Some(normalize(tree, Cursor { seq: target, index }));
}
from = tree.before_parent(from.seq)?;
}
}
pub(crate) fn next_empty_slot(tree: &Tree, at: Cursor, forward: bool) -> Option<SeqId> {
let mut armed = false;
walk(tree, tree.root(), at, forward, &mut armed)
}
fn walk(tree: &Tree, seq: SeqId, at: Cursor, forward: bool, armed: &mut bool) -> Option<SeqId> {
if *armed && tree.is_empty(seq) {
return Some(seq);
}
let items = tree.items(seq);
let gaps: Box<dyn Iterator<Item = usize>> =
if forward { Box::new(0..=items.len()) } else { Box::new((0..=items.len()).rev()) };
for gap in gaps {
if seq == at.seq && gap == at.index {
*armed = true;
}
let node = if forward { items.get(gap) } else { gap.checked_sub(1).and_then(|g| items.get(g)) };
let Some(&node) = node else { continue };
let mut slots = tree.child_seqs(node);
if !forward {
slots.reverse();
}
for s in slots {
if let Some(found) = walk(tree, s, at, forward, armed) {
return Some(found);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{FracStyle, MathClass, ScriptSlot, Symbol};
fn atom(c: &str) -> Symbol {
Symbol { latex: c.into(), class: MathClass::Ord }
}
fn put(t: &mut Tree, seq: SeqId, s: &str) {
for ch in s.chars() {
let at = Cursor { seq, index: t.len(seq) };
t.insert_atom(at, None, atom(&ch.to_string())).unwrap();
}
}
fn x_squared(t: &mut Tree) -> (SeqId, NodeId, SeqId, SeqId) {
let root = t.root();
put(t, root, "x");
let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup, None).unwrap();
put(t, c.seq, "2");
let script = t.items(root)[0];
let slots = t.child_seqs(script);
(root, script, slots[0], slots[1])
}
#[test]
fn script_base_is_entered_at_its_end() {
let mut t = Tree::new();
let (root, _script, base, sup) = x_squared(&mut t);
let r = move_right(&t, Cursor { seq: root, index: 0 }).unwrap();
assert_eq!(r, Cursor { seq: base, index: 1 });
assert_eq!(move_right(&t, r).unwrap(), Cursor { seq: sup, index: 0 });
assert_eq!(move_left(&t, Cursor { seq: base, index: 1 }).unwrap(), Cursor { seq: root, index: 0 });
assert_eq!(normalize(&t, Cursor { seq: base, index: 0 }), Cursor { seq: root, index: 0 });
}
#[test]
fn structural_script_base_is_enterable() {
let mut t = Tree::new();
let root = t.root();
t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None).unwrap();
let frac = t.items(root)[0];
t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup, None).unwrap();
let base = t.child_seqs(t.items(root)[0])[0];
let (num, den) = (t.child_seqs(frac)[0], t.child_seqs(frac)[1]);
assert_eq!(move_right(&t, Cursor { seq: root, index: 0 }).unwrap(), Cursor { seq: num, index: 0 });
assert_eq!(move_left(&t, Cursor { seq: base, index: 1 }).unwrap().seq, den);
}
#[test]
fn bigop_left_navigation_visits_lower_then_upper() {
let mut t = Tree::new();
let root = t.root();
let op = Symbol { latex: "\\sum".into(), class: MathClass::Op };
t.insert_big_op(Cursor { seq: root, index: 0 }, None, op).unwrap();
let cs = t.child_seqs(t.items(root)[0]);
let (upper, lower) = (cs[0], cs[1]);
put(&mut t, lower, "2");
put(&mut t, upper, "3");
let mut c = Cursor { seq: root, index: 1 };
for (i, (seq, index)) in [(lower, 1), (lower, 0), (upper, 1), (upper, 0), (root, 0)].into_iter().enumerate() {
c = move_left(&t, c).unwrap_or_else(|| panic!("step {i}: no move"));
assert_eq!(c, Cursor { seq, index }, "left step {i}");
}
}
#[test]
fn vertical_climbs_to_a_movable_ancestor() {
let mut t = Tree::new();
let root = t.root();
let num = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None).unwrap();
let body = t.insert_delimiters(num, '(', ')', None).unwrap();
let den = t.child_seqs(t.items(root)[0])[1];
assert_eq!(vertical(&t, body, false).unwrap().seq, den);
assert_eq!(vertical(&t, body, true), None);
}
#[test]
fn vertical_moves_through_sqrt_and_under_over() {
let mut t = Tree::new();
let root = t.root();
let radicand = t.insert_sqrt(Cursor { seq: root, index: 0 }, None).unwrap();
let index = t.child_seqs(t.items(root)[0])[0];
assert_eq!(vertical(&t, radicand, true).unwrap().seq, index);
assert_eq!(vertical(&t, Cursor { seq: index, index: 0 }, false).unwrap().seq, radicand.seq);
let spec = crate::model::UnderOverSpec {
over: true,
under: true,
over_deco: crate::model::Deco::Brace,
under_deco: crate::model::Deco::None,
};
let base = t.insert_under_over(Cursor { seq: root, index: 1 }, spec, None).unwrap();
let slots = t.child_seqs(t.items(root)[1]);
assert_eq!(vertical(&t, base, true).unwrap().seq, slots[0]);
assert_eq!(vertical(&t, base, false).unwrap().seq, slots[2]);
}
#[test]
fn empty_slot_walk_matches_reading_order() {
let mut t = Tree::new();
let root = t.root();
let a = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None).unwrap();
let b = t.insert_fraction(Cursor { seq: root, index: 1 }, FracStyle::Bar, None).unwrap();
let den_a = t.child_seqs(t.items(root)[0])[1];
assert_eq!(next_empty_slot(&t, Cursor { seq: root, index: 0 }, true), Some(a.seq));
assert_eq!(next_empty_slot(&t, a, true), Some(den_a));
assert_eq!(next_empty_slot(&t, Cursor { seq: den_a, index: 0 }, true), Some(b.seq));
assert_eq!(next_empty_slot(&t, b, false), Some(den_a));
assert_eq!(next_empty_slot(&t, Cursor { seq: root, index: 0 }, false), None);
}
}