diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index fe9455bd29..cb49ba96c6 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.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index a76222385e..0cbf272297 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -12,39 +12,55 @@ namespace Avalonia.Controls.Documents [WhitespaceSignificantCollection] public class InlineCollection : AvaloniaList { - private readonly IInlineHost? _host; + private ILogical? _parent; + private IInlineHost? _inlineHost; private string? _text = string.Empty; /// /// Initializes a new instance of the class. /// - public InlineCollection(ILogical parent) : this(parent, null) { } - - /// - /// Initializes a new instance of the class. - /// - internal InlineCollection(ILogical parent, IInlineHost? host = null) : base(0) + public InlineCollection() { - _host = host; - ResetBehavior = ResetBehavior.Remove; this.ForEachItem( x => { - ((ISetLogicalParent)x).SetParent(parent); - x.InlineHost = host; - host?.Invalidate(); + ((ISetLogicalParent)x).SetParent(Parent); + x.InlineHost = InlineHost; + Invalidate(); }, x => { ((ISetLogicalParent)x).SetParent(null); - x.InlineHost = host; - host?.Invalidate(); + x.InlineHost = InlineHost; + Invalidate(); }, () => 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; /// @@ -57,14 +73,16 @@ namespace Avalonia.Controls.Documents { get { + return _text; + if (!HasComplexContent) { return _text; } - + var builder = new StringBuilder(); - foreach(var inline in this) + foreach (var inline in this) { inline.AppendText(builder); } @@ -100,7 +118,7 @@ namespace Avalonia.Controls.Documents } else { - _text += text; + _text = text; } } @@ -136,14 +154,30 @@ namespace Avalonia.Controls.Documents /// protected void Invalidate() { - if(_host != null) + if(InlineHost != null) { - _host.Invalidate(); + InlineHost.Invalidate(); } Invalidated?.Invoke(this, EventArgs.Empty); } 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; + } + } } } diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs index bd1b4fc5e1..c2576ec231 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Text; using Avalonia.Media.TextFormatting; -using Avalonia.Metadata; namespace Avalonia.Controls.Documents { @@ -14,25 +13,42 @@ namespace Avalonia.Controls.Documents /// /// Defines the property. /// - public static readonly DirectProperty InlinesProperty = - AvaloniaProperty.RegisterDirect( - nameof(Inlines), - o => o.Inlines); + public static readonly StyledProperty InlinesProperty = + AvaloniaProperty.Register( + nameof(Inlines)); - /// - /// Initializes a new instance of a Span element. - /// public Span() { - Inlines = new InlineCollection(this); - Inlines.Invalidated += (s, e) => InlineHost?.Invalidate(); + Inlines = new InlineCollection + { + Parent = this + }; } /// /// Gets or sets the inlines. - /// - [Content] - public InlineCollection Inlines { get; } + /// + public InlineCollection Inlines + { + 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 textRuns) { @@ -52,7 +68,7 @@ namespace Avalonia.Controls.Documents var textCharacters = new TextCharacters(text.AsMemory(), textRunProperties); textRuns.Add(textCharacters); - } + } } } @@ -71,5 +87,45 @@ namespace Avalonia.Controls.Documents 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(); + } + } } } diff --git a/src/Avalonia.Controls/Documents/TextElement.cs b/src/Avalonia.Controls/Documents/TextElement.cs index f228519e60..e75fd87615 100644 --- a/src/Avalonia.Controls/Documents/TextElement.cs +++ b/src/Avalonia.Controls/Documents/TextElement.cs @@ -67,6 +67,8 @@ namespace Avalonia.Controls.Documents Brushes.Black, inherits: true); + private IInlineHost? _inlineHost; + /// /// Gets or sets a brush used to paint the control's background. /// @@ -250,7 +252,21 @@ namespace Avalonia.Controls.Documents 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) { diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs new file mode 100644 index 0000000000..16d0254f4a --- /dev/null +++ b/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 +{ + /// + /// A control that displays a block of text. + /// + public class RichTextBlock : TextBlock, IInlineHost + { + /// + /// Defines the property. + /// + public static readonly StyledProperty InlinesProperty = + AvaloniaProperty.Register( + nameof(Inlines)); + + public RichTextBlock() + { + Inlines = new InlineCollection + { + Parent = this, + InlineHost = this + }; + } + + /// + /// Gets or sets the inlines. + /// + 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); + } + } + + /// + /// Creates the used to render the text. + /// + /// The constraint of the text. + /// The text to format. + /// A object. + 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(); + + 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 _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 1a69d1218c..87966e9a6f 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using Avalonia.Automation.Peers; using Avalonia.Controls.Documents; using Avalonia.Layout; @@ -14,7 +13,7 @@ namespace Avalonia.Controls /// /// A control that displays a block of text. /// - public class TextBlock : Control, IInlineHost + public class TextBlock : Control { /// /// Defines the property. @@ -101,14 +100,6 @@ namespace Avalonia.Controls o => o.Text, (o, v) => o.Text = v); - /// - /// Defines the property. - /// - public static readonly DirectProperty InlinesProperty = - AvaloniaProperty.RegisterDirect( - nameof(Inlines), - o => o.Inlines); - /// /// Defines the property. /// @@ -137,6 +128,7 @@ namespace Avalonia.Controls public static readonly StyledProperty TextDecorationsProperty = AvaloniaProperty.Register(nameof(TextDecorations)); + private string? _text; private TextLayout? _textLayout; private Size _constraint; @@ -150,14 +142,6 @@ namespace Avalonia.Controls AffectsRender(BackgroundProperty, ForegroundProperty); } - /// - /// Initializes a new instance of the class. - /// - public TextBlock() - { - Inlines = new InlineCollection(this, this); - } - /// /// Gets the used to render the text. /// @@ -165,7 +149,7 @@ namespace Avalonia.Controls { get { - return _textLayout ?? (_textLayout = CreateTextLayout(_constraint, Text)); + return _textLayout ??= CreateTextLayout(_constraint, Text); } } @@ -192,28 +176,13 @@ namespace Avalonia.Controls /// public string? Text { - get => Inlines.Text; + get => _text; set { - var old = Text; - - if (value == old) - { - return; - } - - Inlines.Text = value; - - RaisePropertyChanged(TextProperty, old, value); + SetAndRaise(TextProperty, ref _text, value); } } - /// - /// Gets the inlines. - /// - [Content] - public InlineCollection Inlines { get; } - /// /// Gets or sets the font family used to draw the control's text. /// @@ -333,6 +302,11 @@ namespace Avalonia.Controls set { SetValue(BaselineOffsetProperty, value); } } + public void Add(string text) + { + Text = text; + } + /// /// Reads the attached property from the given element /// @@ -559,26 +533,8 @@ namespace Avalonia.Controls var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false, defaultProperties, TextWrapping, LineHeight, 0); - ITextSource textSource; - - if (Inlines.HasComplexContent) - { - var textRuns = new List(); - - foreach (var inline in Inlines) - { - inline.BuildTextRun(textRuns); - } - - textSource = new InlinesTextSource(textRuns); - } - else - { - textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties); - } - return new TextLayout( - textSource, + new SimpleTextSource((text ?? "").AsMemory(), defaultProperties), paragraphProperties, TextTrimming, constraint.Width, @@ -599,11 +555,6 @@ namespace Avalonia.Controls protected override Size MeasureOverride(Size availableSize) { - if (!Inlines.HasComplexContent && string.IsNullOrEmpty(Text)) - { - return new Size(); - } - var scale = LayoutHelper.GetLayoutScale(this); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); @@ -683,57 +634,7 @@ namespace Avalonia.Controls } } - private void InlinesChanged(object? sender, EventArgs 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 _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; - } - } - - private readonly struct SimpleTextSource : ITextSource + protected readonly struct SimpleTextSource : ITextSource { private readonly ReadOnlySlice _text; private readonly TextRunProperties _defaultProperties;