diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml
index 32914428ed..6bb428e2c7 100644
--- a/samples/ControlCatalog/Pages/TextBlockPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml
@@ -118,7 +118,7 @@
-
+
This is a
TextBlock
with several
@@ -126,7 +126,7 @@
using a variety of styles
.
-
+
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
index 5c43a1c94f..145c99cadc 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
@@ -505,7 +505,7 @@ namespace Avalonia.Media.TextFormatting
case { } drawableTextRun:
{
- if (currentWidth + drawableTextRun.Size.Width > paragraphWidth)
+ if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
{
goto found;
}
@@ -668,7 +668,7 @@ namespace Avalonia.Media.TextFormatting
if (!breakFound)
{
- currentLength += currentRun.Text.Length;
+ currentLength += currentRun.TextSourceLength;
continue;
}
diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs
index 1ba65b3e8f..a265f88e21 100644
--- a/src/Avalonia.Controls/Documents/InlineCollection.cs
+++ b/src/Avalonia.Controls/Documents/InlineCollection.cs
@@ -12,7 +12,7 @@ namespace Avalonia.Controls.Documents
[WhitespaceSignificantCollection]
public class InlineCollection : AvaloniaList
{
- private ILogical? _parent;
+ private IAvaloniaList? _logicalChildren;
private IInlineHost? _inlineHost;
///
@@ -24,28 +24,30 @@ namespace Avalonia.Controls.Documents
this.ForEachItem(
x =>
- {
- ((ISetLogicalParent)x).SetParent(Parent);
+ {
x.InlineHost = InlineHost;
+ LogicalChildren?.Add(x);
Invalidate();
},
x =>
{
- ((ISetLogicalParent)x).SetParent(null);
+ LogicalChildren?.Remove(x);
x.InlineHost = InlineHost;
Invalidate();
},
() => throw new NotSupportedException());
}
- internal ILogical? Parent
+ internal IAvaloniaList? LogicalChildren
{
- get => _parent;
+ get => _logicalChildren;
set
{
- _parent = value;
+ var oldValue = _logicalChildren;
+
+ _logicalChildren = value;
- OnParentChanged(value);
+ OnParentChanged(oldValue, value);
}
}
@@ -70,6 +72,11 @@ namespace Avalonia.Controls.Documents
{
get
{
+ if (Count == 0)
+ {
+ return null;
+ }
+
var builder = StringBuilderCache.Acquire();
foreach (var inline in this)
@@ -111,7 +118,7 @@ namespace Avalonia.Controls.Documents
private void AddText(string text)
{
- if (Parent is RichTextBlock textBlock && !textBlock.HasComplexContent)
+ if (LogicalChildren is TextBlock textBlock && !textBlock.HasComplexContent)
{
textBlock._text += text;
}
@@ -123,7 +130,7 @@ namespace Avalonia.Controls.Documents
private void OnAdd()
{
- if (Parent is RichTextBlock textBlock)
+ if (LogicalChildren is TextBlock textBlock)
{
if (!textBlock.HasComplexContent && !string.IsNullOrEmpty(textBlock._text))
{
@@ -152,20 +159,21 @@ namespace Avalonia.Controls.Documents
Invalidated?.Invoke(this, EventArgs.Empty);
}
- private void OnParentChanged(ILogical? parent)
+ private void OnParentChanged(IAvaloniaList? oldParent, IAvaloniaList? newParent)
{
foreach (var child in this)
{
- var oldParent = child.Parent;
-
- if (oldParent != parent)
+ if (oldParent != newParent)
{
if (oldParent != null)
{
- ((ISetLogicalParent)child).SetParent(null);
+ oldParent.Remove(child);
}
- ((ISetLogicalParent)child).SetParent(parent);
+ if(newParent != null)
+ {
+ newParent.Add(child);
+ }
}
}
}
diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs
index 363ce1011b..a7a702ceae 100644
--- a/src/Avalonia.Controls/Documents/Span.cs
+++ b/src/Avalonia.Controls/Documents/Span.cs
@@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents
{
Inlines = new InlineCollection
{
- Parent = this
+ LogicalChildren = LogicalChildren
};
}
@@ -78,14 +78,14 @@ namespace Avalonia.Controls.Documents
{
if (oldValue is not null)
{
- oldValue.Parent = null;
+ oldValue.LogicalChildren = null;
oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate();
}
if (newValue is not null)
{
- newValue.Parent = this;
+ newValue.LogicalChildren = LogicalChildren;
newValue.InlineHost = InlineHost;
newValue.Invalidated += (s, e) => InlineHost?.Invalidate();
}
diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs
similarity index 55%
rename from src/Avalonia.Controls/RichTextBlock.cs
rename to src/Avalonia.Controls/SelectableTextBlock.cs
index d0b713ba56..b343439f98 100644
--- a/src/Avalonia.Controls/RichTextBlock.cs
+++ b/src/Avalonia.Controls/SelectableTextBlock.cs
@@ -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
///
/// A control that displays a block of formatted text.
///
- public class RichTextBlock : TextBlock, IInlineHost
+ public class SelectableTextBlock : TextBlock, IInlineHost
{
- public static readonly StyledProperty IsTextSelectionEnabledProperty =
- AvaloniaProperty.Register(nameof(IsTextSelectionEnabled), false);
-
- public static readonly DirectProperty SelectionStartProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty SelectionStartProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(SelectionStart),
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
- public static readonly DirectProperty SelectionEndProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty SelectionEndProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(SelectionEnd),
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
- public static readonly DirectProperty SelectedTextProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty SelectedTextProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(SelectedText),
o => o.SelectedText);
public static readonly StyledProperty SelectionBrushProperty =
- AvaloniaProperty.Register(nameof(SelectionBrush), Brushes.Blue);
+ AvaloniaProperty.Register(nameof(SelectionBrush), Brushes.Blue);
- ///
- /// Defines the property.
- ///
- public static readonly StyledProperty InlinesProperty =
- AvaloniaProperty.Register(
- nameof(Inlines));
- public static readonly DirectProperty CanCopyProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty CanCopyProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(CanCopy),
o => o.CanCopy);
public static readonly RoutedEvent CopyingToClipboardEvent =
- RoutedEvent.Register(
+ RoutedEvent.Register(
nameof(CopyingToClipboard), RoutingStrategies.Bubble);
private bool _canCopy;
private int _selectionStart;
private int _selectionEnd;
private int _wordSelectionStart = -1;
- private IReadOnlyList? _textRuns;
- static RichTextBlock()
+ static SelectableTextBlock()
{
- FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true);
-
- AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty, IsTextSelectionEnabledProperty);
+ FocusableProperty.OverrideDefaultValue(typeof(SelectableTextBlock), true);
+ AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty);
}
- public RichTextBlock()
+ public event EventHandler? CopyingToClipboard
{
- Inlines = new InlineCollection
- {
- Parent = this,
- InlineHost = this
- };
+ add => AddHandler(CopyingToClipboardEvent, value);
+ remove => RemoveHandler(CopyingToClipboardEvent, value);
}
///
@@ -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();
}
- ///
- /// Gets or sets a value that indicates whether text selection is enabled, either through user action or calling selection-related API.
- ///
- public bool IsTextSelectionEnabled
- {
- get => GetValue(IsTextSelectionEnabledProperty);
- set => SetValue(IsTextSelectionEnabledProperty, value);
- }
-
- ///
- /// Gets or sets the inlines.
- ///
- [Content]
- public InlineCollection? Inlines
- {
- get => GetValue(InlinesProperty);
- set => SetValue(InlinesProperty, value);
- }
-
///
/// Property for determining if the Copy command can be executed.
///
@@ -154,20 +124,12 @@ namespace Avalonia.Controls
private set => SetAndRaise(CanCopyProperty, ref _canCopy, value);
}
- public event EventHandler? CopyingToClipboard
- {
- add => AddHandler(CopyingToClipboardEvent, value);
- remove => RemoveHandler(CopyingToClipboardEvent, value);
- }
-
- internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
-
///
/// Copies the current selection to the Clipboard.
///
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);
- }
+ }
///
/// Select all text in the TextBox
///
public void SelectAll()
{
- if (!IsTextSelectionEnabled)
- {
- return;
- }
-
var text = Text;
SelectionStart = 0;
@@ -238,94 +168,52 @@ namespace Avalonia.Controls
///
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();
}
- ///
- /// Creates the used to render the text.
- ///
- /// A object.
- 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();
-
- 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 _textRuns;
-
- public InlinesTextSource(IReadOnlyList 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;
- }
- }
}
}
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 99c8068b3d..f79d3f8296 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents;
using Avalonia.Layout;
@@ -12,7 +13,7 @@ namespace Avalonia.Controls
///
/// A control that displays a block of text.
///
- public class TextBlock : Control, IAddChild
+ public class TextBlock : Control, IInlineHost
{
///
/// Defines the property.
@@ -96,15 +97,15 @@ namespace Avalonia.Controls
public static readonly DirectProperty TextProperty =
AvaloniaProperty.RegisterDirect(
nameof(Text),
- o => o.Text,
- (o, v) => o.Text = v);
+ o => o.GetText(),
+ (o, v) => o.SetText(v));
///
/// Defines the property.
///
public static readonly AttachedProperty TextAlignmentProperty =
AvaloniaProperty.RegisterAttached(
- nameof(TextAlignment),
+ nameof(TextAlignment),
defaultValue: TextAlignment.Start,
inherits: true);
@@ -112,14 +113,14 @@ namespace Avalonia.Controls
/// Defines the property.
///
public static readonly AttachedProperty TextWrappingProperty =
- AvaloniaProperty.RegisterAttached(nameof(TextWrapping),
+ AvaloniaProperty.RegisterAttached(nameof(TextWrapping),
inherits: true);
///
/// Defines the property.
///
public static readonly AttachedProperty TextTrimmingProperty =
- AvaloniaProperty.RegisterAttached(nameof(TextTrimming),
+ AvaloniaProperty.RegisterAttached(nameof(TextTrimming),
defaultValue: TextTrimming.None,
inherits: true);
@@ -129,9 +130,17 @@ namespace Avalonia.Controls
public static readonly StyledProperty TextDecorationsProperty =
AvaloniaProperty.Register(nameof(TextDecorations));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty InlinesProperty =
+ AvaloniaProperty.Register(
+ nameof(Inlines));
+
internal string? _text;
protected TextLayout? _textLayout;
protected Size _constraint;
+ private IReadOnlyList? _textRuns;
///
/// Initializes static members of the class.
@@ -139,10 +148,19 @@ namespace Avalonia.Controls
static TextBlock()
{
ClipToBoundsProperty.OverrideDefaultValue(true);
-
+
AffectsRender(BackgroundProperty, ForegroundProperty);
}
+ public TextBlock()
+ {
+ Inlines = new InlineCollection
+ {
+ LogicalChildren = LogicalChildren,
+ InlineHost = this
+ };
+ }
+
///
/// Gets the used to render the text.
///
@@ -288,9 +306,21 @@ namespace Avalonia.Controls
get => GetValue(TextDecorationsProperty);
set => SetValue(TextDecorationsProperty, value);
}
-
+
+ ///
+ /// Gets or sets the inlines.
+ ///
+ [Content]
+ public InlineCollection? Inlines
+ {
+ get => GetValue(InlinesProperty);
+ set => SetValue(InlinesProperty, value);
+ }
+
protected override bool BypassFlowDirectionPolicies => true;
+ internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
+
///
/// The BaselineOffset property provides an adjustment to baseline offset
///
@@ -513,19 +543,30 @@ namespace Avalonia.Controls
TextLayout.Draw(context, origin);
}
- void IAddChild.AddChild(string text)
- {
- _text = text;
- }
-
protected virtual string? GetText()
{
- return _text;
+ return _text ?? Inlines?.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);
+ }
}
///
@@ -534,8 +575,10 @@ namespace Avalonia.Controls
/// A object.
protected virtual TextLayout CreateTextLayout(string? text)
{
+ var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
+
var defaultProperties = new GenericTextRunProperties(
- new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
+ typeface,
FontSize,
TextDecorations,
Foreground);
@@ -543,8 +586,19 @@ namespace Avalonia.Controls
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0);
+ ITextSource textSource;
+
+ if (_textRuns != null)
+ {
+ textSource = new InlinesTextSource(_textRuns);
+ }
+ else
+ {
+ textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
+ }
+
return new TextLayout(
- new SimpleTextSource((text ?? "").AsMemory(), defaultProperties),
+ textSource,
paragraphProperties,
TextTrimming,
_constraint.Width,
@@ -560,6 +614,8 @@ namespace Avalonia.Controls
{
_textLayout = null;
+ InvalidateVisual();
+
InvalidateMeasure();
}
@@ -573,7 +629,46 @@ namespace Avalonia.Controls
_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();
+
+ 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);
@@ -584,16 +679,11 @@ namespace Avalonia.Controls
{
var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
- if(finalSize.Width < textWidth)
+ if (finalSize.Width < textWidth)
{
finalSize = finalSize.WithWidth(textWidth);
}
- if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
- {
- return finalSize;
- }
-
var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
@@ -602,6 +692,32 @@ namespace Avalonia.Controls
_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;
}
@@ -610,42 +726,70 @@ namespace Avalonia.Controls
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)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
- case nameof (FontSize):
- case nameof (FontWeight):
- case nameof (FontStyle):
- case nameof (FontFamily):
- case nameof (FontStretch):
+ case nameof(FontSize):
+ case nameof(FontWeight):
+ case nameof(FontStyle):
+ case nameof(FontFamily):
+ 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):
- case nameof (TextTrimming):
- case nameof (TextAlignment):
+ private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
- case nameof (FlowDirection):
+ private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;
- case nameof (Padding):
- case nameof (LineHeight):
- case nameof (MaxLines):
+ private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
+ {
+ if (oldValue is not null)
+ {
+ oldValue.LogicalChildren = null;
+ oldValue.InlineHost = null;
+ oldValue.Invalidated -= (s, e) => InvalidateTextLayout();
+ }
- case nameof (Text):
- case nameof (TextDecorations):
- case nameof (Foreground):
- {
- InvalidateTextLayout();
- break;
- }
+ if (newValue is not null)
+ {
+ newValue.LogicalChildren = LogicalChildren;
+ newValue.InlineHost = this;
+ newValue.Invalidated += (s, e) => InvalidateTextLayout();
}
}
+ void IInlineHost.Invalidate()
+ {
+ InvalidateTextLayout();
+ }
+
protected readonly struct SimpleTextSource : ITextSource
{
private readonly ReadOnlySlice _text;
@@ -674,5 +818,46 @@ namespace Avalonia.Controls
return new TextCharacters(runText, _defaultProperties);
}
}
+
+ private readonly struct InlinesTextSource : ITextSource
+ {
+ private readonly IReadOnlyList _textRuns;
+
+ public InlinesTextSource(IReadOnlyList textRuns)
+ {
+ _textRuns = textRuns;
+ }
+
+ public IReadOnlyList 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;
+ }
+ }
}
}
diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
index 577539b26b..5383aa3180 100644
--- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
@@ -68,7 +68,7 @@
-
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml b/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml
deleted file mode 100644
index 75af2efcb1..0000000000
--- a/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml b/src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml
new file mode 100644
index 0000000000..f630969ae6
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml b/src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml
deleted file mode 100644
index c0570282cb..0000000000
--- a/src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml b/src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml
new file mode 100644
index 0000000000..aaa6448aea
--- /dev/null
+++ b/src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
index 644c6ed416..4aefa0136c 100644
--- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
+++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
@@ -64,7 +64,7 @@
-
+
diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
index b07deb1f4d..d6bb37a06a 100644
--- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs
+++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
@@ -64,7 +64,7 @@ namespace Avalonia.Skia
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
- if(glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t')
+ if(text.Buffer.Span[glyphCluster] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');
diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
index 064320f809..7f2cbc6182 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
@@ -64,7 +64,7 @@ namespace Avalonia.Direct2D1.Media
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
- if (glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t')
+ if (text.Buffer.Span[glyphCluster] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');
diff --git a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs
deleted file mode 100644
index 05007e4f2e..0000000000
--- a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs
+++ /dev/null
@@ -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