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.
///
///
/// This property uses to share the same
/// definition as , ensuring consistent behavior across text
/// elements and templated controls.
///
public static readonly StyledProperty LetterSpacingProperty =
TextElement.LetterSpacingProperty.AddOwner();
///
/// 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 padding = Padding;
if (UseLayoutRounding)
{
var scale = LayoutHelper.GetLayoutScale(this);
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);
}
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 padding = Padding;
if (UseLayoutRounding)
{
var scale = LayoutHelper.GetLayoutScale(this);
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 aligned.
InvalidateArrange();
}
var inlines = Inlines;
if (HasComplexContent)
{
var textRuns = new List();
foreach (var inline in inlines!)
{
inline.BuildTextRun(textRuns, deflatedSize);
}
_textRuns = textRuns;
}
//This implicitly recreated the TextLayout with a new constraint if we previously reset it.
var textLayout = TextLayout;
// The textWidth used here is matching that TextPresenter uses to measure the text.
return new Size(textLayout.WidthIncludingTrailingWhitespace, textLayout.Height).Inflate(padding);
}
protected override Size ArrangeOverride(Size finalSize)
{
var scale = 1.0;
var padding = Padding;
if (UseLayoutRounding)
{
scale = LayoutHelper.GetLayoutScale(this);
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);
var offsetY = TextLineImpl.GetBaselineOffset(textLine, drawable);
control.Arrange(
new Rect((new Point(currentX, currentY + offsetY)),
control.DesiredSize));
}
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();
}
}
}
}