From 93b77b1b6a2181822f3bd1b074a504366aa4e0b4 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 21 Oct 2022 07:43:36 +0200 Subject: [PATCH 01/19] 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 10/19] 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 11/19] 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 12/19] 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 13/19] 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 14/19] 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 15/19] 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); } } From 17b2834d21f7e7acd63586b69f8ece4d7099f131 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 2 Nov 2022 10:56:06 +0100 Subject: [PATCH 16/19] Implement letter spacing --- src/Avalonia.Base/Media/FormattedText.cs | 3 +- src/Avalonia.Base/Media/GlyphRun.cs | 77 +--------- .../GenericTextParagraphProperties.cs | 19 ++- .../Media/TextFormatting/TextFormatterImpl.cs | 3 +- .../Media/TextFormatting/TextLayout.cs | 21 ++- .../TextFormatting/TextParagraphProperties.cs | 9 +- .../Media/TextFormatting/TextShaperOptions.cs | 9 +- .../Platform/IPlatformRenderInterface.cs | 37 +---- .../Presenters/TextPresenter.cs | 20 ++- src/Avalonia.Controls/TextBlock.cs | 56 +++++++- src/Avalonia.Controls/TextBox.cs | 12 ++ .../HeadlessPlatformRenderInterface.cs | 42 +----- .../Controls/TextBox.xaml | 1 + .../Controls/TextBox.xaml | 4 +- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 4 - .../Avalonia.Skia/PlatformRenderInterface.cs | 126 +++++++++-------- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 2 +- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 131 +++++++++--------- .../VisualTree/MockRenderInterface.cs | 17 +-- .../NullRenderingPlatform.cs | 15 +- .../TextFormatting/TextFormatterTests.cs | 2 +- .../Media/TextFormatting/TextLineTests.cs | 6 +- tests/Avalonia.UnitTests/MockGlyphRun.cs | 12 ++ .../MockPlatformRenderInterface.cs | 2 +- 24 files changed, 306 insertions(+), 324 deletions(-) create mode 100644 tests/Avalonia.UnitTests/MockGlyphRun.cs diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 27d99bdc10..90b9755493 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -93,7 +93,8 @@ namespace Avalonia.Media runProps, TextWrapping.WrapWithOverflow, 0, // line height not specified - 0 // indentation not specified + 0, // indentation not specified + 0 ); InvalidateMetrics(); diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index a1cb00e209..d93a68e78b 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -170,7 +170,7 @@ namespace Avalonia.Media } /// - /// Gets the scale of the current + /// Gets the scale of the current /// internal double Scale => FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight; @@ -860,82 +860,9 @@ namespace Avalonia.Media private IGlyphRunImpl CreateGlyphRunImpl() { - IGlyphRunImpl glyphRunImpl; - var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - var count = GlyphIndices.Count; - var scale = (float)(FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight); - - if (GlyphOffsets == null) - { - if (GlyphTypeface.Metrics.IsFixedPitch) - { - var buffer = platformRenderInterface.AllocateGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count); - - var glyphs = buffer.GlyphIndices; - - for (int i = 0; i < glyphs.Length; i++) - { - glyphs[i] = GlyphIndices[i]; - } - - glyphRunImpl = buffer.Build(); - } - else - { - var buffer = platformRenderInterface.AllocateHorizontalGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count); - var glyphs = buffer.GlyphIndices; - var positions = buffer.GlyphPositions; - var width = 0d; - - for (var i = 0; i < count; i++) - { - positions[i] = (float)width; - - if (GlyphAdvances == null) - { - width += GlyphTypeface.GetGlyphAdvance(GlyphIndices[i]) * scale; - } - else - { - width += GlyphAdvances[i]; - } - - glyphs[i] = GlyphIndices[i]; - } - - glyphRunImpl = buffer.Build(); - } - } - else - { - var buffer = platformRenderInterface.AllocatePositionedGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count); - var glyphs = buffer.GlyphIndices; - var glyphPositions = buffer.GlyphPositions; - var currentX = 0.0; - - for (var i = 0; i < count; i++) - { - var glyphOffset = GlyphOffsets[i]; - - glyphPositions[i] = new PointF((float)(currentX + glyphOffset.X), (float)glyphOffset.Y); - - if (GlyphAdvances == null) - { - currentX += GlyphTypeface.GetGlyphAdvance(GlyphIndices[i]) * scale; - } - else - { - currentX += GlyphAdvances[i]; - } - - glyphs[i] = GlyphIndices[i]; - } - - glyphRunImpl = buffer.Build(); - } - return glyphRunImpl; + return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); } void IDisposable.Dispose() diff --git a/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs b/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs index dccad1e647..b9ed31523e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs @@ -17,15 +17,18 @@ /// logical horizontal alignment /// text wrap option /// Paragraph line height + /// letter spacing public GenericTextParagraphProperties(TextRunProperties defaultTextRunProperties, TextAlignment textAlignment = TextAlignment.Left, TextWrapping textWrap = TextWrapping.NoWrap, - double lineHeight = 0) + double lineHeight = 0, + double letterSpacing = 0) { DefaultTextRunProperties = defaultTextRunProperties; _textAlignment = textAlignment; _textWrap = textWrap; _lineHeight = lineHeight; + LetterSpacing = letterSpacing; } /// @@ -39,6 +42,7 @@ /// text wrap option /// Paragraph line height /// line indentation + /// letter spacing public GenericTextParagraphProperties( FlowDirection flowDirection, TextAlignment textAlignment, @@ -47,8 +51,8 @@ TextRunProperties defaultTextRunProperties, TextWrapping textWrap, double lineHeight, - double indent - ) + double indent, + double letterSpacing) { _flowDirection = flowDirection; _textAlignment = textAlignment; @@ -57,6 +61,7 @@ DefaultTextRunProperties = defaultTextRunProperties; _textWrap = textWrap; _lineHeight = lineHeight; + LetterSpacing = letterSpacing; Indent = indent; } @@ -72,7 +77,8 @@ textParagraphProperties.DefaultTextRunProperties, textParagraphProperties.TextWrapping, textParagraphProperties.LineHeight, - textParagraphProperties.Indent) + textParagraphProperties.Indent, + textParagraphProperties.LetterSpacing) { } @@ -131,6 +137,11 @@ /// public override double Indent { get; } + /// + /// The letter spacing + /// + public override double LetterSpacing { get; } + /// /// Set text flow direction /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 145c99cadc..7bad95c4a2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -249,7 +249,8 @@ namespace Avalonia.Media.TextFormatting var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface, currentRun.Properties.FontRenderingEmSize, - shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab); + shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, + paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions)); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 0828b6518a..dc79e61333 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -31,6 +31,7 @@ namespace Avalonia.Media.TextFormatting /// The maximum width. /// The maximum height. /// The height of each line of text. + /// The letter spacing that is applied to rendered glyphs. /// The maximum number of text lines. /// The text style overrides. public TextLayout( @@ -46,12 +47,13 @@ namespace Avalonia.Media.TextFormatting double maxWidth = double.PositiveInfinity, double maxHeight = double.PositiveInfinity, double lineHeight = double.NaN, + double letterSpacing = 0, int maxLines = 0, IReadOnlyList>? textStyleOverrides = null) { _paragraphProperties = CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, - textDecorations, flowDirection, lineHeight); + textDecorations, flowDirection, lineHeight, letterSpacing); _textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides); @@ -63,6 +65,8 @@ namespace Avalonia.Media.TextFormatting MaxHeight = maxHeight; + LetterSpacing = letterSpacing; + MaxLines = maxLines; TextLines = CreateTextLines(); @@ -77,6 +81,7 @@ namespace Avalonia.Media.TextFormatting /// The maximum width. /// The maximum height. /// The height of each line of text. + /// The letter spacing that is applied to rendered glyphs. /// The maximum number of text lines. public TextLayout( ITextSource textSource, @@ -85,6 +90,7 @@ namespace Avalonia.Media.TextFormatting double maxWidth = double.PositiveInfinity, double maxHeight = double.PositiveInfinity, double lineHeight = double.NaN, + double letterSpacing = 0, int maxLines = 0) { _textSource = textSource; @@ -99,6 +105,8 @@ namespace Avalonia.Media.TextFormatting MaxHeight = maxHeight; + LetterSpacing = letterSpacing; + MaxLines = maxLines; TextLines = CreateTextLines(); @@ -128,6 +136,11 @@ namespace Avalonia.Media.TextFormatting /// public int MaxLines { get; } + /// + /// Gets the text spacing. + /// + public double LetterSpacing { get; } + /// /// Gets the text lines. /// @@ -374,15 +387,17 @@ namespace Avalonia.Media.TextFormatting /// The text decorations. /// The text flow direction. /// The height of each line of text. + /// The letter spacing that is applied to rendered glyphs. /// private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping, - TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight) + TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, + double letterSpacing) { var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground); return new GenericTextParagraphProperties(flowDirection, textAlignment, true, false, - textRunStyle, textWrapping, lineHeight, 0); + textRunStyle, textWrapping, lineHeight, 0, letterSpacing); } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs index 82a0ba14d8..5691dd8ad0 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs @@ -57,7 +57,7 @@ public abstract double Indent { get; } /// - /// Paragraph indentation + /// Get the paragraph indentation. /// public virtual double ParagraphIndent { @@ -65,11 +65,16 @@ } /// - /// Default Incremental Tab + /// Gets the default incremental tab width. /// public virtual double DefaultIncrementalTab { get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; } } + + /// + /// Gets the letter spacing. + /// + public virtual double LetterSpacing { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs index 0d00bed51e..80bbbcdbfe 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs @@ -12,13 +12,15 @@ namespace Avalonia.Media.TextFormatting double fontRenderingEmSize = 12, sbyte bidiLevel = 0, CultureInfo? culture = null, - double incrementalTabWidth = 0) + double incrementalTabWidth = 0, + double letterSpacing = 0) { Typeface = typeface; FontRenderingEmSize = fontRenderingEmSize; BidiLevel = bidiLevel; Culture = culture; IncrementalTabWidth = incrementalTabWidth; + LetterSpacing = letterSpacing; } /// @@ -45,5 +47,10 @@ namespace Avalonia.Media.TextFormatting /// public double IncrementalTabWidth { get; } + /// + /// Get the letter spacing. + /// + public double LetterSpacing { get; } + } } diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 9d0d7974b4..518c5f37b8 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -171,40 +171,15 @@ namespace Avalonia.Platform IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride); /// - /// Allocates a platform glyph run buffer. + /// Creates a platform implementation of a glyph run. /// /// The glyph typeface. /// The font rendering em size. - /// The length. - /// An . - /// - /// This buffer only holds glyph indices. - /// - IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length); - - /// - /// Allocates a horizontal platform glyph run buffer. - /// - /// The glyph typeface. - /// The font rendering em size. - /// The length. - /// An . - /// - /// This buffer holds glyph indices and glyph advances. - /// - IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length); - - /// - /// Allocates a positioned platform glyph run buffer. - /// - /// The glyph typeface. - /// The font rendering em size. - /// The length. - /// An . - /// - /// This buffer holds glyph indices, glyph advances and glyph positions. - /// - IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length); + /// The glyph indices. + /// The glyph advances. + /// The glyph offsets. + /// + IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances, IReadOnlyList? glyphOffsets); /// /// Gets a value indicating whether the platform directly supports rectangles with rounded corners. diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index a9bb16c7df..adf0569551 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -80,6 +80,12 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty LineHeightProperty = TextBlock.LineHeightProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty LetterSpacingProperty = + TextBlock.LetterSpacingProperty.AddOwner(); + /// /// Defines the property. /// @@ -212,6 +218,15 @@ namespace Avalonia.Controls.Presenters set => SetValue(LineHeightProperty, value); } + /// + /// Gets or sets the letter spacing. + /// + public double LetterSpacing + { + get => GetValue(LetterSpacingProperty); + set => SetValue(LetterSpacingProperty, value); + } + /// /// Gets or sets the text alignment. /// @@ -333,7 +348,7 @@ namespace Avalonia.Controls.Presenters var textLayout = new TextLayout(text, typeface, FontSize, foreground, TextAlignment, TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides, - flowDirection: FlowDirection, lineHeight: LineHeight); + flowDirection: FlowDirection, lineHeight: LineHeight, letterSpacing: LetterSpacing); return textLayout; } @@ -916,6 +931,9 @@ namespace Avalonia.Controls.Presenters case nameof(TextAlignment): case nameof(TextWrapping): + case nameof(LineHeight): + case nameof(LetterSpacing): + case nameof(SelectionStart): case nameof(SelectionEnd): case nameof(SelectionForegroundBrush): diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index f79d3f8296..0492c2c1e3 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -82,6 +82,15 @@ namespace Avalonia.Controls validate: IsValidLineHeight, inherits: true); + /// + /// Defines the property. + /// + public static readonly AttachedProperty LetterSpacingProperty = + AvaloniaProperty.RegisterAttached( + nameof(LetterSpacing), + 0, + inherits: true); + /// /// Defines the property. /// @@ -262,6 +271,15 @@ namespace Avalonia.Controls set => SetValue(LineHeightProperty, 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. /// @@ -475,6 +493,35 @@ namespace Avalonia.Controls 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 /// @@ -584,7 +631,7 @@ namespace Avalonia.Controls Foreground); var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false, - defaultProperties, TextWrapping, LineHeight, 0); + defaultProperties, TextWrapping, LineHeight, 0, LetterSpacing); ITextSource textSource; @@ -744,9 +791,10 @@ namespace Avalonia.Controls case nameof(FlowDirection): - case nameof(Padding): - case nameof(LineHeight): - case nameof(MaxLines): + case nameof (Padding): + case nameof (LineHeight): + case nameof (LetterSpacing): + case nameof (MaxLines): case nameof(Text): case nameof(TextDecorations): diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index da4e90fb66..85c1c9a9d1 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -114,6 +114,12 @@ namespace Avalonia.Controls public static readonly StyledProperty LineHeightProperty = TextBlock.LineHeightProperty.AddOwner(); + /// + /// Defines see property. + /// + public static readonly StyledProperty LetterSpacingProperty = + TextBlock.LetterSpacingProperty.AddOwner(); + public static readonly StyledProperty WatermarkProperty = AvaloniaProperty.Register(nameof(Watermark)); @@ -378,6 +384,12 @@ namespace Avalonia.Controls set => SetValue(MaxLinesProperty, value); } + public double LetterSpacing + { + get => GetValue(LetterSpacingProperty); + set => SetValue(LetterSpacingProperty, value); + } + /// /// Gets or sets the line height. /// diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index fcd7f1e31f..501d239cee 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -115,19 +115,16 @@ namespace Avalonia.Headless return new HeadlessGeometryStub(new Rect(glyphRun.Size)); } - public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) { - return new HeadlessGlyphRunBufferStub(); + return new HeadlessGlyphRunStub(); } - public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - { - return new HeadlessHorizontalGlyphRunBufferStub(); - } - - public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) + class HeadlessGlyphRunStub : IGlyphRunImpl { - return new HeadlessPositionedGlyphRunBufferStub(); + public void Dispose() + { + } } class HeadlessGeometryStub : IGeometryImpl @@ -213,33 +210,6 @@ namespace Avalonia.Headless public Matrix Transform { get; } } - class HeadlessGlyphRunBufferStub : IGlyphRunBuffer - { - public Span GlyphIndices => Span.Empty; - - public IGlyphRunImpl Build() - { - return new HeadlessGlyphRunStub(); - } - } - - class HeadlessHorizontalGlyphRunBufferStub : HeadlessGlyphRunBufferStub, IHorizontalGlyphRunBuffer - { - public Span GlyphPositions => Span.Empty; - } - - class HeadlessPositionedGlyphRunBufferStub : HeadlessGlyphRunBufferStub, IPositionedGlyphRunBuffer - { - public Span GlyphPositions => Span.Empty; - } - - class HeadlessGlyphRunStub : IGlyphRunImpl - { - public void Dispose() - { - } - } - class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl { public HeadlessStreamingGeometryStub() : base(Rect.Empty) diff --git a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml index 17c69da8fd..db487ef76b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml @@ -161,6 +161,7 @@ TextAlignment="{TemplateBinding TextAlignment}" TextWrapping="{TemplateBinding TextWrapping}" LineHeight="{TemplateBinding LineHeight}" + LetterSpacing="{TemplateBinding LetterSpacing}" PasswordChar="{TemplateBinding PasswordChar}" RevealPassword="{TemplateBinding RevealPassword}" SelectionBrush="{TemplateBinding SelectionBrush}" diff --git a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml index 5fa6412688..0bcb425ca9 100644 --- a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml +++ b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml @@ -149,14 +149,14 @@ CaretBrush="{TemplateBinding CaretBrush}" CaretIndex="{TemplateBinding CaretIndex}" LineHeight="{TemplateBinding LineHeight}" + LetterSpacing="{TemplateBinding LetterSpacing}" PasswordChar="{TemplateBinding PasswordChar}" RevealPassword="{TemplateBinding RevealPassword}" SelectionBrush="{TemplateBinding SelectionBrush}" SelectionEnd="{TemplateBinding SelectionEnd}" SelectionForegroundBrush="{TemplateBinding SelectionForegroundBrush}" SelectionStart="{TemplateBinding SelectionStart}" - Text="{TemplateBinding Text, - Mode=TwoWay}" + Text="{TemplateBinding Text,Mode=TwoWay}" TextAlignment="{TemplateBinding TextAlignment}" TextWrapping="{TemplateBinding TextWrapping}" /> diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index d11f4aa7d3..71bdc1bd6b 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -69,10 +69,6 @@ namespace Avalonia.Skia public int GlyphCount { get; } - public bool IsFakeBold { get; } - - public bool IsFakeItalic { get; } - public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) { metrics = default; diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index a9696efbd4..dd3badb2d8 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -12,8 +12,6 @@ using Avalonia.OpenGL.Imaging; using Avalonia.Platform; using Avalonia.Media.Imaging; using SkiaSharp; -using System.Runtime.InteropServices; -using System.Drawing; namespace Avalonia.Skia { @@ -79,7 +77,7 @@ namespace Avalonia.Skia var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize) { Size = fontRenderingEmSize, - Edging = SKFontEdging.Antialias, + Edging = SKFontEdging.Alias, Hinting = SKFontHinting.None, LinearMetrics = true }; @@ -244,85 +242,91 @@ namespace Avalonia.Skia "Current GPU acceleration backend does not support OpenGL integration"); } - public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - => new SKGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length); + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, + IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + { + if (glyphTypeface == null) + { + throw new ArgumentNullException(nameof(glyphTypeface)); + } - public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - => new SKHorizontalGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length); + if (glyphIndices == null) + { + throw new ArgumentNullException(nameof(glyphIndices)); + } - public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - => new SKPositionedGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length); + var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl; - private abstract class SKGlyphRunBufferBase : IGlyphRunBuffer - { - protected readonly SKTextBlobBuilder _builder; - protected readonly SKFont _font; + var font = new SKFont + { + LinearMetrics = true, + Subpixel = true, + Edging = SKFontEdging.SubpixelAntialias, + Hinting = SKFontHinting.Full, + Size = (float)fontRenderingEmSize, + Typeface = glyphTypefaceImpl.Typeface, + Embolden = (glyphTypefaceImpl.FontSimulations & FontSimulations.Bold) != 0, + SkewX = (glyphTypefaceImpl.FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0 + }; + + var builder = new SKTextBlobBuilder(); - public SKGlyphRunBufferBase(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) + var count = glyphIndices.Count; + + if(glyphOffsets != null && glyphAdvances != null) { - _builder = new SKTextBlobBuilder(); + var runBuffer = builder.AllocatePositionedRun(font, count); - var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; + var glyphSpan = runBuffer.GetGlyphSpan(); + var positionSpan = runBuffer.GetPositionSpan(); - _font = new SKFont - { - Subpixel = true, - Edging = SKFontEdging.SubpixelAntialias, - Hinting = SKFontHinting.Full, - LinearMetrics = true, - Size = fontRenderingEmSize, - Typeface = glyphTypefaceImpl.Typeface, - Embolden = glyphTypefaceImpl.IsFakeBold, - SkewX = glyphTypefaceImpl.IsFakeItalic ? -0.2f : 0 - }; - } + var currentX = 0.0; - public abstract Span GlyphIndices { get; } + for (int i = 0; i < glyphOffsets.Count; i++) + { + var offset = glyphOffsets[i]; - public IGlyphRunImpl Build() - { - return new GlyphRunImpl(_builder.Build()); - } - } + glyphSpan[i] = glyphIndices[i]; - private sealed class SKGlyphRunBuffer : SKGlyphRunBufferBase - { - private readonly SKRunBuffer _buffer; + positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y); - public SKGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length) - { - _buffer = _builder.AllocateRun(_font, length, 0, 0); + currentX += glyphAdvances[i]; + } } + else + { + if(glyphAdvances != null) + { + var runBuffer = builder.AllocateHorizontalRun(font, count, 0); - public override Span GlyphIndices => _buffer.GetGlyphSpan(); - } + var glyphSpan = runBuffer.GetGlyphSpan(); + var positionSpan = runBuffer.GetPositionSpan(); - private sealed class SKHorizontalGlyphRunBuffer : SKGlyphRunBufferBase, IHorizontalGlyphRunBuffer - { - private readonly SKHorizontalRunBuffer _buffer; + var currentX = 0.0; - public SKHorizontalGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length) - { - _buffer = _builder.AllocateHorizontalRun(_font, length, 0); - } + for (int i = 0; i < glyphOffsets.Count; i++) + { + glyphSpan[i] = glyphIndices[i]; - public override Span GlyphIndices => _buffer.GetGlyphSpan(); + positionSpan[i] = (float)currentX; - public Span GlyphPositions => _buffer.GetPositionSpan(); - } + currentX += glyphAdvances[i]; + } + } + else + { + var runBuffer = builder.AllocateRun(font, count, 0, 0); - private sealed class SKPositionedGlyphRunBuffer : SKGlyphRunBufferBase, IPositionedGlyphRunBuffer - { - private readonly SKPositionedRunBuffer _buffer; + var glyphSpan = runBuffer.GetGlyphSpan(); - public SKPositionedGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length) - { - _buffer = _builder.AllocatePositionedRun(_font, length); + for (int i = 0; i < glyphOffsets.Count; i++) + { + glyphSpan[i] = glyphIndices[i]; + } + } } - public override Span GlyphIndices => _buffer.GetGlyphSpan(); - - public Span GlyphPositions => MemoryMarshal.Cast(_buffer.GetPositionSpan()); + return new GlyphRunImpl(builder.Build()); } } } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index d6bb37a06a..eaf588c27d 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -60,7 +60,7 @@ namespace Avalonia.Skia var glyphCluster = (int)(sourceInfo.Cluster); - var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); + var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing; var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 8103f89dad..4d307c9762 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -10,10 +10,7 @@ using Avalonia.Media.Imaging; using Avalonia.Platform; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; -using TextAlignment = Avalonia.Media.TextAlignment; using SharpDX.Mathematics.Interop; -using System.Runtime.InteropServices; -using System.Drawing; namespace Avalonia { @@ -160,6 +157,72 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, + IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + { + var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; + + var glyphCount = glyphIndices.Count; + + var run = new SharpDX.DirectWrite.GlyphRun + { + FontFace = glyphTypefaceImpl.FontFace, + FontSize = (float)fontRenderingEmSize + }; + + var indices = new short[glyphCount]; + + for (var i = 0; i < glyphCount; i++) + { + indices[i] = (short)glyphIndices[i]; + } + + run.Indices = indices; + + run.Advances = new float[glyphCount]; + + var scale = (float)(fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight); + + if (glyphAdvances == null) + { + for (var i = 0; i < glyphCount; i++) + { + var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[i]) * scale; + + run.Advances[i] = advance; + } + } + else + { + for (var i = 0; i < glyphCount; i++) + { + var advance = (float)glyphAdvances[i]; + + run.Advances[i] = advance; + } + } + + if (glyphOffsets == null) + { + return new GlyphRunImpl(run); + } + + run.Offsets = new GlyphOffset[glyphCount]; + + for (var i = 0; i < glyphCount; i++) + { + var (x, y) = glyphOffsets[i]; + + run.Offsets[i] = new GlyphOffset + { + AdvanceOffset = (float)x, + AscenderOffset = (float)y + }; + } + + return new GlyphRunImpl(run); + } + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { if (glyphRun.GlyphTypeface is not GlyphTypefaceImpl glyphTypeface) @@ -260,68 +323,6 @@ namespace Avalonia.Direct2D1 return new WicBitmapImpl(format, alphaFormat, data, size, dpi, stride); } - private class DWGlyphRunBuffer : IGlyphRunBuffer - { - protected readonly SharpDX.DirectWrite.GlyphRun _dwRun; - - public DWGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - { - var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; - - _dwRun = new SharpDX.DirectWrite.GlyphRun - { - FontFace = glyphTypefaceImpl.FontFace, - FontSize = fontRenderingEmSize, - Indices = new short[length] - }; - } - - public Span GlyphIndices => MemoryMarshal.Cast(_dwRun.Indices.AsSpan()); - - public IGlyphRunImpl Build() - { - return new GlyphRunImpl(_dwRun); - } - } - - private class DWHorizontalGlyphRunBuffer : DWGlyphRunBuffer, IHorizontalGlyphRunBuffer - { - public DWHorizontalGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - : base(glyphTypeface, fontRenderingEmSize, length) - { - _dwRun.Advances = new float[length]; - } - - public Span GlyphPositions => _dwRun.Advances.AsSpan(); - } - - private class DWPositionedGlyphRunBuffer : DWGlyphRunBuffer, IPositionedGlyphRunBuffer - { - public DWPositionedGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - : base(glyphTypeface, fontRenderingEmSize, length) - { - _dwRun.Advances = new float[length]; - _dwRun.Offsets = new GlyphOffset[length]; - } - - public Span GlyphPositions => MemoryMarshal.Cast(_dwRun.Offsets.AsSpan()); - } - - public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - { - return new DWGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length); - } - - public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - { - return new DWHorizontalGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length); - } - - public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - { - return new DWPositionedGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length); - } - public bool SupportsIndividualRoundRects => false; public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 5f8eb45f71..10db08f302 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -72,7 +72,7 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } - public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) { throw new NotImplementedException(); } @@ -126,21 +126,6 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } - public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - { - throw new NotImplementedException(); - } - - public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - { - throw new NotImplementedException(); - } - - public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - { - throw new NotImplementedException(); - } - class MockStreamGeometry : IStreamGeometryImpl { private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 34f0dfef11..4170de71e6 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -5,6 +5,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.Media.Imaging; +using Microsoft.Diagnostics.Runtime; namespace Avalonia.Benchmarks { @@ -117,19 +118,9 @@ namespace Avalonia.Benchmarks return new MockStreamGeometryImpl(); } - public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) { - throw new NotImplementedException(); - } - - public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - { - throw new NotImplementedException(); - } - - public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) - { - throw new NotImplementedException(); + return new MockGlyphRun(); } public bool SupportsIndividualRoundRects => true; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index a315158e1b..316926b00c 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -425,7 +425,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var defaultProperties = new GenericTextRunProperties(Typeface.Default); var paragraphProperties = new GenericTextParagraphProperties(flowDirection, textAlignment, true, true, - defaultProperties, TextWrapping.NoWrap, 0, 0); + defaultProperties, TextWrapping.NoWrap, 0, 0, 0); var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 33c18c5064..87de9ed11f 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -878,7 +878,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, 0, 200, - new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0)); + new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, + true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); var textBounds = textLine.GetTextBounds(0, 3); @@ -924,7 +925,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, 0, 200, - new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0)); + new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, + true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); var textBounds = textLine.GetTextBounds(0, 4); diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs new file mode 100644 index 0000000000..24948aff01 --- /dev/null +++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs @@ -0,0 +1,12 @@ +using Avalonia.Platform; + +namespace Avalonia.UnitTests +{ + public class MockGlyphRun : IGlyphRunImpl + { + public void Dispose() + { + + } + } +} diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 586436ef7f..0f951ed867 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -142,7 +142,7 @@ namespace Avalonia.UnitTests throw new NotImplementedException(); } - public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) { return Mock.Of(); } From 79e2d44005f27b3d1611ee90a8c89c3196122770 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 2 Nov 2022 16:09:01 +0600 Subject: [PATCH 17/19] Only use scoped keyword when building with .NET 7 SDK --- Directory.Build.targets | 5 +++++ src/Avalonia.Base/Media/PathMarkupParser.cs | 12 ++++++++++-- src/Avalonia.Base/Utilities/IdentifierParser.cs | 6 +++++- .../Markup/Parsers/BindingExpressionGrammar.cs | 12 ++++++++++-- 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 Directory.Build.targets diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000000..73954c7f4d --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,5 @@ + + + $(DefineConstants);NET7SDK + + diff --git a/src/Avalonia.Base/Media/PathMarkupParser.cs b/src/Avalonia.Base/Media/PathMarkupParser.cs index cf12bf5126..5e808488fc 100644 --- a/src/Avalonia.Base/Media/PathMarkupParser.cs +++ b/src/Avalonia.Base/Media/PathMarkupParser.cs @@ -188,7 +188,11 @@ namespace Avalonia.Media _isOpen = true; } - private void SetFillRule(scoped ref ReadOnlySpan span) + private void SetFillRule( +#if NET7SDK + scoped +#endif + ref ReadOnlySpan span) { ThrowIfDisposed(); @@ -452,7 +456,11 @@ namespace Avalonia.Media return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || span[0] == '.' || char.IsDigit(span[0])); } - private static bool ReadArgument(scoped ref ReadOnlySpan remaining, out ReadOnlySpan argument) + private static bool ReadArgument( +#if NET7SDK + scoped +#endif + ref ReadOnlySpan remaining, out ReadOnlySpan argument) { remaining = SkipWhitespace(remaining); if (remaining.IsEmpty) diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs index ee176a6b85..76e6459e2e 100644 --- a/src/Avalonia.Base/Utilities/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -8,7 +8,11 @@ namespace Avalonia.Utilities #endif static class IdentifierParser { - public static ReadOnlySpan ParseIdentifier(this scoped ref CharacterReader r) + public static ReadOnlySpan ParseIdentifier(this +#if NET7SDK + scoped +#endif + ref CharacterReader r) { if (IsValidIdentifierStart(r.Peek)) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs index 0a9fbcfacb..21c0d97c74 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -168,7 +168,11 @@ namespace Avalonia.Markup.Parsers } } - private static State ParseAttachedProperty(scoped ref CharacterReader r, List nodes) + private static State ParseAttachedProperty( +#if NET7SDK + scoped +#endif + ref CharacterReader r, List nodes) { var (ns, owner) = ParseTypeName(ref r); @@ -318,7 +322,11 @@ namespace Avalonia.Markup.Parsers return State.AfterMember; } - private static TypeName ParseTypeName(scoped ref CharacterReader r) + private static TypeName ParseTypeName( +#if NET7SDK + scoped +#endif + ref CharacterReader r) { ReadOnlySpan ns, typeName; ns = ReadOnlySpan.Empty; From 1b7544509d39fba46af148e1d1f4412884dc7491 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 2 Nov 2022 11:13:51 +0000 Subject: [PATCH 18/19] fix minimise button being disabled when either a parent or a dialog. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index ddc50c26b6..2443965957 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -63,7 +63,7 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) { START_COM_CALL; @autoreleasepool { - _isDialog = isDialog; + _isDialog = isDialog || _parent != nullptr; WindowBaseImpl::Show(activate, isDialog); @@ -96,6 +96,8 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) { auto cparent = dynamic_cast(parent); _parent = cparent; + + _isDialog = _parent != nullptr; if(_parent != nullptr && Window != nullptr){ // If one tries to show a child window with a minimized parent window, then the parent window will be From 9aca3d827c24003106a60da5acbded5810e982c9 Mon Sep 17 00:00:00 2001 From: hacklex Date: Wed, 2 Nov 2022 18:00:05 +0300 Subject: [PATCH 19/19] Replaced hard-coded indexer with its actual name This was possibly just overlooked since nobody probably ever changed the name from "Item". --- src/Avalonia.Base/Collections/AvaloniaDictionary.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs index 750fb263f5..35a391f2cb 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs @@ -81,7 +81,7 @@ namespace Avalonia.Collections if (replace) { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]")); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]")); if (CollectionChanged != null) { @@ -148,7 +148,7 @@ namespace Avalonia.Collections { _inner.Remove(key); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]")); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]")); if (CollectionChanged != null) { @@ -208,7 +208,7 @@ namespace Avalonia.Collections private void NotifyAdd(TKey key, TValue value) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]")); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]")); if (CollectionChanged != null)