Browse Source

Merge branch 'master' into feat/SelectedValue_SelectedValueBinding

pull/10180/head
amwx 3 years ago
committed by GitHub
parent
commit
a4752ef656
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 25
      .editorconfig
  2. 6
      build/HarfBuzzSharp.props
  3. 1
      build/SharedVersion.props
  4. 6
      build/SkiaSharp.props
  5. 4
      samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj
  6. 1
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  7. 4
      samples/ControlCatalog/MainView.xaml
  8. 54
      samples/IntegrationTestApp/MainWindow.axaml
  9. 91
      samples/IntegrationTestApp/MainWindow.axaml.cs
  10. 1
      samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj
  11. 12
      samples/SampleControls/HamburgerMenu/HamburgerMenu.cs
  12. 39
      src/Avalonia.Base/Animation/Animatable.cs
  13. 2
      src/Avalonia.Base/Data/InstancedBinding.cs
  14. 32
      src/Avalonia.Base/Media/FormattedText.cs
  15. 2
      src/Avalonia.Base/Media/GlyphRun.cs
  16. 4
      src/Avalonia.Base/Media/TextFormatting/ITextSource.cs
  17. 36
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  18. 2
      src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs
  19. 18
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  20. 37
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  21. 684
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  22. 4
      src/Avalonia.Base/Metadata/AmbientAttribute.cs
  23. 2
      src/Avalonia.Base/Metadata/ContentAttribute.cs
  24. 4
      src/Avalonia.Base/Metadata/DataTypeAttribute.cs
  25. 2
      src/Avalonia.Base/Metadata/DependsOnAttribute.cs
  26. 4
      src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
  27. 2
      src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs
  28. 2
      src/Avalonia.Base/Metadata/TemplateContent.cs
  29. 2
      src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs
  30. 3
      src/Avalonia.Base/Metadata/UnstableAttribute.cs
  31. 4
      src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs
  32. 2
      src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs
  33. 2
      src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs
  34. 3
      src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs
  35. 2
      src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs
  36. 5
      src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs
  37. 2
      src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs
  38. 12
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  39. 3
      src/Avalonia.Controls/ResolveByNameAttribute.cs
  40. 88
      src/Avalonia.Controls/ScrollViewer.cs
  41. 51
      src/Avalonia.Controls/TextBlock.cs
  42. 2
      src/Avalonia.Controls/TreeViewItem.cs
  43. 2
      src/Avalonia.Controls/Window.cs
  44. 2
      src/Avalonia.Native/WindowImplBase.cs
  45. 4
      src/Avalonia.OpenGL/GlEntryPointAttribute.cs
  46. 2
      src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs
  47. 2
      src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj
  48. 2
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  49. 6
      src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
  50. 3
      src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs
  51. 3
      src/Shared/ModuleInitializer.cs
  52. 11
      src/Shared/SourceGeneratorAttributes.cs
  53. 2
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  54. 2
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  55. 2
      src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
  56. 2
      src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
  57. 71
      src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs
  58. 2
      src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs
  59. 4
      src/tools/DevGenerators/CompositionGenerator/Generator.cs
  60. 2
      src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs
  61. 2
      src/tools/DevGenerators/GetProcAddressInitialization.cs
  62. 1
      tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj
  63. 45
      tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
  64. BIN
      tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf
  65. 51
      tests/Avalonia.RenderTests/Controls/TextBlockTests.cs
  66. 2
      tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj
  67. 2
      tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
  68. 10
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  69. 84
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  70. 66
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  71. 27
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  72. BIN
      tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png
  73. BIN
      tests/TestFiles/Skia/Controls/TextBlock/Should_Draw_TextDecorations.expected.png

25
.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

6
build/HarfBuzzSharp.props

@ -1,7 +1,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="HarfBuzzSharp" Version="2.8.2.1-preview.108" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.1-preview.108" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="2.8.2.1-preview.108" />
<PackageReference Include="HarfBuzzSharp" Version="2.8.2.3" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.3" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="2.8.2.3" />
</ItemGroup>
</Project>

1
build/SharedVersion.props

@ -3,6 +3,7 @@
<PropertyGroup>
<Product>Avalonia</Product>
<Version>11.0.999</Version>
<Authors>Avalonia Team</Authors>
<Copyright>Copyright 2022 &#169; The AvaloniaUI Project</Copyright>
<PackageProjectUrl>https://avaloniaui.net</PackageProjectUrl>
<RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>

6
build/SkiaSharp.props

@ -1,7 +1,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.88.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.1" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.3" />
</ItemGroup>
</Project>

4
samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj

@ -9,8 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.0-rc.1.22427.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.0-rc.1.22427.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.2" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>

1
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -31,7 +31,6 @@
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
<!-- For native controls test -->
<PackageReference Include="MonoMac.NetStandard" Version="0.0.4" />
</ItemGroup>

4
samples/ControlCatalog/MainView.xaml

@ -14,8 +14,8 @@
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
</Grid.Styles>
<controls:HamburgerMenu Name="Sidebar">
<TabItem Header="Composition">
<controls:HamburgerMenu Name="Sidebar">
<TabItem Header="Composition">
<pages:CompositionPage/>
</TabItem>
<TabItem Header="Acrylic">

54
samples/IntegrationTestApp/MainWindow.axaml

@ -120,30 +120,36 @@
</TabItem>
<TabItem Header="Window">
<StackPanel>
<TextBox Name="ShowWindowSize" Watermark="Window Size"/>
<ComboBox Name="ShowWindowMode" SelectedIndex="0">
<ComboBoxItem>NonOwned</ComboBoxItem>
<ComboBoxItem>Owned</ComboBoxItem>
<ComboBoxItem>Modal</ComboBoxItem>
</ComboBox>
<ComboBox Name="ShowWindowLocation" SelectedIndex="0">
<ComboBoxItem>Manual</ComboBoxItem>
<ComboBoxItem>CenterScreen</ComboBoxItem>
<ComboBoxItem>CenterOwner</ComboBoxItem>
</ComboBox>
<ComboBox Name="ShowWindowState" SelectedIndex="0">
<ComboBoxItem Name="ShowWindowStateNormal">Normal</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateMinimized">Minimized</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<Button Name="ShowWindow">Show Window</Button>
<Button Name="SendToBack">Send to Back</Button>
<Button Name="EnterFullscreen">Enter Fullscreen</Button>
<Button Name="ExitFullscreen">Exit Fullscreen</Button>
<Button Name="RestoreAll">Restore All</Button>
</StackPanel>
<Grid ColumnDefinitions="*,8,*">
<StackPanel Grid.Column="0">
<TextBox Name="ShowWindowSize" Watermark="Window Size"/>
<ComboBox Name="ShowWindowMode" SelectedIndex="0">
<ComboBoxItem>NonOwned</ComboBoxItem>
<ComboBoxItem>Owned</ComboBoxItem>
<ComboBoxItem>Modal</ComboBoxItem>
</ComboBox>
<ComboBox Name="ShowWindowLocation" SelectedIndex="0">
<ComboBoxItem>Manual</ComboBoxItem>
<ComboBoxItem>CenterScreen</ComboBoxItem>
<ComboBoxItem>CenterOwner</ComboBoxItem>
</ComboBox>
<ComboBox Name="ShowWindowState" SelectedIndex="0">
<ComboBoxItem Name="ShowWindowStateNormal">Normal</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateMinimized">Minimized</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<Button Name="ShowWindow">Show Window</Button>
<Button Name="SendToBack">Send to Back</Button>
<Button Name="EnterFullscreen">Enter Fullscreen</Button>
<Button Name="ExitFullscreen">Exit Fullscreen</Button>
<Button Name="RestoreAll">Restore All</Button>
</StackPanel>
<StackPanel Grid.Column="2">
<Button Name="ShowTransparentWindow">Transparent Window</Button>
<Button Name="ShowTransparentPopup">Transparent Popup</Button>
</StackPanel>
</Grid>
</TabItem>
</TabControl>
</DockPanel>

91
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<ListBox>("BasicListBox").SelectedIndex = -1;
if (source?.Name == "MenuClickedMenuItemReset")
this.Get<TextBlock>("ClickedMenuItem").Text = "None";
if (source?.Name == "ShowTransparentWindow")
ShowTransparentWindow();
if (source?.Name == "ShowTransparentPopup")
ShowTransparentPopup();
if (source?.Name == "ShowWindow")
ShowWindow();
if (source?.Name == "SendToBack")

1
samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj

@ -24,7 +24,6 @@
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\MobileSandbox\MobileSandbox.csproj" />
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
<!-- For native controls test -->
<PackageReference Include="MonoMac.NetStandard" Version="0.0.4" />
</ItemGroup>

12
samples/SampleControls/HamburgerMenu/HamburgerMenu.cs

@ -52,6 +52,14 @@ namespace ControlSamples
var (oldBounds, newBounds) = change.GetOldAndNewValue<Rect>();
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;

39
src/Avalonia.Base/Animation/Animatable.cs

@ -27,7 +27,11 @@ namespace Avalonia.Animation
AvaloniaProperty.Register<Animatable, Transitions?>(nameof(Transitions));
private bool _transitionsEnabled = true;
private bool _isSubscribedToTransitionsCollection = false;
private Dictionary<ITransition, TransitionState>? _transitionState;
private NotifyCollectionChangedEventHandler? _collectionChanged;
private NotifyCollectionChangedEventHandler TransitionsCollectionChangedHandler =>
_collectionChanged ??= TransitionsCollectionChanged;
/// <summary>
/// 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
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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<IGlobalClock>();
state.Instance?.Dispose();
state.Instance = transition.Apply(
this,
Clock ?? AvaloniaLocator.Current.GetRequiredService<IGlobalClock>(),
clock,
oldValue,
newValue);
return;

2
src/Avalonia.Base/Data/InstancedBinding.cs

@ -23,7 +23,7 @@ namespace Avalonia.Data
/// <param name="priority">The priority of the binding.</param>
/// <remarks>
/// This constructor can be used to create any type of binding and as such requires an
/// <see cref="ISubject{Object}"/> as the binding source because this is the only binding
/// <see cref="IObservable{Object}"/> 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.
/// </remarks>

32
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
/// <summary>
/// Wrapper of TextFormatter.FormatLine that auto-collapses the line if needed.
/// </summary>
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
}
/// <inheritdoc/>
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);

2
src/Avalonia.Base/Media/GlyphRun.cs

@ -166,7 +166,7 @@ namespace Avalonia.Media
/// </summary>
public Point BaselineOrigin
{
get => _baselineOrigin ?? default;
get => PlatformImpl.Item.BaselineOrigin;
set => Set(ref _baselineOrigin, value);
}

4
src/Avalonia.Base/Media/TextFormatting/ITextSource.cs

@ -1,6 +1,4 @@
using Avalonia.Metadata;
namespace Avalonia.Media.TextFormatting
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Produces <see cref="TextRun"/> objects that are used by the <see cref="TextFormatter"/>.

36
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
/// <param name="glyphTypeface">The typeface that is used to find matching characters.</param>
/// <param name="defaultGlyphTypeface">The default typeface.</param>
/// <param name="length">The shapeable length.</param>
/// <param name="script"></param>
/// <returns></returns>
internal static bool TryGetShapeableLength(
ReadOnlySpan<char> 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)
{

2
src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs

@ -38,7 +38,7 @@
/// <param name="previousLineBreak">A <see cref="TextLineBreak"/> value that specifies the text formatter state,
/// in terms of where the previous line in the paragraph was broken by the text formatting process.</param>
/// <returns>The formatted line.</returns>
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);
}
}

18
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -18,7 +18,7 @@ namespace Avalonia.Media.TextFormatting
[ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm;
/// <inheritdoc cref="TextFormatter.FormatLine"/>
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)
{

37
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
/// <returns></returns>
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);

684
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; }
/// <inheritdoc/>
public override TextLineBreak? TextLineBreak { get; }
public override TextLineBreak? TextLineBreak => _textLineBreak;
/// <inheritdoc/>
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<TextBounds> GetTextBoundsLeftToRight(int firstTextSourceIndex, int textLength)
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
{
var characterIndex = firstTextSourceIndex + textLength;
if (_textRuns.Length == 0)
{
return Array.Empty<TextBounds>();
}
var result = new List<TextBounds>(_textRuns.Length);
var lastDirection = FlowDirection.LeftToRight;
var currentDirection = lastDirection;
var result = new List<TextBounds>();
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<TextRunBounds> { 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<TextRunBounds>();
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<TextRunBounds> { 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<TextBounds> GetTextBoundsRightToLeft(int firstTextSourceIndex, int textLength)
{
var characterIndex = firstTextSourceIndex + textLength;
var runWidth = endX - startX;
var result = new List<TextBounds>(_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<TextRunBounds>();
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<TextRunBounds> { 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<TextBounds> 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)

4
src/Avalonia.Base/Metadata/AmbientAttribute.cs

@ -3,10 +3,10 @@ using System;
namespace Avalonia.Metadata
{
/// <summary>
/// Defines the ambient class/property
/// Defines the ambient class/property
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, Inherited = true)]
public class AmbientAttribute : Attribute
public sealed class AmbientAttribute : Attribute
{
}
}

2
src/Avalonia.Base/Metadata/ContentAttribute.cs

@ -6,7 +6,7 @@ namespace Avalonia.Metadata
/// Defines the property that contains the object's content in markup.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ContentAttribute : Attribute
public sealed class ContentAttribute : Attribute
{
}
}

4
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.
/// </remarks>
[AttributeUsage(AttributeTargets.Property)]
public class DataTypeAttribute : Attribute
public sealed class DataTypeAttribute : Attribute
{
}

2
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.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class DependsOnAttribute : Attribute
public sealed class DependsOnAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="DependsOnAttribute"/> class.

4
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.
/// </summary>
public string AncestorItemsProperty { get; }
/// <summary>
/// The ancestor type to be used in a lookup for the <see cref="AncestorProperty"/>.
/// The ancestor type to be used in a lookup for the <see cref="AncestorItemsProperty"/>.
/// If null, the declaring type of the target property is used.
/// </summary>
public Type? AncestorType { get; set; }

2
src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs

@ -11,7 +11,7 @@ namespace Avalonia.Metadata
/// may be added to its API.
/// </remarks>
[AttributeUsage(AttributeTargets.Interface)]
public class NotClientImplementableAttribute : Attribute
public sealed class NotClientImplementableAttribute : Attribute
{
}
}

2
src/Avalonia.Base/Metadata/TemplateContent.cs

@ -6,7 +6,7 @@ namespace Avalonia.Metadata
/// Defines the property that contains the object's content in markup.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class TemplateContentAttribute : Attribute
public sealed class TemplateContentAttribute : Attribute
{
public Type? TemplateResultType { get; set; }
}

2
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
{
}

3
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.
/// </summary>
public class UnstableAttribute : Attribute
[AttributeUsage(AttributeTargets.All)]
public sealed class UnstableAttribute : Attribute
{
}
}

4
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
{
}
}

2
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.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class WhitespaceSignificantCollectionAttribute : Attribute
public sealed class WhitespaceSignificantCollectionAttribute : Attribute
{
}
}

2
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.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public class XmlnsDefinitionAttribute : Attribute
public sealed class XmlnsDefinitionAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="XmlnsDefinitionAttribute"/> class.

3
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; }

2
src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs

@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="p">The point in global coordinates.</param>
/// <returns>True if the point hits the node's geometry; otherwise false.</returns>
/// <remarks>
/// This method does not recurse to child <see cref="IVisualNode"/>s, if you want
/// This method does not recurse to childs, if you want
/// to hit test children they must be hit tested manually.
/// </remarks>
bool HitTest(Point p);

5
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);

2
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.
/// </remarks>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public class ExportAvaloniaModuleAttribute : Attribute
public sealed class ExportAvaloniaModuleAttribute : Attribute
{
public ExportAvaloniaModuleAttribute(string name, Type moduleType)
{

12
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@ -28,19 +28,19 @@ namespace Avalonia.Controls.Presenters
/// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
/// </summary>
public static readonly StyledProperty<bool> AreHorizontalSnapPointsRegularProperty =
AvaloniaProperty.Register<ItemsControl, bool>(nameof(AreHorizontalSnapPointsRegular));
AvaloniaProperty.Register<ItemsPresenter, bool>(nameof(AreHorizontalSnapPointsRegular));
/// <summary>
/// Defines the <see cref="AreVerticalSnapPointsRegular"/> property.
/// </summary>
public static readonly StyledProperty<bool> AreVerticalSnapPointsRegularProperty =
AvaloniaProperty.Register<ItemsControl, bool>(nameof(AreVerticalSnapPointsRegular));
AvaloniaProperty.Register<ItemsPresenter, bool>(nameof(AreVerticalSnapPointsRegular));
/// <summary>
/// Defines the <see cref="HorizontalSnapPointsChanged"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> HorizontalSnapPointsChangedEvent =
RoutedEvent.Register<StackPanel, RoutedEventArgs>(
RoutedEvent.Register<ItemsPresenter, RoutedEventArgs>(
nameof(HorizontalSnapPointsChanged),
RoutingStrategies.Bubble);
@ -48,7 +48,7 @@ namespace Avalonia.Controls.Presenters
/// Defines the <see cref="VerticalSnapPointsChanged"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> VerticalSnapPointsChangedEvent =
RoutedEvent.Register<StackPanel, RoutedEventArgs>(
RoutedEvent.Register<ItemsPresenter, RoutedEventArgs>(
nameof(VerticalSnapPointsChanged),
RoutingStrategies.Bubble);
@ -139,7 +139,7 @@ namespace Avalonia.Controls.Presenters
Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default;
/// <summary>
/// Gets or sets whether the horizontal snap points for the <see cref="ItemsControl"/> are equidistant from each other.
/// Gets or sets whether the horizontal snap points for the <see cref="ItemsPresenter"/> are equidistant from each other.
/// </summary>
public bool AreHorizontalSnapPointsRegular
{
@ -148,7 +148,7 @@ namespace Avalonia.Controls.Presenters
}
/// <summary>
/// Gets or sets whether the vertical snap points for the <see cref="ItemsControl"/> are equidistant from each other.
/// Gets or sets whether the vertical snap points for the <see cref="ItemsPresenter"/> are equidistant from each other.
/// </summary>
public bool AreVerticalSnapPointsRegular
{

3
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.
/// </summary>
public class ResolveByNameAttribute : Attribute
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)]
public sealed class ResolveByNameAttribute : Attribute
{
}
}

88
src/Avalonia.Controls/ScrollViewer.cs

@ -154,15 +154,15 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="HorizontalSnapPointsType"/> property.
/// </summary>
public static readonly StyledProperty<SnapPointsType> HorizontalSnapPointsTypeProperty =
AvaloniaProperty.Register<ScrollViewer, SnapPointsType>(
public static readonly AttachedProperty<SnapPointsType> HorizontalSnapPointsTypeProperty =
AvaloniaProperty.RegisterAttached<ScrollViewer, Control, SnapPointsType>(
nameof(HorizontalSnapPointsType));
/// <summary>
/// Defines the <see cref="VerticalSnapPointsType"/> property.
/// </summary>
public static readonly StyledProperty<SnapPointsType> VerticalSnapPointsTypeProperty =
AvaloniaProperty.Register<ScrollViewer, SnapPointsType>(
public static readonly AttachedProperty<SnapPointsType> VerticalSnapPointsTypeProperty =
AvaloniaProperty.RegisterAttached<ScrollViewer, Control, SnapPointsType>(
nameof(VerticalSnapPointsType));
/// <summary>
@ -625,6 +625,86 @@ namespace Avalonia.Controls
control.SetValue(HorizontalScrollBarVisibilityProperty, value);
}
/// <summary>
/// Gets the value of the HorizontalSnapPointsType attached property.
/// </summary>
/// <param name="control">The control to read the value from.</param>
/// <returns>The value of the property.</returns>
public static SnapPointsType GetHorizontalSnapPointsType(Control control)
{
return control.GetValue(HorizontalSnapPointsTypeProperty);
}
/// <summary>
/// Gets the value of the HorizontalSnapPointsType attached property.
/// </summary>
/// <param name="control">The control to set the value on.</param>
/// <param name="value">The value of the property.</param>
public static void SetHorizontalSnapPointsType(Control control, SnapPointsType value)
{
control.SetValue(HorizontalSnapPointsTypeProperty, value);
}
/// <summary>
/// Gets the value of the VerticalSnapPointsType attached property.
/// </summary>
/// <param name="control">The control to read the value from.</param>
/// <returns>The value of the property.</returns>
public static SnapPointsType GetVerticalSnapPointsType(Control control)
{
return control.GetValue(VerticalSnapPointsTypeProperty);
}
/// <summary>
/// Gets the value of the VerticalSnapPointsType attached property.
/// </summary>
/// <param name="control">The control to set the value on.</param>
/// <param name="value">The value of the property.</param>
public static void SetVerticalSnapPointsType(Control control, SnapPointsType value)
{
control.SetValue(VerticalSnapPointsTypeProperty, value);
}
/// <summary>
/// Gets the value of the HorizontalSnapPointsAlignment attached property.
/// </summary>
/// <param name="control">The control to read the value from.</param>
/// <returns>The value of the property.</returns>
public static SnapPointsAlignment GetHorizontalSnapPointsAlignment(Control control)
{
return control.GetValue(HorizontalSnapPointsAlignmentProperty);
}
/// <summary>
/// Gets the value of the HorizontalSnapPointsAlignment attached property.
/// </summary>
/// <param name="control">The control to set the value on.</param>
/// <param name="value">The value of the property.</param>
public static void SetHorizontalSnapPointsAlignment(Control control, SnapPointsAlignment value)
{
control.SetValue(HorizontalSnapPointsAlignmentProperty, value);
}
/// <summary>
/// Gets the value of the VerticalSnapPointsAlignment attached property.
/// </summary>
/// <param name="control">The control to read the value from.</param>
/// <returns>The value of the property.</returns>
public static SnapPointsAlignment GetVerticalSnapPointsAlignment(Control control)
{
return control.GetValue(VerticalSnapPointsAlignmentProperty);
}
/// <summary>
/// Gets the value of the VerticalSnapPointsAlignment attached property.
/// </summary>
/// <param name="control">The control to set the value on.</param>
/// <param name="value">The value of the property.</param>
public static void SetVerticalSnapPointsAlignment(Control control, SnapPointsAlignment value)
{
control.SetValue(VerticalSnapPointsAlignmentProperty, value);
}
/// <summary>
/// Gets the value of the VerticalScrollBarVisibility attached property.
/// </summary>

51
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();
}
}
}

2
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
}
}
/// <summary>
/// Invoked when the <see cref="InputElement.DoubleTapped"/> event occurs in the header.
/// </summary>

2
src/Avalonia.Controls/Window.cs

@ -450,7 +450,7 @@ namespace Avalonia.Controls
/// resulting task will produce the <see cref="_dialogResult"/> value when the window
/// is closed.
/// </remarks>
public void Close(object dialogResult)
public void Close(object? dialogResult)
{
_dialogResult = dialogResult;
CloseCore(WindowCloseReason.WindowClosing, true);

2
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)
{

4
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)
{

2
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; }

2
src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj

@ -15,7 +15,7 @@
<Import Project="..\..\..\build\TrimmingEnable.props" />
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="7.0.0-*" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="7.0.2" />
</ItemGroup>
<ItemGroup>

2
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;
}
}

6
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)
{

3
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)
{

3
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
{
}

11
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
{
}
}

2
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++)
{

2
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,

2
src/Windows/Avalonia.Win32/Avalonia.Win32.csproj

@ -10,7 +10,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2023020321" />
<PackageReference Include="MicroCom.CodeGenerator.MSBuild" Version="0.11.0" PrivateAssets="all" />
<MicroComIdl Include="WinRT\winrt.idl" CSharpInteropPath="WinRT\WinRT.Generated.cs" />
<MicroComIdl Include="Win32Com\win32.idl" CSharpInteropPath="Win32Com\Win32.Generated.cs" />

2
src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj

@ -23,7 +23,7 @@
<Compile Include="..\..\Avalonia.Base\Utilities\StringBuilderCache.cs" Link="Utilities\StringBuilderCache.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2023020321" />
</ItemGroup>
<Import Project="..\..\..\build\NetFX.props" />
</Project>

71
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
{
}
}
}
}
}

2
src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs

@ -112,7 +112,7 @@ class Template
var defs = cl.Members.OfType<MethodDeclarationSyntax>().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);")));

4
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);
}

2
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<IMethodSymbol,ISymbol>(f => f.ContainingType, SymbolEqualityComparer.Default))
{
var classBuilder = new StringBuilder();
if (typeGroup.Key.ContainingNamespace != null)

2
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<IMethodSymbol,ISymbol>(f => f.ContainingType, SymbolEqualityComparer.Default))
{
var nextContext = 0;
var contexts = new Dictionary<string, int>();

1
tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj

@ -16,4 +16,5 @@
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\ImageSharp.props" />
</Project>

45
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<Rgba32>(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<Rgba32>(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<Size?, ShowWindowMode, WindowStartupLocation> StartupLocationData()
{
var sizes = new Size?[] { null, new Size(400, 300) };

BIN
tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf

Binary file not shown.

51
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()
{

2
tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj

@ -8,7 +8,7 @@
<Compile Include="..\Avalonia.RenderTests\**\*.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="..\Avalonia.RenderTests\**\*.ttf" />
<EmbeddedResource Include="..\Avalonia.RenderTests\*\*.ttf" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />

2
tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj

@ -9,7 +9,7 @@
<Import Project="..\..\build\Microsoft.Reactive.Testing.props" />
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<EmbeddedResource Include="..\Avalonia.RenderTests\**\*.ttf" />
<EmbeddedResource Include="..\Avalonia.RenderTests\*\*.ttf" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />

10
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":
{

84
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)

66
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()

27
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);
}
}

BIN
tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
tests/TestFiles/Skia/Controls/TextBlock/Should_Draw_TextDecorations.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Loading…
Cancel
Save