diff --git a/.editorconfig b/.editorconfig
index eac5870f96..a144ec8843 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -55,16 +55,17 @@ dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
-# static fields should have s_ prefix
-dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion
-dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields
-dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style
+# private static fields should have s_ prefix
+dotnet_naming_rule.private_static_fields_should_have_prefix.severity = suggestion
+dotnet_naming_rule.private_static_fields_should_have_prefix.symbols = private_static_fields
+dotnet_naming_rule.private_static_fields_should_have_prefix.style = private_static_prefix_style
-dotnet_naming_symbols.static_fields.applicable_kinds = field
-dotnet_naming_symbols.static_fields.required_modifiers = static
+dotnet_naming_symbols.private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_fields.required_modifiers = static
+dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private
-dotnet_naming_style.static_prefix_style.required_prefix = s_
-dotnet_naming_style.static_prefix_style.capitalization = camel_case
+dotnet_naming_style.private_static_prefix_style.required_prefix = s_
+dotnet_naming_style.private_static_prefix_style.capitalization = camel_case
# internal and private fields should be _camelCase
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
@@ -117,7 +118,7 @@ csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
-csharp_space_around_declaration_statements = do_not_ignore
+csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
@@ -145,10 +146,14 @@ dotnet_diagnostic.CS1591.severity = suggestion
# CS0162: Remove unreachable code
dotnet_diagnostic.CS0162.severity = error
+# CA1018: Mark attributes with AttributeUsageAttribute
+dotnet_diagnostic.CA1018.severity = error
# CA1304: Specify CultureInfo
dotnet_diagnostic.CA1304.severity = warning
# CA1802: Use literals where appropriate
dotnet_diagnostic.CA1802.severity = warning
+# CA1813: Avoid unsealed attributes
+dotnet_diagnostic.CA1813.severity = error
# CA1815: Override equals and operator equals on value types
dotnet_diagnostic.CA1815.severity = warning
# CA1820: Test for empty strings using string length
@@ -207,5 +212,5 @@ indent_size = 2
# Shell scripts
[*.sh]
end_of_line = lf
-[*.{cmd, bat}]
+[*.{cmd,bat}]
end_of_line = crlf
diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props
index 620ec58ff3..75d317be1a 100644
--- a/build/HarfBuzzSharp.props
+++ b/build/HarfBuzzSharp.props
@@ -1,7 +1,7 @@
-
-
-
+
+
+
diff --git a/build/SharedVersion.props b/build/SharedVersion.props
index eca3ba37b0..2849262591 100644
--- a/build/SharedVersion.props
+++ b/build/SharedVersion.props
@@ -3,6 +3,7 @@
Avalonia
11.0.999
+ Avalonia Team
Copyright 2022 © The AvaloniaUI Project
https://avaloniaui.net
https://github.com/AvaloniaUI/Avalonia/
diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props
index 31619399f9..f45addaa2a 100644
--- a/build/SkiaSharp.props
+++ b/build/SkiaSharp.props
@@ -1,7 +1,7 @@
-
-
-
+
+
+
diff --git a/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj b/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj
index d0fb614840..733a4b7194 100644
--- a/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj
+++ b/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj
@@ -9,8 +9,8 @@
-
-
+
+
diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
index e4c83dca49..e465e9caf3 100644
--- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
+++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
@@ -31,7 +31,6 @@
-
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 0695d9d17a..3681298a72 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -14,8 +14,8 @@
-
-
+
+
diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml
index 54c0cb0655..b116e4c789 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml
+++ b/samples/IntegrationTestApp/MainWindow.axaml
@@ -120,30 +120,36 @@
-
-
-
- NonOwned
- Owned
- Modal
-
-
- Manual
- CenterScreen
- CenterOwner
-
-
- Normal
- Minimized
- Maximized
- FullScreen
-
-
-
-
-
-
-
+
+
+
+
+ NonOwned
+ Owned
+ Modal
+
+
+ Manual
+ CenterScreen
+ CenterOwner
+
+
+ Normal
+ Minimized
+ Maximized
+ FullScreen
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs
index 841947673a..3cd5350cce 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml.cs
+++ b/samples/IntegrationTestApp/MainWindow.axaml.cs
@@ -7,9 +7,13 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
using Avalonia.Interactivity;
+using Avalonia.Media;
using Avalonia.Markup.Xaml;
using Avalonia.VisualTree;
using Microsoft.CodeAnalysis;
+using Avalonia.Controls.Primitives;
+using Avalonia.Threading;
+using Avalonia.Controls.Primitives.PopupPositioning;
namespace IntegrationTestApp
{
@@ -103,6 +107,89 @@ namespace IntegrationTestApp
}
}
+ private void ShowTransparentWindow()
+ {
+ // Show a background window to make sure the color behind the transparent window is
+ // a known color (green).
+ var backgroundWindow = new Window
+ {
+ Title = "Transparent Window Background",
+ Name = "TransparentWindowBackground",
+ Width = 300,
+ Height = 300,
+ Background = Brushes.Green,
+ WindowStartupLocation = WindowStartupLocation.CenterOwner,
+ };
+
+ // This is the transparent window with a red circle.
+ var window = new Window
+ {
+ Title = "Transparent Window",
+ Name = "TransparentWindow",
+ SystemDecorations = SystemDecorations.None,
+ Background = Brushes.Transparent,
+ TransparencyLevelHint = WindowTransparencyLevel.Transparent,
+ WindowStartupLocation = WindowStartupLocation.CenterOwner,
+ Width = 200,
+ Height = 200,
+ Content = new Border
+ {
+ Background = Brushes.Red,
+ CornerRadius = new CornerRadius(100),
+ }
+ };
+
+ window.PointerPressed += (_, _) =>
+ {
+ window.Close();
+ backgroundWindow.Close();
+ };
+
+ backgroundWindow.Show(this);
+ window.Show(backgroundWindow);
+ }
+
+ private void ShowTransparentPopup()
+ {
+ var popup = new Popup
+ {
+ WindowManagerAddShadowHint = false,
+ PlacementMode = PlacementMode.AnchorAndGravity,
+ PlacementAnchor = PopupAnchor.Top,
+ PlacementGravity = PopupGravity.Bottom,
+ Width= 200,
+ Height= 200,
+ Child = new Border
+ {
+ Background = Brushes.Red,
+ CornerRadius = new CornerRadius(100),
+ }
+ };
+
+ // Show a background window to make sure the color behind the transparent window is
+ // a known color (green).
+ var backgroundWindow = new Window
+ {
+ Title = "Transparent Popup Background",
+ Name = "TransparentPopupBackground",
+ Width = 200,
+ Height = 200,
+ Background = Brushes.Green,
+ WindowStartupLocation = WindowStartupLocation.CenterOwner,
+ Content = new Border
+ {
+ Name = "PopupContainer",
+ Child = popup,
+ [AutomationProperties.AccessibilityViewProperty] = AccessibilityView.Content,
+ }
+ };
+
+ backgroundWindow.PointerPressed += (_, _) => backgroundWindow.Close();
+ backgroundWindow.Show(this);
+
+ popup.Open();
+ }
+
private void SendToBack()
{
var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
@@ -175,6 +262,10 @@ namespace IntegrationTestApp
this.Get("BasicListBox").SelectedIndex = -1;
if (source?.Name == "MenuClickedMenuItemReset")
this.Get("ClickedMenuItem").Text = "None";
+ if (source?.Name == "ShowTransparentWindow")
+ ShowTransparentWindow();
+ if (source?.Name == "ShowTransparentPopup")
+ ShowTransparentPopup();
if (source?.Name == "ShowWindow")
ShowWindow();
if (source?.Name == "SendToBack")
diff --git a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj
index 1b83a3e567..a24e55de81 100644
--- a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj
+++ b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj
@@ -24,7 +24,6 @@
-
diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs b/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs
index ab61dcde91..7ff8160720 100644
--- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs
+++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs
@@ -52,6 +52,14 @@ namespace ControlSamples
var (oldBounds, newBounds) = change.GetOldAndNewValue();
EnsureSplitViewMode(oldBounds, newBounds);
}
+
+ if (change.Property == SelectedItemProperty)
+ {
+ if (_splitView is not null && _splitView.DisplayMode == SplitViewDisplayMode.Overlay)
+ {
+ _splitView.SetValue(SplitView.IsPaneOpenProperty, false, Avalonia.Data.BindingPriority.Animation);
+ }
+ }
}
private void EnsureSplitViewMode(Rect oldBounds, Rect newBounds)
@@ -60,12 +68,12 @@ namespace ControlSamples
{
var threshold = ExpandedModeThresholdWidth;
- if (newBounds.Width >= threshold && oldBounds.Width < threshold)
+ if (newBounds.Width >= threshold)
{
_splitView.DisplayMode = SplitViewDisplayMode.Inline;
_splitView.IsPaneOpen = true;
}
- else if (newBounds.Width < threshold && oldBounds.Width >= threshold)
+ else if (newBounds.Width < threshold)
{
_splitView.DisplayMode = SplitViewDisplayMode.Overlay;
_splitView.IsPaneOpen = false;
diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs
index edaa76233e..5208c8b218 100644
--- a/src/Avalonia.Base/Animation/Animatable.cs
+++ b/src/Avalonia.Base/Animation/Animatable.cs
@@ -27,7 +27,11 @@ namespace Avalonia.Animation
AvaloniaProperty.Register(nameof(Transitions));
private bool _transitionsEnabled = true;
+ private bool _isSubscribedToTransitionsCollection = false;
private Dictionary? _transitionState;
+ private NotifyCollectionChangedEventHandler? _collectionChanged;
+ private NotifyCollectionChangedEventHandler TransitionsCollectionChangedHandler =>
+ _collectionChanged ??= TransitionsCollectionChanged;
///
/// Gets or sets the clock which controls the animations on the control.
@@ -60,9 +64,14 @@ namespace Avalonia.Animation
{
_transitionsEnabled = true;
- if (Transitions is object)
+ if (Transitions is Transitions transitions)
{
- AddTransitions(Transitions);
+ if (!_isSubscribedToTransitionsCollection)
+ {
+ _isSubscribedToTransitionsCollection = true;
+ transitions.CollectionChanged += TransitionsCollectionChangedHandler;
+ }
+ AddTransitions(transitions);
}
}
}
@@ -72,7 +81,7 @@ namespace Avalonia.Animation
///
///
/// This method should not be called from user code, it will be called automatically by the framework
- /// when a control is added to the visual tree.
+ /// when a control is removed from the visual tree.
///
protected void DisableTransitions()
{
@@ -80,9 +89,14 @@ namespace Avalonia.Animation
{
_transitionsEnabled = false;
- if (Transitions is object)
+ if (Transitions is Transitions transitions)
{
- RemoveTransitions(Transitions);
+ if (_isSubscribedToTransitionsCollection)
+ {
+ _isSubscribedToTransitionsCollection = false;
+ transitions.CollectionChanged -= TransitionsCollectionChangedHandler;
+ }
+ RemoveTransitions(transitions);
}
}
}
@@ -109,7 +123,8 @@ namespace Avalonia.Animation
toAdd = newTransitions.Except(oldTransitions).ToList();
}
- newTransitions.CollectionChanged += TransitionsCollectionChanged;
+ newTransitions.CollectionChanged += TransitionsCollectionChangedHandler;
+ _isSubscribedToTransitionsCollection = true;
AddTransitions(toAdd);
}
@@ -122,19 +137,19 @@ namespace Avalonia.Animation
toRemove = oldTransitions.Except(newTransitions).ToList();
}
- oldTransitions.CollectionChanged -= TransitionsCollectionChanged;
+ oldTransitions.CollectionChanged -= TransitionsCollectionChangedHandler;
RemoveTransitions(toRemove);
}
}
else if (_transitionsEnabled &&
- Transitions is object &&
+ Transitions is Transitions transitions &&
_transitionState is object &&
!change.Property.IsDirect &&
change.Priority > BindingPriority.Animation)
{
- for (var i = Transitions.Count -1; i >= 0; --i)
+ for (var i = transitions.Count - 1; i >= 0; --i)
{
- var transition = Transitions[i];
+ var transition = transitions[i];
if (transition.Property == change.Property &&
_transitionState.TryGetValue(transition, out var state))
@@ -154,11 +169,11 @@ namespace Avalonia.Animation
{
oldValue = animatedValue;
}
-
+ var clock = Clock ?? AvaloniaLocator.Current.GetRequiredService();
state.Instance?.Dispose();
state.Instance = transition.Apply(
this,
- Clock ?? AvaloniaLocator.Current.GetRequiredService(),
+ clock,
oldValue,
newValue);
return;
diff --git a/src/Avalonia.Base/Data/InstancedBinding.cs b/src/Avalonia.Base/Data/InstancedBinding.cs
index 00e5c3d8e6..c09c31632e 100644
--- a/src/Avalonia.Base/Data/InstancedBinding.cs
+++ b/src/Avalonia.Base/Data/InstancedBinding.cs
@@ -23,7 +23,7 @@ namespace Avalonia.Data
/// The priority of the binding.
///
/// This constructor can be used to create any type of binding and as such requires an
- /// as the binding source because this is the only binding
+ /// as the binding source because this is the only binding
/// source which can be used for all binding modes. If you wish to create an instance with
/// something other than a subject, use one of the static creation methods on this class.
///
diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs
index 0bab473442..28757b1a1d 100644
--- a/src/Avalonia.Base/Media/FormattedText.cs
+++ b/src/Avalonia.Base/Media/FormattedText.cs
@@ -741,6 +741,11 @@ namespace Avalonia.Media
null // no previous line break
);
+ if(Current is null)
+ {
+ return false;
+ }
+
// check if this line fits the text height
if (_totalHeight + Current.Height > _that._maxTextHeight)
{
@@ -779,7 +784,7 @@ namespace Avalonia.Media
// maybe there is no next line at all
if (Position + Current.Length < _that._text.Length)
{
- bool nextLineFits;
+ bool nextLineFits = false;
if (_lineCount + 1 >= _that._maxLineCount)
{
@@ -795,7 +800,10 @@ namespace Avalonia.Media
currentLineBreak
);
- nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight);
+ if(_nextLine != null)
+ {
+ nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight);
+ }
}
if (!nextLineFits)
@@ -819,16 +827,22 @@ namespace Avalonia.Media
_previousLineBreak
);
- currentLineBreak = Current.TextLineBreak;
+ if(Current != null)
+ {
+ currentLineBreak = Current.TextLineBreak;
+ }
_that._defaultParaProps.SetTextWrapping(currentWrap);
}
}
}
- _previousHeight = Current.Height;
+ if(Current != null)
+ {
+ _previousHeight = Current.Height;
- Length = Current.Length;
+ Length = Current.Length;
+ }
_previousLineBreak = currentLineBreak;
@@ -838,7 +852,7 @@ namespace Avalonia.Media
///
/// Wrapper of TextFormatter.FormatLine that auto-collapses the line if needed.
///
- private TextLine FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak)
+ private TextLine? FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak)
{
var line = _formatter.FormatLine(
textSource,
@@ -848,7 +862,7 @@ namespace Avalonia.Media
lineBreak
);
- if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0)
+ if (line != null && _that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0)
{
// what I really need here is the last displayed text run of the line
// textSourcePosition + line.Length - 1 works except the end of paragraph case,
@@ -1601,11 +1615,11 @@ namespace Avalonia.Media
}
///
- public TextRun? GetTextRun(int textSourceCharacterIndex)
+ public TextRun GetTextRun(int textSourceCharacterIndex)
{
if (textSourceCharacterIndex >= _that._text.Length)
{
- return null;
+ return new TextEndOfParagraph();
}
var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex);
diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs
index 0ec7152359..2966ceee8d 100644
--- a/src/Avalonia.Base/Media/GlyphRun.cs
+++ b/src/Avalonia.Base/Media/GlyphRun.cs
@@ -166,7 +166,7 @@ namespace Avalonia.Media
///
public Point BaselineOrigin
{
- get => _baselineOrigin ?? default;
+ get => PlatformImpl.Item.BaselineOrigin;
set => Set(ref _baselineOrigin, value);
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs b/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs
index 26966b37bc..32012ab8e9 100644
--- a/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs
@@ -1,6 +1,4 @@
-using Avalonia.Metadata;
-
-namespace Avalonia.Media.TextFormatting
+namespace Avalonia.Media.TextFormatting
{
///
/// Produces objects that are used by the .
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
index 82cf3297fd..b4734d702b 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
@@ -82,24 +82,15 @@ namespace Avalonia.Media.TextFormatting
var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface;
var textSpan = text.Span;
- if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count, out var script))
+ if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count))
{
- if (script == Script.Common && previousGlyphTypeface is not null)
- {
- if (TryGetShapeableLength(textSpan, previousGlyphTypeface, null, out var fallbackCount, out _))
- {
- return new UnshapedTextRun(text.Slice(0, fallbackCount),
- defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
- }
- }
-
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface),
biDiLevel);
}
if (previousGlyphTypeface is not null)
{
- if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count, out _))
+ if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count))
{
return new UnshapedTextRun(text.Slice(0, count),
defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
@@ -127,14 +118,17 @@ namespace Avalonia.Media.TextFormatting
fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out var fallbackTypeface);
-
- var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
-
- if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count, out _))
+
+ if (matchFound)
{
- //Fallback found
- return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
- biDiLevel);
+ // Fallback found
+ var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
+
+ if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count))
+ {
+ return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
+ biDiLevel);
+ }
}
// no fallback found
@@ -160,17 +154,15 @@ namespace Avalonia.Media.TextFormatting
/// The typeface that is used to find matching characters.
/// The default typeface.
/// The shapeable length.
- ///
///
internal static bool TryGetShapeableLength(
ReadOnlySpan text,
IGlyphTypeface glyphTypeface,
IGlyphTypeface? defaultGlyphTypeface,
- out int length,
- out Script script)
+ out int length)
{
length = 0;
- script = Script.Unknown;
+ var script = Script.Unknown;
if (text.IsEmpty)
{
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs
index 0b5d7649d7..ff8c1c4860 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs
@@ -38,7 +38,7 @@
/// A value that specifies the text formatter state,
/// in terms of where the previous line in the paragraph was broken by the text formatting process.
/// The formatted line.
- public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
+ public abstract TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null);
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
index 812c4e9eb8..7f74f49982 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
@@ -18,7 +18,7 @@ namespace Avalonia.Media.TextFormatting
[ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm;
///
- public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
+ public override TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null)
{
TextLineBreak? nextLineBreak = null;
@@ -41,6 +41,11 @@ namespace Avalonia.Media.TextFormatting
fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine,
out var textSourceLength);
+ if (fetchedRuns.Count == 0)
+ {
+ return null;
+ }
+
shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager,
out var resolvedFlowDirection);
@@ -491,16 +496,7 @@ namespace Avalonia.Media.TextFormatting
while (textRunEnumerator.MoveNext())
{
- var textRun = textRunEnumerator.Current;
-
- if (textRun == null)
- {
- textRuns.Add(new TextEndOfParagraph());
-
- textSourceLength += TextRun.DefaultTextSourceLength;
-
- break;
- }
+ TextRun textRun = textRunEnumerator.Current!;
if (textRun is TextEndOfLine textEndOfLine)
{
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index 8e85c10bba..4dbc472133 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
@@ -238,7 +238,7 @@ namespace Avalonia.Media.TextFormatting
foreach (var textLine in _textLines)
{
//Current line isn't covered.
- if (textLine.FirstTextSourceIndex + textLine.Length < start)
+ if (textLine.FirstTextSourceIndex + textLine.Length <= start)
{
currentY += textLine.Height;
@@ -348,14 +348,36 @@ namespace Avalonia.Media.TextFormatting
{
var (x, y) = point;
- var lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
-
var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height;
- if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
+ var lastTrailingIndex = 0;
+
+ if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight)
{
- lastTrailingIndex -= textLine.NewLineLength;
+ lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
+
+ if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
+ {
+ lastTrailingIndex -= textLine.NewLineLength;
+ }
+
+ if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine)
+ {
+ lastTrailingIndex -= textEndOfLine.Length;
+ }
}
+ else
+ {
+ if (x <= textLine.WidthIncludingTrailingWhitespace - textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
+ {
+ lastTrailingIndex += textLine.NewLineLength;
+ }
+
+ if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine)
+ {
+ lastTrailingIndex += textEndOfLine.Length;
+ }
+ }
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
@@ -391,7 +413,7 @@ namespace Avalonia.Media.TextFormatting
///
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);
@@ -456,7 +478,7 @@ namespace Avalonia.Media.TextFormatting
var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
- if (textLine.Length == 0)
+ if (textLine is null)
{
if (previousLine != null && previousLine.NewLineLength > 0)
{
@@ -518,7 +540,6 @@ namespace Avalonia.Media.TextFormatting
}
}
- //Make sure the TextLayout always contains at least on empty line
if (textLines.Count == 0)
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
index d29063e07d..187b3154ad 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
@@ -10,6 +10,7 @@ namespace Avalonia.Media.TextFormatting
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
private TextLineMetrics _textLineMetrics;
+ private TextLineBreak? _textLineBreak;
private readonly FlowDirection _resolvedFlowDirection;
public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
@@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting
{
FirstTextSourceIndex = firstTextSourceIndex;
Length = length;
- TextLineBreak = lineBreak;
+ _textLineBreak = lineBreak;
HasCollapsed = hasCollapsed;
_textRuns = textRuns;
@@ -38,7 +39,7 @@ namespace Avalonia.Media.TextFormatting
public override int Length { get; }
///
- public override TextLineBreak? TextLineBreak { get; }
+ public override TextLineBreak? TextLineBreak => _textLineBreak;
///
public override bool HasCollapsed { get; }
@@ -167,50 +168,54 @@ namespace Avalonia.Media.TextFormatting
{
if (_textRuns.Length == 0)
{
- return new CharacterHit();
+ return new CharacterHit(FirstTextSourceIndex);
}
distance -= Start;
- var firstRunIndex = 0;
+ var lastIndex = _textRuns.Length - 1;
- if (_textRuns[firstRunIndex] is TextEndOfLine)
+ if (_textRuns[lastIndex] is TextEndOfLine)
{
- firstRunIndex++;
+ lastIndex--;
}
- if(firstRunIndex >= _textRuns.Length)
+ var currentPosition = FirstTextSourceIndex;
+
+ if (lastIndex < 0)
{
- return new CharacterHit(FirstTextSourceIndex);
+ return new CharacterHit(currentPosition);
}
if (distance <= 0)
{
- var firstRun = _textRuns[firstRunIndex];
+ var firstRun = _textRuns[0];
- return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0);
+ if (_paragraphProperties.FlowDirection == FlowDirection.RightToLeft)
+ {
+ currentPosition = Length - firstRun.Length;
+ }
+
+ return GetRunCharacterHit(firstRun, currentPosition, 0);
}
if (distance >= WidthIncludingTrailingWhitespace)
{
- var lastRun = _textRuns[_textRuns.Length - 1];
-
- var size = 0.0;
+ var lastRun = _textRuns[lastIndex];
- if (lastRun is DrawableTextRun drawableTextRun)
+ if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
{
- size = drawableTextRun.Size.Width;
+ currentPosition = Length - lastRun.Length;
}
- return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, size);
+ return GetRunCharacterHit(lastRun, currentPosition, distance);
}
// process hit that happens within the line
var characterHit = new CharacterHit();
- var currentPosition = FirstTextSourceIndex;
var currentDistance = 0.0;
- for (var i = 0; i < _textRuns.Length; i++)
+ for (var i = 0; i <= lastIndex; i++)
{
var currentRun = _textRuns[i];
@@ -242,7 +247,7 @@ namespace Avalonia.Media.TextFormatting
currentRun = _textRuns[j];
- if(currentRun is not ShapedTextRun)
+ if (currentRun is not ShapedTextRun)
{
continue;
}
@@ -274,10 +279,6 @@ namespace Avalonia.Media.TextFormatting
continue;
}
}
- else
- {
- continue;
- }
break;
}
@@ -422,10 +423,10 @@ namespace Avalonia.Media.TextFormatting
{
if (currentGlyphRun != null)
{
- distance = currentGlyphRun.Size.Width - distance;
+ currentDistance -= currentGlyphRun.Size.Width;
}
- return Math.Max(0, currentDistance - distance);
+ return currentDistance + distance;
}
if (currentRun is DrawableTextRun drawableTextRun)
@@ -575,386 +576,505 @@ namespace Avalonia.Media.TextFormatting
return GetPreviousCaretCharacterHit(characterHit);
}
- private IReadOnlyList GetTextBoundsLeftToRight(int firstTextSourceIndex, int textLength)
+ public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength)
{
- var characterIndex = firstTextSourceIndex + textLength;
+ if (_textRuns.Length == 0)
+ {
+ return Array.Empty();
+ }
- var result = new List(_textRuns.Length);
- var lastDirection = FlowDirection.LeftToRight;
- var currentDirection = lastDirection;
+ var result = new List();
var currentPosition = FirstTextSourceIndex;
var remainingLength = textLength;
- var startX = Start;
- double currentWidth = 0;
- var currentRect = default(Rect);
-
- TextRunBounds lastRunBounds = default;
-
- for (var index = 0; index < _textRuns.Length; index++)
+ static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection)
{
- if (_textRuns[index] is not DrawableTextRun currentRun)
+ if (textRun is ShapedTextRun shapedTextRun)
{
- continue;
+ return shapedTextRun.ShapedBuffer.IsLeftToRight ?
+ FlowDirection.LeftToRight :
+ FlowDirection.RightToLeft;
}
- var characterLength = 0;
- var endX = startX;
-
- TextRunBounds currentRunBounds;
+ return currentDirection;
+ }
- double combinedWidth;
+ if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
+ {
+ var currentX = Start;
- if (currentRun is ShapedTextRun currentShapedRun)
+ for (int i = 0; i < _textRuns.Length; i++)
{
- var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster;
+ var currentRun = _textRuns[i];
+
+ var firstRunIndex = i;
+ var lastRunIndex = firstRunIndex;
+ var currentDirection = GetDirection(currentRun, FlowDirection.LeftToRight);
+ var directionalWidth = 0.0;
- if (currentPosition + currentRun.Length <= firstTextSourceIndex)
+ if (currentRun is DrawableTextRun currentDrawable)
{
- startX += currentRun.Size.Width;
+ directionalWidth = currentDrawable.Size.Width;
+ }
- currentPosition += currentRun.Length;
+ // Find consecutive runs of same direction
+ for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++)
+ {
+ var nextRun = _textRuns[lastRunIndex + 1];
- continue;
+ var nextDirection = GetDirection(nextRun, currentDirection);
+
+ if (currentDirection != nextDirection)
+ {
+ break;
+ }
+
+ if (nextRun is DrawableTextRun nextDrawable)
+ {
+ directionalWidth += nextDrawable.Size.Width;
+ }
}
- if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+ //Skip runs that are not part of the hit test range
+ switch (currentDirection)
{
- var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition);
+ case FlowDirection.RightToLeft:
+ {
+ for (; lastRunIndex >= firstRunIndex; lastRunIndex--)
+ {
+ currentRun = _textRuns[lastRunIndex];
+
+ if (currentPosition + currentRun.Length > firstTextSourceIndex)
+ {
+ break;
+ }
+
+ currentPosition += currentRun.Length;
- double startOffset;
+ if (currentRun is DrawableTextRun drawableTextRun)
+ {
+ directionalWidth -= drawableTextRun.Size.Width;
+ currentX += drawableTextRun.Size.Width;
+ }
- double endOffset;
+ if(lastRunIndex - 1 < 0)
+ {
+ break;
+ }
+ }
- startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ break;
+ }
+ default:
+ {
+ for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
+ {
+ currentRun = _textRuns[firstRunIndex];
- endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ if (currentPosition + currentRun.Length > firstTextSourceIndex)
+ {
+ break;
+ }
- startX += startOffset;
+ currentPosition += currentRun.Length;
- endX += endOffset;
+ if (currentRun is DrawableTextRun drawableTextRun)
+ {
+ currentX += drawableTextRun.Size.Width;
+ directionalWidth -= drawableTextRun.Size.Width;
+ }
- var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+ if(firstRunIndex + 1 == _textRuns.Length)
+ {
+ break;
+ }
+ }
- var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+ break;
+ }
+ }
- characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
+ i = lastRunIndex;
- currentDirection = FlowDirection.LeftToRight;
+ if (directionalWidth == 0)
+ {
+ continue;
}
- else
+
+ var coveredLength = 0;
+ TextBounds? textBounds = null;
+
+ switch (currentDirection)
{
- var rightToLeftIndex = index;
- var rightToLeftWidth = currentShapedRun.Size.Width;
- while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun)
- {
- if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
+ case FlowDirection.RightToLeft:
{
+ textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex,
+ currentPosition, remainingLength, out coveredLength, out currentPosition);
+
+ currentX += directionalWidth;
+
break;
}
+ default:
+ {
+ textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
+ currentPosition, remainingLength, out coveredLength, out currentPosition);
- rightToLeftIndex++;
-
- rightToLeftWidth += nextShapedRun.Size.Width;
+ currentX = textBounds.Rectangle.Right;
- if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength)
- {
break;
}
+ }
- currentShapedRun = nextShapedRun;
- }
+ if (coveredLength > 0)
+ {
+ result.Add(textBounds);
+
+ remainingLength -= coveredLength;
+ }
+
+ if (remainingLength <= 0)
+ {
+ break;
+ }
+ }
+ }
+ else
+ {
+ var currentX = Start + WidthIncludingTrailingWhitespace;
- startX += rightToLeftWidth;
+ for (int i = _textRuns.Length - 1; i >= 0; i--)
+ {
+ var currentRun = _textRuns[i];
+ var firstRunIndex = i;
+ var lastRunIndex = firstRunIndex;
+ var currentDirection = GetDirection(currentRun, FlowDirection.RightToLeft);
+ var directionalWidth = 0.0;
- currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
+ if (currentRun is DrawableTextRun currentDrawable)
+ {
+ directionalWidth = currentDrawable.Size.Width;
+ }
- remainingLength -= currentRunBounds.Length;
- currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
- endX = currentRunBounds.Rectangle.Right;
- startX = currentRunBounds.Rectangle.Left;
+ // Find consecutive runs of same direction
+ for (; firstRunIndex - 1 > 0; firstRunIndex--)
+ {
+ var previousRun = _textRuns[firstRunIndex - 1];
- var rightToLeftRunBounds = new List { currentRunBounds };
+ var previousDirection = GetDirection(previousRun, currentDirection);
- for (int i = rightToLeftIndex - 1; i >= index; i--)
+ if (currentDirection != previousDirection)
{
- if (_textRuns[i] is not ShapedTextRun shapedRun)
+ break;
+ }
+
+ if (currentRun is DrawableTextRun previousDrawable)
+ {
+ directionalWidth += previousDrawable.Size.Width;
+ }
+ }
+
+ //Skip runs that are not part of the hit test range
+ switch (currentDirection)
+ {
+ case FlowDirection.RightToLeft:
{
- continue;
- }
+ for (; lastRunIndex >= firstRunIndex; lastRunIndex--)
+ {
+ currentRun = _textRuns[lastRunIndex];
- currentShapedRun = shapedRun;
+ if (currentPosition + currentRun.Length <= firstTextSourceIndex)
+ {
+ currentPosition += currentRun.Length;
- currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
+ if (currentRun is DrawableTextRun drawableTextRun)
+ {
+ currentX -= drawableTextRun.Size.Width;
+ directionalWidth -= drawableTextRun.Size.Width;
+ }
- rightToLeftRunBounds.Insert(0, currentRunBounds);
+ continue;
+ }
- remainingLength -= currentRunBounds.Length;
- startX = currentRunBounds.Rectangle.Left;
+ break;
+ }
- currentPosition += currentRunBounds.Length;
- }
+ break;
+ }
+ default:
+ {
+ for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
+ {
+ currentRun = _textRuns[firstRunIndex];
- combinedWidth = endX - startX;
+ if (currentPosition + currentRun.Length <= firstTextSourceIndex)
+ {
+ currentPosition += currentRun.Length;
- currentRect = new Rect(startX, 0, combinedWidth, Height);
+ if (currentRun is DrawableTextRun drawableTextRun)
+ {
+ currentX += drawableTextRun.Size.Width;
+ directionalWidth -= drawableTextRun.Size.Width;
+ }
- currentDirection = FlowDirection.RightToLeft;
+ continue;
+ }
- if (!MathUtilities.IsZero(combinedWidth))
- {
- result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
- }
+ break;
+ }
- startX = endX;
+ break;
+ }
}
- }
- else
- {
- if (currentPosition + currentRun.Length <= firstTextSourceIndex)
- {
- startX += currentRun.Size.Width;
- currentPosition += currentRun.Length;
+ i = firstRunIndex;
+ if (directionalWidth == 0)
+ {
continue;
}
- if (currentPosition < firstTextSourceIndex)
- {
- startX += currentRun.Size.Width;
- }
+ var coveredLength = 0;
- if (currentPosition + currentRun.Length <= characterIndex)
+ TextBounds? textBounds = null;
+
+ switch (currentDirection)
{
- endX += currentRun.Size.Width;
+ case FlowDirection.LeftToRight:
+ {
+ textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX - directionalWidth, firstTextSourceIndex,
+ currentPosition, remainingLength, out coveredLength, out currentPosition);
+
+ currentX -= directionalWidth;
- characterLength = currentRun.Length;
+ break;
+ }
+ default:
+ {
+ textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
+ currentPosition, remainingLength, out coveredLength, out currentPosition);
+
+ currentX = textBounds.Rectangle.Left;
+
+ break;
+ }
}
- }
- if (endX < startX)
- {
- (endX, startX) = (startX, endX);
- }
+ //Visual order is always left to right so we need to insert
+ result.Insert(0, textBounds);
- //Lines that only contain a linebreak need to be covered here
- if (characterLength == 0)
- {
- characterLength = NewLineLength;
+ remainingLength -= coveredLength;
+
+ if (remainingLength <= 0)
+ {
+ break;
+ }
}
+ }
- combinedWidth = endX - startX;
+ return result;
+ }
- currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
+ private TextBounds GetTextRunBoundsRightToLeft(int firstRunIndex, int lastRunIndex, double endX,
+ int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
+ {
+ coveredLength = 0;
+ var textRunBounds = new List();
+ var startX = endX;
- currentPosition += characterLength;
+ for (int i = lastRunIndex; i >= firstRunIndex; i--)
+ {
+ var currentRun = _textRuns[i];
- remainingLength -= characterLength;
+ if (currentRun is ShapedTextRun shapedTextRun)
+ {
+ var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
- startX = endX;
+ textRunBounds.Insert(0, runBounds);
- if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
- {
- if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
+ if (offset > 0)
{
- currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
+ endX = runBounds.Rectangle.Right;
- var textBounds = result[result.Count - 1];
+ startX = endX;
+ }
- textBounds.Rectangle = currentRect;
+ startX -= runBounds.Rectangle.Width;
- textBounds.TextRunBounds.Add(currentRunBounds);
- }
- else
+ currentPosition += runBounds.Length + offset;
+
+ coveredLength += runBounds.Length;
+
+ remainingLength -= runBounds.Length;
+ }
+ else
+ {
+ if (currentRun is DrawableTextRun drawableTextRun)
{
- currentRect = currentRunBounds.Rectangle;
+ startX -= drawableTextRun.Size.Width;
- result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
+ textRunBounds.Insert(0,
+ new TextRunBounds(
+ new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun));
}
- }
- lastRunBounds = currentRunBounds;
+ currentPosition += currentRun.Length;
+
+ coveredLength += currentRun.Length;
- currentWidth += combinedWidth;
+ remainingLength -= currentRun.Length;
+ }
- if (remainingLength <= 0 || currentPosition >= characterIndex)
+ if (remainingLength <= 0)
{
break;
}
-
- lastDirection = currentDirection;
}
- return result;
- }
+ newPosition = currentPosition;
- private IReadOnlyList GetTextBoundsRightToLeft(int firstTextSourceIndex, int textLength)
- {
- var characterIndex = firstTextSourceIndex + textLength;
+ var runWidth = endX - startX;
- var result = new List(_textRuns.Length);
- var lastDirection = FlowDirection.LeftToRight;
- var currentDirection = lastDirection;
+ var bounds = new Rect(startX, 0, runWidth, Height);
- var currentPosition = FirstTextSourceIndex;
- var remainingLength = textLength;
+ return new TextBounds(bounds, FlowDirection.RightToLeft, textRunBounds);
+ }
- var startX = WidthIncludingTrailingWhitespace;
- double currentWidth = 0;
- var currentRect = default(Rect);
+ private TextBounds GetTextBoundsLeftToRight(int firstRunIndex, int lastRunIndex, double startX,
+ int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
+ {
+ coveredLength = 0;
+ var textRunBounds = new List();
+ var endX = startX;
- for (var index = _textRuns.Length - 1; index >= 0; index--)
+ for (int i = firstRunIndex; i <= lastRunIndex; i++)
{
- if (_textRuns[index] is not DrawableTextRun currentRun)
- {
- continue;
- }
-
- if (currentPosition + currentRun.Length < firstTextSourceIndex)
- {
- startX -= currentRun.Size.Width;
-
- currentPosition += currentRun.Length;
-
- continue;
- }
-
- var characterLength = 0;
- var endX = startX;
+ var currentRun = _textRuns[i];
- if (currentRun is ShapedTextRun currentShapedRun)
+ if (currentRun is ShapedTextRun shapedTextRun)
{
- var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
-
- currentPosition += offset;
-
- var startIndex = currentPosition;
- double startOffset;
- double endOffset;
+ var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
- if (currentShapedRun.ShapedBuffer.IsLeftToRight)
- {
- if (currentPosition < startIndex)
- {
- startOffset = endOffset = 0;
- }
- else
- {
- endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ textRunBounds.Add(runBounds);
- startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
- }
- }
- else
+ if (offset > 0)
{
- endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ startX = runBounds.Rectangle.Left;
- startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ endX = startX;
}
- startX -= currentRun.Size.Width - startOffset;
- endX -= currentRun.Size.Width - endOffset;
+ currentPosition += runBounds.Length + offset;
- var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
- var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+ endX += runBounds.Rectangle.Width;
- characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
+ coveredLength += runBounds.Length;
- currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ?
- FlowDirection.LeftToRight :
- FlowDirection.RightToLeft;
+ remainingLength -= runBounds.Length;
}
else
{
- if (currentPosition + currentRun.Length <= characterIndex)
+ if (currentRun is DrawableTextRun drawableTextRun)
{
- endX -= currentRun.Size.Width;
+ textRunBounds.Add(
+ new TextRunBounds(
+ new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun));
+
+ endX += drawableTextRun.Size.Width;
}
- if (currentPosition < firstTextSourceIndex)
- {
- startX -= currentRun.Size.Width;
+ currentPosition += currentRun.Length;
- characterLength = currentRun.Length;
- }
- }
+ coveredLength += currentRun.Length;
- if (endX < startX)
- {
- (endX, startX) = (startX, endX);
+ remainingLength -= currentRun.Length;
}
- //Lines that only contain a linebreak need to be covered here
- if (characterLength == 0)
+ if (remainingLength <= 0)
{
- characterLength = NewLineLength;
+ break;
}
+ }
- var runWidth = endX - startX;
+ newPosition = currentPosition;
- var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
+ var runWidth = endX - startX;
- if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
- {
- if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX))
- {
- currentRect = currentRect.WithWidth(currentWidth + runWidth);
+ var bounds = new Rect(startX, 0, runWidth, Height);
- var textBounds = result[result.Count - 1];
+ return new TextBounds(bounds, FlowDirection.LeftToRight, textRunBounds);
+ }
- textBounds.Rectangle = currentRect;
+ private TextRunBounds GetRunBoundsLeftToRight(ShapedTextRun currentRun, double startX,
+ int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset)
+ {
+ var startIndex = currentPosition;
- textBounds.TextRunBounds.Add(currentRunBounds);
- }
- else
- {
- currentRect = currentRunBounds.Rectangle;
+ offset = Math.Max(0, firstTextSourceIndex - currentPosition);
- result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
- }
- }
+ var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
- currentWidth += runWidth;
- currentPosition += characterLength;
+ if (currentPosition != firstCluster)
+ {
+ startIndex = firstCluster + offset;
+ }
+ else
+ {
+ startIndex += offset;
+ }
- if (currentPosition > characterIndex)
- {
- break;
- }
+ var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
- lastDirection = currentDirection;
- remainingLength -= characterLength;
+ var endX = startX + endOffset;
+ startX += startOffset;
- if (remainingLength <= 0)
- {
- break;
- }
+ var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+ var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+
+ var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
+
+ if (endX < startX)
+ {
+ (endX, startX) = (startX, endX);
+ }
+
+ //Lines that only contain a linebreak need to be covered here
+ if (characterLength == 0)
+ {
+ characterLength = NewLineLength;
}
- result.Reverse();
+ var runWidth = endX - startX;
- return result;
+ return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
}
- private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength)
+ private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX,
+ int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset)
{
var startX = endX;
- var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+ var startIndex = currentPosition;
- currentPosition += offset;
+ offset = Math.Max(0, firstTextSourceIndex - currentPosition);
- var startIndex = currentPosition;
+ var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
- double startOffset;
- double endOffset;
+ if (currentPosition != firstCluster)
+ {
+ startIndex = firstCluster + offset;
+ }
+ else
+ {
+ startIndex += offset;
+ }
- endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
- startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
startX -= currentRun.Size.Width - startOffset;
endX -= currentRun.Size.Width - endOffset;
@@ -980,16 +1100,6 @@ namespace Avalonia.Media.TextFormatting
return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
}
- public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength)
- {
- if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
- {
- return GetTextBoundsLeftToRight(firstTextSourceIndex, textLength);
- }
-
- return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength);
- }
-
public override void Dispose()
{
for (int i = 0; i < _textRuns.Length; i++)
@@ -1005,6 +1115,11 @@ namespace Avalonia.Media.TextFormatting
{
_textLineMetrics = CreateLineMetrics();
+ if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine)
+ {
+ _textLineBreak = new TextLineBreak(textEndOfLine);
+ }
+
BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection);
}
@@ -1328,7 +1443,7 @@ namespace Avalonia.Media.TextFormatting
{
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
- newLineLength = textRun.GlyphRun.Metrics.NewLineLength;
+ newLineLength += textRun.GlyphRun.Metrics.NewLineLength;
}
widthIncludingWhitespace += textRun.Size.Width;
@@ -1340,31 +1455,10 @@ namespace Avalonia.Media.TextFormatting
{
widthIncludingWhitespace += drawableTextRun.Size.Width;
- switch (_paragraphProperties.FlowDirection)
+ if (index == lastRunIndex)
{
- case FlowDirection.LeftToRight:
- {
- if (index == lastRunIndex)
- {
- width = widthIncludingWhitespace;
- trailingWhitespaceLength = 0;
- newLineLength = 0;
- }
-
- break;
- }
-
- case FlowDirection.RightToLeft:
- {
- if (index == lastRunIndex)
- {
- width = widthIncludingWhitespace;
- trailingWhitespaceLength = 0;
- newLineLength = 0;
- }
-
- break;
- }
+ width = widthIncludingWhitespace;
+ trailingWhitespaceLength = 0;
}
if (drawableTextRun.Size.Height > height)
diff --git a/src/Avalonia.Base/Metadata/AmbientAttribute.cs b/src/Avalonia.Base/Metadata/AmbientAttribute.cs
index 85ca6c4ec9..1c85a67641 100644
--- a/src/Avalonia.Base/Metadata/AmbientAttribute.cs
+++ b/src/Avalonia.Base/Metadata/AmbientAttribute.cs
@@ -3,10 +3,10 @@ using System;
namespace Avalonia.Metadata
{
///
- /// Defines the ambient class/property
+ /// Defines the ambient class/property
///
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, Inherited = true)]
- public class AmbientAttribute : Attribute
+ public sealed class AmbientAttribute : Attribute
{
}
}
diff --git a/src/Avalonia.Base/Metadata/ContentAttribute.cs b/src/Avalonia.Base/Metadata/ContentAttribute.cs
index a0b2fa0e1d..f32c8e78f6 100644
--- a/src/Avalonia.Base/Metadata/ContentAttribute.cs
+++ b/src/Avalonia.Base/Metadata/ContentAttribute.cs
@@ -6,7 +6,7 @@ namespace Avalonia.Metadata
/// Defines the property that contains the object's content in markup.
///
[AttributeUsage(AttributeTargets.Property)]
- public class ContentAttribute : Attribute
+ public sealed class ContentAttribute : Attribute
{
}
}
diff --git a/src/Avalonia.Base/Metadata/DataTypeAttribute.cs b/src/Avalonia.Base/Metadata/DataTypeAttribute.cs
index ac46a0d30a..dd9603b4a9 100644
--- a/src/Avalonia.Base/Metadata/DataTypeAttribute.cs
+++ b/src/Avalonia.Base/Metadata/DataTypeAttribute.cs
@@ -9,7 +9,7 @@ namespace Avalonia.Metadata;
/// Used on DataTemplate.DataType property so it can be inherited in compiled bindings inside of the template.
///
[AttributeUsage(AttributeTargets.Property)]
-public class DataTypeAttribute : Attribute
+public sealed class DataTypeAttribute : Attribute
{
-
+
}
diff --git a/src/Avalonia.Base/Metadata/DependsOnAttribute.cs b/src/Avalonia.Base/Metadata/DependsOnAttribute.cs
index caee71ebfd..ca58a91eb9 100644
--- a/src/Avalonia.Base/Metadata/DependsOnAttribute.cs
+++ b/src/Avalonia.Base/Metadata/DependsOnAttribute.cs
@@ -6,7 +6,7 @@ namespace Avalonia.Metadata
/// Indicates that the property depends on the value of another property in markup.
///
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
- public class DependsOnAttribute : Attribute
+ public sealed class DependsOnAttribute : Attribute
{
///
/// Initializes a new instance of the class.
diff --git a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
index 6bb820d214..fac8cd8737 100644
--- a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
+++ b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
@@ -25,9 +25,9 @@ public sealed class InheritDataTypeFromItemsAttribute : Attribute
/// The name of the property whose item type should be used on the target property.
///
public string AncestorItemsProperty { get; }
-
+
///
- /// The ancestor type to be used in a lookup for the .
+ /// The ancestor type to be used in a lookup for the .
/// If null, the declaring type of the target property is used.
///
public Type? AncestorType { get; set; }
diff --git a/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs b/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs
index 348c983c03..75fe7b8031 100644
--- a/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs
+++ b/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs
@@ -11,7 +11,7 @@ namespace Avalonia.Metadata
/// may be added to its API.
///
[AttributeUsage(AttributeTargets.Interface)]
- public class NotClientImplementableAttribute : Attribute
+ public sealed class NotClientImplementableAttribute : Attribute
{
}
}
diff --git a/src/Avalonia.Base/Metadata/TemplateContent.cs b/src/Avalonia.Base/Metadata/TemplateContent.cs
index 258154aba4..78bcc2ff29 100644
--- a/src/Avalonia.Base/Metadata/TemplateContent.cs
+++ b/src/Avalonia.Base/Metadata/TemplateContent.cs
@@ -6,7 +6,7 @@ namespace Avalonia.Metadata
/// Defines the property that contains the object's content in markup.
///
[AttributeUsage(AttributeTargets.Property)]
- public class TemplateContentAttribute : Attribute
+ public sealed class TemplateContentAttribute : Attribute
{
public Type? TemplateResultType { get; set; }
}
diff --git a/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs
index c46891b3ad..a644c9afe6 100644
--- a/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs
+++ b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs
@@ -3,7 +3,7 @@
namespace Avalonia.Metadata
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
- public class TrimSurroundingWhitespaceAttribute : Attribute
+ public sealed class TrimSurroundingWhitespaceAttribute : Attribute
{
}
diff --git a/src/Avalonia.Base/Metadata/UnstableAttribute.cs b/src/Avalonia.Base/Metadata/UnstableAttribute.cs
index 3b6fa5168a..361f6d30fd 100644
--- a/src/Avalonia.Base/Metadata/UnstableAttribute.cs
+++ b/src/Avalonia.Base/Metadata/UnstableAttribute.cs
@@ -6,7 +6,8 @@ namespace Avalonia.Metadata
/// This API is unstable and is not covered by API compatibility guarantees between minor and
/// patch releases.
///
- public class UnstableAttribute : Attribute
+ [AttributeUsage(AttributeTargets.All)]
+ public sealed class UnstableAttribute : Attribute
{
}
}
diff --git a/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs b/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs
index 753a96b9ce..d2d163b368 100644
--- a/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs
+++ b/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs
@@ -3,8 +3,8 @@ using System;
namespace Avalonia.Metadata
{
[AttributeUsage(AttributeTargets.Class)]
- public class UsableDuringInitializationAttribute : Attribute
+ public sealed class UsableDuringInitializationAttribute : Attribute
{
-
+
}
}
diff --git a/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs
index aeaa38dad9..2fd2b1da3b 100644
--- a/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs
+++ b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs
@@ -6,7 +6,7 @@ namespace Avalonia.Metadata
/// Indicates that a collection type should be processed as being whitespace significant by a XAML processor.
///
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
- public class WhitespaceSignificantCollectionAttribute : Attribute
+ public sealed class WhitespaceSignificantCollectionAttribute : Attribute
{
}
}
diff --git a/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs b/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs
index d43fa55f5c..c6b79ba987 100644
--- a/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs
+++ b/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs
@@ -6,7 +6,7 @@ namespace Avalonia.Metadata
/// Maps an XML namespace to a CLR namespace for use in XAML.
///
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
- public class XmlnsDefinitionAttribute : Attribute
+ public sealed class XmlnsDefinitionAttribute : Attribute
{
///
/// Initializes a new instance of the class.
diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs
index ff2069e71e..560ee05c10 100644
--- a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs
@@ -39,7 +39,8 @@ namespace Avalonia.Rendering.Composition.Expressions
}
}
- internal class PrettyPrintStringAttribute : Attribute
+ [AttributeUsage(AttributeTargets.Field)]
+ internal sealed class PrettyPrintStringAttribute : Attribute
{
public string Name { get; }
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs
index 6d30358119..2bfd2080c3 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs
@@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph
/// The point in global coordinates.
/// True if the point hits the node's geometry; otherwise false.
///
- /// This method does not recurse to child s, if you want
+ /// This method does not recurse to childs, if you want
/// to hit test children they must be hit tested manually.
///
bool HitTest(Point p);
diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs
index 85f139a6a3..aea91b5e26 100644
--- a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs
@@ -1,5 +1,4 @@
-using System;
-using Avalonia.Automation.Provider;
+using Avalonia.Automation.Provider;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Selection;
@@ -64,7 +63,7 @@ namespace Avalonia.Automation.Peers
if (Owner.Parent is ItemsControl parent &&
parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel)
{
- var index = parent.ItemContainerGenerator.IndexFromContainer(Owner);
+ var index = parent.IndexFromContainer(Owner);
if (index != -1)
selectionModel.Deselect(index);
diff --git a/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs b/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs
index 5a34c5c0e1..f271abb59a 100644
--- a/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs
+++ b/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs
@@ -41,7 +41,7 @@ namespace Avalonia.Platform
/// The fallback module will only be initialized if the Skia-specific module is not applicable.
///
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
- public class ExportAvaloniaModuleAttribute : Attribute
+ public sealed class ExportAvaloniaModuleAttribute : Attribute
{
public ExportAvaloniaModuleAttribute(string name, Type moduleType)
{
diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs
index 8594b584fa..e8eaac7d17 100644
--- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs
@@ -28,19 +28,19 @@ namespace Avalonia.Controls.Presenters
/// Defines the property.
///
public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty =
- AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular));
+ AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular));
///
/// Defines the property.
///
public static readonly StyledProperty AreVerticalSnapPointsRegularProperty =
- AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular));
+ AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular));
///
/// Defines the event.
///
public static readonly RoutedEvent HorizontalSnapPointsChangedEvent =
- RoutedEvent.Register(
+ RoutedEvent.Register(
nameof(HorizontalSnapPointsChanged),
RoutingStrategies.Bubble);
@@ -48,7 +48,7 @@ namespace Avalonia.Controls.Presenters
/// Defines the event.
///
public static readonly RoutedEvent VerticalSnapPointsChangedEvent =
- RoutedEvent.Register(
+ RoutedEvent.Register(
nameof(VerticalSnapPointsChanged),
RoutingStrategies.Bubble);
@@ -139,7 +139,7 @@ namespace Avalonia.Controls.Presenters
Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default;
///
- /// Gets or sets whether the horizontal snap points for the are equidistant from each other.
+ /// Gets or sets whether the horizontal snap points for the are equidistant from each other.
///
public bool AreHorizontalSnapPointsRegular
{
@@ -148,7 +148,7 @@ namespace Avalonia.Controls.Presenters
}
///
- /// Gets or sets whether the vertical snap points for the are equidistant from each other.
+ /// Gets or sets whether the vertical snap points for the are equidistant from each other.
///
public bool AreVerticalSnapPointsRegular
{
diff --git a/src/Avalonia.Controls/ResolveByNameAttribute.cs b/src/Avalonia.Controls/ResolveByNameAttribute.cs
index a13b10d630..3c56c20db0 100644
--- a/src/Avalonia.Controls/ResolveByNameAttribute.cs
+++ b/src/Avalonia.Controls/ResolveByNameAttribute.cs
@@ -7,7 +7,8 @@ namespace Avalonia.Controls
/// When applying this to attached properties, ensure to put on both
/// the Getter and Setter methods.
///
- public class ResolveByNameAttribute : Attribute
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)]
+ public sealed class ResolveByNameAttribute : Attribute
{
}
}
diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs
index 1c23919d0e..ab114da933 100644
--- a/src/Avalonia.Controls/ScrollViewer.cs
+++ b/src/Avalonia.Controls/ScrollViewer.cs
@@ -154,15 +154,15 @@ namespace Avalonia.Controls
///
/// Defines the property.
///
- public static readonly StyledProperty HorizontalSnapPointsTypeProperty =
- AvaloniaProperty.Register(
+ public static readonly AttachedProperty HorizontalSnapPointsTypeProperty =
+ AvaloniaProperty.RegisterAttached(
nameof(HorizontalSnapPointsType));
///
/// Defines the property.
///
- public static readonly StyledProperty VerticalSnapPointsTypeProperty =
- AvaloniaProperty.Register(
+ public static readonly AttachedProperty VerticalSnapPointsTypeProperty =
+ AvaloniaProperty.RegisterAttached(
nameof(VerticalSnapPointsType));
///
@@ -625,6 +625,86 @@ namespace Avalonia.Controls
control.SetValue(HorizontalScrollBarVisibilityProperty, value);
}
+ ///
+ /// Gets the value of the HorizontalSnapPointsType attached property.
+ ///
+ /// The control to read the value from.
+ /// The value of the property.
+ public static SnapPointsType GetHorizontalSnapPointsType(Control control)
+ {
+ return control.GetValue(HorizontalSnapPointsTypeProperty);
+ }
+
+ ///
+ /// Gets the value of the HorizontalSnapPointsType attached property.
+ ///
+ /// The control to set the value on.
+ /// The value of the property.
+ public static void SetHorizontalSnapPointsType(Control control, SnapPointsType value)
+ {
+ control.SetValue(HorizontalSnapPointsTypeProperty, value);
+ }
+
+ ///
+ /// Gets the value of the VerticalSnapPointsType attached property.
+ ///
+ /// The control to read the value from.
+ /// The value of the property.
+ public static SnapPointsType GetVerticalSnapPointsType(Control control)
+ {
+ return control.GetValue(VerticalSnapPointsTypeProperty);
+ }
+
+ ///
+ /// Gets the value of the VerticalSnapPointsType attached property.
+ ///
+ /// The control to set the value on.
+ /// The value of the property.
+ public static void SetVerticalSnapPointsType(Control control, SnapPointsType value)
+ {
+ control.SetValue(VerticalSnapPointsTypeProperty, value);
+ }
+
+ ///
+ /// Gets the value of the HorizontalSnapPointsAlignment attached property.
+ ///
+ /// The control to read the value from.
+ /// The value of the property.
+ public static SnapPointsAlignment GetHorizontalSnapPointsAlignment(Control control)
+ {
+ return control.GetValue(HorizontalSnapPointsAlignmentProperty);
+ }
+
+ ///
+ /// Gets the value of the HorizontalSnapPointsAlignment attached property.
+ ///
+ /// The control to set the value on.
+ /// The value of the property.
+ public static void SetHorizontalSnapPointsAlignment(Control control, SnapPointsAlignment value)
+ {
+ control.SetValue(HorizontalSnapPointsAlignmentProperty, value);
+ }
+
+ ///
+ /// Gets the value of the VerticalSnapPointsAlignment attached property.
+ ///
+ /// The control to read the value from.
+ /// The value of the property.
+ public static SnapPointsAlignment GetVerticalSnapPointsAlignment(Control control)
+ {
+ return control.GetValue(VerticalSnapPointsAlignmentProperty);
+ }
+
+ ///
+ /// Gets the value of the VerticalSnapPointsAlignment attached property.
+ ///
+ /// The control to set the value on.
+ /// The value of the property.
+ public static void SetVerticalSnapPointsAlignment(Control control, SnapPointsAlignment value)
+ {
+ control.SetValue(VerticalSnapPointsAlignmentProperty, value);
+ }
+
///
/// Gets the value of the VerticalScrollBarVisibility attached property.
///
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 9bd1dc95f9..df9a3eb8f3 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -720,6 +720,16 @@ namespace Avalonia.Controls
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
+ if (HasComplexContent)
+ {
+ ArrangeComplexContent(TextLayout, padding);
+ }
+
+ if (MathUtilities.AreClose(_constraint.Inflate(padding).Width, finalSize.Width))
+ {
+ return finalSize;
+ }
+
_constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity);
_textLayout?.Dispose();
@@ -727,31 +737,36 @@ namespace Avalonia.Controls
if (HasComplexContent)
{
- var currentY = padding.Top;
+ ArrangeComplexContent(TextLayout, padding);
+ }
- foreach (var textLine in TextLayout.TextLines)
- {
- var currentX = padding.Left + textLine.Start;
+ return finalSize;
+ }
- foreach (var run in textLine.TextRuns)
+ private static void ArrangeComplexContent(TextLayout textLayout, Thickness padding)
+ {
+ 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 (run is DrawableTextRun drawable)
+ if (drawable is EmbeddedControlRun controlRun
+ && controlRun.Control is Control control)
{
- if (drawable is EmbeddedControlRun controlRun
- && controlRun.Control is Control control)
- {
- control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
- }
-
- currentX += drawable.Size.Width;
+ control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
}
- }
- currentY += textLine.Height;
+ currentX += drawable.Size.Width;
+ }
}
- }
- return finalSize;
+ currentY += textLine.Height;
+ }
}
protected override AutomationPeer OnCreateAutomationPeer()
@@ -892,7 +907,7 @@ namespace Avalonia.Controls
return textRun;
}
- return null;
+ return new TextEndOfParagraph();
}
}
}
diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs
index 5674874c01..9f8e3e38c0 100644
--- a/src/Avalonia.Controls/TreeViewItem.cs
+++ b/src/Avalonia.Controls/TreeViewItem.cs
@@ -257,7 +257,7 @@ namespace Avalonia.Controls
Dispatcher.UIThread.Post(this.BringIntoView); // must use the Dispatcher, otherwise the TreeView doesn't scroll
}
}
-
+
///
/// Invoked when the event occurs in the header.
///
diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs
index a20b4eee58..b1110ece55 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -450,7 +450,7 @@ namespace Avalonia.Controls
/// resulting task will produce the value when the window
/// is closed.
///
- public void Close(object dialogResult)
+ public void Close(object? dialogResult)
{
_dialogResult = dialogResult;
CloseCore(WindowCloseReason.WindowClosing, true);
diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs
index 1f290acd86..50bee0d395 100644
--- a/src/Avalonia.Native/WindowImplBase.cs
+++ b/src/Avalonia.Native/WindowImplBase.cs
@@ -501,7 +501,7 @@ namespace Avalonia.Native
}
}
- public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.Transparent;
+ public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.None;
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
{
diff --git a/src/Avalonia.OpenGL/GlEntryPointAttribute.cs b/src/Avalonia.OpenGL/GlEntryPointAttribute.cs
index 3e31de6995..386db30f92 100644
--- a/src/Avalonia.OpenGL/GlEntryPointAttribute.cs
+++ b/src/Avalonia.OpenGL/GlEntryPointAttribute.cs
@@ -3,7 +3,7 @@ using System;
namespace Avalonia.OpenGL
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
- class GlMinVersionEntryPoint : Attribute
+ sealed class GlMinVersionEntryPoint : Attribute
{
public GlMinVersionEntryPoint(string entry, int minVersionMajor, int minVersionMinor)
{
@@ -28,7 +28,7 @@ namespace Avalonia.OpenGL
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
- class GlExtensionEntryPoint : Attribute
+ sealed class GlExtensionEntryPoint : Attribute
{
public GlExtensionEntryPoint(string entry, string extension)
{
diff --git a/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs b/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs
index 98a843bad1..44605a2ffb 100644
--- a/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs
+++ b/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs
@@ -3,7 +3,7 @@
namespace Avalonia.Remote.Protocol
{
[AttributeUsage(AttributeTargets.Class)]
- public class AvaloniaRemoteMessageGuidAttribute : Attribute
+ public sealed class AvaloniaRemoteMessageGuidAttribute : Attribute
{
public Guid Guid { get; }
diff --git a/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj b/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj
index a9cad0538f..9017ce1546 100644
--- a/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj
+++ b/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
index 2dcce12df9..c3e90f5fd7 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
@@ -154,7 +154,7 @@ public static class LinuxFramebufferPlatformExtensions
var lifetime = LinuxFramebufferPlatform.Initialize(builder, outputBackend, inputBackend);
builder.SetupWithLifetime(lifetime);
lifetime.Start(args);
- builder.Instance.Run(lifetime.Token);
+ builder.Instance!.Run(lifetime.Token);
return lifetime.ExitCode;
}
}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
index d61dcd4f91..0135cb3d1f 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
@@ -43,13 +43,13 @@ namespace Avalonia.LinuxFramebuffer.Output
public IPlatformGraphics PlatformGraphics { get; private set; }
public DrmOutput(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo,
- DrmOutputOptions? options = null)
+ DrmOutputOptions options = null)
{
if(options != null)
_outputOptions = options;
Init(card, resources, connector, modeInfo);
}
- public DrmOutput(string path = null, bool connectorsForceProbe = false, DrmOutputOptions? options = null)
+ public DrmOutput(string path = null, bool connectorsForceProbe = false, DrmOutputOptions options = null)
{
if(options != null)
_outputOptions = options;
@@ -63,7 +63,7 @@ namespace Avalonia.LinuxFramebuffer.Output
if(connector == null)
throw new InvalidOperationException("Unable to find connected DRM connector");
- DrmModeInfo? mode = null;
+ DrmModeInfo mode = null;
if (options?.VideoMode != null)
{
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs
index 8d6f8cdf3a..da4d7374d4 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs
@@ -34,7 +34,8 @@ namespace Avalonia.Markup.Xaml
}
- public class ConstructorArgumentAttribute : Attribute
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class ConstructorArgumentAttribute : Attribute
{
public ConstructorArgumentAttribute(string name)
{
diff --git a/src/Shared/ModuleInitializer.cs b/src/Shared/ModuleInitializer.cs
index a72929e06f..e58b296474 100644
--- a/src/Shared/ModuleInitializer.cs
+++ b/src/Shared/ModuleInitializer.cs
@@ -1,7 +1,8 @@
namespace System.Runtime.CompilerServices
{
#if NETSTANDARD2_0
- internal class ModuleInitializerAttribute : Attribute
+ [AttributeUsage(AttributeTargets.Method)]
+ internal sealed class ModuleInitializerAttribute : Attribute
{
}
diff --git a/src/Shared/SourceGeneratorAttributes.cs b/src/Shared/SourceGeneratorAttributes.cs
index 3f00fbef57..bdd21d0426 100644
--- a/src/Shared/SourceGeneratorAttributes.cs
+++ b/src/Shared/SourceGeneratorAttributes.cs
@@ -16,7 +16,9 @@ namespace Avalonia.SourceGenerator
}
- internal class GetProcAddressAttribute : Attribute
+
+ [AttributeUsage(AttributeTargets.Method)]
+ internal sealed class GetProcAddressAttribute : Attribute
{
public GetProcAddressAttribute(string proc)
{
@@ -39,11 +41,14 @@ namespace Avalonia.SourceGenerator
}
}
- internal class GenerateEnumValueDictionaryAttribute : Attribute
+ [AttributeUsage(AttributeTargets.Method)]
+ internal sealed class GenerateEnumValueDictionaryAttribute : Attribute
{
}
- internal class GenerateEnumValueListAttribute : Attribute
+
+ [AttributeUsage(AttributeTargets.Method)]
+ internal sealed class GenerateEnumValueListAttribute : Attribute
{
}
}
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index d12db39ad6..e795f3d304 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -86,7 +86,7 @@ namespace Avalonia.Skia
SKPath path = new SKPath();
- var (currentX, currentY) = glyphRun.PlatformImpl.Item.BaselineOrigin;
+ var (currentX, currentY) = glyphRun.BaselineOrigin;
for (var i = 0; i < glyphRun.GlyphInfos.Count; i++)
{
diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
index eb3f9911df..99c01dd111 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
@@ -257,7 +257,7 @@ namespace Avalonia.Direct2D1
sink.Close();
}
- var (baselineOriginX, baselineOriginY) = glyphRun.PlatformImpl.Item.BaselineOrigin;
+ var (baselineOriginX, baselineOriginY) = glyphRun.BaselineOrigin;
var transformedGeometry = new SharpDX.Direct2D1.TransformedGeometry(
Direct2D1Factory,
diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
index b7dca78845..a24fe31df8 100644
--- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
+++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
index 0c0fe5b921..f3af312d1a 100644
--- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
+++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs
index 181883656c..690926a193 100644
--- a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs
+++ b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs
@@ -1,16 +1,79 @@
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Reflection;
+using System.Text.RegularExpressions;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.XamlIl;
-namespace Avalonia.Designer.HostApp
+namespace Avalonia.Designer.HostApp;
+
+class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader
{
- class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader
+ public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration)
+ {
+ PreloadDepsAssemblies(configuration.LocalAssembly ?? Assembly.GetEntryAssembly());
+
+ return AvaloniaXamlIlRuntimeCompiler.Load(document, configuration);
+ }
+
+ private void PreloadDepsAssemblies(Assembly targetAssembly)
{
- public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration)
+ // Assemblies loaded in memory (e.g. single file) return empty string from Location.
+ // In these cases, don't try probing next to the assembly.
+ var assemblyLocation = targetAssembly.Location;
+ if (string.IsNullOrEmpty(assemblyLocation))
+ {
+ return;
+ }
+
+ var depsJsonFile = Path.ChangeExtension(assemblyLocation, ".deps.json");
+ if (!File.Exists(depsJsonFile))
+ {
+ return;
+ }
+
+ using var stream = File.OpenRead(depsJsonFile);
+
+ /*
+ We can't use any references in the Avalonia.Designer.HostApp. Including even json.
+ Ideally we would prefer Microsoft.Extensions.DependencyModel package, but can't use it here.
+ So, instead we need to fallback to some JSON parsing using pretty easy regex.
+
+ Json part example:
+"Avalonia.Xaml.Interactions/11.0.0-preview5": {
+ "dependencies": {
+ "Avalonia": "11.0.999",
+ "Avalonia.Xaml.Interactivity": "11.0.0-preview5"
+ },
+ "runtime": {
+ "lib/net6.0/Avalonia.Xaml.Interactions.dll": {
+ "assemblyVersion": "11.0.0.0",
+ "fileVersion": "11.0.0.0"
+ }
+ }
+},
+ We want to extract "lib/net6.0/Avalonia.Xaml.Interactions.dll" from here.
+ No need to resolve real path of ref assemblies.
+ No need to handle special cases with .NET Framework and GAC.
+ */
+ var text = new StreamReader(stream).ReadToEnd();
+ var matches = Regex.Matches( text, """runtime"\s*:\s*{\s*"([^"]+)""");
+
+ foreach (Match match in matches)
{
- return AvaloniaXamlIlRuntimeCompiler.Load(document, configuration);
+ if (match.Groups[1] is { Success: true } g)
+ {
+ var assemblyName = Path.GetFileNameWithoutExtension(g.Value);
+ try
+ {
+ _ = Assembly.Load(new AssemblyName(assemblyName));
+ }
+ catch
+ {
+ }
+ }
}
}
}
diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs
index 135ab0426e..c293a9101d 100644
--- a/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs
+++ b/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs
@@ -112,7 +112,7 @@ class Template
var defs = cl.Members.OfType().First(m => m.Identifier.Text == "InitializeDefaults");
- cl = cl.ReplaceNode(defs.Body, defs.Body.AddStatements(
+ cl = cl.ReplaceNode(defs.Body!, defs.Body!.AddStatements(
ParseStatement($"_list = new ServerListProxyHelper<{itemType}, {serverItemType}>(this);")));
diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs
index 3b5d3d8c3f..dfc8b45579 100644
--- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs
+++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs
@@ -297,8 +297,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
server = server.WithBaseList(
server.BaseList?.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName))));
- client = client.AddMembers(
- ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;"));
+ if(ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;") is { } member)
+ client = client.AddMembers(member);
}
diff --git a/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs b/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs
index 86dbb3a452..c975bb8444 100644
--- a/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs
+++ b/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs
@@ -32,7 +32,7 @@ public class EnumMemberDictionaryGenerator : IIncrementalGenerator
).Collect();
context.RegisterSourceOutput(all, static (context, methods) =>
{
- foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default))
+ foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default))
{
var classBuilder = new StringBuilder();
if (typeGroup.Key.ContainingNamespace != null)
diff --git a/src/tools/DevGenerators/GetProcAddressInitialization.cs b/src/tools/DevGenerators/GetProcAddressInitialization.cs
index aedc13e7f6..e8d7c251fa 100644
--- a/src/tools/DevGenerators/GetProcAddressInitialization.cs
+++ b/src/tools/DevGenerators/GetProcAddressInitialization.cs
@@ -34,7 +34,7 @@ public class GetProcAddressInitializationGenerator : IIncrementalGenerator
var all = fieldsWithAttribute.Collect();
context.RegisterSourceOutput(all, static (context, methods) =>
{
- foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default))
+ foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default))
{
var nextContext = 0;
var contexts = new Dictionary();
diff --git a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj
index 57338a1e08..3ff91139f1 100644
--- a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj
+++ b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj
@@ -16,4 +16,5 @@
+
diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
index 4d833cdb1f..7bb991aae6 100644
--- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
+++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
@@ -1,11 +1,14 @@
using System;
+using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Controls;
+using Avalonia.Media.Imaging;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Interactions;
+using SixLabors.ImageSharp.PixelFormats;
using Xunit;
using Xunit.Sdk;
@@ -141,7 +144,6 @@ namespace Avalonia.IntegrationTests.Appium
}
}
-
[Theory]
[InlineData(ShowWindowMode.NonOwned)]
[InlineData(ShowWindowMode.Owned)]
@@ -187,6 +189,47 @@ namespace Avalonia.IntegrationTests.Appium
}
}
+ [Fact]
+ public void TransparentWindow()
+ {
+ var showTransparentWindow = _session.FindElementByAccessibilityId("ShowTransparentWindow");
+ showTransparentWindow.Click();
+ Thread.Sleep(1000);
+
+ var window = _session.FindElementByAccessibilityId("TransparentWindow");
+ var screenshot = window.GetScreenshot();
+
+ window.Click();
+
+ var img = SixLabors.ImageSharp.Image.Load(screenshot.AsByteArray);
+ var topLeftColor = img[10, 10];
+ var centerColor = img[img.Width / 2, img.Height / 2];
+
+ Assert.Equal(new Rgba32(0, 128, 0), topLeftColor);
+ Assert.Equal(new Rgba32(255, 0, 0), centerColor);
+ }
+
+ [Fact]
+ public void TransparentPopup()
+ {
+ var showTransparentWindow = _session.FindElementByAccessibilityId("ShowTransparentPopup");
+ showTransparentWindow.Click();
+ Thread.Sleep(1000);
+
+ var window = _session.FindElementByAccessibilityId("TransparentPopupBackground");
+ var container = window.FindElementByAccessibilityId("PopupContainer");
+ var screenshot = container.GetScreenshot();
+
+ window.Click();
+
+ var img = SixLabors.ImageSharp.Image.Load(screenshot.AsByteArray);
+ var topLeftColor = img[10, 10];
+ var centerColor = img[img.Width / 2, img.Height / 2];
+
+ Assert.Equal(new Rgba32(0, 128, 0), topLeftColor);
+ Assert.Equal(new Rgba32(255, 0, 0), centerColor);
+ }
+
public static TheoryData StartupLocationData()
{
var sizes = new Size?[] { null, new Size(400, 300) };
diff --git a/tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf b/tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf
new file mode 100644
index 0000000000..703cfa472d
Binary files /dev/null and b/tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf differ
diff --git a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs
index c11bd2b816..4210ee8238 100644
--- a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs
+++ b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs
@@ -1,3 +1,4 @@
+using System.Net;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Layout;
@@ -17,6 +18,56 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
{
}
+ [Win32Fact("Has text")]
+ public async Task Should_Draw_TextDecorations()
+ {
+ Border target = new Border
+ {
+ Padding = new Thickness(8),
+ Width = 200,
+ Height = 30,
+ Background = Brushes.White,
+ Child = new TextBlock
+ {
+ FontFamily = TestFontFamily,
+ FontSize = 12,
+ Foreground = Brushes.Black,
+ Text = "Neque porro quisquam est qui dolorem",
+ VerticalAlignment = VerticalAlignment.Top,
+ TextWrapping = TextWrapping.NoWrap,
+ TextDecorations = new TextDecorationCollection
+ {
+ new TextDecoration
+ {
+ Location = TextDecorationLocation.Overline,
+ StrokeThickness= 1.5,
+ StrokeThicknessUnit = TextDecorationUnit.Pixel,
+ Stroke = new SolidColorBrush(Colors.Red)
+ },
+ new TextDecoration
+ {
+ Location = TextDecorationLocation.Baseline,
+ StrokeThickness= 1.5,
+ StrokeThicknessUnit = TextDecorationUnit.Pixel,
+ Stroke = new SolidColorBrush(Colors.Green)
+ },
+ new TextDecoration
+ {
+ Location = TextDecorationLocation.Underline,
+ StrokeThickness= 1.5,
+ StrokeThicknessUnit = TextDecorationUnit.Pixel,
+ Stroke = new SolidColorBrush(Colors.Blue),
+ StrokeOffset = 2,
+ StrokeOffsetUnit = TextDecorationUnit.Pixel
+ }
+ }
+ }
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
+
[Win32Fact("Has text")]
public async Task Wrapping_NoWrap()
{
diff --git a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj
index ba45bbbc2e..0d182678ef 100644
--- a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj
+++ b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
index ea91b8c196..86a680fac5 100644
--- a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
+++ b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
index a748f6cf00..5a6d7f2cdf 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
@@ -17,6 +17,8 @@ namespace Avalonia.Skia.UnitTests.Media
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
private readonly Typeface _arabicTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Arabic");
+ private readonly Typeface _hebrewTypeface =
+ new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Hebrew");
private readonly Typeface _italicTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans", FontStyle.Italic);
private readonly Typeface _emojiTypeface =
@@ -24,7 +26,7 @@ namespace Avalonia.Skia.UnitTests.Media
public CustomFontManagerImpl()
{
- _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _arabicTypeface, _defaultTypeface };
+ _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _arabicTypeface, _hebrewTypeface, _defaultTypeface };
_defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName;
}
@@ -88,6 +90,12 @@ namespace Avalonia.Skia.UnitTests.Media
skTypeface = typefaceCollection.Get(typeface);
break;
}
+ case "Noto Sans Hebrew":
+ {
+ var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_hebrewTypeface.FontFamily);
+ skTypeface = typefaceCollection.Get(typeface);
+ break;
+ }
case FontFamily.DefaultFontFamilyName:
case "Noto Mono":
{
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
index 954169f975..8a2d4ecc6b 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
@@ -660,6 +660,90 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
+ [Fact]
+ public void Should_Return_Null_For_Empty_TextSource()
+ {
+ using (Start())
+ {
+ var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
+ var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
+ var textSource = new EmptyTextSource();
+
+ var textLine = TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
+
+ Assert.Null(textLine);
+ }
+ }
+
+ [Fact]
+ public void Should_Retain_TextEndOfParagraph_With_TextWrapping()
+ {
+ using (Start())
+ {
+ var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
+ var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrap: TextWrapping.Wrap);
+
+ var text = "Hello World";
+
+ var textSource = new SimpleTextSource(text, defaultRunProperties);
+
+ var pos = 0;
+
+ TextLineBreak previousLineBreak = null;
+ TextLine textLine = null;
+
+ while (pos < text.Length)
+ {
+ textLine = TextFormatter.Current.FormatLine(textSource, pos, 30, paragraphProperties, previousLineBreak);
+
+ pos += textLine.Length;
+
+ previousLineBreak = textLine.TextLineBreak;
+ }
+
+ Assert.NotNull(textLine);
+
+ Assert.NotNull(textLine.TextLineBreak.TextEndOfLine);
+ }
+ }
+
+ protected readonly record struct SimpleTextSource : ITextSource
+ {
+ private readonly string _text;
+ private readonly TextRunProperties _defaultProperties;
+
+ public SimpleTextSource(string text, TextRunProperties defaultProperties)
+ {
+ _text = text;
+ _defaultProperties = defaultProperties;
+ }
+
+ public TextRun? GetTextRun(int textSourceIndex)
+ {
+ if (textSourceIndex > _text.Length)
+ {
+ return new TextEndOfParagraph();
+ }
+
+ var runText = _text.AsMemory(textSourceIndex);
+
+ if (runText.IsEmpty)
+ {
+ return new TextEndOfParagraph();
+ }
+
+ return new TextCharacters(runText, _defaultProperties);
+ }
+ }
+
+ private class EmptyTextSource : ITextSource
+ {
+ public TextRun GetTextRun(int textSourceIndex)
+ {
+ return null;
+ }
+ }
+
private class EndOfLineTextSource : ITextSource
{
public TextRun GetTextRun(int textSourceIndex)
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
index 2b63f24cf6..9a7460c218 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
@@ -9,7 +9,6 @@ using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.UnitTests;
using Avalonia.Utilities;
using Xunit;
-
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
public class TextLayoutTests
@@ -725,7 +724,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var selectedRect = rects[0];
- Assert.Equal(selectedText.Bounds.Width, selectedRect.Width);
+ Assert.Equal(selectedText.Bounds.Width, selectedRect.Width, 2);
}
}
@@ -886,7 +885,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var distance = hitRange.First().Left;
- Assert.Equal(currentX, distance);
+ Assert.Equal(currentX, distance, 2);
currentX += advance;
}
@@ -916,7 +915,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var distance = hitRange.First().Left + 0.5;
- Assert.Equal(currentX, distance);
+ Assert.Equal(currentX, distance, 2);
currentX += advance;
}
@@ -1028,6 +1027,65 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
+ [InlineData("mgfg🧐df f sdf", "g🧐d", 20, 40)]
+ [InlineData("وه. وقد تعرض لانتقادات", "دات", 5, 30)]
+ [InlineData("وه. وقد تعرض لانتقادات", "تعرض", 20, 50)]
+ [InlineData(" علمية 😱ومضللة ،", " علمية 😱ومضللة ،", 40, 100)]
+ [InlineData("في عام 2018 ، رفعت ل", "في عام 2018 ، رفعت ل", 100, 120)]
+ [Theory]
+ public void HitTestTextRange_Range_ValidLength(string text, string textToSelect, double minWidth, double maxWidth)
+ {
+ using (Start())
+ {
+ var layout = new TextLayout(text, Typeface.Default, 12, Brushes.Black);
+ var start = text.IndexOf(textToSelect);
+ var selectionRectangles = layout.HitTestTextRange(start, textToSelect.Length);
+ Assert.Equal(1, selectionRectangles.Count());
+ var rect = selectionRectangles.First();
+ Assert.InRange(rect.Width, minWidth, maxWidth);
+ }
+ }
+
+ [InlineData("012🧐210", 2, 4, FlowDirection.LeftToRight, "14.40234375,40.8046875")]
+ [InlineData("210🧐012", 2, 4, FlowDirection.RightToLeft, "0,7.201171875;21.603515625,33.603515625;48.005859375,55.20703125")]
+ [InlineData("שנב🧐שנב", 2, 4, FlowDirection.LeftToRight, "11.268,38.208")]
+ [InlineData("שנב🧐שנב", 2, 4, FlowDirection.RightToLeft, "11.268,38.208")]
+ [Theory]
+ public void Should_HitTextTextRangeBetweenRuns(string text, int start, int length,
+ FlowDirection flowDirection, string expected)
+ {
+ using (Start())
+ {
+ var expectedRects = expected.Split(';').Select(x =>
+ {
+ var startEnd = x.Split(',');
+
+ var start = double.Parse(startEnd[0], CultureInfo.InvariantCulture);
+
+ var end = double.Parse(startEnd[1], CultureInfo.InvariantCulture);
+
+ return new Rect(start, 0, end - start, 0);
+ }).ToArray();
+
+ var textLayout = new TextLayout(text, Typeface.Default, 12, Brushes.Black, flowDirection: flowDirection);
+
+ var rects = textLayout.HitTestTextRange(start, length).ToArray();
+
+ Assert.Equal(expectedRects.Length, rects.Length);
+
+ var endX = textLayout.TextLines[0].GetDistanceFromCharacterHit(new CharacterHit(2));
+ var startX = textLayout.TextLines[0].GetDistanceFromCharacterHit(new CharacterHit(5, 1));
+
+ for (int i = 0; i < expectedRects.Length; i++)
+ {
+ var expectedRect = expectedRects[i];
+
+ Assert.Equal(expectedRect.Left, rects[i].Left, 2);
+
+ Assert.Equal(expectedRect.Right, rects[i].Right, 2);
+ }
+ }
+ }
private static IDisposable Start()
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index 544b84912e..70e74cdf83 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
@@ -604,19 +604,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textBounds = textLine.GetTextBounds(0, 20);
- Assert.Equal(2, textBounds.Count);
+ Assert.Equal(1, textBounds.Count);
Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 30);
- Assert.Equal(3, textBounds.Count);
+ Assert.Equal(1, textBounds.Count);
Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 40);
- Assert.Equal(4, textBounds.Count);
+ Assert.Equal(1, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
}
@@ -658,7 +658,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(TextTestHelper.GetStartCharIndex(run.Text), bounds.TextSourceCharacterIndex);
Assert.Equal(run, bounds.TextRun);
- Assert.Equal(run.Size.Width, bounds.Rectangle.Width);
+ Assert.Equal(run.Size.Width, bounds.Rectangle.Width, 2);
}
for (var i = 0; i < textBounds.Count; i++)
@@ -667,19 +667,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
if (lastBounds != null)
{
- Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left);
+ Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left, 2);
}
var sumOfRunWidth = currentBounds.TextRunBounds.Sum(x => x.Rectangle.Width);
- Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width);
+ Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width, 2);
lastBounds = currentBounds;
}
var sumOfBoundsWidth = textBounds.Sum(x => x.Rectangle.Width);
- Assert.Equal(lineWidth, sumOfBoundsWidth);
+ Assert.Equal(lineWidth, sumOfBoundsWidth, 2);
}
}
@@ -847,7 +847,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textBounds = textLine.GetTextBounds(0, textLine.Length);
- Assert.Equal(6, textBounds.Count);
+ Assert.Equal(1, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 1);
@@ -857,7 +857,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textBounds = textLine.GetTextBounds(0, firstRun.Length + 1);
- Assert.Equal(2, textBounds.Count);
+ Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(1, firstRun.Length);
@@ -867,7 +867,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textBounds = textLine.GetTextBounds(0, 1 + firstRun.Length);
- Assert.Equal(2, textBounds.Count);
+ Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
}
}
@@ -958,14 +958,15 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width);
Assert.Equal(7.201171875, textBounds[0].Rectangle.Width);
- Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right);
- Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left);
+
+ Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right, 2);
+ Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left, 2);
textBounds = textLine.GetTextBounds(0, text.Length);
Assert.Equal(2, textBounds.Count);
Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length)));
- Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
+ Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width), 2);
}
}
diff --git a/tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png b/tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png
new file mode 100644
index 0000000000..494c8a9002
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png differ
diff --git a/tests/TestFiles/Skia/Controls/TextBlock/Should_Draw_TextDecorations.expected.png b/tests/TestFiles/Skia/Controls/TextBlock/Should_Draw_TextDecorations.expected.png
new file mode 100644
index 0000000000..297bd592ff
Binary files /dev/null and b/tests/TestFiles/Skia/Controls/TextBlock/Should_Draw_TextDecorations.expected.png differ