using System; using System.Collections.Generic; using System.Text; using Avalonia.Automation.Peers; using Avalonia.Collections; using Avalonia.Controls.Documents; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; using Avalonia.Utilities; namespace Avalonia.Controls { /// /// A control that displays a block of text. /// public class TextBlock : Control, IInlineHost { /// /// Defines the property. /// public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty PaddingProperty = Decorator.PaddingProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty FontSizeProperty = TextElement.FontSizeProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty FontStyleProperty = TextElement.FontStyleProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty FontWeightProperty = TextElement.FontWeightProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty FontStretchProperty = TextElement.FontStretchProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty ForegroundProperty = TextElement.ForegroundProperty.AddOwner(); /// /// DependencyProperty for property. /// public static readonly AttachedProperty BaselineOffsetProperty = AvaloniaProperty.RegisterAttached( nameof(BaselineOffset), 0, true); /// /// Defines the property. /// public static readonly AttachedProperty LineHeightProperty = AvaloniaProperty.RegisterAttached( nameof(LineHeight), double.NaN, validate: IsValidLineHeight, inherits: true); /// /// Defines the property. /// public static readonly AttachedProperty LineSpacingProperty = AvaloniaProperty.RegisterAttached( nameof(LineSpacing), 0, validate: IsValidLineSpacing, inherits: true); /// /// Defines the property. /// public static readonly AttachedProperty LetterSpacingProperty = AvaloniaProperty.RegisterAttached( nameof(LetterSpacing), 0, inherits: true); /// /// Defines the property. /// public static readonly AttachedProperty MaxLinesProperty = AvaloniaProperty.RegisterAttached( nameof(MaxLines), validate: IsValidMaxLines, inherits: true); /// /// Defines the property. /// public static readonly StyledProperty TextProperty = AvaloniaProperty.Register(nameof(Text)); /// /// Defines the property. /// public static readonly AttachedProperty TextAlignmentProperty = AvaloniaProperty.RegisterAttached( nameof(TextAlignment), defaultValue: TextAlignment.Start, inherits: true); /// /// Defines the property. /// public static readonly AttachedProperty TextWrappingProperty = AvaloniaProperty.RegisterAttached(nameof(TextWrapping), inherits: true); /// /// Defines the property. /// public static readonly AttachedProperty TextTrimmingProperty = AvaloniaProperty.RegisterAttached(nameof(TextTrimming), defaultValue: TextTrimming.None, inherits: true); /// /// Defines the property. /// public static readonly StyledProperty TextDecorationsProperty = Inline.TextDecorationsProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty FontFeaturesProperty = TextElement.FontFeaturesProperty.AddOwner(); /// /// Defines the property. /// public static readonly DirectProperty InlinesProperty = AvaloniaProperty.RegisterDirect( nameof(Inlines), t => t.Inlines, (t, v) => t.Inlines = v); private TextLayout? _textLayout; protected Size _constraint = new(double.NaN, double.NaN); protected IReadOnlyList? _textRuns; private InlineCollection? _inlines; /// /// Initializes static members of the class. /// static TextBlock() { ClipToBoundsProperty.OverrideDefaultValue(true); AffectsRender(BackgroundProperty, ForegroundProperty); } public TextBlock() { Inlines = new InlineCollection { LogicalChildren = LogicalChildren, InlineHost = this }; } /// /// Gets the used to render the text. /// public TextLayout TextLayout => _textLayout ??= CreateTextLayout(Text); /// /// Gets or sets the padding to place around the . /// public Thickness Padding { get => GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); } /// /// Gets or sets a brush used to paint the control's background. /// public IBrush? Background { get => GetValue(BackgroundProperty); set => SetValue(BackgroundProperty, value); } /// /// Gets or sets the text. /// public string? Text { get => GetValue(TextProperty); set => SetValue(TextProperty, value); } /// /// Gets or sets the font family used to draw the control's text. /// public FontFamily FontFamily { get => GetValue(FontFamilyProperty); set => SetValue(FontFamilyProperty, value); } /// /// Gets or sets the size of the control's text in points. /// public double FontSize { get => GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); } /// /// Gets or sets the font style used to draw the control's text. /// public FontStyle FontStyle { get => GetValue(FontStyleProperty); set => SetValue(FontStyleProperty, value); } /// /// Gets or sets the font weight used to draw the control's text. /// public FontWeight FontWeight { get => GetValue(FontWeightProperty); set => SetValue(FontWeightProperty, value); } /// /// Gets or sets the font stretch used to draw the control's text. /// public FontStretch FontStretch { get => GetValue(FontStretchProperty); set => SetValue(FontStretchProperty, value); } /// /// Gets or sets the brush used to draw the control's text and other foreground elements. /// public IBrush? Foreground { get => GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); } /// /// Gets or sets the height of each line of content. /// public double LineHeight { get => GetValue(LineHeightProperty); set => SetValue(LineHeightProperty, value); } /// /// Gets or sets the extra distance of each line to the next line. /// public double LineSpacing { get => GetValue(LineSpacingProperty); set => SetValue(LineSpacingProperty, value); } /// /// Gets or sets the letter spacing. /// public double LetterSpacing { get => GetValue(LetterSpacingProperty); set => SetValue(LetterSpacingProperty, value); } /// /// Gets or sets the maximum number of text lines. /// public int MaxLines { get => GetValue(MaxLinesProperty); set => SetValue(MaxLinesProperty, value); } /// /// Gets or sets the control's text wrapping mode. /// public TextWrapping TextWrapping { get => GetValue(TextWrappingProperty); set => SetValue(TextWrappingProperty, value); } /// /// Gets or sets the control's text trimming mode. /// public TextTrimming TextTrimming { get => GetValue(TextTrimmingProperty); set => SetValue(TextTrimmingProperty, value); } /// /// Gets or sets the text alignment. /// public TextAlignment TextAlignment { get => GetValue(TextAlignmentProperty); set => SetValue(TextAlignmentProperty, value); } /// /// Gets or sets the text decorations. /// public TextDecorationCollection? TextDecorations { get => GetValue(TextDecorationsProperty); set => SetValue(TextDecorationsProperty, value); } /// /// Gets or sets the font features. /// public FontFeatureCollection? FontFeatures { get => GetValue(FontFeaturesProperty); set => SetValue(FontFeaturesProperty, value); } /// /// Gets or sets the inlines. /// [Content] public InlineCollection? Inlines { get => _inlines; set => SetAndRaise(InlinesProperty, ref _inlines, value); } protected override bool BypassFlowDirectionPolicies => true; internal bool HasComplexContent => Inlines != null && Inlines.Count > 0; private protected Size GetMaxSizeFromConstraint() { var maxWidth = double.IsNaN(_constraint.Width) ? 0.0 : _constraint.Width; var maxHeight = double.IsNaN(_constraint.Height) ? 0.0 : _constraint.Height; return new Size(maxWidth, maxHeight); } /// /// The BaselineOffset property provides an adjustment to baseline offset /// public double BaselineOffset { get => (double)GetValue(BaselineOffsetProperty); set => SetValue(BaselineOffsetProperty, value); } /// /// Reads the attached property from the given element /// /// The element to which to read the attached property. public static double GetBaselineOffset(Control control) { if (control == null) { throw new ArgumentNullException(nameof(control)); } return control.GetValue(BaselineOffsetProperty); } /// /// Writes the attached property BaselineOffset to the given element. /// /// The element to which to write the attached property. /// The property value to set public static void SetBaselineOffset(Control control, double value) { if (control == null) { throw new ArgumentNullException(nameof(control)); } control.SetValue(BaselineOffsetProperty, value); } /// /// Reads the attached property from the given element /// /// The element to which to read the attached property. public static TextAlignment GetTextAlignment(Control control) { if (control == null) { throw new ArgumentNullException(nameof(control)); } return control.GetValue(TextAlignmentProperty); } /// /// Writes the attached property BaselineOffset to the given element. /// /// The element to which to write the attached property. /// The property value to set public static void SetTextAlignment(Control control, TextAlignment alignment) { if (control == null) { throw new ArgumentNullException(nameof(control)); } control.SetValue(TextAlignmentProperty, alignment); } /// /// Reads the attached property from the given element /// /// The element to which to read the attached property. public static TextWrapping GetTextWrapping(Control control) { if (control == null) { throw new ArgumentNullException(nameof(control)); } return control.GetValue(TextWrappingProperty); } /// /// Writes the attached property BaselineOffset to the given element. /// /// The element to which to write the attached property. /// The property value to set public static void SetTextWrapping(Control control, TextWrapping wrapping) { if (control == null) { throw new ArgumentNullException(nameof(control)); } control.SetValue(TextWrappingProperty, wrapping); } /// /// Reads the attached property from the given element /// /// The element to which to read the attached property. public static TextTrimming GetTextTrimming(Control control) { if (control == null) { throw new ArgumentNullException(nameof(control)); } return control.GetValue(TextTrimmingProperty); } /// /// Writes the attached property BaselineOffset to the given element. /// /// The element to which to write the attached property. /// The property value to set public static void SetTextTrimming(Control control, TextTrimming trimming) { if (control == null) { throw new ArgumentNullException(nameof(control)); } control.SetValue(TextTrimmingProperty, trimming); } /// /// Reads the attached property from the given element /// /// The element to which to read the attached property. public static double GetLineHeight(Control control) { if (control == null) { throw new ArgumentNullException(nameof(control)); } return control.GetValue(LineHeightProperty); } /// /// Writes the attached property BaselineOffset to the given element. /// /// The element to which to write the attached property. /// The property value to set public static void SetLineHeight(Control control, double height) { if (control == null) { throw new ArgumentNullException(nameof(control)); } control.SetValue(LineHeightProperty, height); } /// /// Reads the attached property from the given element /// /// The element to which to read the attached property. public static double GetLetterSpacing(Control control) { if (control == null) { throw new ArgumentNullException(nameof(control)); } return control.GetValue(LetterSpacingProperty); } /// /// Writes the attached property LetterSpacing to the given element. /// /// The element to which to write the attached property. /// The property value to set public static void SetLetterSpacing(Control control, double letterSpacing) { if (control == null) { throw new ArgumentNullException(nameof(control)); } control.SetValue(LetterSpacingProperty, letterSpacing); } /// /// Reads the attached property from the given element /// /// The element to which to read the attached property. public static int GetMaxLines(Control control) { if (control == null) { throw new ArgumentNullException(nameof(control)); } return control.GetValue(MaxLinesProperty); } /// /// Writes the attached property BaselineOffset to the given element. /// /// The element to which to write the attached property. /// The property value to set public static void SetMaxLines(Control control, int maxLines) { if (control == null) { throw new ArgumentNullException(nameof(control)); } control.SetValue(MaxLinesProperty, maxLines); } /// /// Renders the to a drawing context. /// /// The drawing context. public sealed override void Render(DrawingContext context) { RenderCore(context); } // Workaround to seal Render method, we need to make so because AccessText was overriding Render method which is sealed now. private protected virtual void RenderCore(DrawingContext context) { var background = Background; if (background != null) { context.FillRectangle(background, new Rect(Bounds.Size)); } var scale = LayoutHelper.GetLayoutScale(this); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale); var top = padding.Top; var textHeight = TextLayout.Height; if (Bounds.Height < textHeight) { switch (VerticalAlignment) { case VerticalAlignment.Center: top += (Bounds.Height - textHeight) / 2; break; case VerticalAlignment.Bottom: top += (Bounds.Height - textHeight); break; } } RenderTextLayout(context, new Point(padding.Left, top)); } protected virtual void RenderTextLayout(DrawingContext context, Point origin) { TextLayout.Draw(context, origin + new Point(TextLayout.OverhangLeading, 0)); } private bool _clearTextInternal; internal void ClearTextInternal() { _clearTextInternal = true; try { SetCurrentValue(TextProperty, null); } finally { _clearTextInternal = false; } } /// /// Creates the used to render the text. /// /// A object. protected virtual TextLayout CreateTextLayout(string? text) { var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); var defaultProperties = new GenericTextRunProperties( typeface, FontFeatures, FontSize, TextDecorations, Foreground); var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, IsMeasureValid ? TextAlignment : TextAlignment.Left, true, false, defaultProperties, TextWrapping, LineHeight, 0, LetterSpacing) { LineSpacing = LineSpacing }; ITextSource textSource; if (_textRuns != null) { textSource = new InlinesTextSource(_textRuns); } else { textSource = new SimpleTextSource(text ?? "", defaultProperties); } var maxSize = GetMaxSizeFromConstraint(); return new TextLayout( textSource, paragraphProperties, TextTrimming, maxSize.Width, maxSize.Height, MaxLines); } /// /// Invalidates . /// protected void InvalidateTextLayout() { InvalidateVisual(); InvalidateMeasure(); } protected override void OnMeasureInvalidated() { _textLayout?.Dispose(); _textLayout = null; _textRuns = null; base.OnMeasureInvalidated(); } protected override Size MeasureOverride(Size availableSize) { var scale = LayoutHelper.GetLayoutScale(this); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale); var deflatedSize = availableSize.Deflate(padding); if (_constraint != deflatedSize) { //Reset TextLayout when the constraint is not matching. _textLayout?.Dispose(); _textLayout = null; _constraint = deflatedSize; //Force arrange so text will be properly alligned. InvalidateArrange(); } var inlines = Inlines; if (HasComplexContent) { var textRuns = new List(); foreach (var inline in inlines!) { inline.BuildTextRun(textRuns); } _textRuns = textRuns; } //This implicitly recreated the TextLayout with a new constraint if we previously reset it. var textLayout = TextLayout; var size = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.MinTextWidth, textLayout.Height).Inflate(padding), 1); return size; } protected override Size ArrangeOverride(Size finalSize) { var scale = LayoutHelper.GetLayoutScale(this); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale); var availableSize = finalSize.Deflate(padding); //ToDo: Introduce a text run cache to be able to reuse shaped runs etc. _textLayout?.Dispose(); _textLayout = null; _constraint = availableSize; //This implicitly recreated the TextLayout with a new constraint. var textLayout = TextLayout; if (HasComplexContent) { //Clear visual children before complex run arrangement VisualChildren.Clear(); 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) { //Add again to prevent clipping //Fixes: #17194 VisualChildren.Add(control); control.Arrange( new Rect(new Point(currentX, currentY), new Size(control.DesiredSize.Width, textLine.Height))); } currentX += drawable.Size.Width; } } currentY += textLine.Height; } } return finalSize; } protected override AutomationPeer OnCreateAutomationPeer() { return new TextBlockAutomationPeer(this); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == TextProperty) { if (HasComplexContent && !_clearTextInternal) { Inlines?.Clear(); } } switch (change.Property.Name) { 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(LetterSpacing): case nameof(MaxLines): case nameof(Text): case nameof(TextDecorations): case nameof(FontFeatures): case nameof(Foreground): { InvalidateTextLayout(); break; } case nameof(Inlines): { OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection); InvalidateTextLayout(); break; } } } private static bool IsValidMaxLines(int maxLines) => maxLines >= 0; private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0; private static bool IsValidLineSpacing(double lineSpacing) => !double.IsNaN(lineSpacing) && !double.IsInfinity(lineSpacing); private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue) { VisualChildren.Clear(); if (oldValue is not null) { oldValue.LogicalChildren = null; oldValue.InlineHost = null; oldValue.Invalidated -= Invalidated; } if (newValue is not null) { newValue.LogicalChildren = LogicalChildren; newValue.InlineHost = this; newValue.Invalidated += Invalidated; } return; void Invalidated(object? sender, EventArgs e) => InvalidateMeasure(); } void IInlineHost.Invalidate() { InvalidateMeasure(); } IAvaloniaList IInlineHost.VisualChildren => VisualChildren; internal override void BuildDebugDisplay(StringBuilder builder, bool includeContent) { base.BuildDebugDisplay(builder, includeContent); if (includeContent) { DebugDisplayHelper.AppendOptionalValue(builder, nameof(Text), Text ?? Inlines?.Text, true); } } protected readonly record struct SimpleTextSource : ITextSource { private readonly string _text; private readonly TextRunProperties _defaultProperties; public SimpleTextSource(string text, TextRunProperties defaultProperties) { _text = text; _defaultProperties = defaultProperties; } public TextRun? GetTextRun(int textSourceIndex) { if (textSourceIndex > _text.Length) { return new TextEndOfParagraph(); } var runText = _text.AsMemory(textSourceIndex); if (runText.IsEmpty) { return new TextEndOfParagraph(); } return new TextCharacters(runText, _defaultProperties); } } #pragma warning disable CA1815 protected readonly struct InlinesTextSource : ITextSource #pragma warning restore CA1815 { private readonly IReadOnlyList _textRuns; private readonly IReadOnlyList>? _textModifier; public InlinesTextSource(IReadOnlyList textRuns, IReadOnlyList>? textModifier = null) { _textRuns = textRuns; _textModifier = textModifier; } public IReadOnlyList TextRuns => _textRuns; public TextRun? GetTextRun(int textSourceIndex) { var currentPosition = 0; foreach (var textRun in _textRuns) { if (textRun.Length == 0) { continue; } if (textSourceIndex >= currentPosition + textRun.Length) { currentPosition += textRun.Length; continue; } if (textRun is TextCharacters textCharacters) { var skip = Math.Max(0, textSourceIndex - currentPosition); var textStyleRun = FormattedTextSource.CreateTextStyleRun(textRun.Text.Slice(skip).Span, textSourceIndex, textCharacters.Properties, _textModifier); return new TextCharacters(textRun.Text.Slice(skip, textStyleRun.Length), textStyleRun.Value); } return textRun; } return new TextEndOfParagraph(); } } } }