@ -8,7 +8,6 @@ using Avalonia.Input.Platform;
using Avalonia.Interactivity ;
using Avalonia.Media ;
using Avalonia.Media.TextFormatting ;
using Avalonia.Metadata ;
using Avalonia.Utilities ;
namespace Avalonia.Controls
@ -16,67 +15,53 @@ namespace Avalonia.Controls
/// <summary>
/// A control that displays a block of formatted text.
/// </summary>
public class Rich TextBlock : TextBlock , IInlineHost
public class Selectable TextBlock : TextBlock , IInlineHost
{
public static readonly StyledProperty < bool > IsTextSelectionEnabledProperty =
AvaloniaProperty . Register < RichTextBlock , bool > ( nameof ( IsTextSelectionEnabled ) , false ) ;
public static readonly DirectProperty < RichTextBlock , int > SelectionStartProperty =
AvaloniaProperty . RegisterDirect < RichTextBlock , int > (
public static readonly DirectProperty < SelectableTextBlock , int > SelectionStartProperty =
AvaloniaProperty . RegisterDirect < SelectableTextBlock , int > (
nameof ( SelectionStart ) ,
o = > o . SelectionStart ,
( o , v ) = > o . SelectionStart = v ) ;
public static readonly DirectProperty < Rich TextBlock, int > SelectionEndProperty =
AvaloniaProperty . RegisterDirect < Rich TextBlock, int > (
public static readonly DirectProperty < Selectable TextBlock, int > SelectionEndProperty =
AvaloniaProperty . RegisterDirect < Selectable TextBlock, int > (
nameof ( SelectionEnd ) ,
o = > o . SelectionEnd ,
( o , v ) = > o . SelectionEnd = v ) ;
public static readonly DirectProperty < Rich TextBlock, string > SelectedTextProperty =
AvaloniaProperty . RegisterDirect < Rich TextBlock, string > (
public static readonly DirectProperty < Selectable TextBlock, string > SelectedTextProperty =
AvaloniaProperty . RegisterDirect < Selectable TextBlock, string > (
nameof ( SelectedText ) ,
o = > o . SelectedText ) ;
public static readonly StyledProperty < IBrush ? > SelectionBrushProperty =
AvaloniaProperty . Register < Rich TextBlock, IBrush ? > ( nameof ( SelectionBrush ) , Brushes . Blue ) ;
AvaloniaProperty . Register < Selectable TextBlock, IBrush ? > ( nameof ( SelectionBrush ) , Brushes . Blue ) ;
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly StyledProperty < InlineCollection ? > InlinesProperty =
AvaloniaProperty . Register < RichTextBlock , InlineCollection ? > (
nameof ( Inlines ) ) ;
public static readonly DirectProperty < TextBox , bool > CanCopyProperty =
AvaloniaProperty . RegisterDirect < TextBox , bool > (
public static readonly DirectProperty < SelectableTextBlock , bool > CanCopyProperty =
AvaloniaProperty . RegisterDirect < SelectableTextBlock , bool > (
nameof ( CanCopy ) ,
o = > o . CanCopy ) ;
public static readonly RoutedEvent < RoutedEventArgs > CopyingToClipboardEvent =
RoutedEvent . Register < Rich TextBlock, RoutedEventArgs > (
RoutedEvent . Register < Selectable TextBlock, RoutedEventArgs > (
nameof ( CopyingToClipboard ) , RoutingStrategies . Bubble ) ;
private bool _ canCopy ;
private int _ selectionStart ;
private int _ selectionEnd ;
private int _ wordSelectionStart = - 1 ;
private IReadOnlyList < TextRun > ? _ textRuns ;
static Rich TextBlock( )
static Selectable TextBlock( )
{
FocusableProperty . OverrideDefaultValue ( typeof ( RichTextBlock ) , true ) ;
AffectsRender < RichTextBlock > ( SelectionStartProperty , SelectionEndProperty , SelectionBrushProperty , IsTextSelectionEnabledProperty ) ;
FocusableProperty . OverrideDefaultValue ( typeof ( SelectableTextBlock ) , true ) ;
AffectsRender < SelectableTextBlock > ( SelectionStartProperty , SelectionEndProperty , SelectionBrushProperty ) ;
}
public RichTextBlock ( )
public event EventHandler < RoutedEventArgs > ? CopyingToClipboard
{
Inlines = new InlineCollection
{
Parent = this ,
InlineHost = this
} ;
add = > AddHandler ( CopyingToClipboardEvent , value ) ;
remove = > RemoveHandler ( CopyingToClipboardEvent , value ) ;
}
/// <summary>
@ -99,6 +84,8 @@ namespace Avalonia.Controls
if ( SetAndRaise ( SelectionStartProperty , ref _ selectionStart , value ) )
{
RaisePropertyChanged ( SelectedTextProperty , "" , "" ) ;
UpdateCommandStates ( ) ;
}
}
}
@ -114,6 +101,8 @@ namespace Avalonia.Controls
if ( SetAndRaise ( SelectionEndProperty , ref _ selectionEnd , value ) )
{
RaisePropertyChanged ( SelectedTextProperty , "" , "" ) ;
UpdateCommandStates ( ) ;
}
}
}
@ -126,25 +115,6 @@ namespace Avalonia.Controls
get = > GetSelection ( ) ;
}
/// <summary>
/// Gets or sets a value that indicates whether text selection is enabled, either through user action or calling selection-related API.
/// </summary>
public bool IsTextSelectionEnabled
{
get = > GetValue ( IsTextSelectionEnabledProperty ) ;
set = > SetValue ( IsTextSelectionEnabledProperty , value ) ;
}
/// <summary>
/// Gets or sets the inlines.
/// </summary>
[Content]
public InlineCollection ? Inlines
{
get = > GetValue ( InlinesProperty ) ;
set = > SetValue ( InlinesProperty , value ) ;
}
/// <summary>
/// Property for determining if the Copy command can be executed.
/// </summary>
@ -154,20 +124,12 @@ namespace Avalonia.Controls
private set = > SetAndRaise ( CanCopyProperty , ref _ canCopy , value ) ;
}
public event EventHandler < RoutedEventArgs > ? CopyingToClipboard
{
add = > AddHandler ( CopyingToClipboardEvent , value ) ;
remove = > RemoveHandler ( CopyingToClipboardEvent , value ) ;
}
internal bool HasComplexContent = > Inlines ! = null & & Inlines . Count > 0 ;
/// <summary>
/// Copies the current selection to the Clipboard.
/// </summary>
public async void Copy ( )
{
if ( _ canCopy | | ! IsTextSelectionEnabled )
if ( ! _ canCopy )
{
return ;
}
@ -188,45 +150,13 @@ namespace Avalonia.Controls
await ( ( IClipboard ) AvaloniaLocator . Current . GetRequiredService ( typeof ( IClipboard ) ) )
. SetTextAsync ( text ) ;
}
}
protected override void RenderTextLayout ( DrawingContext context , Point origin )
{
var selectionStart = SelectionStart ;
var selectionEnd = SelectionEnd ;
var selectionBrush = SelectionBrush ;
var selectionEnabled = IsTextSelectionEnabled ;
if ( selectionEnabled & & selectionStart ! = selectionEnd & & selectionBrush ! = null )
{
var start = Math . Min ( selectionStart , selectionEnd ) ;
var length = Math . Max ( selectionStart , selectionEnd ) - start ;
var rects = TextLayout . HitTestTextRange ( start , length ) ;
using ( context . PushPostTransform ( Matrix . CreateTranslation ( origin ) ) )
{
foreach ( var rect in rects )
{
context . FillRectangle ( selectionBrush , PixelRect . FromRect ( rect , 1 ) . ToRect ( 1 ) ) ;
}
}
}
base . RenderTextLayout ( context , origin ) ;
}
}
/// <summary>
/// Select all text in the TextBox
/// </summary>
public void SelectAll ( )
{
if ( ! IsTextSelectionEnabled )
{
return ;
}
var text = Text ;
SelectionStart = 0 ;
@ -238,94 +168,52 @@ namespace Avalonia.Controls
/// </summary>
public void ClearSelection ( )
{
if ( ! IsTextSelectionEnabled )
{
return ;
}
SelectionEnd = SelectionStart ;
}
protected void AddText ( string? text )
protected override void OnGotFocus ( GotFocusEventArgs e )
{
if ( string . IsNullOrEmpty ( text ) )
{
return ;
}
if ( ! HasComplexContent & & string . IsNullOrEmpty ( _ text ) )
{
_ text = text ;
}
else
{
if ( ! string . IsNullOrEmpty ( _ text ) )
{
Inlines ? . Add ( _ text ) ;
_ text = null ;
}
Inlines ? . Add ( text ) ;
}
}
base . OnGotFocus ( e ) ;
protected override string? GetText ( )
{
return _ text ? ? Inlines ? . Text ;
UpdateCommandStates ( ) ;
}
protected override void SetText ( string? text )
protected override void OnLostFocus ( RoutedEventArgs e )
{
var oldValue = GetText ( ) ;
base . OnLostFocus ( e ) ;
AddText ( text ) ;
if ( ( ContextFlyout = = null | | ! ContextFlyout . IsOpen ) & &
( ContextMenu = = null | | ! ContextMenu . IsOpen ) )
{
ClearSelection ( ) ;
}
RaisePropertyChanged ( TextProperty , oldValue , text ) ;
UpdateCommandStates ( ) ;
}
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected override TextLayout CreateTextLayout ( string? text )
protected override void RenderTextLayout ( DrawingContext context , Point origin )
{
var typeface = new Typeface ( FontFamily , FontStyle , FontWeight , FontStretch ) ;
var defaultProperties = new GenericTextRunProperties (
typeface ,
FontSize ,
TextDecorations ,
Foreground ) ;
var paragraphProperties = new GenericTextParagraphProperties ( FlowDirection , TextAlignment , true , false ,
defaultProperties , TextWrapping , LineHeight , 0 ) ;
ITextSource textSource ;
var selectionStart = SelectionStart ;
var selectionEnd = SelectionEnd ;
var selectionBrush = SelectionBrush ;
if ( _ textRuns ! = null )
if ( selectionStart ! = selectionEnd & & selectionBrush ! = null )
{
textSource = new InlinesTextSource ( _ textRuns ) ;
}
else
{
textSource = new SimpleTextSource ( ( text ? ? "" ) . AsMemory ( ) , defaultProperties ) ;
}
var start = Math . Min ( selectionStart , selectionEnd ) ;
var length = Math . Max ( selectionStart , selectionEnd ) - start ;
return new TextLayout (
textSource ,
paragraphProperties ,
TextTrimming ,
_ constraint . Width ,
_ constraint . Height ,
maxLines : MaxLines ,
lineHeight : LineHeight ) ;
}
var rects = TextLayout . HitTestTextRange ( start , length ) ;
protected override void OnLostFocus ( RoutedEventArgs e )
{
base . OnLostFocus ( e ) ;
using ( context . PushPostTransform ( Matrix . CreateTranslation ( origin ) ) )
{
foreach ( var rect in rects )
{
context . FillRectangle ( selectionBrush , PixelRect . FromRect ( rect , 1 ) . ToRect ( 1 ) ) ;
}
}
}
ClearSelection ( ) ;
base . RenderTextLayout ( context , origin ) ;
}
protected override void OnKeyDown ( KeyEventArgs e )
@ -352,11 +240,6 @@ namespace Avalonia.Controls
{
base . OnPointerPressed ( e ) ;
if ( ! IsTextSelectionEnabled )
{
return ;
}
var text = Text ;
var clickInfo = e . GetCurrentPoint ( this ) ;
@ -435,11 +318,6 @@ namespace Avalonia.Controls
{
base . OnPointerMoved ( e ) ;
if ( ! IsTextSelectionEnabled )
{
return ;
}
// selection should not change during pointer move if the user right clicks
if ( e . Pointer . Captured = = this & & e . GetCurrentPoint ( this ) . Properties . IsLeftButtonPressed )
{
@ -486,11 +364,6 @@ namespace Avalonia.Controls
{
base . OnPointerReleased ( e ) ;
if ( ! IsTextSelectionEnabled )
{
return ;
}
if ( e . Pointer . Captured ! = this )
{
return ;
@ -521,100 +394,15 @@ namespace Avalonia.Controls
e . Pointer . Capture ( null ) ;
}
protected override void OnPropertyChanged ( AvaloniaPropertyChangedEventArgs change )
{
base . OnPropertyChanged ( change ) ;
switch ( change . Property . Name )
{
case nameof ( Inlines ) :
{
OnInlinesChanged ( change . OldValue as InlineCollection , change . NewValue as InlineCollection ) ;
InvalidateTextLayout ( ) ;
break ;
}
}
}
protected override Size MeasureOverride ( Size availableSize )
private void UpdateCommandStates ( )
{
if ( _ textRuns ! = null )
{
LogicalChildren . Clear ( ) ;
VisualChildren . Clear ( ) ;
_ textRuns = null ;
}
if ( Inlines ! = null & & Inlines . Count > 0 )
{
var inlines = Inlines ;
var textRuns = new List < TextRun > ( ) ;
foreach ( var inline in inlines )
{
inline . BuildTextRun ( textRuns ) ;
}
foreach ( var textRun in textRuns )
{
if ( textRun is EmbeddedControlRun controlRun & &
controlRun . Control is Control control )
{
LogicalChildren . Add ( control ) ;
VisualChildren . Add ( control ) ;
control . Measure ( Size . Infinity ) ;
}
}
_ textRuns = textRuns ;
}
return base . MeasureOverride ( availableSize ) ;
}
protected override Size ArrangeOverride ( Size finalSize )
{
if ( HasComplexContent )
{
var currentY = 0.0 ;
foreach ( var textLine in TextLayout . TextLines )
{
var currentX = textLine . Start ;
foreach ( var run in textLine . TextRuns )
{
if ( run is DrawableTextRun drawable )
{
if ( drawable is EmbeddedControlRun controlRun
& & controlRun . Control is Control control )
{
control . Arrange ( new Rect ( new Point ( currentX , currentY ) , control . DesiredSize ) ) ;
}
currentX + = drawable . Size . Width ;
}
}
var text = GetSelection ( ) ;
currentY + = textLine . Height ;
}
}
return base . ArrangeOverride ( finalSize ) ;
CanCopy = ! string . IsNullOrEmpty ( text ) ;
}
private string GetSelection ( )
{
if ( ! IsTextSelectionEnabled )
{
return "" ;
}
var text = GetText ( ) ;
if ( string . IsNullOrEmpty ( text ) )
@ -638,59 +426,5 @@ namespace Avalonia.Controls
return selectedText ;
}
private void OnInlinesChanged ( InlineCollection ? oldValue , InlineCollection ? newValue )
{
if ( oldValue is not null )
{
oldValue . Parent = null ;
oldValue . InlineHost = null ;
oldValue . Invalidated - = ( s , e ) = > InvalidateTextLayout ( ) ;
}
if ( newValue is not null )
{
newValue . Parent = this ;
newValue . InlineHost = this ;
newValue . Invalidated + = ( s , e ) = > InvalidateTextLayout ( ) ;
}
}
void IInlineHost . Invalidate ( )
{
InvalidateTextLayout ( ) ;
}
private readonly struct InlinesTextSource : ITextSource
{
private readonly IReadOnlyList < TextRun > _ textRuns ;
public InlinesTextSource ( IReadOnlyList < TextRun > textRuns )
{
_ textRuns = textRuns ;
}
public TextRun ? GetTextRun ( int textSourceIndex )
{
var currentPosition = 0 ;
foreach ( var textRun in _ textRuns )
{
if ( textRun . TextSourceLength = = 0 )
{
continue ;
}
if ( currentPosition > = textSourceIndex )
{
return textRun ;
}
currentPosition + = textRun . TextSourceLength ;
}
return null ;
}
}
}
}