Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CSharpMath.Core.Tests/Editor/KeyPressTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -619,5 +619,57 @@ public void AssigningInsertionIndexClearsVerticalNavigationState() {
Assert.Equal("1^■", keyboard.LaTeX);
}

[Fact]
public void TableIndexRetainsRowAndColumnAndMovesVertically() {
var table = new CSharpMath.Atom.Atoms.Table();
table.SetCell(new CSharpMath.Atom.MathList(new CSharpMath.Atom.Atoms.Number("12")), 0, 0);
table.SetCell(new CSharpMath.Atom.MathList(new CSharpMath.Atom.Atoms.Number("3")), 1, 0);
var keyboard = new MathKeyboard<TestFont, TGlyph>(context, new TestFont(10));
keyboard.MathList.Add(table);
var atom = new MathListIndex(0).TableCell(0, 0, new MathListIndex(0));
var first = new MathListIndex(0).TableCell(0, 0, new MathListIndex(1));
Assert.IsType<CSharpMath.Atom.Atoms.Number>(keyboard.MathList.AtomAt(atom));
keyboard.InsertionIndex = first;
keyboard.KeyPress(K.Down);
Assert.Equal(new MathListIndex(0).TableCell(1, 0, new MathListIndex(1)), keyboard.InsertionIndex);
keyboard.KeyPress(K.Up);
Assert.Equal(first, keyboard.InsertionIndex);
}

[Fact]
public void TableNavigationSkipsMissingRowsAndStopsAtBoundaries() {
var table = new CSharpMath.Atom.Atoms.Table();
table.SetCell(new CSharpMath.Atom.MathList(new CSharpMath.Atom.Atoms.Number("1")), 0, 0);
table.SetCell(new CSharpMath.Atom.MathList(new CSharpMath.Atom.Atoms.Number("2")), 2, 0);
var keyboard = new MathKeyboard<TestFont, TGlyph>(context, new TestFont(10));
keyboard.MathList.Add(table);
var first = new MathListIndex(0).TableCell(0, 0, new MathListIndex(0));
keyboard.InsertionIndex = first;
keyboard.KeyPress(K.Up);
Assert.Equal(first, keyboard.InsertionIndex);
keyboard.KeyPress(K.Down);
var last = new MathListIndex(0).TableCell(2, 0, new MathListIndex(0));
Assert.Equal(last, keyboard.InsertionIndex);
keyboard.KeyPress(K.Up);
Assert.Equal(first, keyboard.InsertionIndex);
}

[Fact]
public void TableNavigationComposesWithFractionPathAndSkipsEmptyRows() {
var table = new CSharpMath.Atom.Atoms.Table();
table.SetCell(new CSharpMath.Atom.MathList(new CSharpMath.Atom.Atoms.Number("1")), 0, 0);
table.SetCell(new CSharpMath.Atom.MathList(new CSharpMath.Atom.Atoms.Number("2")), 2, 0);
var fraction = new CSharpMath.Atom.Atoms.Fraction(
new CSharpMath.Atom.MathList(table), new CSharpMath.Atom.MathList());
var keyboard = new MathKeyboard<TestFont, TGlyph>(context, new TestFont(10));
keyboard.MathList.Add(fraction);
var tableIndex = new MathListIndex(0).TableCell(0, 0, new MathListIndex(0));
var nestedIndex = tableIndex.Wrap(0, MathListSubIndexType.Numerator);
Assert.IsType<CSharpMath.Atom.Atoms.Number>(keyboard.MathList.AtomAt(nestedIndex));
keyboard.InsertionIndex = nestedIndex;
keyboard.KeyPress(K.Down);
Assert.Equal(new MathListIndex(0).TableCell(2, 0, new MathListIndex(0)).Wrap(0, MathListSubIndexType.Numerator), keyboard.InsertionIndex);
}

}
}
11 changes: 9 additions & 2 deletions CSharpMath/Editor/Extensions/MathList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ when self.Atoms[start.AtomIndex] is Atoms.Inner inner ? true
}

public static MathAtom? AtomAt(this MathList self, MathListIndex? index) {
if (index is null || index.AtomIndex >= self.Atoms.Count) return null;
if (index is null || index.AtomIndex < 0 || index.AtomIndex >= self.Atoms.Count) return null;
var atom = self.Atoms[index.AtomIndex];
return index.SubIndexInfo switch {
null => atom,
Expand All @@ -217,8 +217,15 @@ when self.Atoms[start.AtomIndex] is Atoms.Inner inner ? true
(MathListSubIndexType.Numerator, var subIndex) => atom is Atoms.Fraction frac ? frac.Numerator.AtomAt(subIndex) : null,
(MathListSubIndexType.Denominator, var subIndex) => atom is Atoms.Fraction frac ? frac.Denominator.AtomAt(subIndex) : null,
(MathListSubIndexType.Inner, var subIndex) => atom is Atoms.Inner inner ? inner.InnerList.AtomAt(subIndex) : null,
(MathListSubIndexType.TableRow, var rowIndex) => atom is Atoms.Table table
&& rowIndex.AtomIndex >= 0 && rowIndex.AtomIndex < table.Cells.Count
&& rowIndex.SubIndexInfo is (MathListSubIndexType.TableColumn, var columnIndex)
&& columnIndex.AtomIndex >= 0 && columnIndex.AtomIndex < table.Cells[rowIndex.AtomIndex].Count
&& columnIndex.SubIndexInfo is (MathListSubIndexType.TableCell, var cellIndex)
? table.Cells[rowIndex.AtomIndex][columnIndex.AtomIndex].AtomAt(cellIndex)
: null,
(var type, _) => throw new ArgumentOutOfRangeException(nameof(index), type, "Index type out of valid range."),
};
}
}
}
}
67 changes: 67 additions & 0 deletions CSharpMath/Editor/MathKeyboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,73 @@ static MathListIndex Enter(MathListIndex owner, MathAtom atom,
break;
}
}
// Tables use an explicit row/column path; this keeps vertical movement
// independent of the display tree (which may contain empty cells).
static MathList? ChildList(MathAtom atom, MathListSubIndexType type) => type switch {
MathListSubIndexType.Superscript => atom.Superscript,
MathListSubIndexType.Subscript => atom.Subscript,
MathListSubIndexType.Numerator when atom is Atoms.Fraction f => f.Numerator,
MathListSubIndexType.Denominator when atom is Atoms.Fraction f => f.Denominator,
MathListSubIndexType.Radicand when atom is Atoms.Radical r => r.Radicand,
MathListSubIndexType.Degree when atom is Atoms.Radical r => r.Degree,
MathListSubIndexType.Inner when atom is Atoms.Inner i => i.InnerList,
_ => null,
};

MathListIndex FindTable(MathList list, MathListIndex index, bool down, out bool handled) {
handled = false;
if (index.AtomIndex < 0 || index.AtomIndex >= list.Count || index.SubIndexInfo is not { } info)
return index;
var atom = list[index.AtomIndex];
if (info.SubIndexType == MathListSubIndexType.TableRow
&& atom is Atoms.Table table
&& info.SubIndex.SubIndexInfo is (MathListSubIndexType.TableColumn, var column)
&& column.SubIndexInfo is (MathListSubIndexType.TableCell, var cell)) {
if (info.SubIndex.AtomIndex < 0 || info.SubIndex.AtomIndex >= table.Cells.Count
|| column.AtomIndex < 0 || column.AtomIndex >= table.Cells[info.SubIndex.AtomIndex].Count) {
handled = true;
return index;
}
// Prefer a nested table in the current cell, retaining this complete prefix.
var cellResult = FindTable(table.Cells[info.SubIndex.AtomIndex][column.AtomIndex], cell, down, out handled);
if (handled)
return new(index.AtomIndex, (info.SubIndexType,
new(info.SubIndex.AtomIndex, (MathListSubIndexType.TableColumn,
new(column.AtomIndex, (MathListSubIndexType.TableCell, cellResult))))));
var row = info.SubIndex.AtomIndex;
var sourceColumn = column.AtomIndex;
var targetRow = row + (down ? 1 : -1);
while (targetRow >= 0 && targetRow < table.NRows && table.Cells[targetRow].Count == 0)
targetRow += down ? 1 : -1;
if (targetRow < 0 || targetRow >= table.NRows) { handled = true; return index; }
var targetColumn = Math.Min(sourceColumn, table.Cells[targetRow].Count - 1);
var targetCell = table.Cells[targetRow][targetColumn];
var targetCaret = Math.Max(0, Math.Min(cell.AtomIndex, targetCell.Count));
var candidate = new MathListIndex(index.AtomIndex).TableCell(targetRow, targetColumn, new MathListIndex(targetCaret));
// Older display backends do not yet expose table row paths to PointForIndex;
// retain the deterministic caret fallback when that seam cannot resolve one.
try {
var sourcePoint = ClosestPointToIndex(index);
if (sourcePoint is PointF point && ClosestPointToIndex(candidate) is PointF)
candidate = VerticalIndexAtPoint(candidate, point) ?? candidate;
} catch (ArgumentOutOfRangeException) {
// The model path remains valid even when the display path is not indexed.
}
handled = true;
return candidate;
}
var child = ChildList(atom, info.SubIndexType);
if (child is null) return index;
var nested = FindTable(child, info.SubIndex, down, out handled);
return handled ? new(index.AtomIndex, (info.SubIndexType, nested)) : index;
}
bool MoveCursorInTable(bool down) {
var result = FindTable(MathList, _insertionIndex, down, out var handled);
if (handled) _insertionIndex = result;
return handled;
}
void MoveCursorUp() {
if (MoveCursorInTable(false)) return;
if (Display is null) RecreateDisplayFromMathList();
if (MathList.AtomAt(_insertionIndex) is Atoms.Placeholder { Superscript: { Count: var superCount } } && superCount > 0) {
_insertionIndex = _insertionIndex.LevelUpWithSubIndex(MathListSubIndexType.Superscript, 0);
Expand Down Expand Up @@ -455,6 +521,7 @@ atom is Atoms.Placeholder
}
}
void MoveCursorDown() {
if (MoveCursorInTable(true)) return;
if (Display is null) RecreateDisplayFromMathList();
if (MathList.AtomAt(_insertionIndex) is Atoms.Placeholder { Subscript: { Count: var subCount } } && subCount > 0) {
_insertionIndex = _insertionIndex.LevelUpWithSubIndex(MathListSubIndexType.Subscript, 0);
Expand Down
15 changes: 13 additions & 2 deletions CSharpMath/Editor/MathListIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ public enum MathListSubIndexType : byte {
///<summary>The subindex indexes into the degree (only valid for radicals)</summary>
Degree,
///<summary>The subindex indexes into the inner list (only valid for inners)</summary>
Inner
Inner,
/// <summary>The subindex selects a row in a table.</summary>
TableRow,
/// <summary>The subindex selects a cell in a table row.</summary>
TableColumn,
/// <summary>The subindex is the caret index within a table cell.</summary>
TableCell
}

/** <summary>
Expand All @@ -35,6 +41,11 @@ public enum MathListSubIndexType : byte {
* The level of an index is the number of nodes in the LinkedList to get to the final path.
* </summary>*/
public record class MathListIndex(int AtomIndex, (MathListSubIndexType SubIndexType, MathListIndex SubIndex)? SubIndexInfo = null) {
/// <summary>Creates an index into a table cell while retaining its row and column.</summary>
public MathListIndex TableCell(int row, int column, MathListIndex cellIndex) =>
new(AtomIndex, (MathListSubIndexType.TableRow,
new(row, (MathListSubIndexType.TableColumn,
new(column, (MathListSubIndexType.TableCell, cellIndex))))));
/// <summary>
/// Creates a new MathListIndex that represents a subindex within this list, wrapped at the specified outer atom
/// index and subindex type.
Expand Down Expand Up @@ -111,4 +122,4 @@ public override string ToString() =>
var (type, subIndex) => $@"[{AtomIndex}, {type}:{subIndex.ToString().Trim('[', ']')}]"
};
}
}
}
4 changes: 4 additions & 0 deletions CSharpMath/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ CSharpMath.Editor.MathKeyboardHorizontalNavigationMode
CSharpMath.Editor.MathKeyboardHorizontalNavigationMode.Exhaustive = 0 -> CSharpMath.Editor.MathKeyboardHorizontalNavigationMode
CSharpMath.Editor.MathKeyboardHorizontalNavigationMode.VisualLower = 2 -> CSharpMath.Editor.MathKeyboardHorizontalNavigationMode
CSharpMath.Editor.MathKeyboardHorizontalNavigationMode.VisualUpper = 1 -> CSharpMath.Editor.MathKeyboardHorizontalNavigationMode
CSharpMath.Editor.MathListSubIndexType.TableRow = 8 -> CSharpMath.Editor.MathListSubIndexType
CSharpMath.Editor.MathListSubIndexType.TableColumn = 9 -> CSharpMath.Editor.MathListSubIndexType
CSharpMath.Editor.MathListSubIndexType.TableCell = 10 -> CSharpMath.Editor.MathListSubIndexType
CSharpMath.Editor.MathListIndex.TableCell(int row, int column, CSharpMath.Editor.MathListIndex! cellIndex) -> CSharpMath.Editor.MathListIndex!
Loading