@ -53,7 +53,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty < char > PasswordCharProperty =
public static readonly StyledProperty < char > PasswordCharProperty =
AvaloniaProperty . Register < TextBox , char > ( nameof ( PasswordChar ) ) ;
AvaloniaProperty . Register < TextBox , char > ( nameof ( PasswordChar ) ) ;
public static readonly StyledProperty < IBrush ? > SelectionBrushProperty =
public static readonly StyledProperty < IBrush ? > SelectionBrushProperty =
AvaloniaProperty . Register < TextBox , IBrush ? > ( nameof ( SelectionBrush ) ) ;
AvaloniaProperty . Register < TextBox , IBrush ? > ( nameof ( SelectionBrush ) ) ;
@ -80,7 +80,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty < int > MaxLinesProperty =
public static readonly StyledProperty < int > MaxLinesProperty =
AvaloniaProperty . Register < TextBox , int > ( nameof ( MaxLines ) , defaultValue : 0 ) ;
AvaloniaProperty . Register < TextBox , int > ( nameof ( MaxLines ) , defaultValue : 0 ) ;
public static readonly DirectProperty < TextBox , string? > TextProperty =
public static readonly DirectProperty < TextBox , string? > TextProperty =
TextBlock . TextProperty . AddOwnerWithDataValidation < TextBox > (
TextBlock . TextProperty . AddOwnerWithDataValidation < TextBox > (
o = > o . Text ,
o = > o . Text ,
@ -105,7 +105,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty < TextWrapping > TextWrappingProperty =
public static readonly StyledProperty < TextWrapping > TextWrappingProperty =
TextBlock . TextWrappingProperty . AddOwner < TextBox > ( ) ;
TextBlock . TextWrappingProperty . AddOwner < TextBox > ( ) ;
/// <summary>
/// <summary>
/// Defines see <see cref="TextPresenter.LineHeight"/> property.
/// Defines see <see cref="TextPresenter.LineHeight"/> property.
/// </summary>
/// </summary>
@ -202,6 +202,7 @@ namespace Avalonia.Controls
private string _ newLine = Environment . NewLine ;
private string _ newLine = Environment . NewLine ;
private static readonly string [ ] invalidCharacters = new String [ 1 ] { "\u007f" } ;
private static readonly string [ ] invalidCharacters = new String [ 1 ] { "\u007f" } ;
private int _ wordSelectionStart = - 1 ;
private int _ selectedTextChangesMadeSinceLastUndoSnapshot ;
private int _ selectedTextChangesMadeSinceLastUndoSnapshot ;
private bool _ hasDoneSnapshotOnce ;
private bool _ hasDoneSnapshotOnce ;
private const int _ maxCharsBeforeUndoSnapshot = 7 ;
private const int _ maxCharsBeforeUndoSnapshot = 7 ;
@ -275,7 +276,7 @@ namespace Avalonia.Controls
get = > GetValue ( IsReadOnlyProperty ) ;
get = > GetValue ( IsReadOnlyProperty ) ;
set = > SetValue ( IsReadOnlyProperty , value ) ;
set = > SetValue ( IsReadOnlyProperty , value ) ;
}
}
public char PasswordChar
public char PasswordChar
{
{
get = > GetValue ( PasswordCharProperty ) ;
get = > GetValue ( PasswordCharProperty ) ;
@ -307,7 +308,7 @@ namespace Avalonia.Controls
{
{
value = CoerceCaretIndex ( value ) ;
value = CoerceCaretIndex ( value ) ;
var changed = SetAndRaise ( SelectionStartProperty , ref _ selectionStart , value ) ;
var changed = SetAndRaise ( SelectionStartProperty , ref _ selectionStart , value ) ;
if ( changed )
if ( changed )
{
{
UpdateCommandStates ( ) ;
UpdateCommandStates ( ) ;
@ -327,12 +328,12 @@ namespace Avalonia.Controls
{
{
value = CoerceCaretIndex ( value ) ;
value = CoerceCaretIndex ( value ) ;
var changed = SetAndRaise ( SelectionEndProperty , ref _ selectionEnd , value ) ;
var changed = SetAndRaise ( SelectionEndProperty , ref _ selectionEnd , value ) ;
if ( changed )
if ( changed )
{
{
UpdateCommandStates ( ) ;
UpdateCommandStates ( ) ;
}
}
if ( SelectionStart = = value & & CaretIndex ! = value )
if ( SelectionStart = = value & & CaretIndex ! = value )
{
{
CaretIndex = value ;
CaretIndex = value ;
@ -351,7 +352,7 @@ namespace Avalonia.Controls
get = > GetValue ( MaxLinesProperty ) ;
get = > GetValue ( MaxLinesProperty ) ;
set = > SetValue ( MaxLinesProperty , value ) ;
set = > SetValue ( MaxLinesProperty , value ) ;
}
}
/// <summary>
/// <summary>
/// Gets or sets the line height.
/// Gets or sets the line height.
/// </summary>
/// </summary>
@ -370,7 +371,7 @@ namespace Avalonia.Controls
var caretIndex = CaretIndex ;
var caretIndex = CaretIndex ;
var selectionStart = SelectionStart ;
var selectionStart = SelectionStart ;
var selectionEnd = SelectionEnd ;
var selectionEnd = SelectionEnd ;
CaretIndex = CoerceCaretIndex ( caretIndex , value ) ;
CaretIndex = CoerceCaretIndex ( caretIndex , value ) ;
SelectionStart = CoerceCaretIndex ( selectionStart , value ) ;
SelectionStart = CoerceCaretIndex ( selectionStart , value ) ;
SelectionEnd = CoerceCaretIndex ( selectionEnd , value ) ;
SelectionEnd = CoerceCaretIndex ( selectionEnd , value ) ;
@ -567,7 +568,7 @@ namespace Avalonia.Controls
_ presenter = e . NameScope . Get < TextPresenter > ( "PART_TextPresenter" ) ;
_ presenter = e . NameScope . Get < TextPresenter > ( "PART_TextPresenter" ) ;
_ imClient . SetPresenter ( _ presenter , this ) ;
_ imClient . SetPresenter ( _ presenter , this ) ;
if ( IsFocused )
if ( IsFocused )
{
{
_ presenter ? . ShowCaret ( ) ;
_ presenter ? . ShowCaret ( ) ;
@ -577,7 +578,7 @@ namespace Avalonia.Controls
protected override void OnAttachedToVisualTree ( VisualTreeAttachmentEventArgs e )
protected override void OnAttachedToVisualTree ( VisualTreeAttachmentEventArgs e )
{
{
base . OnAttachedToVisualTree ( e ) ;
base . OnAttachedToVisualTree ( e ) ;
if ( IsFocused )
if ( IsFocused )
{
{
_ presenter ? . ShowCaret ( ) ;
_ presenter ? . ShowCaret ( ) ;
@ -587,7 +588,7 @@ namespace Avalonia.Controls
protected override void OnDetachedFromVisualTree ( VisualTreeAttachmentEventArgs e )
protected override void OnDetachedFromVisualTree ( VisualTreeAttachmentEventArgs e )
{
{
base . OnDetachedFromVisualTree ( e ) ;
base . OnDetachedFromVisualTree ( e ) ;
_ imClient . SetPresenter ( null , null ) ;
_ imClient . SetPresenter ( null , null ) ;
}
}
@ -637,7 +638,7 @@ namespace Avalonia.Controls
}
}
UpdateCommandStates ( ) ;
UpdateCommandStates ( ) ;
_ imClient . SetPresenter ( _ presenter , this ) ;
_ imClient . SetPresenter ( _ presenter , this ) ;
_ presenter ? . ShowCaret ( ) ;
_ presenter ? . ShowCaret ( ) ;
@ -657,7 +658,7 @@ namespace Avalonia.Controls
UpdateCommandStates ( ) ;
UpdateCommandStates ( ) ;
_ presenter ? . HideCaret ( ) ;
_ presenter ? . HideCaret ( ) ;
_ imClient . SetPresenter ( null , null ) ;
_ imClient . SetPresenter ( null , null ) ;
}
}
@ -700,14 +701,14 @@ namespace Avalonia.Controls
if ( grapheme . FirstCodepoint . IsBreakChar )
if ( grapheme . FirstCodepoint . IsBreakChar )
{
{
if ( lineCount + 1 > MaxLines )
if ( lineCount + 1 > MaxLines )
{
{
break ;
break ;
}
}
else
else
{
{
lineCount + + ;
lineCount + + ;
}
}
}
}
length + = grapheme . Text . Length ;
length + = grapheme . Text . Length ;
@ -736,7 +737,7 @@ namespace Avalonia.Controls
text = Text ? ? string . Empty ;
text = Text ? ? string . Empty ;
SetTextInternal ( text . Substring ( 0 , caretIndex ) + input + text . Substring ( caretIndex ) ) ;
SetTextInternal ( text . Substring ( 0 , caretIndex ) + input + text . Substring ( caretIndex ) ) ;
ClearSelection ( ) ;
ClearSelection ( ) ;
if ( IsUndoEnabled )
if ( IsUndoEnabled )
{
{
_ undoRedoHelper . DiscardRedo ( ) ;
_ undoRedoHelper . DiscardRedo ( ) ;
@ -746,7 +747,7 @@ namespace Avalonia.Controls
{
{
RaisePropertyChanged ( TextProperty , oldText , _ text ) ;
RaisePropertyChanged ( TextProperty , oldText , _ text ) ;
}
}
CaretIndex = caretIndex + input . Length ;
CaretIndex = caretIndex + input . Length ;
}
}
}
}
@ -828,7 +829,7 @@ namespace Avalonia.Controls
{
{
return ;
return ;
}
}
var text = Text ? ? string . Empty ;
var text = Text ? ? string . Empty ;
var caretIndex = CaretIndex ;
var caretIndex = CaretIndex ;
var movement = false ;
var movement = false ;
@ -985,87 +986,87 @@ namespace Avalonia.Controls
break ;
break ;
case Key . Up :
case Key . Up :
{
selection = DetectSelection ( ) ;
_ presenter . MoveCaretVertical ( LogicalDirection . Backward ) ;
if ( caretIndex ! = _ presenter . CaretIndex )
{
{
movement = true ;
selection = DetectSelection ( ) ;
}
if ( selection )
_ presenter . MoveCaretVertical ( LogicalDirection . Backward ) ;
{
SelectionEnd = _ presenter . CaretIndex ;
if ( caretIndex ! = _ presenter . CaretIndex )
}
{
else
movement = true ;
{
}
CaretIndex = _ presenter . CaretIndex ;
if ( selection )
{
SelectionEnd = _ presenter . CaretIndex ;
}
else
{
CaretIndex = _ presenter . CaretIndex ;
}
break ;
}
}
break ;
}
case Key . Down :
case Key . Down :
{
selection = DetectSelection ( ) ;
_ presenter . MoveCaretVertical ( ) ;
if ( caretIndex ! = _ presenter . CaretIndex )
{
movement = true ;
}
if ( selection )
{
SelectionEnd = _ presenter . CaretIndex ;
}
else
{
{
CaretIndex = _ presenter . CaretIndex ;
selection = DetectSelection ( ) ;
_ presenter . MoveCaretVertical ( ) ;
if ( caretIndex ! = _ presenter . CaretIndex )
{
movement = true ;
}
if ( selection )
{
SelectionEnd = _ presenter . CaretIndex ;
}
else
{
CaretIndex = _ presenter . CaretIndex ;
}
break ;
}
}
break ;
}
case Key . Back :
case Key . Back :
{
SnapshotUndoRedo ( ) ;
if ( hasWholeWordModifiers & & SelectionStart = = SelectionEnd )
{
{
SetSelectionForControlBackspace ( ) ;
SnapshotUndoRedo ( ) ;
}
if ( ! DeleteSelection ( ) )
{
var characterHit = _ presenter . GetNextCharacterHit ( LogicalDirection . Backward ) ;
var backspacePosition = characterHit . FirstCharacterIndex + characterHit . TrailingLength ;
if ( hasWholeWordModifiers & & SelectionStart = = SelectionEnd )
{
SetSelectionForControlBackspace ( ) ;
}
if ( caretIndex ! = backspacePosition )
if ( ! DeleteSelection ( ) )
{
{
var start = Math . Min ( backspacePosition , caretIndex ) ;
var characterHit = _ presenter . GetNextCharacterHit ( LogicalDirection . Backward ) ;
var end = Math . Max ( backspacePosition , caretIndex ) ;
var length = end - start ;
var backspacePosition = characterHit . FirstCharacterIndex + characterHit . TrailingLength ;
var editedText = text . Substring ( 0 , start ) + text . Substring ( Math . Min ( end , text . Length ) ) ;
if ( caretIndex ! = backspacePosition )
{
var start = Math . Min ( backspacePosition , caretIndex ) ;
var end = Math . Max ( backspacePosition , caretIndex ) ;
SetTextInternal ( editedText ) ;
var length = end - start ;
CaretIndex = start ;
var editedText = text . Substring ( 0 , start ) + text . Substring ( Math . Min ( end , text . Length ) ) ;
}
}
SnapshotUndoRedo ( ) ;
handled = true ;
SetTextInternal ( editedText ) ;
break ;
}
CaretIndex = start ;
}
}
SnapshotUndoRedo ( ) ;
handled = true ;
break ;
}
case Key . Delete :
case Key . Delete :
SnapshotUndoRedo ( ) ;
SnapshotUndoRedo ( ) ;
if ( hasWholeWordModifiers & & SelectionStart = = SelectionEnd )
if ( hasWholeWordModifiers & & SelectionStart = = SelectionEnd )
{
{
SetSelectionForControlDelete ( ) ;
SetSelectionForControlDelete ( ) ;
@ -1077,7 +1078,7 @@ namespace Avalonia.Controls
var nextPosition = characterHit . FirstCharacterIndex + characterHit . TrailingLength ;
var nextPosition = characterHit . FirstCharacterIndex + characterHit . TrailingLength ;
if ( nextPosition ! = caretIndex )
if ( nextPosition ! = caretIndex )
{
{
var start = Math . Min ( nextPosition , caretIndex ) ;
var start = Math . Min ( nextPosition , caretIndex ) ;
var end = Math . Max ( nextPosition , caretIndex ) ;
var end = Math . Max ( nextPosition , caretIndex ) ;
@ -1144,7 +1145,7 @@ namespace Avalonia.Controls
{
{
return ;
return ;
}
}
var text = Text ;
var text = Text ;
var clickInfo = e . GetCurrentPoint ( this ) ;
var clickInfo = e . GetCurrentPoint ( this ) ;
@ -1170,24 +1171,50 @@ namespace Avalonia.Controls
case 1 :
case 1 :
if ( clickToSelect )
if ( clickToSelect )
{
{
SelectionStart = Math . Min ( oldIndex , index ) ;
if ( _ wordSelectionStart > = 0 )
SelectionEnd = Math . Max ( oldIndex , index ) ;
{
var previousWord = StringUtils . PreviousWord ( text , index ) ;
if ( index > _ wordSelectionStart )
{
SelectionEnd = StringUtils . NextWord ( text , index ) ;
}
if ( index < _ wordSelectionStart | | previousWord = = _ wordSelectionStart )
{
SelectionStart = previousWord ;
}
}
else
{
SelectionStart = Math . Min ( oldIndex , index ) ;
SelectionEnd = Math . Max ( oldIndex , index ) ;
}
}
}
else
else
{
{
SelectionStart = SelectionEnd = index ;
if ( _ wordSelectionStart = = - 1 | | index < SelectionStart | | index > SelectionEnd )
{
SelectionStart = SelectionEnd = index ;
_ wordSelectionStart = - 1 ;
}
}
}
break ;
break ;
case 2 :
case 2 :
if ( ! StringUtils . IsStartOfWord ( text , index ) )
if ( ! StringUtils . IsStartOfWord ( text , index ) )
{
{
SelectionStart = StringUtils . PreviousWord ( text , index ) ;
SelectionStart = StringUtils . PreviousWord ( text , index ) ;
}
}
_ wordSelectionStart = SelectionStart ;
SelectionEnd = StringUtils . NextWord ( text , index ) ;
SelectionEnd = StringUtils . NextWord ( text , index ) ;
break ;
break ;
case 3 :
case 3 :
_ wordSelectionStart = - 1 ;
SelectAll ( ) ;
SelectAll ( ) ;
break ;
break ;
}
}
@ -1203,7 +1230,7 @@ namespace Avalonia.Controls
{
{
return ;
return ;
}
}
// selection should not change during pointer move if the user right clicks
// selection should not change during pointer move if the user right clicks
if ( e . Pointer . Captured = = _ presenter & & e . GetCurrentPoint ( this ) . Properties . IsLeftButtonPressed )
if ( e . Pointer . Captured = = _ presenter & & e . GetCurrentPoint ( this ) . Properties . IsLeftButtonPressed )
{
{
@ -1215,7 +1242,32 @@ namespace Avalonia.Controls
_ presenter . MoveCaretToPoint ( point ) ;
_ presenter . MoveCaretToPoint ( point ) ;
SelectionEnd = _ presenter . CaretIndex ;
var caretIndex = _ presenter . CaretIndex ;
var text = Text ;
if ( text ! = null & & _ wordSelectionStart > = 0 )
{
var distance = caretIndex - _ wordSelectionStart ;
if ( distance < = 0 )
{
SelectionStart = StringUtils . PreviousWord ( text , caretIndex ) ;
}
if ( distance > = 0 )
{
if ( SelectionStart ! = _ wordSelectionStart )
{
SelectionStart = _ wordSelectionStart ;
}
SelectionEnd = StringUtils . NextWord ( text , caretIndex ) ;
}
}
else
{
SelectionEnd = _ presenter . CaretIndex ;
}
}
}
}
}
@ -1234,9 +1286,9 @@ namespace Avalonia.Controls
if ( e . InitialPressMouseButton = = MouseButton . Right )
if ( e . InitialPressMouseButton = = MouseButton . Right )
{
{
var point = e . GetPosition ( _ presenter ) ;
var point = e . GetPosition ( _ presenter ) ;
_ presenter . MoveCaretToPoint ( point ) ;
_ presenter . MoveCaretToPoint ( point ) ;
var caretIndex = _ presenter . CaretIndex ;
var caretIndex = _ presenter . CaretIndex ;
// see if mouse clicked inside current selection
// see if mouse clicked inside current selection
@ -1250,7 +1302,7 @@ namespace Avalonia.Controls
CaretIndex = SelectionEnd = SelectionStart = caretIndex ;
CaretIndex = SelectionEnd = SelectionStart = caretIndex ;
}
}
}
}
e . Pointer . Capture ( null ) ;
e . Pointer . Capture ( null ) ;
}
}
@ -1309,7 +1361,7 @@ namespace Avalonia.Controls
{
{
return ;
return ;
}
}
var text = Text ? ? string . Empty ;
var text = Text ? ? string . Empty ;
var selectionStart = SelectionStart ;
var selectionStart = SelectionStart ;
var selectionEnd = SelectionEnd ;
var selectionEnd = SelectionEnd ;
@ -1319,11 +1371,11 @@ namespace Avalonia.Controls
if ( isSelecting )
if ( isSelecting )
{
{
_ presenter . MoveCaretToTextPosition ( selectionEnd ) ;
_ presenter . MoveCaretToTextPosition ( selectionEnd ) ;
_ presenter . MoveCaretHorizontal ( direction > 0 ?
_ presenter . MoveCaretHorizontal ( direction > 0 ?
LogicalDirection . Forward :
LogicalDirection . Forward :
LogicalDirection . Backward ) ;
LogicalDirection . Backward ) ;
SelectionEnd = _ presenter . CaretIndex ;
SelectionEnd = _ presenter . CaretIndex ;
}
}
else
else
@ -1347,7 +1399,7 @@ namespace Avalonia.Controls
else
else
{
{
int offset ;
int offset ;
if ( direction > 0 )
if ( direction > 0 )
{
{
offset = StringUtils . NextWord ( text , selectionEnd ) - selectionEnd ;
offset = StringUtils . NextWord ( text , selectionEnd ) - selectionEnd ;
@ -1356,7 +1408,7 @@ namespace Avalonia.Controls
{
{
offset = StringUtils . PreviousWord ( text , selectionEnd ) - selectionEnd ;
offset = StringUtils . PreviousWord ( text , selectionEnd ) - selectionEnd ;
}
}
SelectionEnd + = offset ;
SelectionEnd + = offset ;
_ presenter . MoveCaretToTextPosition ( SelectionEnd ) ;
_ presenter . MoveCaretToTextPosition ( SelectionEnd ) ;
@ -1378,7 +1430,7 @@ namespace Avalonia.Controls
{
{
return ;
return ;
}
}
var caretIndex = CaretIndex ;
var caretIndex = CaretIndex ;
if ( document )
if ( document )
@ -1401,7 +1453,7 @@ namespace Avalonia.Controls
{
{
return ;
return ;
}
}
var text = Text ? ? string . Empty ;
var text = Text ? ? string . Empty ;
var caretIndex = CaretIndex ;
var caretIndex = CaretIndex ;
@ -1432,8 +1484,9 @@ namespace Avalonia.Controls
private bool DeleteSelection ( bool raiseTextChanged = true )
private bool DeleteSelection ( bool raiseTextChanged = true )
{
{
if ( IsReadOnly ) return true ;
if ( IsReadOnly )
return true ;
var selectionStart = SelectionStart ;
var selectionStart = SelectionStart ;
var selectionEnd = SelectionEnd ;
var selectionEnd = SelectionEnd ;
@ -1444,40 +1497,40 @@ namespace Avalonia.Controls
var text = Text ! ;
var text = Text ! ;
SetTextInternal ( text . Substring ( 0 , start ) + text . Substring ( end ) , raiseTextChanged ) ;
SetTextInternal ( text . Substring ( 0 , start ) + text . Substring ( end ) , raiseTextChanged ) ;
_ presenter ? . MoveCaretToTextPosition ( start ) ;
_ presenter ? . MoveCaretToTextPosition ( start ) ;
CaretIndex = start ;
CaretIndex = start ;
ClearSelection ( ) ;
ClearSelection ( ) ;
return true ;
return true ;
}
}
CaretIndex = SelectionStart ;
CaretIndex = SelectionStart ;
return false ;
return false ;
}
}
private string GetSelection ( )
private string GetSelection ( )
{
{
var text = Text ;
var text = Text ;
if ( string . IsNullOrEmpty ( text ) )
if ( string . IsNullOrEmpty ( text ) )
{
{
return "" ;
return "" ;
}
}
var selectionStart = SelectionStart ;
var selectionStart = SelectionStart ;
var selectionEnd = SelectionEnd ;
var selectionEnd = SelectionEnd ;
var start = Math . Min ( selectionStart , selectionEnd ) ;
var start = Math . Min ( selectionStart , selectionEnd ) ;
var end = Math . Max ( selectionStart , selectionEnd ) ;
var end = Math . Max ( selectionStart , selectionEnd ) ;
if ( start = = end | | ( Text ? . Length ? ? 0 ) < end )
if ( start = = end | | ( Text ? . Length ? ? 0 ) < end )
{
{
return "" ;
return "" ;
}
}
return text . Substring ( start , end - start ) ;
return text . Substring ( start , end - start ) ;
}
}
@ -1496,7 +1549,7 @@ namespace Avalonia.Controls
private void SetSelectionForControlBackspace ( )
private void SetSelectionForControlBackspace ( )
{
{
var selectionStart = CaretIndex ;
var selectionStart = CaretIndex ;
MoveHorizontal ( - 1 , true , false ) ;
MoveHorizontal ( - 1 , true , false ) ;
SelectionStart = selectionStart ;
SelectionStart = selectionStart ;
@ -1508,9 +1561,9 @@ namespace Avalonia.Controls
{
{
return ;
return ;
}
}
SelectionStart = CaretIndex ;
SelectionStart = CaretIndex ;
MoveHorizontal ( 1 , true , true ) ;
MoveHorizontal ( 1 , true , true ) ;
if ( SelectionEnd < _ text . Length & & _ text [ SelectionEnd ] = = ' ' )
if ( SelectionEnd < _ text . Length & & _ text [ SelectionEnd ] = = ' ' )