Skip to content

Commit 7581526

Browse files
author
Muaz
committed
Fix inverted RTL word navigation in the subtitle edit box
In a right-to-left subtitle, Ctrl+Left / Ctrl+Right jumped the wrong way. Avalonia's TextBox moves the caret one character visually (bidi-aware) but moves whole words by logical text offset, so word steps run opposite to the visual direction in RTL text (single-character steps were already correct). Ctrl+Up / Ctrl+Down also did nothing useful. Add a tunnel-phase key handler on the main and original edit text boxes that runs only when the box is in RTL flow: - Ctrl+Left / Ctrl+Right move a word in the visually-correct direction. - Ctrl+Up / Ctrl+Down move to the start / end of the current line. - Shift extends the selection. It no-ops for left-to-right editing, so LTR behaviour and any global Ctrl+Arrow shortcuts are unchanged.
1 parent b7db581 commit 7581526

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

src/ui/Features/Main/Layout/InitListViewAndEditBox.cs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1711,6 +1711,7 @@ private static Avalonia.Controls.Control MakeTextBox(MainViewModel vm)
17111711
textBox.AddHandler(InputElement.PointerPressedEvent, (_, e) => vm.StoreTextEditorPointerArgs(e), RoutingStrategies.Tunnel);
17121712

17131713
SetupMacContextMenuForTextBox(textBox, vm);
1714+
AttachRightToLeftCaretNavigation(textBox);
17141715

17151716
vm.EditTextBox = new TextBoxWrapper(textBox);
17161717
return textBox;
@@ -1821,6 +1822,7 @@ private static Avalonia.Controls.Control MakeTextBoxOriginal(MainViewModel vm)
18211822
}
18221823

18231824
SetupMacContextMenuForTextBox(textBox, vm);
1825+
AttachRightToLeftCaretNavigation(textBox);
18241826

18251827
vm.EditTextBoxOriginal = new TextBoxWrapper(textBox);
18261828
return textBox;
@@ -1945,4 +1947,117 @@ private static void SetupMacContextMenu(TextEditor textEditor, MainViewModel vm)
19451947
},
19461948
RoutingStrategies.Tunnel);
19471949
}
1950+
1951+
/// <summary>
1952+
/// Avalonia's TextBox moves the caret one character visually (bidi-aware) but moves whole words
1953+
/// by logical text offset. For a right-to-left subtitle that makes Ctrl+Left / Ctrl+Right jump
1954+
/// the wrong way (single-character steps are fine). This handler reimplements word movement in
1955+
/// the visually-correct direction for RTL text and adds Ctrl+Up / Ctrl+Down = start / end of the
1956+
/// current line. It only acts when the box is in RTL flow, so left-to-right editing and any global
1957+
/// Ctrl+Arrow shortcuts are left untouched.
1958+
/// </summary>
1959+
private static void AttachRightToLeftCaretNavigation(TextBox textBox)
1960+
{
1961+
textBox.AddHandler(
1962+
InputElement.KeyDownEvent,
1963+
(_, e) =>
1964+
{
1965+
if (textBox.FlowDirection != FlowDirection.RightToLeft ||
1966+
!e.KeyModifiers.HasFlag(KeyModifiers.Control))
1967+
{
1968+
return;
1969+
}
1970+
1971+
var text = textBox.Text ?? string.Empty;
1972+
var caret = Math.Clamp(textBox.CaretIndex, 0, text.Length);
1973+
int target;
1974+
switch (e.Key)
1975+
{
1976+
case Key.Left: // visually left in RTL == later in the logical string
1977+
target = NextWordBoundary(text, caret, forward: true);
1978+
break;
1979+
case Key.Right: // visually right in RTL == earlier in the logical string
1980+
target = NextWordBoundary(text, caret, forward: false);
1981+
break;
1982+
case Key.Up: // head of the current line
1983+
target = CurrentLineStart(text, caret);
1984+
break;
1985+
case Key.Down: // end of the current line
1986+
target = CurrentLineEnd(text, caret);
1987+
break;
1988+
default:
1989+
return;
1990+
}
1991+
1992+
MoveCaret(textBox, target, e.KeyModifiers.HasFlag(KeyModifiers.Shift));
1993+
e.Handled = true;
1994+
},
1995+
RoutingStrategies.Tunnel);
1996+
}
1997+
1998+
private static bool IsWordSeparator(char c) => char.IsWhiteSpace(c) || char.IsPunctuation(c) || char.IsSymbol(c);
1999+
2000+
// Mirrors the usual "Ctrl+Arrow" word stop: forward stops at the start of the next word,
2001+
// backward stops at the start of the current/previous word.
2002+
private static int NextWordBoundary(string text, int index, bool forward)
2003+
{
2004+
if (forward)
2005+
{
2006+
var i = index;
2007+
while (i < text.Length && !IsWordSeparator(text[i])) i++;
2008+
while (i < text.Length && IsWordSeparator(text[i])) i++;
2009+
return i;
2010+
}
2011+
else
2012+
{
2013+
var i = index;
2014+
while (i > 0 && IsWordSeparator(text[i - 1])) i--;
2015+
while (i > 0 && !IsWordSeparator(text[i - 1])) i--;
2016+
return i;
2017+
}
2018+
}
2019+
2020+
private static int CurrentLineStart(string text, int index)
2021+
{
2022+
var i = index;
2023+
while (i > 0 && text[i - 1] != '\n' && text[i - 1] != '\r') i--;
2024+
return i;
2025+
}
2026+
2027+
private static int CurrentLineEnd(string text, int index)
2028+
{
2029+
var i = index;
2030+
while (i < text.Length && text[i] != '\n' && text[i] != '\r') i++;
2031+
return i;
2032+
}
2033+
2034+
private static void MoveCaret(TextBox textBox, int target, bool extendSelection)
2035+
{
2036+
var length = (textBox.Text ?? string.Empty).Length;
2037+
target = Math.Clamp(target, 0, length);
2038+
2039+
if (extendSelection)
2040+
{
2041+
// Keep the fixed end of the existing selection as the anchor; the caret follows
2042+
// SelectionEnd, so don't touch CaretIndex here (its setter can collapse the selection).
2043+
int anchor;
2044+
if (textBox.SelectionStart != textBox.SelectionEnd)
2045+
{
2046+
anchor = textBox.CaretIndex == textBox.SelectionEnd ? textBox.SelectionStart : textBox.SelectionEnd;
2047+
}
2048+
else
2049+
{
2050+
anchor = textBox.CaretIndex;
2051+
}
2052+
2053+
textBox.SelectionStart = anchor;
2054+
textBox.SelectionEnd = target;
2055+
}
2056+
else
2057+
{
2058+
textBox.CaretIndex = target;
2059+
textBox.SelectionStart = target;
2060+
textBox.SelectionEnd = target;
2061+
}
2062+
}
19482063
}

0 commit comments

Comments
 (0)