From 93b77b1b6a2181822f3bd1b074a504366aa4e0b4 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 21 Oct 2022 07:43:36 +0200 Subject: [PATCH 01/10] Move inlines support to TextBlock and rename RichTextBlock to SelectableTextBlock --- .../ControlCatalog/Pages/TextBlockPage.xaml | 4 +- .../Media/TextFormatting/TextFormatterImpl.cs | 4 +- .../Documents/InlineCollection.cs | 9 +- ...ichTextBlock.cs => SelectableTextBlock.cs} | 374 +++--------------- src/Avalonia.Controls/TextBlock.cs | 272 ++++++++++--- .../Controls/FluentControls.xaml | 2 +- .../Controls/RichTextBlock.xaml | 14 - .../Controls/SelectableTextBlock.xaml | 18 + .../Controls/RichTextBlock.xaml | 14 - .../Controls/SelectableTextBlock.xaml | 18 + .../Controls/SimpleControls.xaml | 2 +- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 2 +- .../Media/TextShaperImpl.cs | 2 +- .../RichTextBlockTests.cs | 132 ------- .../TextBlockTests.cs | 121 ++++++ 15 files changed, 451 insertions(+), 537 deletions(-) rename src/Avalonia.Controls/{RichTextBlock.cs => SelectableTextBlock.cs} (55%) delete mode 100644 src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml create mode 100644 src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml delete mode 100644 src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml create mode 100644 src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml delete mode 100644 tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index 32914428ed..6bb428e2c7 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.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 73dd3366aa..5df458cc3b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -502,7 +502,7 @@ namespace Avalonia.Media.TextFormatting case { } drawableTextRun: { - if (currentWidth + drawableTextRun.Size.Width > paragraphWidth) + if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth) { goto found; } @@ -665,7 +665,7 @@ namespace Avalonia.Media.TextFormatting if (!breakFound) { - currentLength += currentRun.Text.Length; + currentLength += currentRun.TextSourceLength; continue; } diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 1ba65b3e8f..9ff5627434 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -70,6 +70,11 @@ namespace Avalonia.Controls.Documents { get { + if (Count == 0) + { + return null; + } + var builder = StringBuilderCache.Acquire(); foreach (var inline in this) @@ -111,7 +116,7 @@ namespace Avalonia.Controls.Documents private void AddText(string text) { - if (Parent is RichTextBlock textBlock && !textBlock.HasComplexContent) + if (Parent is TextBlock textBlock && !textBlock.HasComplexContent) { textBlock._text += text; } @@ -123,7 +128,7 @@ namespace Avalonia.Controls.Documents private void OnAdd() { - if (Parent is RichTextBlock textBlock) + if (Parent is TextBlock textBlock) { if (!textBlock.HasComplexContent && !string.IsNullOrEmpty(textBlock._text)) { diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs similarity index 55% rename from src/Avalonia.Controls/RichTextBlock.cs rename to src/Avalonia.Controls/SelectableTextBlock.cs index d0b713ba56..b343439f98 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -8,7 +8,6 @@ using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Media.TextFormatting; -using Avalonia.Metadata; using Avalonia.Utilities; namespace Avalonia.Controls @@ -16,67 +15,53 @@ namespace Avalonia.Controls /// /// A control that displays a block of formatted text. /// - public class RichTextBlock : TextBlock, IInlineHost + public class SelectableTextBlock : TextBlock, IInlineHost { - public static readonly StyledProperty IsTextSelectionEnabledProperty = - AvaloniaProperty.Register(nameof(IsTextSelectionEnabled), false); - - public static readonly DirectProperty SelectionStartProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty SelectionStartProperty = + AvaloniaProperty.RegisterDirect( nameof(SelectionStart), o => o.SelectionStart, (o, v) => o.SelectionStart = v); - public static readonly DirectProperty SelectionEndProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty SelectionEndProperty = + AvaloniaProperty.RegisterDirect( nameof(SelectionEnd), o => o.SelectionEnd, (o, v) => o.SelectionEnd = v); - public static readonly DirectProperty SelectedTextProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty SelectedTextProperty = + AvaloniaProperty.RegisterDirect( nameof(SelectedText), o => o.SelectedText); public static readonly StyledProperty SelectionBrushProperty = - AvaloniaProperty.Register(nameof(SelectionBrush), Brushes.Blue); + AvaloniaProperty.Register(nameof(SelectionBrush), Brushes.Blue); - /// - /// Defines the property. - /// - public static readonly StyledProperty InlinesProperty = - AvaloniaProperty.Register( - nameof(Inlines)); - public static readonly DirectProperty CanCopyProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty CanCopyProperty = + AvaloniaProperty.RegisterDirect( nameof(CanCopy), o => o.CanCopy); public static readonly RoutedEvent CopyingToClipboardEvent = - RoutedEvent.Register( + RoutedEvent.Register( nameof(CopyingToClipboard), RoutingStrategies.Bubble); private bool _canCopy; private int _selectionStart; private int _selectionEnd; private int _wordSelectionStart = -1; - private IReadOnlyList? _textRuns; - static RichTextBlock() + static SelectableTextBlock() { - FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true); - - AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty, IsTextSelectionEnabledProperty); + FocusableProperty.OverrideDefaultValue(typeof(SelectableTextBlock), true); + AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty); } - public RichTextBlock() + public event EventHandler? CopyingToClipboard { - Inlines = new InlineCollection - { - Parent = this, - InlineHost = this - }; + add => AddHandler(CopyingToClipboardEvent, value); + remove => RemoveHandler(CopyingToClipboardEvent, value); } /// @@ -99,6 +84,8 @@ namespace Avalonia.Controls if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value)) { RaisePropertyChanged(SelectedTextProperty, "", ""); + + UpdateCommandStates(); } } } @@ -114,6 +101,8 @@ namespace Avalonia.Controls if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value)) { RaisePropertyChanged(SelectedTextProperty, "", ""); + + UpdateCommandStates(); } } } @@ -126,25 +115,6 @@ namespace Avalonia.Controls get => GetSelection(); } - /// - /// Gets or sets a value that indicates whether text selection is enabled, either through user action or calling selection-related API. - /// - public bool IsTextSelectionEnabled - { - get => GetValue(IsTextSelectionEnabledProperty); - set => SetValue(IsTextSelectionEnabledProperty, value); - } - - /// - /// Gets or sets the inlines. - /// - [Content] - public InlineCollection? Inlines - { - get => GetValue(InlinesProperty); - set => SetValue(InlinesProperty, value); - } - /// /// Property for determining if the Copy command can be executed. /// @@ -154,20 +124,12 @@ namespace Avalonia.Controls private set => SetAndRaise(CanCopyProperty, ref _canCopy, value); } - public event EventHandler? CopyingToClipboard - { - add => AddHandler(CopyingToClipboardEvent, value); - remove => RemoveHandler(CopyingToClipboardEvent, value); - } - - internal bool HasComplexContent => Inlines != null && Inlines.Count > 0; - /// /// Copies the current selection to the Clipboard. /// public async void Copy() { - if (_canCopy || !IsTextSelectionEnabled) + if (!_canCopy) { return; } @@ -188,45 +150,13 @@ namespace Avalonia.Controls await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard))) .SetTextAsync(text); } - } - - protected override void RenderTextLayout(DrawingContext context, Point origin) - { - var selectionStart = SelectionStart; - var selectionEnd = SelectionEnd; - var selectionBrush = SelectionBrush; - - var selectionEnabled = IsTextSelectionEnabled; - - if (selectionEnabled && selectionStart != selectionEnd && selectionBrush != null) - { - var start = Math.Min(selectionStart, selectionEnd); - var length = Math.Max(selectionStart, selectionEnd) - start; - - var rects = TextLayout.HitTestTextRange(start, length); - - using (context.PushPostTransform(Matrix.CreateTranslation(origin))) - { - foreach (var rect in rects) - { - context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1)); - } - } - } - - base.RenderTextLayout(context, origin); - } + } /// /// Select all text in the TextBox /// public void SelectAll() { - if (!IsTextSelectionEnabled) - { - return; - } - var text = Text; SelectionStart = 0; @@ -238,94 +168,52 @@ namespace Avalonia.Controls /// public void ClearSelection() { - if (!IsTextSelectionEnabled) - { - return; - } - SelectionEnd = SelectionStart; } - protected void AddText(string? text) + protected override void OnGotFocus(GotFocusEventArgs e) { - if (string.IsNullOrEmpty(text)) - { - return; - } - - if (!HasComplexContent && string.IsNullOrEmpty(_text)) - { - _text = text; - } - else - { - if (!string.IsNullOrEmpty(_text)) - { - Inlines?.Add(_text); - - _text = null; - } - - Inlines?.Add(text); - } - } + base.OnGotFocus(e); - protected override string? GetText() - { - return _text ?? Inlines?.Text; + UpdateCommandStates(); } - protected override void SetText(string? text) + protected override void OnLostFocus(RoutedEventArgs e) { - var oldValue = GetText(); + base.OnLostFocus(e); - AddText(text); + if ((ContextFlyout == null || !ContextFlyout.IsOpen) && + (ContextMenu == null || !ContextMenu.IsOpen)) + { + ClearSelection(); + } - RaisePropertyChanged(TextProperty, oldValue, text); + UpdateCommandStates(); } - /// - /// Creates the used to render the text. - /// - /// A object. - protected override TextLayout CreateTextLayout(string? text) + protected override void RenderTextLayout(DrawingContext context, Point origin) { - var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); - var defaultProperties = new GenericTextRunProperties( - typeface, - FontSize, - TextDecorations, - Foreground); - - var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false, - defaultProperties, TextWrapping, LineHeight, 0); - - ITextSource textSource; + var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; + var selectionBrush = SelectionBrush; - if (_textRuns != null) + if (selectionStart != selectionEnd && selectionBrush != null) { - textSource = new InlinesTextSource(_textRuns); - } - else - { - textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties); - } + var start = Math.Min(selectionStart, selectionEnd); + var length = Math.Max(selectionStart, selectionEnd) - start; - return new TextLayout( - textSource, - paragraphProperties, - TextTrimming, - _constraint.Width, - _constraint.Height, - maxLines: MaxLines, - lineHeight: LineHeight); - } + var rects = TextLayout.HitTestTextRange(start, length); - protected override void OnLostFocus(RoutedEventArgs e) - { - base.OnLostFocus(e); + using (context.PushPostTransform(Matrix.CreateTranslation(origin))) + { + foreach (var rect in rects) + { + context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1)); + } + } + } - ClearSelection(); + base.RenderTextLayout(context, origin); } protected override void OnKeyDown(KeyEventArgs e) @@ -352,11 +240,6 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (!IsTextSelectionEnabled) - { - return; - } - var text = Text; var clickInfo = e.GetCurrentPoint(this); @@ -435,11 +318,6 @@ namespace Avalonia.Controls { base.OnPointerMoved(e); - if (!IsTextSelectionEnabled) - { - return; - } - // selection should not change during pointer move if the user right clicks if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { @@ -486,11 +364,6 @@ namespace Avalonia.Controls { base.OnPointerReleased(e); - if (!IsTextSelectionEnabled) - { - return; - } - if (e.Pointer.Captured != this) { return; @@ -521,100 +394,15 @@ namespace Avalonia.Controls e.Pointer.Capture(null); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - switch (change.Property.Name) - { - case nameof(Inlines): - { - OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection); - InvalidateTextLayout(); - break; - } - } - } - - protected override Size MeasureOverride(Size availableSize) + private void UpdateCommandStates() { - if(_textRuns != null) - { - LogicalChildren.Clear(); - - VisualChildren.Clear(); - - _textRuns = null; - } - - if (Inlines != null && Inlines.Count > 0) - { - var inlines = Inlines; - - var textRuns = new List(); - - foreach (var inline in inlines) - { - inline.BuildTextRun(textRuns); - } - - foreach (var textRun in textRuns) - { - if (textRun is EmbeddedControlRun controlRun && - controlRun.Control is Control control) - { - LogicalChildren.Add(control); - - VisualChildren.Add(control); - - control.Measure(Size.Infinity); - } - } - - _textRuns = textRuns; - } - - return base.MeasureOverride(availableSize); - } - - protected override Size ArrangeOverride(Size finalSize) - { - if (HasComplexContent) - { - var currentY = 0.0; - - foreach (var textLine in TextLayout.TextLines) - { - var currentX = textLine.Start; - - foreach (var run in textLine.TextRuns) - { - if (run is DrawableTextRun drawable) - { - if (drawable is EmbeddedControlRun controlRun - && controlRun.Control is Control control) - { - control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize)); - } - - currentX += drawable.Size.Width; - } - } + var text = GetSelection(); - currentY += textLine.Height; - } - } - - return base.ArrangeOverride(finalSize); + CanCopy = !string.IsNullOrEmpty(text); } private string GetSelection() { - if (!IsTextSelectionEnabled) - { - return ""; - } - var text = GetText(); if (string.IsNullOrEmpty(text)) @@ -638,59 +426,5 @@ namespace Avalonia.Controls return selectedText; } - - 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.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 99c8068b3d..7d4d326a0c 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Automation.Peers; using Avalonia.Controls.Documents; using Avalonia.Layout; @@ -12,7 +13,7 @@ namespace Avalonia.Controls /// /// A control that displays a block of text. /// - public class TextBlock : Control, IAddChild + public class TextBlock : Control, IInlineHost { /// /// Defines the property. @@ -96,15 +97,15 @@ namespace Avalonia.Controls public static readonly DirectProperty TextProperty = AvaloniaProperty.RegisterDirect( nameof(Text), - o => o.Text, - (o, v) => o.Text = v); + o => o.GetText(), + (o, v) => o.SetText(v)); /// /// Defines the property. /// public static readonly AttachedProperty TextAlignmentProperty = AvaloniaProperty.RegisterAttached( - nameof(TextAlignment), + nameof(TextAlignment), defaultValue: TextAlignment.Start, inherits: true); @@ -112,14 +113,14 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly AttachedProperty TextWrappingProperty = - AvaloniaProperty.RegisterAttached(nameof(TextWrapping), + AvaloniaProperty.RegisterAttached(nameof(TextWrapping), inherits: true); /// /// Defines the property. /// public static readonly AttachedProperty TextTrimmingProperty = - AvaloniaProperty.RegisterAttached(nameof(TextTrimming), + AvaloniaProperty.RegisterAttached(nameof(TextTrimming), defaultValue: TextTrimming.None, inherits: true); @@ -129,9 +130,17 @@ namespace Avalonia.Controls public static readonly StyledProperty TextDecorationsProperty = AvaloniaProperty.Register(nameof(TextDecorations)); + /// + /// Defines the property. + /// + public static readonly StyledProperty InlinesProperty = + AvaloniaProperty.Register( + nameof(Inlines)); + internal string? _text; protected TextLayout? _textLayout; protected Size _constraint; + private IReadOnlyList? _textRuns; /// /// Initializes static members of the class. @@ -139,10 +148,19 @@ namespace Avalonia.Controls static TextBlock() { ClipToBoundsProperty.OverrideDefaultValue(true); - + AffectsRender(BackgroundProperty, ForegroundProperty); } + public TextBlock() + { + Inlines = new InlineCollection + { + Parent = this, + InlineHost = this + }; + } + /// /// Gets the used to render the text. /// @@ -288,9 +306,21 @@ namespace Avalonia.Controls get => GetValue(TextDecorationsProperty); set => SetValue(TextDecorationsProperty, value); } - + + /// + /// Gets or sets the inlines. + /// + [Content] + public InlineCollection? Inlines + { + get => GetValue(InlinesProperty); + set => SetValue(InlinesProperty, value); + } + protected override bool BypassFlowDirectionPolicies => true; + internal bool HasComplexContent => Inlines != null && Inlines.Count > 0; + /// /// The BaselineOffset property provides an adjustment to baseline offset /// @@ -513,19 +543,30 @@ namespace Avalonia.Controls TextLayout.Draw(context, origin); } - void IAddChild.AddChild(string text) - { - _text = text; - } - protected virtual string? GetText() { - return _text; + return _text ?? Inlines?.Text; } protected virtual void SetText(string? text) { - SetAndRaise(TextProperty, ref _text, text); + if (Inlines != null && Inlines.Count > 0) + { + var oldValue = Inlines.Text; + + if (!string.IsNullOrEmpty(text)) + { + Inlines.Add(text); + } + + text = Inlines.Text; + + RaisePropertyChanged(TextProperty, oldValue, text); + } + else + { + SetAndRaise(TextProperty, ref _text, text); + } } /// @@ -534,8 +575,10 @@ namespace Avalonia.Controls /// A object. protected virtual TextLayout CreateTextLayout(string? text) { + var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); + var defaultProperties = new GenericTextRunProperties( - new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), + typeface, FontSize, TextDecorations, Foreground); @@ -543,8 +586,19 @@ namespace Avalonia.Controls var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false, defaultProperties, TextWrapping, LineHeight, 0); + ITextSource textSource; + + if (_textRuns != null) + { + textSource = new InlinesTextSource(_textRuns); + } + else + { + textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties); + } + return new TextLayout( - new SimpleTextSource((text ?? "").AsMemory(), defaultProperties), + textSource, paragraphProperties, TextTrimming, _constraint.Width, @@ -560,6 +614,8 @@ namespace Avalonia.Controls { _textLayout = null; + InvalidateVisual(); + InvalidateMeasure(); } @@ -573,7 +629,39 @@ namespace Avalonia.Controls _textLayout = null; - InvalidateArrange(); + var inlines = Inlines; + + if (HasComplexContent) + { + if (_textRuns != null) + { + LogicalChildren.Clear(); + + VisualChildren.Clear(); + } + + var textRuns = new List(); + + foreach (var inline in inlines!) + { + inline.BuildTextRun(textRuns); + } + + foreach (var textRun in textRuns) + { + if (textRun is EmbeddedControlRun controlRun && + controlRun.Control is Control control) + { + VisualChildren.Add(control); + + LogicalChildren.Add(control); + + control.Measure(Size.Infinity); + } + } + + _textRuns = textRuns; + } var measuredSize = TextLayout.Bounds.Size.Inflate(padding); @@ -584,16 +672,11 @@ namespace Avalonia.Controls { var textWidth = Math.Ceiling(TextLayout.Bounds.Width); - if(finalSize.Width < textWidth) + if (finalSize.Width < textWidth) { finalSize = finalSize.WithWidth(textWidth); } - if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) - { - return finalSize; - } - var scale = LayoutHelper.GetLayoutScale(this); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); @@ -602,6 +685,32 @@ namespace Avalonia.Controls _textLayout = null; + if (HasComplexContent) + { + 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) + { + control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize)); + } + + currentX += drawable.Size.Width; + } + } + + currentY += textLine.Height; + } + } + return finalSize; } @@ -610,42 +719,70 @@ namespace Avalonia.Controls 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(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; + } + case nameof(Inlines): + { + OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection); + InvalidateTextLayout(); + break; + } + } + } - case nameof (TextWrapping): - case nameof (TextTrimming): - case nameof (TextAlignment): + private static bool IsValidMaxLines(int maxLines) => maxLines >= 0; - case nameof (FlowDirection): + private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0; - case nameof (Padding): - case nameof (LineHeight): - case nameof (MaxLines): + private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue) + { + if (oldValue is not null) + { + oldValue.Parent = null; + oldValue.InlineHost = null; + oldValue.Invalidated -= (s, e) => InvalidateTextLayout(); + } - case nameof (Text): - case nameof (TextDecorations): - case nameof (Foreground): - { - InvalidateTextLayout(); - break; - } + if (newValue is not null) + { + newValue.Parent = this; + newValue.InlineHost = this; + newValue.Invalidated += (s, e) => InvalidateTextLayout(); } } + void IInlineHost.Invalidate() + { + InvalidateTextLayout(); + } + protected readonly struct SimpleTextSource : ITextSource { private readonly ReadOnlySlice _text; @@ -674,5 +811,46 @@ namespace Avalonia.Controls return new TextCharacters(runText, _defaultProperties); } } + + private readonly struct InlinesTextSource : ITextSource + { + private readonly IReadOnlyList _textRuns; + + public InlinesTextSource(IReadOnlyList textRuns) + { + _textRuns = textRuns; + } + + public IReadOnlyList TextRuns => _textRuns; + + public TextRun? GetTextRun(int textSourceIndex) + { + var currentPosition = 0; + + foreach (var textRun in _textRuns) + { + if (textRun.TextSourceLength == 0) + { + continue; + } + + if (textSourceIndex >= currentPosition + textRun.TextSourceLength) + { + currentPosition += textRun.TextSourceLength; + + continue; + } + + if (textRun is TextCharacters textCharacters) + { + return new TextCharacters(textRun.Text.Skip(Math.Max(0, textSourceIndex - currentPosition)), textRun.Properties!); + } + + return textRun; + } + + return null; + } + } } } diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 577539b26b..5383aa3180 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -68,7 +68,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml b/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml deleted file mode 100644 index 75af2efcb1..0000000000 --- a/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml b/src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml new file mode 100644 index 0000000000..f630969ae6 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml b/src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml deleted file mode 100644 index c0570282cb..0000000000 --- a/src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml b/src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml new file mode 100644 index 0000000000..aaa6448aea --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 644c6ed416..4aefa0136c 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -64,7 +64,7 @@ - + diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index b07deb1f4d..d6bb37a06a 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -64,7 +64,7 @@ namespace Avalonia.Skia var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if(glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t') + if(text.Buffer.Span[glyphCluster] == '\t') { glyphIndex = typeface.GetGlyph(' '); diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 064320f809..7f2cbc6182 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -64,7 +64,7 @@ namespace Avalonia.Direct2D1.Media var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t') + if (text.Buffer.Span[glyphCluster] == '\t') { glyphIndex = typeface.GetGlyph(' '); diff --git a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs deleted file mode 100644 index 05007e4f2e..0000000000 --- a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs +++ /dev/null @@ -1,132 +0,0 @@ -using Avalonia.Controls.Documents; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Templates; -using Avalonia.Media; -using Avalonia.UnitTests; -using Xunit; - -namespace Avalonia.Controls.UnitTests -{ - public class RichTextBlockTests - { - [Fact] - public void Changing_InlinesCollection_Should_Invalidate_Measure() - { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) - { - var target = new RichTextBlock(); - - target.Measure(Size.Infinity); - - Assert.True(target.IsMeasureValid); - - target.Inlines.Add(new Run("Hello")); - - Assert.False(target.IsMeasureValid); - - target.Measure(Size.Infinity); - - Assert.True(target.IsMeasureValid); - } - } - - [Fact] - public void Changing_Inlines_Properties_Should_Invalidate_Measure() - { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) - { - var target = new RichTextBlock(); - - var inline = new Run("Hello"); - - target.Inlines.Add(inline); - - target.Measure(Size.Infinity); - - Assert.True(target.IsMeasureValid); - - inline.Foreground = Brushes.Green; - - Assert.False(target.IsMeasureValid); - } - } - - [Fact] - public void Changing_Inlines_Should_Invalidate_Measure() - { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) - { - var target = new RichTextBlock(); - - var inlines = new InlineCollection { new Run("Hello") }; - - target.Measure(Size.Infinity); - - Assert.True(target.IsMeasureValid); - - target.Inlines = inlines; - - Assert.False(target.IsMeasureValid); - } - } - - [Fact] - public void Changing_Inlines_Should_Reset_Inlines_Parent() - { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) - { - var target = new RichTextBlock(); - - var run = new Run("Hello"); - - target.Inlines.Add(run); - - target.Measure(Size.Infinity); - - Assert.True(target.IsMeasureValid); - - target.Inlines = null; - - Assert.Null(run.Parent); - - target.Inlines = new InlineCollection { run }; - - Assert.Equal(target, run.Parent); - } - } - - [Fact] - public void InlineUIContainer_Child_Schould_Be_Arranged() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var target = new RichTextBlock(); - - var button = new Button { Content = "12345678" }; - - button.Template = new FuncControlTemplate public static readonly RoutedEvent SizeChangedEvent = RoutedEvent.Register( - nameof(SizeChanged), RoutingStrategies.Bubble); + nameof(SizeChanged), RoutingStrategies.Direct); /// /// Defines the property. From 62485f53bc26d5f80795799b36df80b445c60d9f Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 30 Oct 2022 22:10:58 -0400 Subject: [PATCH 05/10] Only raise SizeChanged when the Size component changes (ignore position) --- src/Avalonia.Controls/Control.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index e21f0fd33d..beaee34b07 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -556,13 +556,20 @@ namespace Avalonia.Controls var oldValue = change.GetOldValue(); var newValue = change.GetNewValue(); - var sizeChangedEventArgs = new SizeChangedEventArgs( - SizeChangedEvent, - source: this, - previousSize: new Size(oldValue.Width, oldValue.Height), - newSize: new Size(newValue.Width, newValue.Height)); + // Bounds is a Rect with an X/Y Position as well as Height/Width. + // This means it is possible for the Rect to change position but not size. + // Therefore, we want to explicity check only the size and raise an event + // only when that size has changed. + if (newValue.Size != oldValue.Size) + { + var sizeChangedEventArgs = new SizeChangedEventArgs( + SizeChangedEvent, + source: this, + previousSize: new Size(oldValue.Width, oldValue.Height), + newSize: new Size(newValue.Width, newValue.Height)); - RaiseEvent(sizeChangedEventArgs); + RaiseEvent(sizeChangedEventArgs); + } } else if (change.Property == FlowDirectionProperty) { From bdd637298e7822aa3166c9097c99fd9fd49a91ef Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 31 Oct 2022 20:08:50 -0400 Subject: [PATCH 06/10] Take into account LayoutEpsilon when calculating Height/WidthChanged --- .../Interactivity/RoutedEventArgs.cs | 2 +- src/Avalonia.Controls/SizeChangedEventArgs.cs | 29 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs b/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs index 60a6b64677..2b660e7080 100644 --- a/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs +++ b/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs @@ -3,7 +3,7 @@ using System; namespace Avalonia.Interactivity { /// - /// Provices state information and data specific to a routed event. + /// Provides state information and data specific to a routed event. /// public class RoutedEventArgs : EventArgs { diff --git a/src/Avalonia.Controls/SizeChangedEventArgs.cs b/src/Avalonia.Controls/SizeChangedEventArgs.cs index 201a00a3fc..b3e399ff55 100644 --- a/src/Avalonia.Controls/SizeChangedEventArgs.cs +++ b/src/Avalonia.Controls/SizeChangedEventArgs.cs @@ -1,4 +1,6 @@ using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -42,14 +44,23 @@ namespace Avalonia.Controls { PreviousSize = previousSize; NewSize = newSize; - HeightChanged = newSize.Height != previousSize.Height; - WidthChanged = newSize.Width != previousSize.Width; + + // Only consider changed when the size difference is greater than LayoutEpsilon + // This compensates for any rounding or precision difference between layout cycles + HeightChanged = !MathUtilities.AreClose(newSize.Height, previousSize.Height, LayoutHelper.LayoutEpsilon); + WidthChanged = !MathUtilities.AreClose(newSize.Width, previousSize.Width, LayoutHelper.LayoutEpsilon); } /// - /// Gets a value indicating whether the height of the new size is different - /// than the previous size height. + /// Gets a value indicating whether the height of the new size is considered + /// different than the previous size height. /// + /// + /// This will take into account layout epsilon and will not be true if both + /// heights are considered equivalent for layout purposes. Remember there can + /// be small variations in the calculations between layout cycles due to + /// rounding and precision even when the size has not otherwise changed. + /// public bool HeightChanged { get; init; } /// @@ -63,9 +74,15 @@ namespace Avalonia.Controls public Size PreviousSize { get; init; } /// - /// Gets a value indicating whether the width of the new size is different - /// than the previous size width. + /// Gets a value indicating whether the width of the new size is considered + /// different than the previous size width. /// + /// + /// This will take into account layout epsilon and will not be true if both + /// heights are considered equivalent for layout purposes. Remember there can + /// be small variations in the calculations between layout cycles due to + /// rounding and precision even when the size has not otherwise changed. + /// public bool WidthChanged { get; init; } } } From 3e53621daee8b07be3e295e5e12931bdec658dc4 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 31 Oct 2022 20:12:21 -0400 Subject: [PATCH 07/10] Fix typo --- src/Avalonia.Controls/SizeChangedEventArgs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/SizeChangedEventArgs.cs b/src/Avalonia.Controls/SizeChangedEventArgs.cs index b3e399ff55..fd40c50505 100644 --- a/src/Avalonia.Controls/SizeChangedEventArgs.cs +++ b/src/Avalonia.Controls/SizeChangedEventArgs.cs @@ -79,7 +79,7 @@ namespace Avalonia.Controls /// /// /// This will take into account layout epsilon and will not be true if both - /// heights are considered equivalent for layout purposes. Remember there can + /// widths are considered equivalent for layout purposes. Remember there can /// be small variations in the calculations between layout cycles due to /// rounding and precision even when the size has not otherwise changed. /// From 9403a3d0c7050a1553c0b5a0958304570453ed9b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 1 Nov 2022 09:07:42 +0100 Subject: [PATCH 08/10] Fix TextBlock inlines logical tree handling --- .../Documents/InlineCollection.cs | 25 ++++++++++--------- src/Avalonia.Controls/Documents/Span.cs | 4 +-- src/Avalonia.Controls/TextBlock.cs | 17 +++++++++---- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 9ff5627434..54ee9c99d5 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -12,7 +12,7 @@ namespace Avalonia.Controls.Documents [WhitespaceSignificantCollection] public class InlineCollection : AvaloniaList { - private ILogical? _parent; + private IAvaloniaList? _parent; private IInlineHost? _inlineHost; /// @@ -24,28 +24,28 @@ namespace Avalonia.Controls.Documents this.ForEachItem( x => - { - ((ISetLogicalParent)x).SetParent(Parent); + { x.InlineHost = InlineHost; + Parent?.Add(x); Invalidate(); }, x => { - ((ISetLogicalParent)x).SetParent(null); + Parent?.Remove(x); x.InlineHost = InlineHost; Invalidate(); }, () => throw new NotSupportedException()); } - internal ILogical? Parent + internal IAvaloniaList? Parent { get => _parent; set { _parent = value; - OnParentChanged(value); + OnParentChanged(_parent, value); } } @@ -157,20 +157,21 @@ namespace Avalonia.Controls.Documents Invalidated?.Invoke(this, EventArgs.Empty); } - private void OnParentChanged(ILogical? parent) + private void OnParentChanged(IAvaloniaList? oldParent, IAvaloniaList? newParent) { foreach (var child in this) { - var oldParent = child.Parent; - - if (oldParent != parent) + if (oldParent != newParent) { if (oldParent != null) { - ((ISetLogicalParent)child).SetParent(null); + oldParent.Remove(child); } - ((ISetLogicalParent)child).SetParent(parent); + if(newParent != null) + { + newParent.Add(child); + } } } } diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs index 363ce1011b..041cdc74ce 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents { Inlines = new InlineCollection { - Parent = this + Parent = LogicalChildren }; } @@ -85,7 +85,7 @@ namespace Avalonia.Controls.Documents if (newValue is not null) { - newValue.Parent = this; + newValue.Parent = LogicalChildren; newValue.InlineHost = InlineHost; newValue.Invalidated += (s, e) => InlineHost?.Invalidate(); } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 7d4d326a0c..75a275f778 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -156,7 +156,7 @@ namespace Avalonia.Controls { Inlines = new InlineCollection { - Parent = this, + Parent = LogicalChildren, InlineHost = this }; } @@ -635,9 +635,16 @@ namespace Avalonia.Controls { if (_textRuns != null) { - LogicalChildren.Clear(); + foreach (var textRun in _textRuns) + { + if (textRun is EmbeddedControlRun controlRun && + controlRun.Control is Control control) + { + VisualChildren.Remove(control); - VisualChildren.Clear(); + LogicalChildren.Remove(control); + } + } } var textRuns = new List(); @@ -772,7 +779,7 @@ namespace Avalonia.Controls if (newValue is not null) { - newValue.Parent = this; + newValue.Parent = LogicalChildren; newValue.InlineHost = this; newValue.Invalidated += (s, e) => InvalidateTextLayout(); } @@ -841,7 +848,7 @@ namespace Avalonia.Controls continue; } - if (textRun is TextCharacters textCharacters) + if (textRun is TextCharacters) { return new TextCharacters(textRun.Text.Skip(Math.Max(0, textSourceIndex - currentPosition)), textRun.Properties!); } From e639aead67cd95e96709e8153aee75e5d4f3ab83 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 1 Nov 2022 10:36:31 +0100 Subject: [PATCH 09/10] Fix logical parent cleanup --- .../Documents/InlineCollection.cs | 20 ++++++++++--------- src/Avalonia.Controls/Documents/Span.cs | 6 +++--- src/Avalonia.Controls/TextBlock.cs | 6 +++--- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 54ee9c99d5..a265f88e21 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -12,7 +12,7 @@ namespace Avalonia.Controls.Documents [WhitespaceSignificantCollection] public class InlineCollection : AvaloniaList { - private IAvaloniaList? _parent; + private IAvaloniaList? _logicalChildren; private IInlineHost? _inlineHost; /// @@ -26,26 +26,28 @@ namespace Avalonia.Controls.Documents x => { x.InlineHost = InlineHost; - Parent?.Add(x); + LogicalChildren?.Add(x); Invalidate(); }, x => { - Parent?.Remove(x); + LogicalChildren?.Remove(x); x.InlineHost = InlineHost; Invalidate(); }, () => throw new NotSupportedException()); } - internal IAvaloniaList? Parent + internal IAvaloniaList? LogicalChildren { - get => _parent; + get => _logicalChildren; set { - _parent = value; + var oldValue = _logicalChildren; - OnParentChanged(_parent, value); + _logicalChildren = value; + + OnParentChanged(oldValue, value); } } @@ -116,7 +118,7 @@ namespace Avalonia.Controls.Documents private void AddText(string text) { - if (Parent is TextBlock textBlock && !textBlock.HasComplexContent) + if (LogicalChildren is TextBlock textBlock && !textBlock.HasComplexContent) { textBlock._text += text; } @@ -128,7 +130,7 @@ namespace Avalonia.Controls.Documents private void OnAdd() { - if (Parent is TextBlock textBlock) + if (LogicalChildren is TextBlock textBlock) { if (!textBlock.HasComplexContent && !string.IsNullOrEmpty(textBlock._text)) { diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs index 041cdc74ce..a7a702ceae 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents { Inlines = new InlineCollection { - Parent = LogicalChildren + LogicalChildren = LogicalChildren }; } @@ -78,14 +78,14 @@ namespace Avalonia.Controls.Documents { if (oldValue is not null) { - oldValue.Parent = null; + oldValue.LogicalChildren = null; oldValue.InlineHost = null; oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate(); } if (newValue is not null) { - newValue.Parent = LogicalChildren; + newValue.LogicalChildren = LogicalChildren; newValue.InlineHost = InlineHost; newValue.Invalidated += (s, e) => InlineHost?.Invalidate(); } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 75a275f778..f79d3f8296 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -156,7 +156,7 @@ namespace Avalonia.Controls { Inlines = new InlineCollection { - Parent = LogicalChildren, + LogicalChildren = LogicalChildren, InlineHost = this }; } @@ -772,14 +772,14 @@ namespace Avalonia.Controls { if (oldValue is not null) { - oldValue.Parent = null; + oldValue.LogicalChildren = null; oldValue.InlineHost = null; oldValue.Invalidated -= (s, e) => InvalidateTextLayout(); } if (newValue is not null) { - newValue.Parent = LogicalChildren; + newValue.LogicalChildren = LogicalChildren; newValue.InlineHost = this; newValue.Invalidated += (s, e) => InvalidateTextLayout(); } From 66595bad2e9b532a434b6470eb67c2f32f718527 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 1 Nov 2022 07:45:03 -0400 Subject: [PATCH 10/10] Calculate Height/WidthChanged in the getter directly --- src/Avalonia.Controls/SizeChangedEventArgs.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/SizeChangedEventArgs.cs b/src/Avalonia.Controls/SizeChangedEventArgs.cs index fd40c50505..2dc642b163 100644 --- a/src/Avalonia.Controls/SizeChangedEventArgs.cs +++ b/src/Avalonia.Controls/SizeChangedEventArgs.cs @@ -44,11 +44,6 @@ namespace Avalonia.Controls { PreviousSize = previousSize; NewSize = newSize; - - // Only consider changed when the size difference is greater than LayoutEpsilon - // This compensates for any rounding or precision difference between layout cycles - HeightChanged = !MathUtilities.AreClose(newSize.Height, previousSize.Height, LayoutHelper.LayoutEpsilon); - WidthChanged = !MathUtilities.AreClose(newSize.Width, previousSize.Width, LayoutHelper.LayoutEpsilon); } /// @@ -61,7 +56,7 @@ namespace Avalonia.Controls /// be small variations in the calculations between layout cycles due to /// rounding and precision even when the size has not otherwise changed. /// - public bool HeightChanged { get; init; } + public bool HeightChanged => !MathUtilities.AreClose(NewSize.Height, PreviousSize.Height, LayoutHelper.LayoutEpsilon); /// /// Gets the new size (or bounds) of the object. @@ -83,6 +78,6 @@ namespace Avalonia.Controls /// be small variations in the calculations between layout cycles due to /// rounding and precision even when the size has not otherwise changed. /// - public bool WidthChanged { get; init; } + public bool WidthChanged => !MathUtilities.AreClose(NewSize.Width, PreviousSize.Width, LayoutHelper.LayoutEpsilon); } }