Browse Source

Merge pull request #9245 from Gillibald/feature/txtBlockInlines

Move inlines support to TextBlock and rename RichTextBlock
pull/9299/head
Benedikt Stebner 4 years ago
committed by GitHub
parent
commit
654e4458d7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  2. 4
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  3. 40
      src/Avalonia.Controls/Documents/InlineCollection.cs
  4. 6
      src/Avalonia.Controls/Documents/Span.cs
  5. 374
      src/Avalonia.Controls/SelectableTextBlock.cs
  6. 279
      src/Avalonia.Controls/TextBlock.cs
  7. 2
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  8. 14
      src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml
  9. 18
      src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml
  10. 14
      src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml
  11. 18
      src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml
  12. 2
      src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
  13. 2
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  14. 2
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  15. 132
      tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs
  16. 121
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

4
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -118,7 +118,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<Border> <Border>
<RichTextBlock SelectionBrush="LightBlue" IsTextSelectionEnabled="True" Margin="10" TextWrapping="Wrap"> <SelectableTextBlock SelectionBrush="LightBlue" Margin="10" TextWrapping="Wrap">
This <Span FontWeight="Bold">is</Span> a This <Span FontWeight="Bold">is</Span> a
<Span Background="Silver" Foreground="Maroon">TextBlock</Span> <Span Background="Silver" Foreground="Maroon">TextBlock</Span>
with <Span TextDecorations="Underline">several</Span> with <Span TextDecorations="Underline">several</Span>
@ -126,7 +126,7 @@
<Span Foreground="Blue"> <Span Foreground="Blue">
using a <Bold>variety</Bold> of <Italic>styles</Italic> using a <Bold>variety</Bold> of <Italic>styles</Italic>
</Span>. </Span>.
</RichTextBlock> </SelectableTextBlock>
</Border> </Border>
</WrapPanel> </WrapPanel>
</StackPanel> </StackPanel>

4
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -505,7 +505,7 @@ namespace Avalonia.Media.TextFormatting
case { } drawableTextRun: case { } drawableTextRun:
{ {
if (currentWidth + drawableTextRun.Size.Width > paragraphWidth) if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
{ {
goto found; goto found;
} }
@ -668,7 +668,7 @@ namespace Avalonia.Media.TextFormatting
if (!breakFound) if (!breakFound)
{ {
currentLength += currentRun.Text.Length; currentLength += currentRun.TextSourceLength;
continue; continue;
} }

40
src/Avalonia.Controls/Documents/InlineCollection.cs

@ -12,7 +12,7 @@ namespace Avalonia.Controls.Documents
[WhitespaceSignificantCollection] [WhitespaceSignificantCollection]
public class InlineCollection : AvaloniaList<Inline> public class InlineCollection : AvaloniaList<Inline>
{ {
private ILogical? _parent; private IAvaloniaList<ILogical>? _logicalChildren;
private IInlineHost? _inlineHost; private IInlineHost? _inlineHost;
/// <summary> /// <summary>
@ -24,28 +24,30 @@ namespace Avalonia.Controls.Documents
this.ForEachItem( this.ForEachItem(
x => x =>
{ {
((ISetLogicalParent)x).SetParent(Parent);
x.InlineHost = InlineHost; x.InlineHost = InlineHost;
LogicalChildren?.Add(x);
Invalidate(); Invalidate();
}, },
x => x =>
{ {
((ISetLogicalParent)x).SetParent(null); LogicalChildren?.Remove(x);
x.InlineHost = InlineHost; x.InlineHost = InlineHost;
Invalidate(); Invalidate();
}, },
() => throw new NotSupportedException()); () => throw new NotSupportedException());
} }
internal ILogical? Parent internal IAvaloniaList<ILogical>? LogicalChildren
{ {
get => _parent; get => _logicalChildren;
set set
{ {
_parent = value; var oldValue = _logicalChildren;
_logicalChildren = value;
OnParentChanged(value); OnParentChanged(oldValue, value);
} }
} }
@ -70,6 +72,11 @@ namespace Avalonia.Controls.Documents
{ {
get get
{ {
if (Count == 0)
{
return null;
}
var builder = StringBuilderCache.Acquire(); var builder = StringBuilderCache.Acquire();
foreach (var inline in this) foreach (var inline in this)
@ -111,7 +118,7 @@ namespace Avalonia.Controls.Documents
private void AddText(string text) private void AddText(string text)
{ {
if (Parent is RichTextBlock textBlock && !textBlock.HasComplexContent) if (LogicalChildren is TextBlock textBlock && !textBlock.HasComplexContent)
{ {
textBlock._text += text; textBlock._text += text;
} }
@ -123,7 +130,7 @@ namespace Avalonia.Controls.Documents
private void OnAdd() private void OnAdd()
{ {
if (Parent is RichTextBlock textBlock) if (LogicalChildren is TextBlock textBlock)
{ {
if (!textBlock.HasComplexContent && !string.IsNullOrEmpty(textBlock._text)) if (!textBlock.HasComplexContent && !string.IsNullOrEmpty(textBlock._text))
{ {
@ -152,20 +159,21 @@ namespace Avalonia.Controls.Documents
Invalidated?.Invoke(this, EventArgs.Empty); Invalidated?.Invoke(this, EventArgs.Empty);
} }
private void OnParentChanged(ILogical? parent) private void OnParentChanged(IAvaloniaList<ILogical>? oldParent, IAvaloniaList<ILogical>? newParent)
{ {
foreach (var child in this) foreach (var child in this)
{ {
var oldParent = child.Parent; if (oldParent != newParent)
if (oldParent != parent)
{ {
if (oldParent != null) if (oldParent != null)
{ {
((ISetLogicalParent)child).SetParent(null); oldParent.Remove(child);
} }
((ISetLogicalParent)child).SetParent(parent); if(newParent != null)
{
newParent.Add(child);
}
} }
} }
} }

6
src/Avalonia.Controls/Documents/Span.cs

@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents
{ {
Inlines = new InlineCollection Inlines = new InlineCollection
{ {
Parent = this LogicalChildren = LogicalChildren
}; };
} }
@ -78,14 +78,14 @@ namespace Avalonia.Controls.Documents
{ {
if (oldValue is not null) if (oldValue is not null)
{ {
oldValue.Parent = null; oldValue.LogicalChildren = null;
oldValue.InlineHost = null; oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate(); oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate();
} }
if (newValue is not null) if (newValue is not null)
{ {
newValue.Parent = this; newValue.LogicalChildren = LogicalChildren;
newValue.InlineHost = InlineHost; newValue.InlineHost = InlineHost;
newValue.Invalidated += (s, e) => InlineHost?.Invalidate(); newValue.Invalidated += (s, e) => InlineHost?.Invalidate();
} }

374
src/Avalonia.Controls/RichTextBlock.cs → src/Avalonia.Controls/SelectableTextBlock.cs

@ -8,7 +8,6 @@ using Avalonia.Input.Platform;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -16,67 +15,53 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control that displays a block of formatted text. /// A control that displays a block of formatted text.
/// </summary> /// </summary>
public class RichTextBlock : TextBlock, IInlineHost public class SelectableTextBlock : TextBlock, IInlineHost
{ {
public static readonly StyledProperty<bool> IsTextSelectionEnabledProperty = public static readonly DirectProperty<SelectableTextBlock, int> SelectionStartProperty =
AvaloniaProperty.Register<RichTextBlock, bool>(nameof(IsTextSelectionEnabled), false); AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>(
public static readonly DirectProperty<RichTextBlock, int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
nameof(SelectionStart), nameof(SelectionStart),
o => o.SelectionStart, o => o.SelectionStart,
(o, v) => o.SelectionStart = v); (o, v) => o.SelectionStart = v);
public static readonly DirectProperty<RichTextBlock, int> SelectionEndProperty = public static readonly DirectProperty<SelectableTextBlock, int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, int>( AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>(
nameof(SelectionEnd), nameof(SelectionEnd),
o => o.SelectionEnd, o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v); (o, v) => o.SelectionEnd = v);
public static readonly DirectProperty<RichTextBlock, string> SelectedTextProperty = public static readonly DirectProperty<SelectableTextBlock, string> SelectedTextProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, string>( AvaloniaProperty.RegisterDirect<SelectableTextBlock, string>(
nameof(SelectedText), nameof(SelectedText),
o => o.SelectedText); o => o.SelectedText);
public static readonly StyledProperty<IBrush?> SelectionBrushProperty = public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
AvaloniaProperty.Register<RichTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue); AvaloniaProperty.Register<SelectableTextBlock, 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 = public static readonly DirectProperty<SelectableTextBlock, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>( AvaloniaProperty.RegisterDirect<SelectableTextBlock, bool>(
nameof(CanCopy), nameof(CanCopy),
o => o.CanCopy); o => o.CanCopy);
public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent = public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
RoutedEvent.Register<RichTextBlock, RoutedEventArgs>( RoutedEvent.Register<SelectableTextBlock, RoutedEventArgs>(
nameof(CopyingToClipboard), RoutingStrategies.Bubble); nameof(CopyingToClipboard), RoutingStrategies.Bubble);
private bool _canCopy; private bool _canCopy;
private int _selectionStart; private int _selectionStart;
private int _selectionEnd; private int _selectionEnd;
private int _wordSelectionStart = -1; private int _wordSelectionStart = -1;
private IReadOnlyList<TextRun>? _textRuns;
static RichTextBlock() static SelectableTextBlock()
{ {
FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true); FocusableProperty.OverrideDefaultValue(typeof(SelectableTextBlock), true);
AffectsRender<SelectableTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty);
AffectsRender<RichTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty, IsTextSelectionEnabledProperty);
} }
public RichTextBlock() public event EventHandler<RoutedEventArgs>? CopyingToClipboard
{ {
Inlines = new InlineCollection add => AddHandler(CopyingToClipboardEvent, value);
{ remove => RemoveHandler(CopyingToClipboardEvent, value);
Parent = this,
InlineHost = this
};
} }
/// <summary> /// <summary>
@ -99,6 +84,8 @@ namespace Avalonia.Controls
if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value)) if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
{ {
RaisePropertyChanged(SelectedTextProperty, "", ""); RaisePropertyChanged(SelectedTextProperty, "", "");
UpdateCommandStates();
} }
} }
} }
@ -114,6 +101,8 @@ namespace Avalonia.Controls
if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value)) if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
{ {
RaisePropertyChanged(SelectedTextProperty, "", ""); RaisePropertyChanged(SelectedTextProperty, "", "");
UpdateCommandStates();
} }
} }
} }
@ -126,25 +115,6 @@ namespace Avalonia.Controls
get => GetSelection(); 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> /// <summary>
/// Property for determining if the Copy command can be executed. /// Property for determining if the Copy command can be executed.
/// </summary> /// </summary>
@ -154,20 +124,12 @@ namespace Avalonia.Controls
private set => SetAndRaise(CanCopyProperty, ref _canCopy, value); 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> /// <summary>
/// Copies the current selection to the Clipboard. /// Copies the current selection to the Clipboard.
/// </summary> /// </summary>
public async void Copy() public async void Copy()
{ {
if (_canCopy || !IsTextSelectionEnabled) if (!_canCopy)
{ {
return; return;
} }
@ -188,45 +150,13 @@ namespace Avalonia.Controls
await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard))) await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
.SetTextAsync(text); .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> /// <summary>
/// Select all text in the TextBox /// Select all text in the TextBox
/// </summary> /// </summary>
public void SelectAll() public void SelectAll()
{ {
if (!IsTextSelectionEnabled)
{
return;
}
var text = Text; var text = Text;
SelectionStart = 0; SelectionStart = 0;
@ -238,94 +168,52 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public void ClearSelection() public void ClearSelection()
{ {
if (!IsTextSelectionEnabled)
{
return;
}
SelectionEnd = SelectionStart; SelectionEnd = SelectionStart;
} }
protected void AddText(string? text) protected override void OnGotFocus(GotFocusEventArgs e)
{ {
if (string.IsNullOrEmpty(text)) base.OnGotFocus(e);
{
return;
}
if (!HasComplexContent && string.IsNullOrEmpty(_text))
{
_text = text;
}
else
{
if (!string.IsNullOrEmpty(_text))
{
Inlines?.Add(_text);
_text = null;
}
Inlines?.Add(text);
}
}
protected override string? GetText() UpdateCommandStates();
{
return _text ?? Inlines?.Text;
} }
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> protected override void RenderTextLayout(DrawingContext context, Point origin)
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected override TextLayout CreateTextLayout(string? text)
{ {
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); var selectionStart = SelectionStart;
var defaultProperties = new GenericTextRunProperties( var selectionEnd = SelectionEnd;
typeface, var selectionBrush = SelectionBrush;
FontSize,
TextDecorations,
Foreground);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0);
ITextSource textSource;
if (_textRuns != null) if (selectionStart != selectionEnd && selectionBrush != null)
{ {
textSource = new InlinesTextSource(_textRuns); var start = Math.Min(selectionStart, selectionEnd);
} var length = Math.Max(selectionStart, selectionEnd) - start;
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
}
return new TextLayout( var rects = TextLayout.HitTestTextRange(start, length);
textSource,
paragraphProperties,
TextTrimming,
_constraint.Width,
_constraint.Height,
maxLines: MaxLines,
lineHeight: LineHeight);
}
protected override void OnLostFocus(RoutedEventArgs e) using (context.PushPostTransform(Matrix.CreateTranslation(origin)))
{ {
base.OnLostFocus(e); 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) protected override void OnKeyDown(KeyEventArgs e)
@ -352,11 +240,6 @@ namespace Avalonia.Controls
{ {
base.OnPointerPressed(e); base.OnPointerPressed(e);
if (!IsTextSelectionEnabled)
{
return;
}
var text = Text; var text = Text;
var clickInfo = e.GetCurrentPoint(this); var clickInfo = e.GetCurrentPoint(this);
@ -435,11 +318,6 @@ namespace Avalonia.Controls
{ {
base.OnPointerMoved(e); base.OnPointerMoved(e);
if (!IsTextSelectionEnabled)
{
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 == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{ {
@ -486,11 +364,6 @@ namespace Avalonia.Controls
{ {
base.OnPointerReleased(e); base.OnPointerReleased(e);
if (!IsTextSelectionEnabled)
{
return;
}
if (e.Pointer.Captured != this) if (e.Pointer.Captured != this)
{ {
return; return;
@ -521,100 +394,15 @@ namespace Avalonia.Controls
e.Pointer.Capture(null); e.Pointer.Capture(null);
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) private void UpdateCommandStates()
{
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)
{ {
if(_textRuns != null) var text = GetSelection();
{
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;
}
}
currentY += textLine.Height; CanCopy = !string.IsNullOrEmpty(text);
}
}
return base.ArrangeOverride(finalSize);
} }
private string GetSelection() private string GetSelection()
{ {
if (!IsTextSelectionEnabled)
{
return "";
}
var text = GetText(); var text = GetText();
if (string.IsNullOrEmpty(text)) if (string.IsNullOrEmpty(text))
@ -638,59 +426,5 @@ namespace Avalonia.Controls
return selectedText; 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;
}
}
} }
} }

279
src/Avalonia.Controls/TextBlock.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Layout; using Avalonia.Layout;
@ -12,7 +13,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control that displays a block of text. /// A control that displays a block of text.
/// </summary> /// </summary>
public class TextBlock : Control, IAddChild<string> public class TextBlock : Control, IInlineHost
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Background"/> property. /// Defines the <see cref="Background"/> property.
@ -96,15 +97,15 @@ namespace Avalonia.Controls
public static readonly DirectProperty<TextBlock, string?> TextProperty = public static readonly DirectProperty<TextBlock, string?> TextProperty =
AvaloniaProperty.RegisterDirect<TextBlock, string?>( AvaloniaProperty.RegisterDirect<TextBlock, string?>(
nameof(Text), nameof(Text),
o => o.Text, o => o.GetText(),
(o, v) => o.Text = v); (o, v) => o.SetText(v));
/// <summary> /// <summary>
/// Defines the <see cref="TextAlignment"/> property. /// Defines the <see cref="TextAlignment"/> property.
/// </summary> /// </summary>
public static readonly AttachedProperty<TextAlignment> TextAlignmentProperty = public static readonly AttachedProperty<TextAlignment> TextAlignmentProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextAlignment>( AvaloniaProperty.RegisterAttached<TextBlock, Control, TextAlignment>(
nameof(TextAlignment), nameof(TextAlignment),
defaultValue: TextAlignment.Start, defaultValue: TextAlignment.Start,
inherits: true); inherits: true);
@ -112,14 +113,14 @@ namespace Avalonia.Controls
/// Defines the <see cref="TextWrapping"/> property. /// Defines the <see cref="TextWrapping"/> property.
/// </summary> /// </summary>
public static readonly AttachedProperty<TextWrapping> TextWrappingProperty = public static readonly AttachedProperty<TextWrapping> TextWrappingProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextWrapping>(nameof(TextWrapping), AvaloniaProperty.RegisterAttached<TextBlock, Control, TextWrapping>(nameof(TextWrapping),
inherits: true); inherits: true);
/// <summary> /// <summary>
/// Defines the <see cref="TextTrimming"/> property. /// Defines the <see cref="TextTrimming"/> property.
/// </summary> /// </summary>
public static readonly AttachedProperty<TextTrimming> TextTrimmingProperty = public static readonly AttachedProperty<TextTrimming> TextTrimmingProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextTrimming>(nameof(TextTrimming), AvaloniaProperty.RegisterAttached<TextBlock, Control, TextTrimming>(nameof(TextTrimming),
defaultValue: TextTrimming.None, defaultValue: TextTrimming.None,
inherits: true); inherits: true);
@ -129,9 +130,17 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty = public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations)); AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations));
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly StyledProperty<InlineCollection?> InlinesProperty =
AvaloniaProperty.Register<TextBlock, InlineCollection?>(
nameof(Inlines));
internal string? _text; internal string? _text;
protected TextLayout? _textLayout; protected TextLayout? _textLayout;
protected Size _constraint; protected Size _constraint;
private IReadOnlyList<TextRun>? _textRuns;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="TextBlock"/> class. /// Initializes static members of the <see cref="TextBlock"/> class.
@ -139,10 +148,19 @@ namespace Avalonia.Controls
static TextBlock() static TextBlock()
{ {
ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true); ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty); AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty);
} }
public TextBlock()
{
Inlines = new InlineCollection
{
LogicalChildren = LogicalChildren,
InlineHost = this
};
}
/// <summary> /// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text. /// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary> /// </summary>
@ -288,9 +306,21 @@ namespace Avalonia.Controls
get => GetValue(TextDecorationsProperty); get => GetValue(TextDecorationsProperty);
set => SetValue(TextDecorationsProperty, value); set => SetValue(TextDecorationsProperty, value);
} }
/// <summary>
/// Gets or sets the inlines.
/// </summary>
[Content]
public InlineCollection? Inlines
{
get => GetValue(InlinesProperty);
set => SetValue(InlinesProperty, value);
}
protected override bool BypassFlowDirectionPolicies => true; protected override bool BypassFlowDirectionPolicies => true;
internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
/// <summary> /// <summary>
/// The BaselineOffset property provides an adjustment to baseline offset /// The BaselineOffset property provides an adjustment to baseline offset
/// </summary> /// </summary>
@ -513,19 +543,30 @@ namespace Avalonia.Controls
TextLayout.Draw(context, origin); TextLayout.Draw(context, origin);
} }
void IAddChild<string>.AddChild(string text)
{
_text = text;
}
protected virtual string? GetText() protected virtual string? GetText()
{ {
return _text; return _text ?? Inlines?.Text;
} }
protected virtual void SetText(string? text) protected virtual void SetText(string? text)
{ {
SetAndRaise(TextProperty, ref _text, text); if (Inlines != null && Inlines.Count > 0)
{
var oldValue = Inlines.Text;
if (!string.IsNullOrEmpty(text))
{
Inlines.Add(text);
}
text = Inlines.Text;
RaisePropertyChanged(TextProperty, oldValue, text);
}
else
{
SetAndRaise(TextProperty, ref _text, text);
}
} }
/// <summary> /// <summary>
@ -534,8 +575,10 @@ namespace Avalonia.Controls
/// <returns>A <see cref="TextLayout"/> object.</returns> /// <returns>A <see cref="TextLayout"/> object.</returns>
protected virtual TextLayout CreateTextLayout(string? text) protected virtual TextLayout CreateTextLayout(string? text)
{ {
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var defaultProperties = new GenericTextRunProperties( var defaultProperties = new GenericTextRunProperties(
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), typeface,
FontSize, FontSize,
TextDecorations, TextDecorations,
Foreground); Foreground);
@ -543,8 +586,19 @@ namespace Avalonia.Controls
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false, var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0); defaultProperties, TextWrapping, LineHeight, 0);
ITextSource textSource;
if (_textRuns != null)
{
textSource = new InlinesTextSource(_textRuns);
}
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
}
return new TextLayout( return new TextLayout(
new SimpleTextSource((text ?? "").AsMemory(), defaultProperties), textSource,
paragraphProperties, paragraphProperties,
TextTrimming, TextTrimming,
_constraint.Width, _constraint.Width,
@ -560,6 +614,8 @@ namespace Avalonia.Controls
{ {
_textLayout = null; _textLayout = null;
InvalidateVisual();
InvalidateMeasure(); InvalidateMeasure();
} }
@ -573,7 +629,46 @@ namespace Avalonia.Controls
_textLayout = null; _textLayout = null;
InvalidateArrange(); var inlines = Inlines;
if (HasComplexContent)
{
if (_textRuns != null)
{
foreach (var textRun in _textRuns)
{
if (textRun is EmbeddedControlRun controlRun &&
controlRun.Control is Control control)
{
VisualChildren.Remove(control);
LogicalChildren.Remove(control);
}
}
}
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)
{
VisualChildren.Add(control);
LogicalChildren.Add(control);
control.Measure(Size.Infinity);
}
}
_textRuns = textRuns;
}
var measuredSize = TextLayout.Bounds.Size.Inflate(padding); var measuredSize = TextLayout.Bounds.Size.Inflate(padding);
@ -584,16 +679,11 @@ namespace Avalonia.Controls
{ {
var textWidth = Math.Ceiling(TextLayout.Bounds.Width); var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
if(finalSize.Width < textWidth) if (finalSize.Width < textWidth)
{ {
finalSize = finalSize.WithWidth(textWidth); finalSize = finalSize.WithWidth(textWidth);
} }
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
{
return finalSize;
}
var scale = LayoutHelper.GetLayoutScale(this); var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
@ -602,6 +692,32 @@ namespace Avalonia.Controls
_textLayout = null; _textLayout = null;
if (HasComplexContent)
{
var currentY = padding.Top;
foreach (var textLine in TextLayout.TextLines)
{
var currentX = padding.Left + 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;
}
}
currentY += textLine.Height;
}
}
return finalSize; return finalSize;
} }
@ -610,42 +726,70 @@ namespace Avalonia.Controls
return new TextBlockAutomationPeer(this); return new TextBlockAutomationPeer(this);
} }
private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
switch (change.Property.Name) switch (change.Property.Name)
{ {
case nameof (FontSize): case nameof(FontSize):
case nameof (FontWeight): case nameof(FontWeight):
case nameof (FontStyle): case nameof(FontStyle):
case nameof (FontFamily): case nameof(FontFamily):
case nameof (FontStretch): case nameof(FontStretch):
case nameof(TextWrapping):
case nameof(TextTrimming):
case nameof(TextAlignment):
case nameof(FlowDirection):
case nameof(Padding):
case nameof(LineHeight):
case nameof(MaxLines):
case nameof(Text):
case nameof(TextDecorations):
case nameof(Foreground):
{
InvalidateTextLayout();
break;
}
case nameof(Inlines):
{
OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
InvalidateTextLayout();
break;
}
}
}
case nameof (TextWrapping): private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
case nameof (TextTrimming):
case nameof (TextAlignment):
case nameof (FlowDirection): private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;
case nameof (Padding): private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
case nameof (LineHeight): {
case nameof (MaxLines): if (oldValue is not null)
{
oldValue.LogicalChildren = null;
oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InvalidateTextLayout();
}
case nameof (Text): if (newValue is not null)
case nameof (TextDecorations): {
case nameof (Foreground): newValue.LogicalChildren = LogicalChildren;
{ newValue.InlineHost = this;
InvalidateTextLayout(); newValue.Invalidated += (s, e) => InvalidateTextLayout();
break;
}
} }
} }
void IInlineHost.Invalidate()
{
InvalidateTextLayout();
}
protected readonly struct SimpleTextSource : ITextSource protected readonly struct SimpleTextSource : ITextSource
{ {
private readonly ReadOnlySlice<char> _text; private readonly ReadOnlySlice<char> _text;
@ -674,5 +818,46 @@ namespace Avalonia.Controls
return new TextCharacters(runText, _defaultProperties); return new TextCharacters(runText, _defaultProperties);
} }
} }
private readonly struct InlinesTextSource : ITextSource
{
private readonly IReadOnlyList<TextRun> _textRuns;
public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
{
_textRuns = textRuns;
}
public IReadOnlyList<TextRun> TextRuns => _textRuns;
public TextRun? GetTextRun(int textSourceIndex)
{
var currentPosition = 0;
foreach (var textRun in _textRuns)
{
if (textRun.TextSourceLength == 0)
{
continue;
}
if (textSourceIndex >= currentPosition + textRun.TextSourceLength)
{
currentPosition += textRun.TextSourceLength;
continue;
}
if (textRun is TextCharacters)
{
return new TextCharacters(textRun.Text.Skip(Math.Max(0, textSourceIndex - currentPosition)), textRun.Properties!);
}
return textRun;
}
return null;
}
}
} }
} }

2
src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml

@ -68,7 +68,7 @@
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/Slider.xaml" /> <ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/Slider.xaml" />
<!-- ManagedFileChooser comes last because it uses (and overrides) styles for a multitude of other controls...the dialogs were originally UserControls, after all --> <!-- ManagedFileChooser comes last because it uses (and overrides) styles for a multitude of other controls...the dialogs were originally UserControls, after all -->
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml" /> <ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml"/> <ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml"/>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</Styles.Resources> </Styles.Resources>

14
src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml

@ -1,14 +0,0 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<RichTextBlock IsTextSelectionEnabled="True" Text="Preview"/>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type RichTextBlock}" TargetType="RichTextBlock">
<Style Selector="^[IsTextSelectionEnabled=True]">
<Setter Property="Cursor" Value="IBeam" />
</Style>
</ControlTheme>
</ResourceDictionary>

18
src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml

@ -0,0 +1,18 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<SelectableTextBlock Text="Preview" />
</Design.PreviewWith>
<MenuFlyout x:Key="SelectableTextBlockContextFlyout" Placement="Bottom">
<MenuItem x:Name="SelectableTextBlockContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[SelectableTextBlock].Copy}"
IsEnabled="{Binding $parent[SelectableTextBlock].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}" />
</MenuFlyout>
<ControlTheme x:Key="{x:Type SelectableTextBlock}" TargetType="SelectableTextBlock">
<Style Selector="^[IsEnabled=True]">
<Setter Property="Cursor" Value="IBeam" />
<Setter Property="ContextFlyout" Value="{StaticResource SelectableTextBlockContextFlyout}" />
</Style>
</ControlTheme>
</ResourceDictionary>

14
src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml

@ -1,14 +0,0 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<RichTextBlock IsTextSelectionEnabled="True"
Text="Preview" />
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type RichTextBlock}"
TargetType="RichTextBlock">
<Style Selector="^[IsTextSelectionEnabled=True]">
<Setter Property="Cursor" Value="IBeam" />
</Style>
</ControlTheme>
</ResourceDictionary>

18
src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml

@ -0,0 +1,18 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<SelectableTextBlock Text="Preview" />
</Design.PreviewWith>
<MenuFlyout x:Key="SelectableTextBlockContextFlyout" Placement="Bottom">
<MenuItem x:Name="SelectableTextBlockContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[SelectableTextBlock].Copy}"
IsEnabled="{Binding $parent[SelectableTextBlock].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}" />
</MenuFlyout>
<ControlTheme x:Key="{x:Type SelectableTextBlock}" TargetType="SelectableTextBlock">
<Style Selector="^[IsEnabled=True]">
<Setter Property="Cursor" Value="IBeam" />
<Setter Property="ContextFlyout" Value="{StaticResource SelectableTextBlockContextFlyout}" />
</Style>
</ControlTheme>
</ResourceDictionary>

2
src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml

@ -64,7 +64,7 @@
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/SplitView.xaml" /> <ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/SplitView.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml" /> <ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/SplitButton.xaml" /> <ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/SplitButton.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/RichTextBlock.xaml" /> <ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</Styles.Resources> </Styles.Resources>

2
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -64,7 +64,7 @@ namespace Avalonia.Skia
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
if(glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t') if(text.Buffer.Span[glyphCluster] == '\t')
{ {
glyphIndex = typeface.GetGlyph(' '); glyphIndex = typeface.GetGlyph(' ');

2
src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs

@ -64,7 +64,7 @@ namespace Avalonia.Direct2D1.Media
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
if (glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t') if (text.Buffer.Span[glyphCluster] == '\t')
{ {
glyphIndex = typeface.GetGlyph(' '); glyphIndex = typeface.GetGlyph(' ');

132
tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs

@ -1,132 +0,0 @@
using Avalonia.Controls.Documents;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class RichTextBlockTests
{
[Fact]
public void Changing_InlinesCollection_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new RichTextBlock();
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
target.Inlines.Add(new Run("Hello"));
Assert.False(target.IsMeasureValid);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
}
}
[Fact]
public void Changing_Inlines_Properties_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new RichTextBlock();
var inline = new Run("Hello");
target.Inlines.Add(inline);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
inline.Foreground = Brushes.Green;
Assert.False(target.IsMeasureValid);
}
}
[Fact]
public void Changing_Inlines_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new RichTextBlock();
var inlines = new InlineCollection { new Run("Hello") };
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
target.Inlines = inlines;
Assert.False(target.IsMeasureValid);
}
}
[Fact]
public void Changing_Inlines_Should_Reset_Inlines_Parent()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new RichTextBlock();
var run = new Run("Hello");
target.Inlines.Add(run);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
target.Inlines = null;
Assert.Null(run.Parent);
target.Inlines = new InlineCollection { run };
Assert.Equal(target, run.Parent);
}
}
[Fact]
public void InlineUIContainer_Child_Schould_Be_Arranged()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var target = new RichTextBlock();
var button = new Button { Content = "12345678" };
button.Template = new FuncControlTemplate<Button>((parent, scope) =>
new TextBlock
{
Name = "PART_ContentPresenter",
[!TextBlock.TextProperty] = parent[!ContentControl.ContentProperty],
}.RegisterInNameScope(scope)
);
target.Inlines!.Add("123456");
target.Inlines.Add(new InlineUIContainer(button));
target.Inlines.Add("123456");
target.Measure(Size.Infinity);
Assert.True(button.IsMeasureValid);
Assert.Equal(80, button.DesiredSize.Width);
target.Arrange(new Rect(new Size(200, 50)));
Assert.True(button.IsArrangeValid);
Assert.Equal(60, button.Bounds.Left);
}
}
}
}

121
tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Rendering; using Avalonia.Rendering;
@ -60,5 +61,125 @@ namespace Avalonia.Controls.UnitTests
renderer.Verify(x => x.AddDirty(target), Times.Once); renderer.Verify(x => x.AddDirty(target), Times.Once);
} }
[Fact]
public void Changing_InlinesCollection_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new TextBlock();
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
target.Inlines.Add(new Run("Hello"));
Assert.False(target.IsMeasureValid);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
}
}
[Fact]
public void Changing_Inlines_Properties_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new TextBlock();
var inline = new Run("Hello");
target.Inlines.Add(inline);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
inline.Foreground = Brushes.Green;
Assert.False(target.IsMeasureValid);
}
}
[Fact]
public void Changing_Inlines_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new TextBlock();
var inlines = new InlineCollection { new Run("Hello") };
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
target.Inlines = inlines;
Assert.False(target.IsMeasureValid);
}
}
[Fact]
public void Changing_Inlines_Should_Reset_Inlines_Parent()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new TextBlock();
var run = new Run("Hello");
target.Inlines.Add(run);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
target.Inlines = null;
Assert.Null(run.Parent);
target.Inlines = new InlineCollection { run };
Assert.Equal(target, run.Parent);
}
}
[Fact]
public void InlineUIContainer_Child_Schould_Be_Arranged()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var target = new TextBlock();
var button = new Button { Content = "12345678" };
button.Template = new FuncControlTemplate<Button>((parent, scope) =>
new TextBlock
{
Name = "PART_ContentPresenter",
[!TextBlock.TextProperty] = parent[!ContentControl.ContentProperty],
}.RegisterInNameScope(scope)
);
target.Inlines!.Add("123456");
target.Inlines.Add(new InlineUIContainer(button));
target.Inlines.Add("123456");
target.Measure(Size.Infinity);
Assert.True(button.IsMeasureValid);
Assert.Equal(80, button.DesiredSize.Width);
target.Arrange(new Rect(new Size(200, 50)));
Assert.True(button.IsArrangeValid);
Assert.Equal(60, button.Bounds.Left);
}
}
} }
} }

Loading…
Cancel
Save