Browse Source

Introduce RichTextBlock

pull/8370/head
Benedikt Stebner 4 years ago
parent
commit
0c8f54fe00
  1. 4
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  2. 72
      src/Avalonia.Controls/Documents/InlineCollection.cs
  3. 84
      src/Avalonia.Controls/Documents/Span.cs
  4. 18
      src/Avalonia.Controls/Documents/TextElement.cs
  5. 199
      src/Avalonia.Controls/RichTextBlock.cs
  6. 123
      src/Avalonia.Controls/TextBlock.cs

4
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -118,7 +118,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<Border> <Border>
<TextBlock Margin="10" TextWrapping="Wrap"> <RichTextBlock 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>.
</TextBlock> </RichTextBlock>
</Border> </Border>
</WrapPanel> </WrapPanel>
</StackPanel> </StackPanel>

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

@ -12,39 +12,55 @@ namespace Avalonia.Controls.Documents
[WhitespaceSignificantCollection] [WhitespaceSignificantCollection]
public class InlineCollection : AvaloniaList<Inline> public class InlineCollection : AvaloniaList<Inline>
{ {
private readonly IInlineHost? _host; private ILogical? _parent;
private IInlineHost? _inlineHost;
private string? _text = string.Empty; private string? _text = string.Empty;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="InlineCollection"/> class. /// Initializes a new instance of the <see cref="InlineCollection"/> class.
/// </summary> /// </summary>
public InlineCollection(ILogical parent) : this(parent, null) { } public InlineCollection()
/// <summary>
/// Initializes a new instance of the <see cref="InlineCollection"/> class.
/// </summary>
internal InlineCollection(ILogical parent, IInlineHost? host = null) : base(0)
{ {
_host = host;
ResetBehavior = ResetBehavior.Remove; ResetBehavior = ResetBehavior.Remove;
this.ForEachItem( this.ForEachItem(
x => x =>
{ {
((ISetLogicalParent)x).SetParent(parent); ((ISetLogicalParent)x).SetParent(Parent);
x.InlineHost = host; x.InlineHost = InlineHost;
host?.Invalidate(); Invalidate();
}, },
x => x =>
{ {
((ISetLogicalParent)x).SetParent(null); ((ISetLogicalParent)x).SetParent(null);
x.InlineHost = host; x.InlineHost = InlineHost;
host?.Invalidate(); Invalidate();
}, },
() => throw new NotSupportedException()); () => throw new NotSupportedException());
} }
internal ILogical? Parent
{
get => _parent;
set
{
_parent = value;
OnParentChanged(value);
}
}
internal IInlineHost? InlineHost
{
get => _inlineHost;
set
{
_inlineHost = value;
OnInlineHostChanged(value);
}
}
public bool HasComplexContent => Count > 0; public bool HasComplexContent => Count > 0;
/// <summary> /// <summary>
@ -57,14 +73,16 @@ namespace Avalonia.Controls.Documents
{ {
get get
{ {
return _text;
if (!HasComplexContent) if (!HasComplexContent)
{ {
return _text; return _text;
} }
var builder = new StringBuilder(); var builder = new StringBuilder();
foreach(var inline in this) foreach (var inline in this)
{ {
inline.AppendText(builder); inline.AppendText(builder);
} }
@ -100,7 +118,7 @@ namespace Avalonia.Controls.Documents
} }
else else
{ {
_text += text; _text = text;
} }
} }
@ -136,14 +154,30 @@ namespace Avalonia.Controls.Documents
/// </summary> /// </summary>
protected void Invalidate() protected void Invalidate()
{ {
if(_host != null) if(InlineHost != null)
{ {
_host.Invalidate(); InlineHost.Invalidate();
} }
Invalidated?.Invoke(this, EventArgs.Empty); Invalidated?.Invoke(this, EventArgs.Empty);
} }
private void Invalidate(object? sender, EventArgs e) => Invalidate(); private void Invalidate(object? sender, EventArgs e) => Invalidate();
private void OnParentChanged(ILogical? parent)
{
foreach(var child in this)
{
((ISetLogicalParent)child).SetParent(parent);
}
}
private void OnInlineHostChanged(IInlineHost? inlineHost)
{
foreach (var child in this)
{
child.InlineHost = inlineHost;
}
}
} }
} }

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

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
namespace Avalonia.Controls.Documents namespace Avalonia.Controls.Documents
{ {
@ -14,25 +13,42 @@ namespace Avalonia.Controls.Documents
/// <summary> /// <summary>
/// Defines the <see cref="Inlines"/> property. /// Defines the <see cref="Inlines"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<Span, InlineCollection> InlinesProperty = public static readonly StyledProperty<InlineCollection> InlinesProperty =
AvaloniaProperty.RegisterDirect<Span, InlineCollection>( AvaloniaProperty.Register<Span, InlineCollection>(
nameof(Inlines), nameof(Inlines));
o => o.Inlines);
/// <summary>
/// Initializes a new instance of a Span element.
/// </summary>
public Span() public Span()
{ {
Inlines = new InlineCollection(this); Inlines = new InlineCollection
Inlines.Invalidated += (s, e) => InlineHost?.Invalidate(); {
Parent = this
};
} }
/// <summary> /// <summary>
/// Gets or sets the inlines. /// Gets or sets the inlines.
/// </summary> /// </summary>
[Content] public InlineCollection Inlines
public InlineCollection Inlines { get; } {
get => GetValue(InlinesProperty);
set => SetValue(InlinesProperty, value);
}
public void Add(Inline inline)
{
if (Inlines is not null)
{
Inlines.Add(inline);
}
}
public void Add(string text)
{
if (Inlines is not null)
{
Inlines.Add(text);
}
}
internal override void BuildTextRun(IList<TextRun> textRuns) internal override void BuildTextRun(IList<TextRun> textRuns)
{ {
@ -52,7 +68,7 @@ namespace Avalonia.Controls.Documents
var textCharacters = new TextCharacters(text.AsMemory(), textRunProperties); var textCharacters = new TextCharacters(text.AsMemory(), textRunProperties);
textRuns.Add(textCharacters); textRuns.Add(textCharacters);
} }
} }
} }
@ -71,5 +87,45 @@ namespace Avalonia.Controls.Documents
stringBuilder.Append(text); stringBuilder.Append(text);
} }
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(InlinesProperty):
OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
InlineHost?.Invalidate();
break;
}
}
internal override void OnInlinesHostChanged(IInlineHost? oldValue, IInlineHost? newValue)
{
base.OnInlinesHostChanged(oldValue, newValue);
if(Inlines is not null)
{
Inlines.InlineHost = newValue;
}
}
private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
{
if (oldValue is not null)
{
oldValue.Parent = null;
oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate();
}
if (newValue is not null)
{
newValue.Parent = this;
newValue.InlineHost = InlineHost;
newValue.Invalidated += (s, e) => InlineHost?.Invalidate();
}
}
} }
} }

18
src/Avalonia.Controls/Documents/TextElement.cs

@ -67,6 +67,8 @@ namespace Avalonia.Controls.Documents
Brushes.Black, Brushes.Black,
inherits: true); inherits: true);
private IInlineHost? _inlineHost;
/// <summary> /// <summary>
/// Gets or sets a brush used to paint the control's background. /// Gets or sets a brush used to paint the control's background.
/// </summary> /// </summary>
@ -250,7 +252,21 @@ namespace Avalonia.Controls.Documents
control.SetValue(ForegroundProperty, value); control.SetValue(ForegroundProperty, value);
} }
internal IInlineHost? InlineHost { get; set; } internal IInlineHost? InlineHost
{
get => _inlineHost;
set
{
var oldValue = _inlineHost;
_inlineHost = value;
OnInlinesHostChanged(oldValue, value);
}
}
internal virtual void OnInlinesHostChanged(IInlineHost? oldValue, IInlineHost? newValue)
{
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {

199
src/Avalonia.Controls/RichTextBlock.cs

@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Documents;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
namespace Avalonia.Controls
{
/// <summary>
/// A control that displays a block of text.
/// </summary>
public class RichTextBlock : TextBlock, IInlineHost
{
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly StyledProperty<InlineCollection> InlinesProperty =
AvaloniaProperty.Register<RichTextBlock, InlineCollection>(
nameof(Inlines));
public RichTextBlock()
{
Inlines = new InlineCollection
{
Parent = this,
InlineHost = this
};
}
/// <summary>
/// Gets or sets the inlines.
/// </summary>
public InlineCollection Inlines
{
get => GetValue(InlinesProperty);
set => SetValue(InlinesProperty, value);
}
public void Add(Inline inline)
{
if (Inlines is not null)
{
Inlines.Add(inline);
}
}
public new void Add(string text)
{
if (Inlines is not null)
{
Inlines.Add(text);
}
}
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
/// <param name="constraint">The constraint of the text.</param>
/// <param name="text">The text to format.</param>
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected override TextLayout CreateTextLayout(Size constraint, string? text)
{
var defaultProperties = new GenericTextRunProperties(
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
FontSize,
TextDecorations,
Foreground);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0);
ITextSource textSource;
var inlines = Inlines;
if (inlines is not null && inlines.HasComplexContent)
{
var textRuns = new List<TextRun>();
foreach (var inline in inlines)
{
inline.BuildTextRun(textRuns);
}
textSource = new InlinesTextSource(textRuns);
}
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
}
return new TextLayout(
textSource,
paragraphProperties,
TextTrimming,
constraint.Width,
constraint.Height,
maxLines: MaxLines,
lineHeight: LineHeight);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(InlinesProperty):
{
OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
InvalidateTextLayout();
break;
}
case nameof(TextProperty):
{
OnTextChanged(change.OldValue as string, change.NewValue as string);
break;
}
}
}
private void OnTextChanged(string? oldValue, string? newValue)
{
if (oldValue == newValue)
{
return;
}
if (Inlines is null)
{
return;
}
Inlines.Text = newValue;
}
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.AddVisualChild(IControl child)
{
if (child.VisualParent == null)
{
VisualChildren.Add(child);
}
}
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;
}
}
}
}

123
src/Avalonia.Controls/TextBlock.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Layout; using Avalonia.Layout;
@ -14,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, IInlineHost public class TextBlock : Control
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Background"/> property. /// Defines the <see cref="Background"/> property.
@ -101,14 +100,6 @@ namespace Avalonia.Controls
o => o.Text, o => o.Text,
(o, v) => o.Text = v); (o, v) => o.Text = v);
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly DirectProperty<TextBlock, InlineCollection> InlinesProperty =
AvaloniaProperty.RegisterDirect<TextBlock, InlineCollection>(
nameof(Inlines),
o => o.Inlines);
/// <summary> /// <summary>
/// Defines the <see cref="TextAlignment"/> property. /// Defines the <see cref="TextAlignment"/> property.
/// </summary> /// </summary>
@ -137,6 +128,7 @@ 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));
private string? _text;
private TextLayout? _textLayout; private TextLayout? _textLayout;
private Size _constraint; private Size _constraint;
@ -150,14 +142,6 @@ namespace Avalonia.Controls
AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty); AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty);
} }
/// <summary>
/// Initializes a new instance of the <see cref="TextBlock"/> class.
/// </summary>
public TextBlock()
{
Inlines = new InlineCollection(this, 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>
@ -165,7 +149,7 @@ namespace Avalonia.Controls
{ {
get get
{ {
return _textLayout ?? (_textLayout = CreateTextLayout(_constraint, Text)); return _textLayout ??= CreateTextLayout(_constraint, Text);
} }
} }
@ -192,28 +176,13 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public string? Text public string? Text
{ {
get => Inlines.Text; get => _text;
set set
{ {
var old = Text; SetAndRaise(TextProperty, ref _text, value);
if (value == old)
{
return;
}
Inlines.Text = value;
RaisePropertyChanged(TextProperty, old, value);
} }
} }
/// <summary>
/// Gets the inlines.
/// </summary>
[Content]
public InlineCollection Inlines { get; }
/// <summary> /// <summary>
/// Gets or sets the font family used to draw the control's text. /// Gets or sets the font family used to draw the control's text.
/// </summary> /// </summary>
@ -333,6 +302,11 @@ namespace Avalonia.Controls
set { SetValue(BaselineOffsetProperty, value); } set { SetValue(BaselineOffsetProperty, value); }
} }
public void Add(string text)
{
Text = text;
}
/// <summary> /// <summary>
/// Reads the attached property from the given element /// Reads the attached property from the given element
/// </summary> /// </summary>
@ -559,26 +533,8 @@ 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 (Inlines.HasComplexContent)
{
var textRuns = new List<TextRun>();
foreach (var inline in Inlines)
{
inline.BuildTextRun(textRuns);
}
textSource = new InlinesTextSource(textRuns);
}
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
}
return new TextLayout( return new TextLayout(
textSource, new SimpleTextSource((text ?? "").AsMemory(), defaultProperties),
paragraphProperties, paragraphProperties,
TextTrimming, TextTrimming,
constraint.Width, constraint.Width,
@ -599,11 +555,6 @@ namespace Avalonia.Controls
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
if (!Inlines.HasComplexContent && string.IsNullOrEmpty(Text))
{
return new Size();
}
var scale = LayoutHelper.GetLayoutScale(this); var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
@ -683,57 +634,7 @@ namespace Avalonia.Controls
} }
} }
private void InlinesChanged(object? sender, EventArgs e) protected readonly struct SimpleTextSource : ITextSource
{
InvalidateTextLayout();
}
void IInlineHost.AddVisualChild(IControl child)
{
if (child.VisualParent == null)
{
VisualChildren.Add(child);
}
}
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;
}
}
private readonly struct SimpleTextSource : ITextSource
{ {
private readonly ReadOnlySlice<char> _text; private readonly ReadOnlySlice<char> _text;
private readonly TextRunProperties _defaultProperties; private readonly TextRunProperties _defaultProperties;

Loading…
Cancel
Save