diff --git a/CSharpMath.Core.Tests/Editor/KeyPressTests.cs b/CSharpMath.Core.Tests/Editor/KeyPressTests.cs index 0f0aef58..abf345f7 100644 --- a/CSharpMath.Core.Tests/Editor/KeyPressTests.cs +++ b/CSharpMath.Core.Tests/Editor/KeyPressTests.cs @@ -12,11 +12,128 @@ namespace CSharpMath.Core.EditorTests { using Editor; public class KeyPressTests { private static readonly TypesettingContext context = TestTypesettingContext.Instance; - static void Test(string latex, K[] inputs) { + static void Test(string latex, params K[] inputs) { var keyboard = new MathKeyboard(context, new TestFont()); keyboard.KeyPress(inputs); Assert.Equal(latex, keyboard.LaTeX); } + static void Test(string latex, TestFont font, params K[] inputs) { + var keyboard = new MathKeyboard(context, font); + keyboard.KeyPress(inputs); + Assert.Equal(latex, keyboard.LaTeX); + } + static void TestVisual(MathKeyboardHorizontalNavigationMode mode, string latex, params K[] inputs) { + var keyboard = VisualKeyboard(mode); + keyboard.KeyPress(inputs); + Assert.Equal(latex, keyboard.LaTeX); + } + static MathKeyboard VisualKeyboard(MathKeyboardHorizontalNavigationMode mode) => + new(context, new TestFont(10)) { HorizontalNavigationMode = mode }; + + [Fact] + public void HorizontalNavigationDefaultsToExhaustive() { + var keyboard = new MathKeyboard(context, new TestFont()); + Assert.Equal(MathKeyboardHorizontalNavigationMode.Exhaustive, keyboard.HorizontalNavigationMode); + } + + [Fact] + public void VisualUpperFractionLeavesEitherRowAndReentersNumerator() => + TestVisual(MathKeyboardHorizontalNavigationMode.VisualUpper, @"\frac{13}{2}", + K.D1, K.Slash, K.D2, K.Right, K.Left, K.D3); + + [Fact] + public void VisualLowerFractionLeavesEitherRowAndReentersDenominator() => + TestVisual(MathKeyboardHorizontalNavigationMode.VisualLower, @"\frac{1}{23}", + K.D1, K.Slash, K.D2, K.Right, K.Left, K.D3); + + [Theory] + [InlineData(MathKeyboardHorizontalNavigationMode.VisualUpper, MathListSubIndexType.Numerator)] + [InlineData(MathKeyboardHorizontalNavigationMode.VisualLower, MathListSubIndexType.Denominator)] + public void VisualFractionEntryAndExitAreDirectional( + MathKeyboardHorizontalNavigationMode mode, MathListSubIndexType preferredBranch) { + var keyboard = VisualKeyboard(mode); + keyboard.KeyPress(K.D1, K.Slash, K.D2); + var fraction = Assert.IsType(Assert.Single(keyboard.MathList)); + var branchCount = preferredBranch == MathListSubIndexType.Numerator + ? fraction.Numerator.Count : fraction.Denominator.Count; + + keyboard.InsertionIndex = new(0); + keyboard.KeyPress(K.Right); + Assert.Equal(new MathListIndex(0).LevelUpWithSubIndex(preferredBranch, 0), keyboard.InsertionIndex); + keyboard.KeyPress(K.Left); + Assert.Equal(new MathListIndex(0), keyboard.InsertionIndex); + + keyboard.InsertionIndex = new(1); + keyboard.KeyPress(K.Left); + Assert.Equal(new MathListIndex(0).LevelUpWithSubIndex(preferredBranch, branchCount), keyboard.InsertionIndex); + keyboard.KeyPress(K.Right); + Assert.Equal(new MathListIndex(1), keyboard.InsertionIndex); + } + + [Theory] + [InlineData(MathKeyboardHorizontalNavigationMode.VisualUpper, MathListSubIndexType.Numerator, MathListSubIndexType.Denominator, K.Down)] + [InlineData(MathKeyboardHorizontalNavigationMode.VisualLower, MathListSubIndexType.Denominator, MathListSubIndexType.Numerator, K.Up)] + public void VisualFractionOtherRowRemainsReachableVertically( + MathKeyboardHorizontalNavigationMode mode, MathListSubIndexType preferredBranch, + MathListSubIndexType otherBranch, K verticalInput) { + var keyboard = VisualKeyboard(mode); + keyboard.KeyPress(K.D1, K.Slash, K.D2); + keyboard.InsertionIndex = new MathListIndex(0).LevelUpWithSubIndex(preferredBranch, 0); + + keyboard.KeyPress(verticalInput); + + Assert.Equal(otherBranch, keyboard.InsertionIndex.FinalSubIndexType); + keyboard.InsertionIndex = new MathListIndex(0).LevelUpWithSubIndex(otherBranch, 0); + keyboard.KeyPress(K.Left); + Assert.Equal(new MathListIndex(0), keyboard.InsertionIndex); + } + + [Theory] + [InlineData(MathKeyboardHorizontalNavigationMode.VisualUpper, MathListSubIndexType.Superscript)] + [InlineData(MathKeyboardHorizontalNavigationMode.VisualLower, MathListSubIndexType.Subscript)] + public void VisualScriptPolicyChoosesPreferredScript( + MathKeyboardHorizontalNavigationMode mode, MathListSubIndexType preferredBranch) { + var keyboard = VisualKeyboard(mode); + keyboard.KeyPress(K.SmallX, K.Subscript, K.D2, K.Right, K.Power, K.D3); + + keyboard.InsertionIndex = new(1); + keyboard.KeyPress(K.Left); + + Assert.Equal(preferredBranch, keyboard.InsertionIndex.FinalSubIndexType); + } + + [Theory] + [InlineData(MathKeyboardHorizontalNavigationMode.VisualUpper, K.Subscript, MathListSubIndexType.Subscript)] + [InlineData(MathKeyboardHorizontalNavigationMode.VisualLower, K.Power, MathListSubIndexType.Superscript)] + public void VisualScriptPolicyFallsBackToOnlyScript( + MathKeyboardHorizontalNavigationMode mode, K scriptInput, MathListSubIndexType availableBranch) { + var keyboard = VisualKeyboard(mode); + keyboard.KeyPress(K.SmallX, scriptInput, K.D2); + keyboard.InsertionIndex = new(1); + + keyboard.KeyPress(K.Left); + + Assert.Equal(availableBranch, keyboard.InsertionIndex.FinalSubIndexType); + } + + [Theory] + [InlineData(MathKeyboardHorizontalNavigationMode.VisualUpper, MathListSubIndexType.Numerator, MathListSubIndexType.Superscript)] + [InlineData(MathKeyboardHorizontalNavigationMode.VisualLower, MathListSubIndexType.Denominator, MathListSubIndexType.Subscript)] + public void VisualCompoundRetainsIntrinsicAndScriptStages( + MathKeyboardHorizontalNavigationMode mode, MathListSubIndexType intrinsicBranch, + MathListSubIndexType scriptBranch) { + var keyboard = VisualKeyboard(mode); + keyboard.KeyPress(K.D1, K.Slash, K.D2, K.Right, K.Power, K.D3, K.Right, K.Subscript, K.D4); + var fraction = Assert.IsType(Assert.Single(keyboard.MathList)); + var intrinsicCount = intrinsicBranch == MathListSubIndexType.Numerator + ? fraction.Numerator.Count : fraction.Denominator.Count; + keyboard.InsertionIndex = new MathListIndex(0).LevelUpWithSubIndex(intrinsicBranch, intrinsicCount); + + keyboard.KeyPress(K.Right); + Assert.Equal(MathListSubIndexType.BetweenBaseAndScripts, keyboard.InsertionIndex.FinalSubIndexType); + keyboard.KeyPress(K.Right); + Assert.Equal(scriptBranch, keyboard.InsertionIndex.FinalSubIndexType); + } static void TestEvent(EventInteractor attach, EventInteractor detach, K[] inputs) { var keyboard = new MathKeyboard(context, new TestFont()); Assert.Raises( @@ -402,5 +519,105 @@ public void Return(params K[] inputs) => T(@"\frac{\lim _{x\rightarrow 2}}{■}", K.LimitWithBase, K.SmallX, K.RightArrow, K.D2, K.Right, K.Slash), ] public void Slash(string latex, params K[] inputs) => Test(latex, inputs); + + [Theory, + T(@"", K.Up, K.Up, K.Up, K.Down, K.Down, K.Down), + T(@"1", K.Up, K.D1), T(@"1", K.Down, K.D1), + T(@"\frac{\square }{■}", K.Fraction, K.Down), + T(@"\frac{\square }{1}", K.Fraction, K.Down, K.D1), + T(@"\frac{■}{\square }", K.Fraction, K.Down, K.Up), + T(@"\frac{1}{\square }", K.Fraction, K.Down, K.Up, K.D1), + T(@"\frac{\square }{■}", K.Fraction, K.Down, K.Up, K.Down), + T(@"\frac{\square }{1}", K.Fraction, K.Down, K.Up, K.Down, K.D1), + T(@"\frac{123x456}{y}", K.Fraction, K.D1, K.D2, K.D3, K.D4, K.D5, K.D6, K.Down, K.Up, K.SmallX, K.Down, K.SmallY), + T(@"\frac{y}{123x456}", K.Fraction, K.Down, K.D1, K.D2, K.D3, K.D4, K.D5, K.D6, K.Up, K.Down, K.SmallX, K.Up, K.SmallY), + T(@"\frac{1234z56}{xy}", K.Fraction, K.D1, K.D2, K.D3, K.D4, K.D5, K.D6, K.Down, K.SmallX, K.SmallY, K.Up, K.SmallZ), + T(@"\frac{xy}{1234z56}", K.Fraction, K.Down, K.D1, K.D2, K.D3, K.D4, K.D5, K.D6, K.Up, K.SmallX, K.SmallY, K.Down, K.SmallZ), + T(@"\frac{\sqrt{2}}{■}", K.Fraction, K.SquareRoot, K.D2, K.Down), + T(@"\frac{■}{\sqrt{2}}", K.Fraction, K.Down, K.SquareRoot, K.D2, K.Up), + T(@"\frac{\sqrt{789}}{\square }", K.Fraction, K.SquareRoot, K.D8, K.D9, K.Down, K.Up, K.D7), + T(@"\frac{\square }{\sqrt{789}}", K.Fraction, K.Down, K.SquareRoot, K.D8, K.D9, K.Up, K.Down, K.D7), + T(@"\frac{\left( a\right) }{\left( b\right) }", K.Fraction, K.BothRoundBrackets, K.Down, K.BothRoundBrackets, K.Up, K.SmallA, K.Down, K.SmallB), + T(@"\frac{\left( b\right) }{\left( a\right) }", K.Fraction, K.Down, K.BothRoundBrackets, K.Up, K.BothRoundBrackets, K.Down, K.SmallA, K.Up, K.SmallB), + T(@"2^{ab}c", K.D2, K.Power, K.SmallA, K.Up, K.SmallB, K.Down, K.SmallC), + T(@"2_{ab}c", K.D2, K.Subscript, K.SmallA, K.Down, K.SmallB, K.Up, K.SmallC), + T(@"\square _1^3", K.Subscript, K.Up, K.Power, K.Down, K.D1, K.Up, K.D3), + T(@"123^■", K.D1, K.Power, K.Down, K.D2, K.D3, K.Up), + T(@"123_■", K.D1, K.Subscript, K.Up, K.D2, K.D3, K.Down), + T(@"1^{\square ^{\square ^{4^{\square }}}}", K.Power, K.Power, K.Power, K.Power, K.Down, K.Down, K.Down, K.Down, K.D1, K.Up, K.Up, K.Up, K.D4), + T(@"1_{\square _{\square _{4_{\square }}}}", K.Subscript, K.Subscript, K.Subscript, K.Subscript, K.Up, K.Up, K.Up, K.Up, K.D1, K.Down, K.Down, K.Down, K.D4), + T(@"2_{cab}^{\square }", K.D2, K.Subscript, K.SmallA, K.SmallB, K.Up, K.Power, K.Down, K.SmallC), + T(@"2c_{\square }^{ab}", K.D2, K.Power, K.SmallA, K.SmallB, K.Down, K.Subscript, K.Up, K.SmallC), + T(@"\square ^{\square }2", K.Power, K.Down, K.D2), + T(@"\square _{\square }2", K.Subscript, K.Up, K.D2), + T(@"1^{\square ^{\square ^{\square ^5}}}", K.Power, K.Power, K.Power, K.Power, K.Down, K.Down, K.Down, K.Down, K.D1, K.Up, K.Up, K.Up, K.Up, K.D5), + T(@"1_{\square _{\square _{\square _5}}}", K.Subscript, K.Subscript, K.Subscript, K.Subscript, K.Up, K.Up, K.Up, K.Up, K.D1, K.Down, K.Down, K.Down, K.Down, K.D5), + T(@"\frac{1^{\square ^{\square ^{4^{\square }}}}}{\square }", K.Fraction, K.Power, K.Power, K.Power, K.Power, K.Down, K.Down, K.Down, K.Down, K.D1, K.Up, K.Up, K.Up, K.D4), + T(@"\frac{1_{\square _{\square _{4_{\square }}}}}{\square }", K.Fraction, K.Subscript, K.Subscript, K.Subscript, K.Subscript, K.Up, K.Up, K.Up, K.Up, K.D1, K.Down, K.Down, K.Down, K.D4), + T(@"\frac{\square }{1^{\square ^{\square ^{4^{\square }}}}}", K.Fraction, K.Down, K.Power, K.Power, K.Power, K.Power, K.Down, K.Down, K.Down, K.Down, K.D1, K.Up, K.Up, K.Up, K.D4), + T(@"\frac{\square }{1_{\square _{\square _{4_{\square }}}}}", K.Fraction, K.Down, K.Subscript, K.Subscript, K.Subscript, K.Subscript, K.Up, K.Up, K.Up, K.Up, K.D1, K.Down, K.Down, K.Down, K.D4), + T(@"\frac{1^{\square ^{\square ^{\square ^5}}}}{\square }", K.Fraction, K.Power, K.Power, K.Power, K.Power, K.Down, K.Down, K.Down, K.Down, K.D1, K.Up, K.Up, K.Up, K.Up, K.D5), + T(@"\frac{1_{\square _{\square _{\square _5}}}}{\square }", K.Fraction, K.Subscript, K.Subscript, K.Subscript, K.Subscript, K.Up, K.Up, K.Up, K.Up, K.D1, K.Down, K.Down, K.Down, K.Down, K.D5), + T(@"\frac{\square }{1^{\square ^{\square ^{\square ^5}}}}", K.Fraction, K.Down, K.Power, K.Power, K.Power, K.Power, K.Down, K.Down, K.Down, K.Down, K.D1, K.Up, K.Up, K.Up, K.Up, K.D5), + T(@"\frac{\square }{1_{\square _{\square _{\square _5}}}}", K.Fraction, K.Down, K.Subscript, K.Subscript, K.Subscript, K.Subscript, K.Up, K.Up, K.Up, K.Up, K.D1, K.Down, K.Down, K.Down, K.Down, K.D5)] + public void UpDownNavigation(string latex, params K[] inputs) => Test(latex, new TestFont(10), inputs); + + [Theory, + T(@"\square ^21", K.Power, K.D2, K.Left, K.Down, K.D1), + T(@"\square _21", K.Subscript, K.D2, K.Left, K.Up, K.D1), + T(@"01^2", K.D0, K.Power, K.D2, K.Left, K.Down, K.D1), + T(@"01_2", K.D0, K.Subscript, K.D2, K.Left, K.Up, K.D1), + T(@"1^{23}", K.Power, K.D2, K.D3, K.Left, K.Left, K.Down, K.D1), + T(@"1_{23}", K.Subscript, K.D2, K.D3, K.Left, K.Left, K.Up, K.D1), + T(@"x^{zy}", K.SmallX, K.Power, K.SmallY, K.Left, K.Left, K.Up, K.SmallZ), + T(@"x_{zy}", K.SmallX, K.Subscript, K.SmallY, K.Left, K.Left, K.Down, K.SmallZ), + T(@"1^23", K.D1, K.Power, K.Right, K.D3, K.Left, K.Left, K.Up, K.D2), + T(@"1_23", K.D1, K.Subscript, K.Right, K.D3, K.Left, K.Left, K.Down, K.D2), + T(@"1^{\left( 2\right) }3", K.D1, K.Power, K.BothRoundBrackets, K.Right, K.Right, K.D3, K.Left, K.Up, K.Left, K.D2), + T(@"1_{\left( 2\right) }3", K.D1, K.Subscript, K.BothRoundBrackets, K.Right, K.Right, K.D3, K.Left, K.Down, K.Left, K.D2), + T(@"\frac{a_{123x456}}{b^{6543y21}}", K.Fraction, K.SmallA, K.Subscript, K.D1, K.D2, K.D3, K.D4, K.D5, K.D6, K.Down, K.Down, + K.SmallB, K.Power, K.D6, K.D5, K.D4, K.D3, K.D2, K.D1, K.Left, K.Left, K.Left, K.Up, K.SmallX, K.Down, K.SmallY), + T(@"\frac{a^{1e23456}b}{cd_{654321}}", K.Fraction, K.SmallA, K.Power, K.D1, K.D2, K.D3, K.D4, K.D5, K.D6, K.Down, K.SmallB, K.Down, + K.SmallC, K.Subscript, K.D6, K.D5, K.D4, K.D3, K.D2, K.D1, K.Left, K.Left, K.Left, K.Up, K.SmallD, K.Up, K.SmallE), + T(@"1^{\square ^{\square ^{4^{\square }}}}", K.Power, K.Power, K.Power, K.Power, K.Left, K.Left, K.Left, K.Left, K.D1, K.Up, K.Up, K.Up, K.D4), + T(@"1_{\square _{\square _{4_{\square }}}}", K.Subscript, K.Subscript, K.Subscript, K.Subscript, K.Left, K.Left, K.Left, K.Left, K.D1, K.Down, K.Down, K.Down, K.D4), + T(@"\frac{1^{\square ^{\square ^{4^{\square }}}}}{\square }", K.Fraction, K.Power, K.Power, K.Power, K.Power, K.Left, K.Left, K.Left, K.Left, K.D1, K.Up, K.Up, K.Up, K.D4), + T(@"\frac{1_{\square _{\square _{4_{\square }}}}}{\square }", K.Fraction, K.Subscript, K.Subscript, K.Subscript, K.Subscript, K.Left, K.Left, K.Left, K.Left, K.D1, K.Down, K.Down, K.Down, K.D4), + T(@"\frac{\square }{1^{\square ^{\square ^{4^{\square }}}}}", K.Fraction, K.Down, K.Power, K.Power, K.Power, K.Power, K.Left, K.Left, K.Left, K.Left, K.D1, K.Up, K.Up, K.Up, K.D4), + T(@"\frac{\square }{1_{\square _{\square _{4_{\square }}}}}", K.Fraction, K.Down, K.Subscript, K.Subscript, K.Subscript, K.Subscript, K.Left, K.Left, K.Left, K.Left, K.D1, K.Down, K.Down, K.Down, K.D4), + T(@"1^{\square ^{\square ^{\square ^{\square }}7}8}9", K.D1, K.Power, K.Power, K.Power, K.Power, K.Right, K.Down, K.Down, K.Down, K.D9, K.Left, K.Up, K.Up, K.D7, K.Down, K.D8), + T(@"1_{\square _{\square _{\square _{\square }}7}8}9", K.D1, K.Subscript, K.Subscript, K.Subscript, K.Subscript, K.Right, K.Up, K.Up, K.Up, K.D9, K.Left, K.Down, K.Down, K.D7, K.Up, K.D8), + T(@"\frac{1^{\square ^{\square ^{\square ^{\square }}7}8}9}{\square }", K.Fraction, K.D1, K.Power, K.Power, K.Power, K.Power, K.Right, K.Down, K.Down, K.Down, K.D9, K.Left, K.Up, K.Up, K.D7, K.Down, K.D8), + T(@"\frac{1_{\square _{\square _{\square _{\square }}7}8}9}{\square }", K.Fraction, K.D1, K.Subscript, K.Subscript, K.Subscript, K.Subscript, K.Right, K.Up, K.Up, K.Up, K.D9, K.Left, K.Down, K.Down, K.D7, K.Up, K.D8), + T(@"\frac{\square }{1^{\square ^{\square ^{\square ^{\square }}7}8}9}", K.Fraction, K.Down, K.D1, K.Power, K.Power, K.Power, K.Power, K.Right, K.Down, K.Down, K.Down, K.D9, K.Left, K.Up, K.Up, K.D7, K.Down, K.D8), + T(@"\frac{\square }{1_{\square _{\square _{\square _{\square }}7}8}9}", K.Fraction, K.Down, K.D1, K.Subscript, K.Subscript, K.Subscript, K.Subscript, K.Right, K.Up, K.Up, K.Up, K.D9, K.Left, K.Down, K.Down, K.D7, K.Up, K.D8), + T(@"\frac{ef^{gh}}{\square }", K.Fraction, K.SmallE, K.Power, K.SmallH, K.Left, K.Down, K.SmallF, K.Up, K.SmallG), + T(@"\frac{ef_{gh}}{\square }", K.Fraction, K.SmallE, K.Subscript, K.SmallH, K.Left, K.Up, K.SmallF, K.Down, K.SmallG), + T(@"\frac{\square }{ef^{gh}}", K.Fraction, K.Down, K.SmallE, K.Power, K.SmallH, K.Left, K.Down, K.SmallF, K.Up, K.SmallG), + T(@"\frac{\square }{ef_{gh}}", K.Fraction, K.Down, K.SmallE, K.Subscript, K.SmallH, K.Left, K.Up, K.SmallF, K.Down, K.SmallG), + T(@"\frac{a^b}{c}", K.Fraction, K.SmallA, K.Power, K.SmallB, K.Left, K.Down, K.Down, K.SmallC), + T(@"\frac{a_{tb}}{c}", K.Fraction, K.SmallA, K.Subscript, K.SmallB, K.Left, K.Up, K.Down, K.SmallT, K.Down, K.SmallC), + T(@"\frac{c}{a^{tb}}", K.Fraction, K.Down, K.SmallA, K.Power, K.SmallB, K.Left, K.Down, K.Up, K.SmallT, K.Up, K.SmallC), + T(@"\frac{c}{a_b}", K.Fraction, K.Down, K.SmallA, K.Subscript, K.SmallB, K.Left, K.Up, K.Up, K.SmallC)] + public void FourDirectionalNavigation(string latex, params K[] inputs) => Test(latex, new TestFont(10), inputs); + + [Fact] + public void NavigationUsesNonzeroWidthGeometryAndStaysInBranch() => + Test(@"\frac{a^b}{c_d}", new TestFont(10), K.Fraction, K.SmallA, K.Power, K.SmallB, + K.Left, K.Down, K.Down, K.SmallC, K.Subscript, K.SmallD); + + [Fact] + public void NavigationSupportsZeroWidthFont() => + Test(@"\frac{\square }{1}", K.Fraction, K.Down, K.D1); + + [Fact] + public void AssigningInsertionIndexClearsVerticalNavigationState() { + var keyboard = new MathKeyboard(context, new TestFont(10)); + keyboard.KeyPress(K.Power, K.Down); + keyboard.InsertionIndex = new(0); + keyboard.KeyPress(K.D1, K.Left); + Assert.Equal("1^■", keyboard.LaTeX); + } + } -} \ No newline at end of file +} diff --git a/CSharpMath/Editor/MathKeyboard.cs b/CSharpMath/Editor/MathKeyboard.cs index 0e9305a3..b1c53c87 100644 --- a/CSharpMath/Editor/MathKeyboard.cs +++ b/CSharpMath/Editor/MathKeyboard.cs @@ -8,6 +8,16 @@ namespace CSharpMath.Editor { using Display.FrontEnd; using Atoms = Atom.Atoms; + /// Controls which vertically-related branch horizontal navigation enters. + public enum MathKeyboardHorizontalNavigationMode : byte { + /// Visit all branches in the historical structural order. + Exhaustive, + /// Prefer the upper branch and leave a compound from either branch. + VisualUpper, + /// Prefer the lower branch and leave a compound from either branch. + VisualLower + } + public class MathKeyboard : IDisposable where TFont : IFont { protected Timer blinkTimer; public const double DefaultBlinkMilliseconds = 800; @@ -22,6 +32,8 @@ public MathKeyboard(TypesettingContext context, TFont font, doubl blinkTimer.Start(); } public bool ShouldDrawCaret => InsertionPositionHighlighted && !(MathList.AtomAt(_insertionIndex) is Atoms.Placeholder); + /// Gets or sets the horizontal navigation policy. + public MathKeyboardHorizontalNavigationMode HorizontalNavigationMode { get; set; } public void StartBlinking() => blinkTimer.Start(); public void StopBlinking() => blinkTimer.Stop(); protected TypesettingContext Context { get; } @@ -62,9 +74,11 @@ public bool InsertionPositionHighlighted { public MathList MathList { get; } = []; public string LaTeX => LaTeXParser.MathListToLaTeX(MathList).ToString(); private MathListIndex _insertionIndex = new(0); + private bool _insertionIndexCameFromVerticalNavigation; public MathListIndex InsertionIndex { get => _insertionIndex; set { + _insertionIndexCameFromVerticalNavigation = false; _insertionIndex = value; ResetPlaceholders(MathList); InsertionPositionHighlighted = true; @@ -90,6 +104,84 @@ public void RecreateDisplayFromMathList() { Display?.PointForIndex(Context, index); public MathListIndex? ClosestIndexToPoint(PointF point) => Display?.IndexForPoint(Context, point); + + // Hit-test in the list selected by the destination index. A root-level + // hit-test can select a sibling branch when the destination is nested, + // losing part of the index path. + MathListIndex? IndexForVerticalPoint( + Display.Displays.ListDisplay display, + MathListIndex target, + PointF point) { + if (target.SubIndexInfo is not { } subIndexInfo + || subIndexInfo.SubIndexType is MathListSubIndexType.BetweenBaseAndScripts) + return display.IndexForPoint(Context, point); + + var (type, subIndex) = subIndexInfo; + var child = display.SubDisplayForIndex(target); + if (child is null) + return null; + var translatedPoint = new PointF(point.X - display.Position.X, point.Y - display.Position.Y); + var childIndex = child is Display.Displays.ListDisplay childList + ? IndexForVerticalPoint(childList, subIndex, translatedPoint) + : child.IndexForPoint(Context, translatedPoint); + return childIndex?.Wrap(target.AtomIndex, type); + } + + MathListIndex? VerticalIndexAtPoint(MathListIndex target, PointF sourcePoint, bool constrainToTargetList = true) { + if (Display is null) + return null; + if (Display.Width == 0) + return CenteredVerticalIndex(_insertionIndex, target); + var targetPoint = ClosestPointToIndex(target); + return targetPoint is PointF point + ? constrainToTargetList + ? IndexForVerticalPoint(Display, target, new(sourcePoint.X, point.Y)) + : ClosestIndexToPoint(new(sourcePoint.X, point.Y)) + : null; + } + + MathListIndex CenteredVerticalIndex(MathListIndex source, MathListIndex target) { + MathList? ListAtIndex(MathListIndex index) { + var list = MathList; + for (var path = index; path.SubIndexInfo is { } info; path = info.SubIndex) { + if (path.AtomIndex < 0 || path.AtomIndex >= list.Count) + return null; + var atom = list[path.AtomIndex]; + list = info.SubIndexType switch { + MathListSubIndexType.Superscript => atom.Superscript, + MathListSubIndexType.Subscript => atom.Subscript, + MathListSubIndexType.Numerator when atom is Atoms.Fraction fraction => fraction.Numerator, + MathListSubIndexType.Denominator when atom is Atoms.Fraction fraction => fraction.Denominator, + MathListSubIndexType.Radicand when atom is Atoms.Radical radical => radical.Radicand, + MathListSubIndexType.Degree when atom is Atoms.Radical radical => radical.Degree, + MathListSubIndexType.Inner when atom is Atoms.Inner inner => inner.InnerList, + _ => null!, + }; + if (list is null) + return null; + } + return list; + } + static MathListIndex WithFinalIndex(MathListIndex index, int finalIndex) => + index.SubIndexInfo is not { } info + ? new(finalIndex) + : new(index.AtomIndex, (info.SubIndexType, WithFinalIndex(info.SubIndex, finalIndex))); + + var sourceList = ListAtIndex(source); + var targetList = ListAtIndex(target); + if (sourceList is null || targetList is null) + return target; + if (targetList.Count == 1 && targetList[0] is Atoms.Placeholder) + return WithFinalIndex(target, 0); + var sourcePosition = + sourceList.Count == 1 && sourceList[0] is Atoms.Placeholder && source.FinalIndex == 0 + ? 0.5f + : source.FinalIndex; + var targetPosition = (int)Math.Round( + sourcePosition + (targetList.Count - sourceList.Count) / 2f, + MidpointRounding.AwayFromZero); + return WithFinalIndex(target, Math.Max(0, Math.Min(targetPosition, targetList.Count))); + } public void KeyPress(params MathKeyboardInput[] inputs) { foreach (var input in inputs) KeyPress(input); } @@ -175,176 +267,262 @@ void InsertInner(string left, string right) => new Atoms.Inner(new Boundary(left), LaTeXSettings.PlaceholderList, new Boundary(right)), MathListSubIndexType.Inner); - void MoveCursorLeft() { - var prev = _insertionIndex.Previous; - switch (MathList.AtomAt(prev)) { - case var _ when prev is null: - case null: // At beginning of line - var levelDown = _insertionIndex.LevelDown(); - switch (_insertionIndex.FinalSubIndexType) { - case null: - goto default; - case var _ when levelDown is null: - throw new InvalidCodePathException("Null levelDown despite non-None FinalSubIndexType"); - case MathListSubIndexType.Superscript: - var scriptAtom = MathList.AtomAt(levelDown); - if (scriptAtom is null) - throw new InvalidCodePathException("Invalid levelDown"); - if (scriptAtom.Subscript.IsNonEmpty()) - _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Subscript, scriptAtom.Subscript.Count); - else - goto case MathListSubIndexType.Subscript; - break; - case MathListSubIndexType.Subscript: - _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.BetweenBaseAndScripts, 1); - break; - case MathListSubIndexType.BetweenBaseAndScripts: - if (MathList.AtomAt(levelDown) is Atoms.Radical rad && rad.Radicand.IsNonEmpty()) - _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Radicand, rad.Radicand.Count); - else if (MathList.AtomAt(levelDown) is Atoms.Fraction frac && frac.Denominator.IsNonEmpty()) - _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Denominator, frac.Denominator.Count); - else if (MathList.AtomAt(levelDown) is Atoms.Inner inner && inner.InnerList.IsNonEmpty()) - _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Inner, inner.InnerList.Count); - else goto case MathListSubIndexType.Radicand; - break; - case MathListSubIndexType.Radicand: - if (MathList.AtomAt(levelDown) is Atoms.Radical radDeg && radDeg.Degree.IsNonEmpty()) - _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Degree, radDeg.Degree.Count); - else - goto case MathListSubIndexType.Denominator; - break; - case MathListSubIndexType.Denominator: - if (MathList.AtomAt(levelDown) is Atoms.Fraction fracNum && fracNum.Numerator.IsNonEmpty()) - _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Numerator, fracNum.Numerator.Count); - else - goto default; - break; - case MathListSubIndexType.Degree: - case MathListSubIndexType.Numerator: - case MathListSubIndexType.Inner: - default: - _insertionIndex = levelDown ?? _insertionIndex; - break; + void MoveCursorHorizontal(bool right) { + // Horizontal navigation is a walk over the ordered branches of the + // adjacent compound atom. Keeping this order in one place makes + // entry and exit exact inverses for every navigation policy. + static bool HasContent(MathList list) => list.IsNonEmpty(); + static int Branches(MathAtom atom, MathKeyboardHorizontalNavigationMode mode, + Span branches) { + var count = 0; + if (atom is Atoms.Inner inner) + branches[count++] = MathListSubIndexType.Inner; + else if (atom is Atoms.Fraction fraction) { + if (mode != MathKeyboardHorizontalNavigationMode.VisualLower) + branches[count++] = MathListSubIndexType.Numerator; + if (mode != MathKeyboardHorizontalNavigationMode.VisualUpper) + branches[count++] = MathListSubIndexType.Denominator; + } else if (atom is Atoms.Radical radical) { + if (HasContent(radical.Degree)) + branches[count++] = MathListSubIndexType.Degree; + branches[count++] = MathListSubIndexType.Radicand; + } + + var hasSub = HasContent(atom.Subscript); + var hasSuper = HasContent(atom.Superscript); + if (hasSub || hasSuper) { + // A placeholder's base-to-script seam is an implementation + // detail; it must not become an extra horizontal stop. + if (atom is not Atoms.Placeholder) + branches[count++] = MathListSubIndexType.BetweenBaseAndScripts; + if (mode == MathKeyboardHorizontalNavigationMode.Exhaustive) { + if (hasSub) + branches[count++] = MathListSubIndexType.Subscript; + if (hasSuper) + branches[count++] = MathListSubIndexType.Superscript; + } else { + var preferredSuper = mode == MathKeyboardHorizontalNavigationMode.VisualUpper; + if ((preferredSuper && hasSuper) || (!preferredSuper && !hasSub)) + branches[count++] = MathListSubIndexType.Superscript; + else + branches[count++] = MathListSubIndexType.Subscript; } - break; - case { Superscript: var s } when s.IsNonEmpty(): - _insertionIndex = prev.LevelUpWithSubIndex(MathListSubIndexType.Superscript, s.Count); - break; - case { Subscript: var s } when s.IsNonEmpty(): - _insertionIndex = prev.LevelUpWithSubIndex(MathListSubIndexType.Subscript, s.Count); - break; - case Atoms.Inner { InnerList: var l }: - _insertionIndex = prev.LevelUpWithSubIndex(MathListSubIndexType.Inner, l.Count); - break; - case Atoms.Radical { Radicand: var r }: - _insertionIndex = prev.LevelUpWithSubIndex(MathListSubIndexType.Radicand, r.Count); - break; - case Atoms.Fraction { Denominator: var d }: - _insertionIndex = prev.LevelUpWithSubIndex(MathListSubIndexType.Denominator, d.Count); - break; - default: - _insertionIndex = prev; - break; + } + return count; } - if (_insertionIndex is null) - throw new InvalidOperationException($"{nameof(_insertionIndex)} is null."); - if (_insertionIndex.FinalSubIndexType is MathListSubIndexType.BetweenBaseAndScripts) { - var prevInd = _insertionIndex.LevelDown(); - if (prevInd != null && MathList.AtomAt(prevInd) is Atoms.Placeholder) - _insertionIndex = prevInd; - } else if (MathList.AtomAt(_insertionIndex) is null - && _insertionIndex?.Previous is MathListIndex previous) { - if (MathList.AtomAt(previous) is Atoms.Placeholder p && p.Superscript.IsEmpty() && p.Subscript.IsEmpty()) - _insertionIndex = previous; // Skip right side of placeholders when end of line + + static MathList BranchList(MathAtom atom, MathListSubIndexType branch) => branch switch { + MathListSubIndexType.Superscript => atom.Superscript, + MathListSubIndexType.Subscript => atom.Subscript, + MathListSubIndexType.Numerator when atom is Atoms.Fraction fraction => fraction.Numerator, + MathListSubIndexType.Denominator when atom is Atoms.Fraction fraction => fraction.Denominator, + MathListSubIndexType.Radicand when atom is Atoms.Radical radical => radical.Radicand, + MathListSubIndexType.Degree when atom is Atoms.Radical radical => radical.Degree, + MathListSubIndexType.Inner when atom is Atoms.Inner inner => inner.InnerList, + _ => throw new InvalidCodePathException("Invalid horizontal navigation branch") + }; + + static MathListIndex Enter(MathListIndex owner, MathAtom atom, + MathListSubIndexType branch, bool atEnd) { + var index = branch == MathListSubIndexType.BetweenBaseAndScripts + ? 1 : atEnd ? BranchList(atom, branch).Count : 0; + return owner.LevelUpWithSubIndex(branch, index); } - } - void MoveCursorRight() { - if (_insertionIndex is null) - throw new InvalidOperationException($"{nameof(_insertionIndex)} is null."); - switch (MathList.AtomAt(_insertionIndex)) { - case null: // After Count - var levelDown = _insertionIndex.LevelDown(); - var levelDownAtom = MathList.AtomAt(levelDown); - switch (_insertionIndex.FinalSubIndexType) { - case null: - goto default; - case var _ when levelDown is null: - throw new InvalidCodePathException("Null levelDown despite non-None FinalSubIndexType"); - case var _ when levelDownAtom is null: - throw new InvalidCodePathException("Invalid levelDown"); - case MathListSubIndexType.Degree: - if (levelDownAtom is Atoms.Radical) - _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Radicand, 0); - else - throw new SubIndexTypeMismatchException(nameof(Atoms.Radical), levelDown.AtomIndex); - break; - case MathListSubIndexType.Numerator: - if (levelDownAtom is Atoms.Fraction) - _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Denominator, 0); - else - throw new SubIndexTypeMismatchException(nameof(Atoms.Fraction), levelDown.AtomIndex); - break; - case MathListSubIndexType.Radicand: - case MathListSubIndexType.Denominator: - case MathListSubIndexType.Inner: - if (levelDownAtom.Superscript.IsNonEmpty() || levelDownAtom.Subscript.IsNonEmpty()) - _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.BetweenBaseAndScripts, 1); - else - goto default; - break; - case MathListSubIndexType.BetweenBaseAndScripts: - if (levelDownAtom.Subscript.IsNonEmpty()) - _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Subscript, 0); - else - goto case MathListSubIndexType.Subscript; - break; - case MathListSubIndexType.Subscript: - if (levelDownAtom.Superscript.IsNonEmpty()) - _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Superscript, 0); - else - goto default; - break; - case MathListSubIndexType.Superscript: - default: - _insertionIndex = levelDown?.Next ?? _insertionIndex; - break; + + MathListIndex? Step(bool moveRight) { + var adjacent = moveRight ? MathList.AtomAt(_insertionIndex) : MathList.AtomAt(_insertionIndex.Previous); + var terminalPlaceholder = moveRight + && adjacent is Atoms.Placeholder { + Superscript: { Count: 0 }, Subscript: { Count: 0 } } - break; - case var a when _insertionIndex.FinalSubIndexType is MathListSubIndexType.BetweenBaseAndScripts: - levelDown = _insertionIndex.LevelDown(); - if (levelDown is null) - throw new InvalidCodePathException - ("_insertionIndex.FinalSubIndexType is BetweenBaseAndScripts but levelDown is null"); - _insertionIndex = levelDown.LevelUpWithSubIndex( - a.Subscript.IsNonEmpty() ? MathListSubIndexType.Subscript : MathListSubIndexType.Superscript, 0); - break; - case Atoms.Inner _: - _insertionIndex = _insertionIndex.LevelUpWithSubIndex(MathListSubIndexType.Inner, 0); - break; - case Atoms.Fraction _: - _insertionIndex = _insertionIndex.LevelUpWithSubIndex(MathListSubIndexType.Numerator, 0); - break; - case Atoms.Radical rad: - _insertionIndex = _insertionIndex.LevelUpWithSubIndex( - rad.Degree.IsNonEmpty() ? MathListSubIndexType.Degree : MathListSubIndexType.Radicand, 0); - break; - case var a when a.Superscript.IsNonEmpty() || a.Subscript.IsNonEmpty(): - _insertionIndex = _insertionIndex.LevelUpWithSubIndex(MathListSubIndexType.BetweenBaseAndScripts, 1); - break; - case Atoms.Placeholder _ when MathList.AtomAt(_insertionIndex.Next) is null: - // Skip right side of placeholders when end of line - goto case null; - default: - _insertionIndex = _insertionIndex.Next; - break; + && MathList.AtomAt(_insertionIndex.Next) is null; + if (adjacent is not null && !terminalPlaceholder) { + Span branches = stackalloc MathListSubIndexType[5]; + var branchCount = Branches(adjacent, HorizontalNavigationMode, branches); + if (branchCount == 0) + return moveRight ? _insertionIndex.Next : _insertionIndex.Previous; + var owner = moveRight ? _insertionIndex : _insertionIndex.Previous!; + return Enter(owner, adjacent, moveRight ? branches[0] : branches[branchCount - 1], !moveRight); + } + + var ownerIndex = _insertionIndex.LevelDown(); + if (ownerIndex is null) + return _insertionIndex; // root boundary + var ownerAtom = MathList.AtomAt(ownerIndex); + if (ownerAtom is null) + return _insertionIndex; + Span branchesAtOwner = stackalloc MathListSubIndexType[5]; + var branchCountAtOwner = Branches(ownerAtom, HorizontalNavigationMode, branchesAtOwner); + var currentType = _insertionIndex.FinalSubIndexType; + var currentBranch = -1; + for (var i = 0; i < branchCountAtOwner; i++) + if (branchesAtOwner[i] == currentType) { + currentBranch = i; + break; + } + var nextBranch = currentBranch + (moveRight ? 1 : -1); + if (currentBranch >= 0 && nextBranch >= 0 && nextBranch < branchCountAtOwner) + return Enter(ownerIndex, ownerAtom, branchesAtOwner[nextBranch], !moveRight); + return moveRight ? ownerIndex.Next : ownerIndex; } - if (_insertionIndex is null) - throw new InvalidOperationException($"{nameof(_insertionIndex)} is null."); - if (_insertionIndex.FinalSubIndexType is MathListSubIndexType.BetweenBaseAndScripts + + for (var attempts = 0; attempts < 4; attempts++) { + var next = Step(right); + if (next is null || next == _insertionIndex) + break; + _insertionIndex = next; + // Do not expose the seam after a terminal bare placeholder. + if (right && _insertionIndex.FinalSubIndexType == MathListSubIndexType.BetweenBaseAndScripts && MathList.AtomAt(_insertionIndex.LevelDown()) is Atoms.Placeholder) - MoveCursorRight(); + continue; + if (!right && MathList.AtomAt(_insertionIndex) is null + && _insertionIndex.Previous is { } previous + && MathList.AtomAt(previous) is Atoms.Placeholder { + Superscript: { Count: 0 }, Subscript: { Count: 0 } + }) { + _insertionIndex = previous; + } + break; + } + } + void MoveCursorUp() { + if (Display is null) RecreateDisplayFromMathList(); + if (MathList.AtomAt(_insertionIndex) is Atoms.Placeholder { Superscript: { Count: var superCount } } && superCount > 0) { + _insertionIndex = _insertionIndex.LevelUpWithSubIndex(MathListSubIndexType.Superscript, 0); + return; + } + if (_insertionIndex.Previous is { } prev && MathList.AtomAt(prev) is { Superscript: var super } && super.Count > 0) { + _insertionIndex = prev.LevelUpWithSubIndex(MathListSubIndexType.Superscript, super.Count); + if (_insertionIndex.Previous is { } prev2 && MathList.AtomAt(prev2) is Atoms.Placeholder p && p.Superscript.IsEmpty() && p.Subscript.IsEmpty()) + _insertionIndex = prev2; + return; + } + for (MathListIndex? verticalIndex = _insertionIndex; verticalIndex != null; verticalIndex = verticalIndex.LevelDown()) { + switch (verticalIndex.FinalSubIndexType) { + case MathListSubIndexType.Denominator: + var numerator = + verticalIndex.LevelDown()?.LevelUpWithSubIndex(MathListSubIndexType.Numerator, 0) + ?? throw new InvalidCodePathException("Null levelDown despite non-None " + nameof(verticalIndex.FinalSubIndexType)); + var sourcePoint = + ClosestPointToIndex(_insertionIndex) + ?? throw new InvalidCodePathException("Null closest point despite valid " + nameof(MathListIndex)); + _insertionIndex = VerticalIndexAtPoint(numerator, sourcePoint) ?? numerator; + return; + case MathListSubIndexType.Subscript: + var levelDown = + verticalIndex.LevelDown() + ?? throw new InvalidCodePathException("Null levelDown despite non-None " + nameof(verticalIndex.FinalSubIndexType)); + sourcePoint = + ClosestPointToIndex(_insertionIndex) + ?? throw new InvalidCodePathException("Null closest point despite valid " + nameof(MathListIndex)); + if (MathList.AtomAt(levelDown) is { Superscript: { Count: 0 } } atom) { + var left = + atom is Atoms.Placeholder + ? levelDown + : levelDown.LevelUpWithSubIndex(MathListSubIndexType.BetweenBaseAndScripts, 1); + var leftX = + ClosestPointToIndex(left)?.X + ?? throw new InvalidCodePathException("Null closest point despite valid " + nameof(MathListIndex)); + var right = levelDown.Next; + var rightX = + ClosestPointToIndex(right)?.X + ?? throw new InvalidCodePathException("Null closest point despite valid " + nameof(MathListIndex)); + _insertionIndex = Display?.Width == 0 + ? verticalIndex.FinalIndex switch { + 0 => left, + var index when index >= atom.Subscript.Count => right, + _ => sourcePoint.X - leftX <= rightX - sourcePoint.X ? left : right, + } + : sourcePoint.X - leftX <= rightX - sourcePoint.X ? left : right; + } else { + var superscript = + levelDown?.LevelUpWithSubIndex(MathListSubIndexType.Superscript, 0) + ?? throw new InvalidCodePathException("Null levelDown despite non-None " + nameof(verticalIndex.FinalSubIndexType)); + _insertionIndex = VerticalIndexAtPoint(superscript, sourcePoint, false) ?? superscript; + } + return; + case MathListSubIndexType.BetweenBaseAndScripts: + levelDown = + verticalIndex.LevelDown() + ?? throw new InvalidCodePathException("Null levelDown despite non-None " + nameof(verticalIndex.FinalSubIndexType)); + if (MathList.AtomAt(levelDown)?.Superscript.IsNonEmpty() + ?? throw new InvalidCodePathException(nameof(levelDown) + " is invalid for " + nameof(MathList))) { + _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Superscript, 0); + return; + } + break; + } + } + } + void MoveCursorDown() { + if (Display is null) RecreateDisplayFromMathList(); + if (MathList.AtomAt(_insertionIndex) is Atoms.Placeholder { Subscript: { Count: var subCount } } && subCount > 0) { + _insertionIndex = _insertionIndex.LevelUpWithSubIndex(MathListSubIndexType.Subscript, 0); + return; + } + if (_insertionIndex.Previous is { } prev && MathList.AtomAt(prev) is { Subscript: var sub } && sub.Count > 0) { + _insertionIndex = prev.LevelUpWithSubIndex(MathListSubIndexType.Subscript, sub.Count); + if (_insertionIndex.Previous is { } prev2 && MathList.AtomAt(prev2) is Atoms.Placeholder p && p.Superscript.IsEmpty() && p.Subscript.IsEmpty()) + _insertionIndex = prev2; + return; + } + for (MathListIndex? verticalIndex = _insertionIndex; verticalIndex != null; verticalIndex = verticalIndex.LevelDown()) { + switch (verticalIndex.FinalSubIndexType) { + case MathListSubIndexType.Numerator: + var denominator = + verticalIndex.LevelDown()?.LevelUpWithSubIndex(MathListSubIndexType.Denominator, 0) + ?? throw new InvalidCodePathException("Null levelDown despite non-None " + nameof(verticalIndex.FinalSubIndexType)); + var sourcePoint = + ClosestPointToIndex(_insertionIndex) + ?? throw new InvalidCodePathException("Null closest point despite valid " + nameof(MathListIndex)); + _insertionIndex = VerticalIndexAtPoint(denominator, sourcePoint) ?? denominator; + return; + case MathListSubIndexType.Superscript: + var levelDown = + verticalIndex.LevelDown() + ?? throw new InvalidCodePathException("Null levelDown despite non-None " + nameof(verticalIndex.FinalSubIndexType)); + sourcePoint = + ClosestPointToIndex(_insertionIndex) + ?? throw new InvalidCodePathException("Null closest point despite valid " + nameof(MathListIndex)); + if (MathList.AtomAt(levelDown) is { Subscript: { Count: 0 } } atom) { + var left = + atom is Atoms.Placeholder + ? levelDown + : levelDown.LevelUpWithSubIndex(MathListSubIndexType.BetweenBaseAndScripts, 1); + var leftX = + ClosestPointToIndex(left)?.X + ?? throw new InvalidCodePathException("Null closest point despite valid " + nameof(MathListIndex)); + var right = levelDown.Next; + var rightX = + ClosestPointToIndex(right)?.X + ?? throw new InvalidCodePathException("Null closest point despite valid " + nameof(MathListIndex)); + _insertionIndex = Display?.Width == 0 + ? verticalIndex.FinalIndex switch { + 0 => left, + var index when index >= atom.Superscript.Count => right, + _ => sourcePoint.X - leftX <= rightX - sourcePoint.X ? left : right, + } + : sourcePoint.X - leftX <= rightX - sourcePoint.X ? left : right; + } else { + var subscript = + levelDown?.LevelUpWithSubIndex(MathListSubIndexType.Subscript, 0) + ?? throw new InvalidCodePathException("Null levelDown despite non-None " + nameof(verticalIndex.FinalSubIndexType)); + _insertionIndex = VerticalIndexAtPoint(subscript, sourcePoint, false) ?? subscript; + } + return; + case MathListSubIndexType.BetweenBaseAndScripts: + levelDown = + verticalIndex.LevelDown() + ?? throw new InvalidCodePathException("Null levelDown despite non-None " + nameof(verticalIndex.FinalSubIndexType)); + if (MathList.AtomAt(levelDown)?.Subscript.IsNonEmpty() + ?? throw new InvalidCodePathException(nameof(levelDown) + " is invalid for " + nameof(MathList))) { + _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Subscript, 0); + return; + } + break; + } + } } - void DeleteBackwards() { // delete the last atom from the list if (HasText && _insertionIndex.Previous is MathListIndex previous) @@ -352,14 +530,31 @@ void DeleteBackwards() { } static bool IsPlaceholderList(MathList ml) => ml.Count == 1 && ml[0] is Atoms.Placeholder; - void InsertAtom(MathAtom a) => - _insertionIndex = MathList.InsertAndAdvance(_insertionIndex, a, - a switch { - Atoms.Fraction _ => MathListSubIndexType.Numerator, - Atoms.Radical { Degree: { } d } when IsPlaceholderList(d) => MathListSubIndexType.Degree, - Atoms.Radical _ => MathListSubIndexType.Radicand, - _ => null - }); + void InsertAtom(MathAtom a) { + static bool ContainsNestedScript(MathList list) { + foreach (var atom in list.Atoms) + if (atom.Superscript.IsNonEmpty() || atom.Subscript.IsNonEmpty()) + return true; + return false; + } + var advanceType = a switch { + Atoms.Fraction _ => MathListSubIndexType.Numerator, + Atoms.Radical { Degree: { } d } when IsPlaceholderList(d) => MathListSubIndexType.Degree, + Atoms.Radical _ => MathListSubIndexType.Radicand, + _ => (MathListSubIndexType?)null + }; + var insertionIndexBefore = _insertionIndex; + var preserveVerticalScriptPosition = + advanceType is null + && MathList.AtomAt(insertionIndexBefore) is Atoms.Placeholder placeholder + && (placeholder.Superscript.IsNonEmpty() || placeholder.Subscript.IsNonEmpty()) + && (_insertionIndexCameFromVerticalNavigation + || ContainsNestedScript(placeholder.Superscript) + || ContainsNestedScript(placeholder.Subscript)); + _insertionIndex = MathList.InsertAndAdvance(_insertionIndex, a, advanceType); + if (preserveVerticalScriptPosition) + _insertionIndex = insertionIndexBefore.LevelUpWithSubIndex(MathListSubIndexType.BetweenBaseAndScripts, 1); + } void InsertSymbolName(string name, bool subscript = false, bool superscript = false) { var atom = LaTeXSettings.AtomForCommand(name) ?? @@ -385,16 +580,21 @@ void InsertSymbolName(string name, bool subscript = false, bool superscript = fa } switch (input) { - // TODO: Implement up/down buttons case MathKeyboardInput.Up: + var previousIndex = _insertionIndex; + MoveCursorUp(); + _insertionIndexCameFromVerticalNavigation = previousIndex != _insertionIndex; break; case MathKeyboardInput.Down: + previousIndex = _insertionIndex; + MoveCursorDown(); + _insertionIndexCameFromVerticalNavigation = previousIndex != _insertionIndex; break; case MathKeyboardInput.Left: - MoveCursorLeft(); + MoveCursorHorizontal(false); break; case MathKeyboardInput.Right: - MoveCursorRight(); + MoveCursorHorizontal(true); break; case MathKeyboardInput.Backspace: DeleteBackwards(); @@ -790,6 +990,8 @@ void InsertSymbolName(string name, bool subscript = false, bool superscript = fa default: break; } + if (input is not MathKeyboardInput.Up and not MathKeyboardInput.Down) + _insertionIndexCameFromVerticalNavigation = false; ResetPlaceholders(MathList); InsertionPositionHighlighted = true; } @@ -828,4 +1030,4 @@ public void Dispose() { ((IDisposable)blinkTimer).Dispose(); } } -} \ No newline at end of file +} diff --git a/CSharpMath/PublicAPI.Unshipped.txt b/CSharpMath/PublicAPI.Unshipped.txt index e69de29b..3611bfa8 100644 --- a/CSharpMath/PublicAPI.Unshipped.txt +++ b/CSharpMath/PublicAPI.Unshipped.txt @@ -0,0 +1,6 @@ +CSharpMath.Editor.MathKeyboard.HorizontalNavigationMode.get -> CSharpMath.Editor.MathKeyboardHorizontalNavigationMode +CSharpMath.Editor.MathKeyboard.HorizontalNavigationMode.set -> void +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