using System; using System.Collections.Generic; using System.Text; using Avalonia.Automation.Peers; 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 MaxLinesProperty = AvaloniaProperty.RegisterAttached( nameof(MaxLines), validate: IsValidMaxLines, inherits: true); /// /// Defines the property. /// public static readonly DirectProperty TextProperty = AvaloniaProperty.RegisterDirect( nameof(Text), 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. /// public static readonly AttachedProperty TextAlignmentProperty = AvaloniaProperty.RegisterAttached(nameof(TextAlignment), 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 = AvaloniaProperty.Register(nameof(TextDecorations)); private TextLayout? _textLayout; private Size _constraint; /// /// Initializes static members of the class. /// static TextBlock() { ClipToBoundsProperty.OverrideDefaultValue(true); AffectsRender(BackgroundProperty, ForegroundProperty); } /// /// Initializes a new instance of the class. /// public TextBlock() { Inlines = new InlineCollection(this, this); } /// /// Gets the used to render the text. /// public TextLayout TextLayout { get { return _textLayout ?? (_textLayout = CreateTextLayout(_constraint, Text)); } } /// /// Gets or sets the padding to place around the . /// public Thickness Padding { get { return GetValue(PaddingProperty); } set { SetValue(PaddingProperty, value); } } /// /// Gets or sets a brush used to paint the control's background. /// public IBrush? Background { get { return GetValue(BackgroundProperty); } set { SetValue(BackgroundProperty, value); } } /// /// Gets or sets the text. /// public string? Text { get => Inlines.Text; set { var old = Text; if (value == old) { return; } Inlines.Text = value; RaisePropertyChanged(TextProperty, old, value); } } /// /// Gets the inlines. /// [Content] public InlineCollection Inlines { get; } /// /// Gets or sets the font family used to draw the control's text. /// public FontFamily FontFamily { get { return GetValue(FontFamilyProperty); } set { SetValue(FontFamilyProperty, value); } } /// /// Gets or sets the size of the control's text in points. /// public double FontSize { get { return GetValue(FontSizeProperty); } set { SetValue(FontSizeProperty, value); } } /// /// Gets or sets the font style used to draw the control's text. /// public FontStyle FontStyle { get { return GetValue(FontStyleProperty); } set { SetValue(FontStyleProperty, value); } } /// /// Gets or sets the font weight used to draw the control's text. /// public FontWeight FontWeight { get { return GetValue(FontWeightProperty); } set { SetValue(FontWeightProperty, value); } } /// /// Gets or sets the font stretch used to draw the control's text. /// public FontStretch FontStretch { get { return 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 { return 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 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 { return GetValue(TextWrappingProperty); } set { SetValue(TextWrappingProperty, value); } } /// /// Gets or sets the control's text trimming mode. /// public TextTrimming TextTrimming { get { return GetValue(TextTrimmingProperty); } set { SetValue(TextTrimmingProperty, value); } } /// /// Gets or sets the text alignment. /// public TextAlignment TextAlignment { get { return GetValue(TextAlignmentProperty); } set { SetValue(TextAlignmentProperty, value); } } /// /// Gets or sets the text decorations. /// public TextDecorationCollection? TextDecorations { get => GetValue(TextDecorationsProperty); set => SetValue(TextDecorationsProperty, value); } protected override bool BypassFlowDirectionPolicies => true; /// /// The BaselineOffset property provides an adjustment to baseline offset /// public double BaselineOffset { get { return (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 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 override void Render(DrawingContext context) { var background = Background; if (background != null) { context.FillRectangle(background, new Rect(Bounds.Size)); } var padding = Padding; var top = padding.Top; var textHeight = TextLayout.Bounds.Height; if (Bounds.Height < textHeight) { switch (VerticalAlignment) { case VerticalAlignment.Center: top += (Bounds.Height - textHeight) / 2; break; case VerticalAlignment.Bottom: top += (Bounds.Height - textHeight); break; } } TextLayout.Draw(context, new Point(padding.Left, top)); } /// /// Creates the used to render the text. /// /// The constraint of the text. /// The text to format. /// A object. protected virtual 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; 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, paragraphProperties, TextTrimming, constraint.Width, constraint.Height, maxLines: MaxLines, lineHeight: LineHeight); } /// /// Invalidates . /// protected void InvalidateTextLayout() { _textLayout = null; InvalidateMeasure(); } protected override Size MeasureOverride(Size availableSize) { if (!Inlines.HasComplexContent && string.IsNullOrEmpty(Text)) { return new Size(); } var padding = Padding; _constraint = availableSize.Deflate(padding); _textLayout = null; InvalidateArrange(); var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1); return new Size(measuredSize.Width, measuredSize.Height).Inflate(padding); } protected override Size ArrangeOverride(Size finalSize) { if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) { return finalSize; } var padding = Padding; var textSize = finalSize.Deflate(padding); _constraint = new Size(textSize.Width, Math.Ceiling(textSize.Height)); _textLayout = null; return finalSize; } protected override AutomationPeer OnCreateAutomationPeer() { 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 (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; } } } 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 { private readonly ReadOnlySlice _text; private readonly TextRunProperties _defaultProperties; public SimpleTextSource(ReadOnlySlice text, TextRunProperties defaultProperties) { _text = text; _defaultProperties = defaultProperties; } public TextRun? GetTextRun(int textSourceIndex) { if (textSourceIndex > _text.Length) { return null; } var runText = _text.Skip(textSourceIndex); if (runText.IsEmpty) { return null; } return new TextCharacters(runText, _defaultProperties); } } } }