Browse Source

Merge remote-tracking branch 'upstream/master' into bump-tmds-dbus-sourcegenerator

pull/11298/head
affederaffe 3 years ago
parent
commit
542481a8fd
  1. 4
      .editorconfig
  2. 28
      Avalonia.sln
  3. 5
      dirs.proj
  4. 15
      samples/ControlCatalog/Pages/ComboBoxPage.xaml
  5. 15
      samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs
  6. 1
      samples/Sandbox/MainWindow.axaml.cs
  7. 2
      src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs
  8. 2
      src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs
  9. 2
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  10. 7
      src/Avalonia.Base/Input/FocusManager.cs
  11. 4
      src/Avalonia.Base/Input/InputElement.cs
  12. 2
      src/Avalonia.Base/Input/TextInput/InputMethodManager.cs
  13. 27
      src/Avalonia.Base/Layout/LayoutManager.cs
  14. 28
      src/Avalonia.Base/Media/DrawingContext.cs
  15. 10
      src/Avalonia.Base/Media/DrawingGroup.cs
  16. 4
      src/Avalonia.Base/Media/DrawingImage.cs
  17. 10
      src/Avalonia.Base/Media/EdgeMode.cs
  18. 42
      src/Avalonia.Base/Media/GlyphRun.cs
  19. 23
      src/Avalonia.Base/Media/GlyphRunMetrics.cs
  20. 4
      src/Avalonia.Base/Media/IImage.cs
  21. 8
      src/Avalonia.Base/Media/ITileBrush.cs
  22. 6
      src/Avalonia.Base/Media/Imaging/Bitmap.cs
  23. 6
      src/Avalonia.Base/Media/Imaging/BitmapBlendingMode.cs
  24. 10
      src/Avalonia.Base/Media/Imaging/BitmapInterpolationMode.cs
  25. 4
      src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs
  26. 8
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  27. 5
      src/Avalonia.Base/Media/ImmediateDrawingContext.cs
  28. 7
      src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs
  29. 11
      src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs
  30. 16
      src/Avalonia.Base/Media/PlatformDrawingContext.cs
  31. 127
      src/Avalonia.Base/Media/RenderOptions.cs
  32. 8
      src/Avalonia.Base/Media/TextDecoration.cs
  33. 11
      src/Avalonia.Base/Media/TextRenderingMode.cs
  34. 13
      src/Avalonia.Base/Media/TileBrush.cs
  35. 19
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  36. 22
      src/Avalonia.Base/Platform/IGlyphRunBuffer.cs
  37. 19
      src/Avalonia.Base/Platform/IGlyphRunImpl.cs
  38. 3
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  39. 8
      src/Avalonia.Base/Platform/StandardAssetLoader.cs
  40. 3
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  41. 4
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  42. 36
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
  43. 21
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  44. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  45. 7
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  46. 120
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  47. 68
      src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs
  48. 5
      src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
  49. 21
      src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs
  50. 2
      src/Avalonia.Base/Threading/DispatcherOperation.cs
  51. 4
      src/Avalonia.Base/Visual.cs
  52. 1
      src/Avalonia.Base/composition-schema.xml
  53. 39
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  54. 8
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  55. 1
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  56. 14
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  57. 1
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  58. 4
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  59. 17
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  60. 8
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  61. 1
      src/Avalonia.Controls.DataGrid/Themes/Simple.xaml
  62. 2
      src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs
  63. 28
      src/Avalonia.Controls/ComboBox.cs
  64. 4
      src/Avalonia.Controls/Control.cs
  65. 10
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  66. 101
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  67. 4
      src/Avalonia.Controls/Image.cs
  68. 58
      src/Avalonia.Controls/ItemsControl.cs
  69. 11
      src/Avalonia.Controls/ListBox.cs
  70. 18
      src/Avalonia.Controls/MenuBase.cs
  71. 18
      src/Avalonia.Controls/MenuItem.cs
  72. 8
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  73. 11
      src/Avalonia.Controls/Primitives/TabStrip.cs
  74. 15
      src/Avalonia.Controls/Primitives/Thumb.cs
  75. 28
      src/Avalonia.Controls/Primitives/Track.cs
  76. 11
      src/Avalonia.Controls/TabControl.cs
  77. 11
      src/Avalonia.Controls/TreeView.cs
  78. 10
      src/Avalonia.Controls/TreeViewItem.cs
  79. 3
      src/Avalonia.Controls/Utils/StringUtils.cs
  80. 97
      src/Avalonia.Controls/VirtualizingCarouselPanel.cs
  81. 112
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  82. 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  83. 4
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  84. 13
      src/Avalonia.Native/AvaloniaNativeDragSource.cs
  85. 9
      src/Avalonia.Remote.Protocol/MetsysBson.cs
  86. 21
      src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
  87. 9
      src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs
  88. 6
      src/Browser/Avalonia.Browser/Interop/StorageHelper.cs
  89. 16
      src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
  90. 8
      src/Browser/Avalonia.Browser/WindowingPlatform.cs
  91. 3
      src/Browser/Avalonia.Browser/webapp/build.js
  92. 9
      src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
  93. 7
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts
  94. 23
      src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts
  95. 78
      src/Browser/Avalonia.Browser/webapp/modules/sw.ts
  96. 13
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  97. 50
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs
  98. 10
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs
  99. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  100. 39
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs

4
.editorconfig

@ -177,7 +177,9 @@ dotnet_diagnostic.CA1828.severity = warning
dotnet_diagnostic.CA1829.severity = warning
#CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
dotnet_diagnostic.CA1847.severity = warning
#CACA2211:Non-constant fields should not be visible
#CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method
dotnet_diagnostic.CA1854.severity = warning
#CA2211:Non-constant fields should not be visible
dotnet_diagnostic.CA2211.severity = error
# Wrapping preferences

28
Avalonia.sln

@ -262,9 +262,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.iOS", "samples
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF237916-7150-496B-89ED-6CA3292896E7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.UnitTests", "tests\Avalonia.Headless.UnitTests\Avalonia.Headless.UnitTests.csproj", "{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.UnitTests", "tests\Avalonia.Headless.UnitTests\Avalonia.Headless.UnitTests.csproj", "{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -605,14 +605,6 @@ Global
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.Build.0 = Release|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -637,6 +629,14 @@ Global
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Build.0 = Release|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Deploy.0 = Release|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -681,6 +681,8 @@ Global
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
{351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7}
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7}
{909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098}
@ -704,10 +706,6 @@ Global
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7}
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7}
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7}
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098}
@ -715,6 +713,8 @@ Global
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{4CDAD037-34A2-4CCF-A03A-C6C7B988A572} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7}
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

5
dirs.proj

@ -9,10 +9,11 @@
<ProjectReference Remove="**/*.shproj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/**/*.*proj" />
<!-- Exclude iOS, Android and Web samples from build -->
<!-- Exclude iOS, Android and Browser samples from build -->
<ProjectReference Remove="samples/*.iOS/*.csproj" />
<ProjectReference Remove="samples/*.Android/*.csproj" />
<ProjectReference Remove="samples/*.Web/*.csproj" />
<ProjectReference Remove="samples/*.Browser/*.csproj" />
<ProjectReference Remove="samples/*.Blazor/*.csproj" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsOsPlatform('Windows')) OR '$(MSBuildRuntimeType)' != 'Full'">
<ProjectReference Remove="src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj" />

15
samples/ControlCatalog/Pages/ComboBoxPage.xaml

@ -98,6 +98,21 @@
<ComboBoxItem>Inline Item 3</ComboBoxItem>
<ComboBoxItem>Inline Item 4</ComboBoxItem>
</ComboBox>
<ComboBox WrapSelection="{Binding WrapSelection}" ItemsSource="{Binding Values}" DisplayMemberBinding="{Binding Name}">
</ComboBox>
<ComboBox WrapSelection="{Binding WrapSelection}" ItemsSource="{Binding Values}" >
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"></TextBlock>
<TextBlock Text="{Binding Id}"></TextBlock>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</WrapPanel>
<CheckBox IsChecked="{Binding WrapSelection}">WrapSelection</CheckBox>

15
samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs

@ -16,5 +16,20 @@ namespace ControlCatalog.ViewModels
get => _wrapSelection;
set => this.RaiseAndSetIfChanged(ref _wrapSelection, value);
}
public ObservableCollection<IdAndName> Values { get; set; } = new ObservableCollection<IdAndName>
{
new IdAndName(){ Id = "Id 1", Name = "Name 1" },
new IdAndName(){ Id = "Id 2", Name = "Name 2" },
new IdAndName(){ Id = "Id 3", Name = "Name 3" },
new IdAndName(){ Id = "Id 4", Name = "Name 4" },
new IdAndName(){ Id = "Id 5", Name = "Name 5" },
};
}
public class IdAndName
{
public string Id { get; set; }
public string Name { get; set; }
}
}

1
samples/Sandbox/MainWindow.axaml.cs

@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Input.TextInput;
using Avalonia.Markup.Xaml;
using Avalonia.Win32.WinRT.Composition;

2
src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs

@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins
/// <summary>
/// Validates properties on that have <see cref="ValidationAttribute"/>s.
/// </summary>
internal class DataAnnotationsValidationPlugin : IDataValidationPlugin
public class DataAnnotationsValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]

2
src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs

@ -7,7 +7,7 @@ namespace Avalonia.Data.Core.Plugins
/// <summary>
/// Validates properties that report errors by throwing exceptions.
/// </summary>
internal class ExceptionValidationPlugin : IDataValidationPlugin
public class ExceptionValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]

2
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins
/// <summary>
/// Validates properties on objects that implement <see cref="INotifyDataErrorInfo"/>.
/// </summary>
internal class IndeiValidationPlugin : IDataValidationPlugin
public class IndeiValidationPlugin : IDataValidationPlugin
{
private static readonly WeakEvent<INotifyDataErrorInfo, DataErrorsChangedEventArgs>
ErrorsChangedWeakEvent = WeakEvent.Register<INotifyDataErrorInfo, DataErrorsChangedEventArgs>(

7
src/Avalonia.Base/Input/FocusManager.cs

@ -122,6 +122,11 @@ namespace Avalonia.Input
{
scope = scope ?? throw new ArgumentNullException(nameof(scope));
if (element is not null && !CanFocus(element))
{
return;
}
if (_focusScopes.TryGetValue(scope, out var existingElement))
{
if (element != existingElement)
@ -242,6 +247,6 @@ namespace Avalonia.Input
}
}
private static bool IsVisible(IInputElement e) => (e as Visual)?.IsVisible ?? true;
private static bool IsVisible(IInputElement e) => (e as Visual)?.IsEffectivelyVisible ?? true;
}
}

4
src/Avalonia.Base/Input/InputElement.cs

@ -647,6 +647,10 @@ namespace Avalonia.Input
{
PseudoClasses.Set(":focus-within", change.GetNewValue<bool>());
}
else if (change.Property == IsVisibleProperty && !change.GetNewValue<bool>() && IsFocused)
{
FocusManager.Instance?.Focus(null);
}
}
/// <summary>

2
src/Avalonia.Base/Input/TextInput/InputMethodManager.cs

@ -48,9 +48,9 @@ namespace Avalonia.Input.TextInput
}
_transformTracker.SetVisual(_client?.TextViewVisual);
UpdateCursorRect();
_im?.SetClient(_client);
UpdateCursorRect();
}
else
{

27
src/Avalonia.Base/Layout/LayoutManager.cs

@ -21,6 +21,7 @@ namespace Avalonia.Layout
private readonly Layoutable _owner;
private readonly LayoutQueue<Layoutable> _toMeasure = new LayoutQueue<Layoutable>(v => !v.IsMeasureValid);
private readonly LayoutQueue<Layoutable> _toArrange = new LayoutQueue<Layoutable>(v => !v.IsArrangeValid);
private readonly List<Layoutable> _toArrangeAfterMeasure = new();
private readonly Action _executeLayoutPass;
private List<EffectiveViewportChangedListener>? _effectiveViewportChangedListeners;
private bool _disposed;
@ -266,9 +267,14 @@ namespace Avalonia.Layout
if (!control.IsArrangeValid)
{
Arrange(control);
if (Arrange(control) == ArrangeResult.AncestorMeasureInvalid)
_toArrangeAfterMeasure.Add(control);
}
}
foreach (var i in _toArrangeAfterMeasure)
InvalidateArrange(i);
_toArrangeAfterMeasure.Clear();
}
private bool Measure(Layoutable control)
@ -304,19 +310,19 @@ namespace Avalonia.Layout
return true;
}
private bool Arrange(Layoutable control)
private ArrangeResult Arrange(Layoutable control)
{
if (!control.IsVisible || !control.IsAttachedToVisualTree)
return false;
return ArrangeResult.NotVisible;
if (control.VisualParent is Layoutable parent)
{
if (!Arrange(parent))
return false;
if (Arrange(parent) is var parentResult && parentResult != ArrangeResult.Arranged)
return parentResult;
}
if (!control.IsMeasureValid)
return false;
return ArrangeResult.AncestorMeasureInvalid;
if (!control.IsArrangeValid)
{
@ -332,7 +338,7 @@ namespace Avalonia.Layout
}
}
return true;
return ArrangeResult.Arranged;
}
private void QueueLayoutPass()
@ -435,5 +441,12 @@ namespace Avalonia.Layout
public Layoutable Listener { get; }
public Rect Viewport { get; set; }
}
private enum ArrangeResult
{
Arranged,
NotVisible,
AncestorMeasureInvalid,
}
}
}

28
src/Avalonia.Base/Media/DrawingContext.cs

@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.Media.Imaging;
using System.ComponentModel;
namespace Avalonia.Media
{
@ -54,12 +53,10 @@ namespace Avalonia.Media
/// <param name="source">The image.</param>
/// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
public virtual void DrawImage(IImage source, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode = default)
public virtual void DrawImage(IImage source, Rect sourceRect, Rect destRect)
{
_ = source ?? throw new ArgumentNullException(nameof(source));
source.Draw(this, sourceRect, destRect, bitmapInterpolationMode);
source.Draw(this, sourceRect, destRect);
}
/// <summary>
@ -69,8 +66,7 @@ namespace Avalonia.Media
/// <param name="opacity">The opacity to draw with.</param>
/// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
internal abstract void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default);
internal abstract void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect);
/// <summary>
/// Draws a line.
@ -287,8 +283,7 @@ namespace Avalonia.Media
Opacity,
Clip,
GeometryClip,
OpacityMask,
BitmapBlendMode
OpacityMask
}
public RestoreState(DrawingContext context, PushedStateType type)
@ -313,8 +308,6 @@ namespace Avalonia.Media
_context.PopGeometryClipCore();
else if (_type == PushedStateType.OpacityMask)
_context.PopOpacityMaskCore();
else if (_type == PushedStateType.BitmapBlendMode)
_context.PopBitmapBlendModeCore();
}
}
@ -395,16 +388,6 @@ namespace Avalonia.Media
}
protected abstract void PushOpacityMaskCore(IBrush mask, Rect bounds);
public PushedState PushBitmapBlendMode(BitmapBlendingMode blendingMode)
{
PushBitmapBlendMode(blendingMode);
_states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.BitmapBlendMode));
return new PushedState(this);
}
protected abstract void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode);
/// <summary>
/// Pushes a matrix transformation.
/// </summary>
@ -432,7 +415,6 @@ namespace Avalonia.Media
protected abstract void PopGeometryClipCore();
protected abstract void PopOpacityCore();
protected abstract void PopOpacityMaskCore();
protected abstract void PopBitmapBlendModeCore();
protected abstract void PopTransformCore();
private static bool PenIsVisible(IPen? pen)

10
src/Avalonia.Base/Media/DrawingGroup.cs

@ -196,13 +196,7 @@ namespace Avalonia.Media
throw new NotImplementedException();
}
protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode)
{
throw new NotImplementedException();
}
internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
{
throw new NotImplementedException();
}
@ -321,8 +315,6 @@ namespace Avalonia.Media
protected override void PopOpacityMaskCore() => Pop();
protected override void PopBitmapBlendModeCore() => Pop();
protected override void PopTransformCore() => Pop();
/// <summary>

4
src/Avalonia.Base/Media/DrawingImage.cs

@ -1,6 +1,5 @@
using System;
using Avalonia.Metadata;
using Avalonia.Media.Imaging;
namespace Avalonia.Media
{
@ -43,8 +42,7 @@ namespace Avalonia.Media
void IImage.Draw(
DrawingContext context,
Rect sourceRect,
Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode)
Rect destRect)
{
var drawing = Drawing;

10
src/Avalonia.Base/Media/EdgeMode.cs

@ -0,0 +1,10 @@
namespace Avalonia.Media
{
public enum EdgeMode : byte
{
Unspecified,
Antialias,
Aliased
}
}

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

@ -153,7 +153,7 @@ namespace Avalonia.Media
/// <summary>
/// Gets the conservative bounding box of the <see cref="GlyphRun"/>.
/// </summary>
public Rect Bounds => PlatformImpl.Item.Bounds;
public Rect Bounds => new Rect(new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height));
/// <summary>
///
@ -166,7 +166,7 @@ namespace Avalonia.Media
/// </summary>
public Point BaselineOrigin
{
get => PlatformImpl.Item.BaselineOrigin;
get => _baselineOrigin ?? new Point(0, Metrics.Baseline);
set => Set(ref _baselineOrigin, value);
}
@ -676,13 +676,17 @@ namespace Avalonia.Media
}
}
return new GlyphRunMetrics(
width,
trailingWhitespaceLength,
newLineLength,
firstCluster,
lastCluster
);
return new GlyphRunMetrics
{
Baseline = -GlyphTypeface.Metrics.Ascent * Scale,
Width = width,
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace,
Height = height,
NewLineLength = newLineLength,
TrailingWhitespaceLength = trailingWhitespaceLength,
FirstCluster = firstCluster,
LastCluster = lastCluster
};
}
private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount)
@ -820,10 +824,11 @@ namespace Avalonia.Media
private IRef<IGlyphRunImpl> CreateGlyphRunImpl()
{
var platformImpl = s_renderInterface.CreateGlyphRun(
GlyphTypeface,
FontRenderingEmSize,
GlyphInfos,
_baselineOrigin ?? new Point(0, -GlyphTypeface.Metrics.Ascent * Scale));
GlyphTypeface,
FontRenderingEmSize,
GlyphInfos,
BaselineOrigin,
Bounds);
_platformImpl = RefCountable.Create(platformImpl);
@ -835,5 +840,16 @@ namespace Avalonia.Media
_platformImpl?.Dispose();
_platformImpl = null;
}
/// <summary>
/// Gets the intersections of specified upper and lower limit.
/// </summary>
/// <param name="lowerLimit">Upper limit.</param>
/// <param name="upperLimit">Lower limit.</param>
/// <returns></returns>
public IReadOnlyList<float> GetIntersections(float lowerLimit, float upperLimit)
{
return PlatformImpl.Item.GetIntersections(lowerLimit, upperLimit);
}
}
}

23
src/Avalonia.Base/Media/GlyphRunMetrics.cs

@ -2,23 +2,20 @@
{
public readonly record struct GlyphRunMetrics
{
public GlyphRunMetrics(double width, int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster)
{
Width = width;
TrailingWhitespaceLength = trailingWhitespaceLength;
NewLineLength = newLineLength;
FirstCluster = firstCluster;
LastCluster = lastCluster;
}
public double Baseline { get; init; }
public double Width { get; }
public double Width { get; init; }
public int TrailingWhitespaceLength { get; }
public double WidthIncludingTrailingWhitespace { get; init; }
public int NewLineLength { get; }
public double Height { get; init; }
public int FirstCluster { get; }
public int TrailingWhitespaceLength { get; init; }
public int LastCluster { get; }
public int NewLineLength { get; init; }
public int FirstCluster { get; init; }
public int LastCluster { get; init; }
}
}

4
src/Avalonia.Base/Media/IImage.cs

@ -18,11 +18,9 @@ namespace Avalonia.Media
/// <param name="context">The drawing context.</param>
/// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
void Draw(
DrawingContext context,
Rect sourceRect,
Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode);
Rect destRect);
}
}

8
src/Avalonia.Base/Media/ITileBrush.cs

@ -39,13 +39,5 @@ namespace Avalonia.Media
/// Gets the brush's tile mode.
/// </summary>
TileMode TileMode { get; }
/// <summary>
/// Gets the bitmap interpolation mode.
/// </summary>
/// <value>
/// The bitmap interpolation mode.
/// </value>
BitmapInterpolationMode BitmapInterpolationMode { get; }
}
}

6
src/Avalonia.Base/Media/Imaging/Bitmap.cs

@ -224,15 +224,13 @@ namespace Avalonia.Media.Imaging
void IImage.Draw(
DrawingContext context,
Rect sourceRect,
Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode)
Rect destRect)
{
context.DrawBitmap(
PlatformImpl,
1,
sourceRect,
destRect,
bitmapInterpolationMode);
destRect);
}
private static IPlatformRenderInterface GetFactory()

6
src/Avalonia.Base/Media/Imaging/BitmapBlendingMode.cs

@ -3,8 +3,10 @@ namespace Avalonia.Media.Imaging
/// <summary>
/// Controls the way the bitmaps are drawn together.
/// </summary>
public enum BitmapBlendingMode
public enum BitmapBlendingMode : byte
{
Unspecified,
/// <summary>
/// Source is placed over the destination.
/// </summary>
@ -52,6 +54,6 @@ namespace Avalonia.Media.Imaging
/// <summary>
/// Display the sum of the source image and destination image.
/// </summary>
Plus,
Plus
}
}

10
src/Avalonia.Base/Media/Imaging/BitmapInterpolationMode.cs

@ -3,12 +3,14 @@
/// <summary>
/// Controls the performance and quality of bitmap scaling.
/// </summary>
public enum BitmapInterpolationMode
public enum BitmapInterpolationMode : byte
{
Unspecified,
/// <summary>
/// Uses the default behavior of the underling render backend.
/// Disable interpolation.
/// </summary>
Default,
None,
/// <summary>
/// The best performance but worst image quality.
@ -18,7 +20,7 @@
/// <summary>
/// Good performance and decent image quality.
/// </summary>
MediumQuality,
MediumQuality,
/// <summary>
/// Highest quality but worst performance.

4
src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs

@ -83,12 +83,12 @@ namespace Avalonia.Media.Imaging
}
}
public void Draw(DrawingContext context, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
public void Draw(DrawingContext context, Rect sourceRect, Rect destRect)
{
if (Source is not IBitmap bmp)
return;
var topLeft = SourceRect.TopLeft.ToPointWithDpi(bmp.Dpi);
Source.Draw(context, sourceRect.Translate(new Vector(topLeft.X, topLeft.Y)), destRect, bitmapInterpolationMode);
Source.Draw(context, sourceRect.Translate(new Vector(topLeft.X, topLeft.Y)), destRect);
}
}
}

8
src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs

@ -9,7 +9,7 @@ namespace Avalonia.Media.Imaging
/// <summary>
/// A bitmap that holds the rendering of a <see cref="Visual"/>.
/// </summary>
public class RenderTargetBitmap : Bitmap, IDisposable
public class RenderTargetBitmap : Bitmap
{
/// <summary>
/// Initializes a new instance of the <see cref="RenderTargetBitmap"/> class.
@ -68,5 +68,11 @@ namespace Avalonia.Media.Imaging
platform.Clear(Colors.Transparent);
return new PlatformDrawingContext(platform);
}
public override void Dispose()
{
PlatformImpl.Dispose();
base.Dispose();
}
}
}

5
src/Avalonia.Base/Media/ImmediateDrawingContext.cs

@ -79,11 +79,10 @@ namespace Avalonia.Media
/// <param name="source">The bitmap.</param>
/// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
public void DrawBitmap(IBitmap source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default)
public void DrawBitmap(IBitmap source, Rect sourceRect, Rect destRect)
{
_ = source ?? throw new ArgumentNullException(nameof(source));
PlatformImpl.DrawBitmap(source.PlatformImpl, 1, sourceRect, destRect, bitmapInterpolationMode);
PlatformImpl.DrawBitmap(source.PlatformImpl, 1, sourceRect, destRect);
}
/// <summary>

7
src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs

@ -22,7 +22,6 @@ namespace Avalonia.Media.Immutable
/// How the source rectangle will be stretched to fill the destination rect.
/// </param>
/// <param name="tileMode">The tile mode.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
public ImmutableImageBrush(
IBitmap? source,
AlignmentX alignmentX = AlignmentX.Center,
@ -33,8 +32,7 @@ namespace Avalonia.Media.Immutable
RelativePoint transformOrigin = default,
RelativeRect? sourceRect = null,
Stretch stretch = Stretch.Uniform,
TileMode tileMode = TileMode.None,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
TileMode tileMode = TileMode.None)
: base(
alignmentX,
alignmentY,
@ -44,8 +42,7 @@ namespace Avalonia.Media.Immutable
transformOrigin,
sourceRect ?? RelativeRect.Fill,
stretch,
tileMode,
bitmapInterpolationMode)
tileMode)
{
Source = source;
}

11
src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs

@ -21,7 +21,6 @@ namespace Avalonia.Media.Immutable
/// How the source rectangle will be stretched to fill the destination rect.
/// </param>
/// <param name="tileMode">The tile mode.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
protected ImmutableTileBrush(
AlignmentX alignmentX,
AlignmentY alignmentY,
@ -31,8 +30,7 @@ namespace Avalonia.Media.Immutable
RelativePoint transformOrigin,
RelativeRect sourceRect,
Stretch stretch,
TileMode tileMode,
BitmapInterpolationMode bitmapInterpolationMode)
TileMode tileMode)
{
AlignmentX = alignmentX;
AlignmentY = alignmentY;
@ -43,7 +41,6 @@ namespace Avalonia.Media.Immutable
SourceRect = sourceRect;
Stretch = stretch;
TileMode = tileMode;
BitmapInterpolationMode = bitmapInterpolationMode;
}
/// <summary>
@ -60,8 +57,7 @@ namespace Avalonia.Media.Immutable
source.TransformOrigin,
source.SourceRect,
source.Stretch,
source.TileMode,
source.BitmapInterpolationMode)
source.TileMode)
{
}
@ -95,8 +91,5 @@ namespace Avalonia.Media.Immutable
/// <inheritdoc/>
public TileMode TileMode { get; }
/// <inheritdoc/>
public BitmapInterpolationMode BitmapInterpolationMode { get; }
}
}

16
src/Avalonia.Base/Media/PlatformDrawingContext.cs

@ -26,6 +26,12 @@ internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWi
_ownsImpl = ownsImpl;
}
public RenderOptions RenderOptions
{
get => _impl.RenderOptions;
set => _impl.RenderOptions = value;
}
protected override void DrawLineCore(IPen pen, Point p1, Point p2) =>
_impl.DrawLine(pen, p1, p2);
@ -38,9 +44,8 @@ internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWi
protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) => _impl.DrawEllipse(brush, pen, rect);
internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) =>
_impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode);
internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect) =>
_impl.DrawBitmap(source, opacity, sourceRect, destRect);
public override void Custom(ICustomDrawOperation custom)
{
@ -77,9 +82,6 @@ internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWi
protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) =>
_impl.PushOpacityMask(mask, bounds);
protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) =>
_impl.PushBitmapBlendMode(blendingMode);
protected override void PushTransformCore(Matrix matrix)
{
_transforms ??= TransformStackPool.Get();
@ -96,8 +98,6 @@ internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWi
protected override void PopOpacityMaskCore() => _impl.PopOpacityMask();
protected override void PopBitmapBlendModeCore() => _impl.PopBitmapBlendMode();
protected override void PopTransformCore() =>
_impl.Transform =
(_transforms ?? throw new ObjectDisposedException(nameof(PlatformDrawingContext))).Pop();

127
src/Avalonia.Base/Media/RenderOptions.cs

@ -1,36 +1,131 @@
using Avalonia.Media.Imaging;
namespace Avalonia.Media
{
public class RenderOptions
{
public readonly record struct RenderOptions
{
public BitmapInterpolationMode BitmapInterpolationMode { get; init; }
public EdgeMode EdgeMode { get; init; }
public TextRenderingMode TextRenderingMode { get; init; }
public BitmapBlendingMode BitmapBlendingMode { get; init; }
/// <summary>
/// Gets the value of the BitmapInterpolationMode attached property for a visual.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns>
public static BitmapInterpolationMode GetBitmapInterpolationMode(Visual visual)
{
return visual.RenderOptions.BitmapInterpolationMode;
}
/// <summary>
/// Defines the <see cref="BitmapInterpolationMode"/> property.
/// Sets the value of the BitmapInterpolationMode attached property for a visual.
/// </summary>
public static readonly StyledProperty<BitmapInterpolationMode> BitmapInterpolationModeProperty =
AvaloniaProperty.RegisterAttached<RenderOptions, AvaloniaObject, BitmapInterpolationMode>(
"BitmapInterpolationMode",
BitmapInterpolationMode.MediumQuality,
inherits: true);
/// <param name="visual">The control.</param>
/// <param name="value">The left value.</param>
public static void SetBitmapInterpolationMode(Visual visual, BitmapInterpolationMode value)
{
visual.RenderOptions = visual.RenderOptions with { BitmapInterpolationMode = value };
}
/// <summary>
/// Gets the value of the BitmapInterpolationMode attached property for a control.
/// Gets the value of the BitmapBlendingMode attached property for a visual.
/// </summary>
/// <param name="element">The control.</param>
/// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns>
public static BitmapInterpolationMode GetBitmapInterpolationMode(AvaloniaObject element)
public static BitmapBlendingMode GetBitmapBlendingMode(Visual visual)
{
return element.GetValue(BitmapInterpolationModeProperty);
return visual.RenderOptions.BitmapBlendingMode;
}
/// <summary>
/// Sets the value of the BitmapInterpolationMode attached property for a control.
/// Sets the value of the BitmapBlendingMode attached property for a visual.
/// </summary>
/// <param name="element">The control.</param>
/// <param name="visual">The control.</param>
/// <param name="value">The left value.</param>
public static void SetBitmapInterpolationMode(AvaloniaObject element, BitmapInterpolationMode value)
public static void SetBitmapBlendingMode(Visual visual, BitmapBlendingMode value)
{
element.SetValue(BitmapInterpolationModeProperty, value);
visual.RenderOptions = visual.RenderOptions with { BitmapBlendingMode = value };
}
/// <summary>
/// Gets the value of the EdgeMode attached property for a visual.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns>
public static EdgeMode GetEdgeMode(Visual visual)
{
return visual.RenderOptions.EdgeMode;
}
/// <summary>
/// Sets the value of the EdgeMode attached property for a visual.
/// </summary>
/// <param name="visual">The control.</param>
/// <param name="value">The left value.</param>
public static void SetEdgeMode(Visual visual, EdgeMode value)
{
visual.RenderOptions = visual.RenderOptions with { EdgeMode = value };
}
/// <summary>
/// Gets the value of the TextRenderingMode attached property for a visual.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns>
public static TextRenderingMode GetTextRenderingMode(Visual visual)
{
return visual.RenderOptions.TextRenderingMode;
}
/// <summary>
/// Sets the value of the TextRenderingMode attached property for a visual.
/// </summary>
/// <param name="visual">The control.</param>
/// <param name="value">The left value.</param>
public static void SetTextRenderingMode(Visual visual, TextRenderingMode value)
{
visual.RenderOptions = visual.RenderOptions with { TextRenderingMode = value };
}
public RenderOptions MergeWith(RenderOptions other)
{
var bitmapInterpolationMode = BitmapInterpolationMode;
if (bitmapInterpolationMode == BitmapInterpolationMode.Unspecified)
{
bitmapInterpolationMode = other.BitmapInterpolationMode;
}
var edgeMode = EdgeMode;
if (edgeMode == EdgeMode.Unspecified)
{
edgeMode = other.EdgeMode;
}
var textRenderingMode = TextRenderingMode;
if (textRenderingMode == TextRenderingMode.Unspecified)
{
textRenderingMode = other.TextRenderingMode;
}
var bitmapBlendingMode = BitmapBlendingMode;
if (bitmapBlendingMode == BitmapBlendingMode.Unspecified)
{
bitmapBlendingMode = other.BitmapBlendingMode;
}
return new RenderOptions
{
BitmapInterpolationMode = bitmapInterpolationMode,
EdgeMode = edgeMode,
TextRenderingMode = textRenderingMode,
BitmapBlendingMode = bitmapBlendingMode
};
}
}
}

8
src/Avalonia.Base/Media/TextDecoration.cs

@ -1,10 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using Avalonia.Collections;
using Avalonia.Collections.Pooled;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media
{
@ -218,7 +214,7 @@ namespace Avalonia.Media
{
var offsetY = glyphRun.BaselineOrigin.Y - origin.Y;
var intersections = glyphRun.PlatformImpl.Item.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY));
var intersections = glyphRun.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY));
if (intersections.Count > 0)
{

11
src/Avalonia.Base/Media/TextRenderingMode.cs

@ -0,0 +1,11 @@
namespace Avalonia.Media
{
public enum TextRenderingMode : byte
{
Unspecified,
SubpixelAntialias,
Antialias,
Alias
}
}

13
src/Avalonia.Base/Media/TileBrush.cs

@ -83,7 +83,6 @@ namespace Avalonia.Media
SourceRectProperty,
StretchProperty,
TileModeProperty);
RenderOptions.BitmapInterpolationModeProperty.OverrideDefaultValue<TileBrush>(BitmapInterpolationMode.Default);
}
/// <summary>
@ -140,17 +139,5 @@ namespace Avalonia.Media
get { return (TileMode)GetValue(TileModeProperty); }
set { SetValue(TileModeProperty, value); }
}
/// <summary>
/// Gets or sets the bitmap interpolation mode.
/// </summary>
/// <value>
/// The bitmap interpolation mode.
/// </value>
public BitmapInterpolationMode BitmapInterpolationMode
{
get { return RenderOptions.GetBitmapInterpolationMode(this); }
set { RenderOptions.SetBitmapInterpolationMode(this, value); }
}
}
}

19
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -1,8 +1,8 @@
using System;
using Avalonia.Media;
using Avalonia.Utilities;
using Avalonia.Media.Imaging;
using Avalonia.Metadata;
using Avalonia.Media.Imaging;
namespace Avalonia.Platform
{
@ -12,6 +12,11 @@ namespace Avalonia.Platform
[Unstable]
public interface IDrawingContextImpl : IDisposable
{
/// <summary>
/// Gets or sets the current render options used to control the rendering behavior of drawing operations.
/// </summary>
RenderOptions RenderOptions { get; set; }
/// <summary>
/// Gets or sets the current transform of the drawing context.
/// </summary>
@ -30,8 +35,7 @@ namespace Avalonia.Platform
/// <param name="opacity">The opacity to draw with.</param>
/// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default);
void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect);
/// <summary>
/// Draws a bitmap image.
@ -157,15 +161,6 @@ namespace Avalonia.Platform
void PopGeometryClip();
/// <summary>
/// Pushes a bitmap blending value.
/// </summary>
/// <param name="blendingMode">The bitmap blending mode.</param>
void PushBitmapBlendMode(BitmapBlendingMode blendingMode);
/// <summary>
/// Pops the latest pushed bitmap blending value.
/// </summary>
void PopBitmapBlendMode();
/// <summary>
/// Attempts to get an optional feature from the drawing context implementation

22
src/Avalonia.Base/Platform/IGlyphRunBuffer.cs

@ -1,22 +0,0 @@
using System;
using System.Drawing;
namespace Avalonia.Platform
{
public interface IGlyphRunBuffer
{
Span<ushort> GlyphIndices { get; }
IGlyphRunImpl Build();
}
public interface IHorizontalGlyphRunBuffer : IGlyphRunBuffer
{
Span<float> GlyphPositions { get; }
}
public interface IPositionedGlyphRunBuffer : IGlyphRunBuffer
{
Span<PointF> GlyphPositions { get; }
}
}

19
src/Avalonia.Base/Platform/IGlyphRunImpl.cs

@ -1,25 +1,36 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Metadata;
namespace Avalonia.Platform
{
/// <summary>
/// Actual implementation of a glyph run that stores platform dependent resources.
/// An immutable platform representation of a <see cref="GlyphRun"/>.
/// </summary>
[Unstable]
public interface IGlyphRunImpl : IDisposable
public interface IGlyphRunImpl : IDisposable
{
/// <summary>
/// Gets the conservative bounding box of the glyph run./>.
/// Gets the <see cref="IGlyphTypeface"/> for the <see cref="IGlyphRunImpl"/>.
/// </summary>
Rect Bounds { get; }
IGlyphTypeface GlyphTypeface { get; }
/// <summary>
/// Gets the em size used for rendering the <see cref="IGlyphRunImpl"/>.
/// </summary>
double FontRenderingEmSize { get; }
/// <summary>
/// Gets the baseline origin of the glyph run./>.
/// </summary>
Point BaselineOrigin { get; }
/// <summary>
/// Gets the conservative bounding box of the glyph run./>.
/// </summary>
Rect Bounds { get; }
/// <summary>
/// Gets the intersections of specified upper and lower limit.
/// </summary>

3
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@ -169,8 +169,9 @@ namespace Avalonia.Platform
/// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="glyphInfos">The list of glyphs.</param>
/// <param name="baselineOrigin">The baseline origin of the run. Can be null.</param>
/// <param name="bounds">the conservative bounding box of the run</param>
/// <returns>An <see cref="IGlyphRunImpl"/>.</returns>
IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin);
IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds);
/// <summary>
/// Creates a backend-specific object using a low-level API graphics context

8
src/Avalonia.Base/Platform/StandardAssetLoader.cs

@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using Avalonia.Metadata;
using Avalonia.Platform.Internal;
using Avalonia.Utilities;
@ -12,12 +13,13 @@ namespace Avalonia.Platform;
/// <summary>
/// Loads assets compiled into the application binary.
/// </summary>
internal class StandardAssetLoader : IAssetLoader
[Unstable("StandardAssetLoader is considered unstable. Please use AssetLoader static class instead.")]
public class StandardAssetLoader : IAssetLoader
{
private readonly IAssemblyDescriptorResolver _assemblyDescriptorResolver;
private AssemblyDescriptor? _defaultResmAssembly;
public StandardAssetLoader(IAssemblyDescriptorResolver resolver, Assembly? assembly = null)
internal StandardAssetLoader(IAssemblyDescriptorResolver resolver, Assembly? assembly = null)
{
if (assembly == null)
assembly = Assembly.GetEntryAssembly();
@ -153,6 +155,8 @@ internal class StandardAssetLoader : IAssetLoader
return Enumerable.Empty<Uri>();
}
public static void RegisterResUriParsers() => AssetLoader.RegisterResUriParsers();
private bool TryGetAsset(Uri uri, Uri? baseUri, [NotNullWhen(true)] out IAssetDescriptor? assetDescriptor)
{
assetDescriptor = null;

3
src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs

@ -50,7 +50,8 @@ internal static class StorageProviderHelpers
}
}
public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter)
[return: NotNullIfNotNull(nameof(path))]
public static string? NameWithExtension(string? path, string? defaultExtension, FilePickerFileType? filter)
{
var name = Path.GetFileName(path);
if (name != null && !Path.HasExtension(name))

4
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -260,6 +260,8 @@ public class CompositingRenderer : IRendererWithCompositor
if (!comp.Effect.EffectEquals(visual.Effect))
comp.Effect = visual.Effect?.ToImmutable();
comp.RenderOptions = visual.RenderOptions;
var renderTransform = Matrix.Identity;
if (visual.HasMirrorTransform)
@ -272,8 +274,6 @@ public class CompositingRenderer : IRendererWithCompositor
renderTransform *= (-offset) * visual.RenderTransform.Value * (offset);
}
comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform);
_recorder.BeginUpdate(comp.DrawList);

36
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs

@ -78,15 +78,14 @@ internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContex
}
}
internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
{
var next = NextDrawAs<ImageNode>();
if (next == null ||
!next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode))
!next.Item.Equals(Transform, source, opacity, sourceRect, destRect))
{
Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode));
Add(new ImageNode(Transform, source, opacity, sourceRect, destRect));
}
else
{
@ -227,20 +226,6 @@ internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContex
}
}
protected override void PopBitmapBlendModeCore()
{
var next = NextDrawAs<BitmapBlendModeNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new BitmapBlendModeNode());
}
else
{
++_drawOperationIndex;
}
}
protected override void PopOpacityCore()
{
var next = NextDrawAs<OpacityNode>();
@ -354,21 +339,6 @@ internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContex
_needsToPopOpacityMask.Push(needsToPop);
}
/// <inheritdoc/>
protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode)
{
var next = NextDrawAs<BitmapBlendModeNode>();
if (next == null || !next.Item.Equals(blendingMode))
{
Add(new BitmapBlendModeNode(blendingMode));
}
else
{
++_drawOperationIndex;
}
}
private void Add<T>(T node) where T : class, IDrawOperation
{
if (_drawOperationIndex < _builder.Count)

21
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@ -42,15 +42,20 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl,
set => _impl.Transform = (_transform = value) * PostTransform;
}
public RenderOptions RenderOptions
{
get => _impl.RenderOptions;
set => _impl.RenderOptions = value;
}
public void Clear(Color color)
{
_impl.Clear(color);
}
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
{
_impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode);
_impl.DrawBitmap(source, opacity, sourceRect, destRect);
}
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
@ -133,16 +138,6 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl,
_impl.PopGeometryClip();
}
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
{
_impl.PushBitmapBlendMode(blendingMode);
}
public void PopBitmapBlendMode()
{
_impl.PopBitmapBlendMode();
}
public object? GetFeature(Type t) => _impl.GetFeature(t);

2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -181,7 +181,7 @@ namespace Avalonia.Rendering.Composition.Server
else
targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1,
new Rect(_layerSize),
new Rect(Size), BitmapInterpolationMode.LowQuality);
new Rect(Size));
if (DebugOverlays != RendererDebugOverlays.None)
{

7
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -66,6 +66,8 @@ namespace Avalonia.Rendering.Composition.Server
if (OpacityMaskBrush != null)
canvas.PushOpacityMask(OpacityMaskBrush, boundsRect);
canvas.RenderOptions = RenderOptions;
RenderCore(canvas, currentTransformedClip);
// Hack to force invalidation of SKMatrix
@ -122,6 +124,11 @@ namespace Avalonia.Rendering.Composition.Server
var wasVisible = IsVisibleInFrame;
if(Parent != null)
{
RenderOptions = RenderOptions.MergeWith(Parent.RenderOptions);
}
// Calculate new parent-relative transform
if (_combinedTransformDirty)
{

120
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@ -44,79 +44,99 @@ namespace Avalonia.Rendering
public static void Render(DrawingContext context, Visual visual, Rect clipRect)
{
var opacity = visual.Opacity;
var clipToBounds = visual.ClipToBounds;
var bounds = new Rect(visual.Bounds.Size);
var currentRenderOptions = default(RenderOptions);
var platformContext = context as PlatformDrawingContext;
if (visual.IsVisible && opacity > 0)
try
{
var m = Matrix.CreateTranslation(visual.Bounds.Position);
var renderTransform = Matrix.Identity;
// this should be calculated BEFORE renderTransform
if (visual.HasMirrorTransform)
if (platformContext != null)
{
var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
renderTransform *= mirrorMatrix;
}
currentRenderOptions = platformContext.RenderOptions;
if (visual.RenderTransform != null)
{
var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
var offset = Matrix.CreateTranslation(origin);
var finalTransform = (-offset) * visual.RenderTransform.Value * (offset);
renderTransform *= finalTransform;
platformContext.RenderOptions = visual.RenderOptions.MergeWith(platformContext.RenderOptions);
}
m = renderTransform * m;
var opacity = visual.Opacity;
var clipToBounds = visual.ClipToBounds;
var bounds = new Rect(visual.Bounds.Size);
if (clipToBounds)
if (visual.IsVisible && opacity > 0)
{
var m = Matrix.CreateTranslation(visual.Bounds.Position);
var renderTransform = Matrix.Identity;
// this should be calculated BEFORE renderTransform
if (visual.HasMirrorTransform)
{
var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
renderTransform *= mirrorMatrix;
}
if (visual.RenderTransform != null)
{
clipRect = new Rect(visual.Bounds.Size);
var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
var offset = Matrix.CreateTranslation(origin);
var finalTransform = (-offset) * visual.RenderTransform.Value * (offset);
renderTransform *= finalTransform;
}
else
m = renderTransform * m;
if (clipToBounds)
{
clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size));
if (visual.RenderTransform != null)
{
clipRect = new Rect(visual.Bounds.Size);
}
else
{
clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size));
}
}
}
using (context.PushTransform(m))
using (context.PushOpacity(opacity, bounds))
using (clipToBounds
using (context.PushTransform(m))
using (context.PushOpacity(opacity, bounds))
using (clipToBounds
#pragma warning disable CS0618 // Type or member is obsolete
? visual is IVisualWithRoundRectClip roundClipVisual
? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius))
: context.PushClip(bounds)
: default)
? visual is IVisualWithRoundRectClip roundClipVisual
? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius))
: context.PushClip(bounds)
: default)
#pragma warning restore CS0618 // Type or member is obsolete
using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default)
using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default)
using (context.PushTransform(Matrix.Identity))
{
visual.Render(context);
var childrenEnumerable = visual.HasNonUniformZIndexChildren
? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)
: (IEnumerable<Visual>)visual.VisualChildren;
foreach (var child in childrenEnumerable)
using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default)
using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default)
using (context.PushTransform(Matrix.Identity))
{
var childBounds = GetTransformedBounds(child);
visual.Render(context);
var childrenEnumerable = visual.HasNonUniformZIndexChildren
? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)
: (IEnumerable<Visual>)visual.VisualChildren;
if (!child.ClipToBounds || clipRect.Intersects(childBounds))
foreach (var child in childrenEnumerable)
{
var childClipRect = child.RenderTransform == null
? clipRect.Translate(-childBounds.Position)
: clipRect;
Render(context, child, childClipRect);
}
var childBounds = GetTransformedBounds(child);
if (!child.ClipToBounds || clipRect.Intersects(childBounds))
{
var childClipRect = child.RenderTransform == null
? clipRect.Translate(-childBounds.Position)
: clipRect;
Render(context, child, childClipRect);
}
}
}
}
}
finally
{
if (platformContext != null)
{
platformContext.RenderOptions = currentRenderOptions;
}
}
}
}
}

68
src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs

@ -1,68 +0,0 @@
using Avalonia.Platform;
using Avalonia.Media.Imaging;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents an bitmap blending mode push or pop.
/// </summary>
internal class BitmapBlendModeNode : IDrawOperation
{
/// <summary>
/// Initializes a new instance of the <see cref="BitmapBlendModeNode"/> class that represents an
/// <see cref="BitmapBlendingMode"/> push.
/// </summary>
/// <param name="bitmapBlend">The <see cref="BitmapBlendingMode"/> to push.</param>
public BitmapBlendModeNode(BitmapBlendingMode bitmapBlend)
{
BlendingMode = bitmapBlend;
}
/// <summary>
/// Initializes a new instance of the <see cref="BitmapBlendModeNode"/> class that represents an
/// <see cref="BitmapBlendingMode"/> pop.
/// </summary>
public BitmapBlendModeNode()
{
}
/// <inheritdoc/>
public Rect Bounds => default;
/// <summary>
/// Gets the BitmapBlend to be pushed or null if the operation represents a pop.
/// </summary>
public BitmapBlendingMode? BlendingMode { get; }
/// <inheritdoc/>
public bool HitTest(Point p) => false;
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="blendingMode">the <see cref="BitmapBlendModeNode"/> how to compare</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(BitmapBlendingMode? blendingMode) => BlendingMode == blendingMode;
/// <inheritdoc/>
public void Render(IDrawingContextImpl context)
{
if (BlendingMode.HasValue)
{
context.PushBitmapBlendMode(BlendingMode.Value);
}
else
{
context.PopBitmapBlendMode();
}
}
public void Dispose()
{
}
}
}

5
src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs

@ -1,5 +1,4 @@
using System;
using Avalonia.Media;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Utilities;
@ -18,7 +17,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="glyphRun">The glyph run to draw.</param>
public GlyphRunNode(
Matrix transform,
IImmutableBrush foreground,
IImmutableBrush? foreground,
IRef<IGlyphRunImpl> glyphRun)
: base(glyphRun.Item.Bounds, transform, foreground)
{

21
src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs

@ -1,6 +1,5 @@
using Avalonia.Platform;
using Avalonia.Utilities;
using Avalonia.Media.Imaging;
namespace Avalonia.Rendering.SceneGraph
{
@ -17,15 +16,13 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="opacity">The draw opacity.</param>
/// <param name="sourceRect">The source rect.</param>
/// <param name="destRect">The destination rect.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
public ImageNode(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
public ImageNode(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
: base(destRect, transform)
{
Source = source.Clone();
Opacity = opacity;
SourceRect = sourceRect;
DestRect = destRect;
BitmapInterpolationMode = bitmapInterpolationMode;
SourceVersion = Source.Item.Version;
}
@ -53,14 +50,6 @@ namespace Avalonia.Rendering.SceneGraph
/// Gets the destination rect.
/// </summary>
public Rect DestRect { get; }
/// <summary>
/// Gets the bitmap interpolation mode.
/// </summary>
/// <value>
/// The scaling mode.
/// </value>
public BitmapInterpolationMode BitmapInterpolationMode { get; }
/// <summary>
/// Determines if this draw operation equals another.
@ -70,27 +59,25 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="opacity">The opacity of the other draw operation.</param>
/// <param name="sourceRect">The source rect of the other draw operation.</param>
/// <param name="destRect">The dest rect of the other draw operation.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
public bool Equals(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
{
return transform == Transform &&
Equals(source.Item, Source.Item) &&
source.Item.Version == SourceVersion &&
opacity == Opacity &&
sourceRect == SourceRect &&
destRect == DestRect &&
bitmapInterpolationMode == BitmapInterpolationMode;
destRect == DestRect;
}
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode);
context.DrawBitmap(Source, Opacity, SourceRect, DestRect);
}
/// <inheritdoc/>

2
src/Avalonia.Base/Threading/DispatcherOperation.cs

@ -331,6 +331,8 @@ public class DispatcherOperation<T> : DispatcherOperation
private TaskCompletionSource<T> TaskCompletionSource => (TaskCompletionSource<T>)TaskSource!;
public new TaskAwaiter<T> GetAwaiter() => GetTask().GetAwaiter();
public new Task<T> GetTask() => TaskCompletionSource!.Task;
protected override Task GetTaskCore() => GetTask();

4
src/Avalonia.Base/Visual.cs

@ -318,7 +318,9 @@ namespace Avalonia
internal CompositionDrawListVisual? CompositionVisual { get; private set; }
internal CompositionVisual? ChildCompositionVisual { get; set; }
internal RenderOptions RenderOptions { get; set; }
public bool HasNonUniformZIndexChildren { get; private set; }
/// <summary>

1
src/Avalonia.Base/composition-schema.xml

@ -30,6 +30,7 @@
<Property Name="AdornerIsClipped" Type="bool" Internal="true" />
<Property Name="OpacityMaskBrush" Type="Avalonia.Media.IImmutableBrush?" Internal="true" />
<Property Name="Effect" Type="Avalonia.Media.IImmutableEffect?" Internal="true" />
<Property Name="RenderOptions" Type="Avalonia.Media.RenderOptions" />
</Object>
<Object Name="CompositionContainerVisual" Inherits="CompositionVisual"/>
<Object Name="CompositionSolidColorVisual" Inherits="CompositionContainerVisual">

39
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -729,6 +729,8 @@ namespace Avalonia.Controls
RowDetailsTemplateProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnRowDetailsTemplateChanged(e));
RowDetailsVisibilityModeProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnRowDetailsVisibilityModeChanged(e));
AutoGenerateColumnsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnAutoGenerateColumnsChanged(e));
FocusableProperty.OverrideDefaultValue<DataGrid>(true);
}
/// <summary>
@ -2478,7 +2480,7 @@ namespace Avalonia.Controls
if (_hScrollBar != null)
{
//_hScrollBar.IsTabStop = false;
_hScrollBar.IsTabStop = false;
_hScrollBar.Maximum = 0.0;
_hScrollBar.Orientation = Orientation.Horizontal;
_hScrollBar.IsVisible = false;
@ -2494,7 +2496,7 @@ namespace Avalonia.Controls
if (_vScrollBar != null)
{
//_vScrollBar.IsTabStop = false;
_vScrollBar.IsTabStop = false;
_vScrollBar.Maximum = 0.0;
_vScrollBar.Orientation = Orientation.Vertical;
_vScrollBar.IsVisible = false;
@ -3734,7 +3736,7 @@ namespace Avalonia.Controls
if (sender is Control editingElement)
{
editingElement.LostFocus -= EditingElement_LostFocus;
if (EditingRow != null && EditingColumnIndex != -1)
if (EditingRow != null && _editingColumnIndex != -1)
{
FocusEditingCell(true);
}
@ -4039,18 +4041,22 @@ namespace Avalonia.Controls
return true;
}
Debug.Assert(EditingRow != null);
var editingRow = EditingRow;
if (editingRow is null)
{
return true;
}
Debug.Assert(_editingColumnIndex >= 0);
Debug.Assert(_editingColumnIndex < ColumnsItemsInternal.Count);
Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot);
// Cache these to see if they change later
int currentSlot = CurrentSlot;
int currentColumnIndex = CurrentColumnIndex;
// We're ready to start ending, so raise the event
DataGridCell editingCell = EditingRow.Cells[_editingColumnIndex];
DataGridCell editingCell = editingRow.Cells[_editingColumnIndex];
var editingElement = editingCell.Content as Control;
if (editingElement == null)
{
@ -4058,7 +4064,7 @@ namespace Avalonia.Controls
}
if (raiseEvents)
{
DataGridCellEditEndingEventArgs e = new DataGridCellEditEndingEventArgs(CurrentColumn, EditingRow, editingElement, editAction);
DataGridCellEditEndingEventArgs e = new DataGridCellEditEndingEventArgs(CurrentColumn, editingRow, editingElement, editAction);
OnCellEditEnding(e);
if (e.Cancel)
{
@ -4112,7 +4118,7 @@ namespace Avalonia.Controls
}
else
{
if (EditingRow != null)
if (editingRow != null)
{
if (editingCell.IsValid)
{
@ -4120,10 +4126,10 @@ namespace Avalonia.Controls
editingCell.UpdatePseudoClasses();
}
if (EditingRow.IsValid)
if (editingRow.IsValid)
{
EditingRow.IsValid = false;
EditingRow.UpdatePseudoClasses();
editingRow.IsValid = false;
editingRow.UpdatePseudoClasses();
}
}
@ -4169,22 +4175,22 @@ namespace Avalonia.Controls
PopulateCellContent(
isCellEdited: !exitEditingMode,
dataGridColumn: CurrentColumn,
dataGridRow: EditingRow,
dataGridRow: editingRow,
dataGridCell: editingCell);
EditingRow.InvalidateDesiredHeight();
editingRow.InvalidateDesiredHeight();
var column = editingCell.OwningColumn;
if (column.Width.IsSizeToCells || column.Width.IsAuto)
{// Invalidate desired width and force recalculation
column.SetWidthDesiredValue(0);
EditingRow.OwningGrid.AutoSizeColumn(column, editingCell.DesiredSize.Width);
editingRow.OwningGrid.AutoSizeColumn(column, editingCell.DesiredSize.Width);
}
}
// We're done, so raise the CellEditEnded event
if (raiseEvents)
{
OnCellEditEnded(new DataGridCellEditEndedEventArgs(CurrentColumn, EditingRow, editAction));
OnCellEditEnded(new DataGridCellEditEndedEventArgs(CurrentColumn, editingRow, editAction));
}
// There's a chance that somebody reopened this cell for edit within the CellEditEnded handler,
@ -4427,8 +4433,7 @@ namespace Avalonia.Controls
dataGridCell.Focus();
success = dataGridCell.ContainsFocusedElement();
}
//TODO Check
//success = dataGridCell.ContainsFocusedElement() ? true : dataGridCell.Focus();
_focusEditingControl = !success;
}
return success;

8
src/Avalonia.Controls.DataGrid/DataGridCell.cs

@ -33,6 +33,8 @@ namespace Avalonia.Controls
{
PointerPressedEvent.AddClassHandler<DataGridCell>(
(x,e) => x.DataGridCell_PointerPressed(e), handledEventsToo: true);
FocusableProperty.OverrideDefaultValue<DataGridCell>(true);
IsTabStopProperty.OverrideDefaultValue<DataGridCell>(false);
}
public DataGridCell()
{ }
@ -169,8 +171,7 @@ namespace Avalonia.Controls
OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e));
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)
if (!e.Handled && OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}
@ -190,8 +191,7 @@ namespace Avalonia.Controls
}
else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)
if (!e.Handled && OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}

1
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -72,6 +72,7 @@ namespace Avalonia.Controls
{
AreSeparatorsVisibleProperty.Changed.AddClassHandler<DataGridColumnHeader>((x, e) => x.OnAreSeparatorsVisibleChanged(e));
PressedMixin.Attach<DataGridColumnHeader>();
IsTabStopProperty.OverrideDefaultValue<DataGridColumnHeader>(false);
}
/// <summary>

14
src/Avalonia.Controls.DataGrid/DataGridColumns.cs

@ -12,6 +12,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Reflection;
using Avalonia.Layout;
namespace Avalonia.Controls
{
@ -489,7 +490,7 @@ namespace Avalonia.Controls
{
DataGridFillerColumn fillerColumn = ColumnsInternal.FillerColumn;
double totalColumnsWidth = ColumnsInternal.VisibleEdgedColumnsWidth;
if (finalWidth > totalColumnsWidth)
if (finalWidth - totalColumnsWidth > LayoutHelper.LayoutEpsilon)
{
fillerColumn.FillerWidth = finalWidth - totalColumnsWidth;
}
@ -971,6 +972,12 @@ namespace Avalonia.Controls
{
cx += _negHorizontalOffset;
_horizontalOffset -= _negHorizontalOffset;
if (_horizontalOffset < LayoutHelper.LayoutEpsilon)
{
// Snap to zero to avoid trying to partially scroll in first scrolled off column below
_horizontalOffset = 0;
}
_negHorizontalOffset = 0;
}
else
@ -979,6 +986,11 @@ namespace Avalonia.Controls
_negHorizontalOffset -= displayWidth - cx;
cx = displayWidth;
}
// Make sure the HorizontalAdjustment is not greater than the new HorizontalOffset
// since it would cause an assertion failure in DataGridCellsPresenter.ShouldDisplayCell
// called by DataGridCellsPresenter.MeasureOverride.
HorizontalAdjustment = Math.Min(HorizontalAdjustment, _horizontalOffset);
}
// second try to scroll entire columns
if (cx < displayWidth && _horizontalOffset > 0)

1
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@ -128,6 +128,7 @@ namespace Avalonia.Controls
DetailsTemplateProperty.Changed.AddClassHandler<DataGridRow>((x, e) => x.OnDetailsTemplateChanged(e));
AreDetailsVisibleProperty.Changed.AddClassHandler<DataGridRow>((x, e) => x.OnAreDetailsVisibleChanged(e));
PointerPressedEvent.AddClassHandler<DataGridRow>((x, e) => x.DataGridRow_PointerPressed(e), handledEventsToo: true);
IsTabStopProperty.OverrideDefaultValue<DataGridRow>(false);
}
/// <summary>

4
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@ -106,6 +106,7 @@ namespace Avalonia.Controls
{
SublevelIndentProperty.Changed.AddClassHandler<DataGridRowGroupHeader>((x,e) => x.OnSublevelIndentChanged(e));
PressedMixin.Attach<DataGridRowGroupHeader>();
IsTabStopProperty.OverrideDefaultValue<DataGridRowGroupHeader>(false);
}
/// <summary>
@ -301,8 +302,7 @@ namespace Avalonia.Controls
}
else
{
//if (!e.Handled && OwningGrid.IsTabStop)
if (!e.Handled)
if (!e.Handled && OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}

17
src/Avalonia.Controls.DataGrid/DataGridRows.cs

@ -1589,6 +1589,23 @@ namespace Avalonia.Controls
CorrectSlotsAfterDeletion(slot, isRow);
OnRemovedElement(slot, item);
// Synchronize CurrentCellCoordinates, CurrentColumn, CurrentColumnIndex, CurrentItem
// and CurrentSlot with the currently edited cell, since OnRemovingElement called
// SetCurrentCellCore(-1, -1) to temporarily reset the current cell.
if (_temporarilyResetCurrentCell &&
_editingColumnIndex != -1 &&
_previousCurrentItem != null &&
EditingRow != null &&
EditingRow.Slot != -1)
{
ProcessSelectionAndCurrency(
columnIndex: _editingColumnIndex,
item: _previousCurrentItem,
backupSlot: this.EditingRow.Slot,
action: DataGridSelectionAction.None,
scrollIntoView: false);
}
}
private void RemoveNonDisplayedRows(int newFirstDisplayedSlot, int newLastDisplayedSlot)

8
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -82,7 +82,6 @@
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="FontSize" Value="15" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="CellBorder"
@ -157,7 +156,6 @@
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Focusable" Value="False" />
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="Padding" Value="12,0,0,0" />
<Setter Property="FontSize" Value="12" />
@ -268,7 +266,6 @@
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridRowHeader}" TargetType="DataGridRowHeader">
<Setter Property="Focusable" Value="False" />
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="AreSeparatorsVisible" Value="False" />
<Setter Property="Template">
@ -310,7 +307,6 @@
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridRow}" TargetType="DataGridRow">
<Setter Property="Focusable" Value="False" />
<Setter Property="Background" Value="{Binding $parent[DataGrid].RowBackground}" />
<Setter Property="Template">
<ControlTemplate>
@ -408,7 +404,6 @@
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridRowGroupHeader}" TargetType="DataGridRowGroupHeader">
<Setter Property="Focusable" Value="False" />
<Setter Property="Foreground" Value="{DynamicResource DataGridRowGroupHeaderForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderBackgroundBrush}" />
<Setter Property="FontSize" Value="15" />
@ -433,7 +428,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}"
Focusable="False"
IsTabStop="False"
Foreground="{TemplateBinding Foreground}" />
<StackPanel Grid.Column="3"
@ -503,6 +498,7 @@
<Setter Property="GridLinesVisibility" Value="None" />
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="FocusAdorner" Value="{x:Null}" />
<Setter Property="DropLocationIndicatorTemplate">
<Template>
<Rectangle Fill="{DynamicResource DataGridDropLocationIndicatorBackground}"

1
src/Avalonia.Controls.DataGrid/Themes/Simple.xaml

@ -126,7 +126,6 @@
<ControlTheme x:Key="{x:Type DataGridRowHeader}"
TargetType="DataGridRowHeader">
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Grid x:Name="PART_Root"

2
src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs

@ -54,7 +54,7 @@ namespace Avalonia.Controls.Utils
/// <returns>True if the currently focused element is within the visual tree of the parent</returns>
internal static bool ContainsFocusedElement(this Visual element)
{
return (element == null) ? false : element.ContainsChild(FocusManager.Instance.Current as Visual);
return element is InputElement { IsKeyboardFocusWithin: true };
}
}
}

28
src/Avalonia.Controls/ComboBox.cs

@ -170,8 +170,15 @@ namespace Avalonia.Controls
UpdateFlowDirection();
}
protected internal override Control CreateContainerForItemOverride() => new ComboBoxItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ComboBoxItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ComboBoxItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<ComboBoxItem>(item, out recycleKey);
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
@ -443,7 +450,22 @@ namespace Avalonia.Controls
}
else
{
SelectionBoxItem = item;
if(ItemTemplate is null && DisplayMemberBinding is { } binding)
{
var template = new FuncDataTemplate<object?>((_, _) =>
new TextBlock
{
[TextBlock.DataContextProperty] = item,
[!TextBlock.TextProperty] = binding,
});
var text = template.Build(item);
SelectionBoxItem = text;
}
else
{
SelectionBoxItem = item;
}
}
}

4
src/Avalonia.Controls/Control.cs

@ -403,7 +403,9 @@ namespace Avalonia.Controls
{
if (_focusAdorner == null)
{
var template = GetValue(FocusAdornerProperty) ?? adornerLayer.DefaultFocusAdorner;
var template = IsSet(FocusAdornerProperty)
? GetValue(FocusAdornerProperty)
: adornerLayer.DefaultFocusAdorner;
if (template != null)
{

10
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@ -24,9 +24,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="ItemTemplate"/> property
/// </summary>
public static readonly DirectProperty<MenuFlyout, IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.RegisterDirect<MenuFlyout, IDataTemplate?>(nameof(ItemTemplate),
x => x.ItemTemplate, (x, v) => x.ItemTemplate = v);
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.Register<MenuFlyout, IDataTemplate?>(nameof(ItemTemplate));
/// <summary>
/// Defines the <see cref="ItemContainerTheme"/> property.
@ -59,8 +58,8 @@ namespace Avalonia.Controls
/// </summary>
public IDataTemplate? ItemTemplate
{
get => _itemTemplate;
set => SetAndRaise(ItemTemplateProperty, ref _itemTemplate, value);
get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
/// <summary>
@ -82,7 +81,6 @@ namespace Avalonia.Controls
}
private Classes? _classes;
private IDataTemplate? _itemTemplate;
protected override Control CreatePresenter()
{

101
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@ -10,34 +10,44 @@ namespace Avalonia.Controls.Generators
/// When creating a container for an item from a <see cref="VirtualizingPanel"/>, the following
/// process should be followed:
///
/// - <see cref="IsItemItsOwnContainer(Control)"/> should first be called if the item is
/// derived from the <see cref="Control"/> class. If this method returns true then the
/// item itself should be used as the container.
/// - If <see cref="IsItemItsOwnContainer(Control)"/> returns false then
/// <see cref="CreateContainer"/> should be called to create a new container.
/// - <see cref="NeedsContainer(object, int, out object?)"/> should first be called to
/// determine whether the item needs a container. This method will return true if the item
/// should be wrapped in a container control, or false if the item itself can be used as a
/// container.
/// - If <see cref="NeedsContainer(object, int, out object?)"/> returns true then the
/// <see cref="CreateContainer"/> method should be called to create a new container, passing
/// the recycle key returned from <see cref="NeedsContainer(object, int, out object?)"/>.
/// - If the panel supports recycling and the recycle key is non-null then the recycle key
/// should be recorded for the container (e.g. in an attached property or the realized
/// container list).
/// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
/// container.
/// - The container should then be added to the panel using
/// <see cref="VirtualizingPanel.AddInternalChild(Control)"/>
/// - Finally, <see cref="ItemContainerPrepared(Control, object?, int)"/> should be called.
///
/// NOTE: If <see cref="IsItemItsOwnContainer(Control)"/> in the first step above returns true
/// then the above steps should be carried out a single time; the first time the item is
/// displayed. Otherwise the steps should be carried out each time a new container is realized
/// for an item.
/// NOTE: If <see cref="NeedsContainer(object, int, out object?)"/> in the first step above
/// returns false then the above steps should be carried out a single time: the first time the
/// item is displayed. Otherwise the steps should be carried out each time a new container is
/// realized for an item.
///
/// When unrealizing a container, the following process should be followed:
///
/// - If <see cref="IsItemItsOwnContainer(Control)"/> for the item returned true then the item
/// cannot be unrealized or recycled.
/// - If <see cref="NeedsContainer(object, int, out object?)"/> for the item returned false
/// then the item cannot be unrealized or recycled.
/// - Otherwise, <see cref="ClearItemContainer(Control)"/> should be called for the container
/// - If recycling is supported then the container should be added to a recycle pool.
/// - It is assumed that recyclable containers will not be removed from the panel but instead
/// hidden from view using e.g. `container.IsVisible = false`.
/// - If recycling is supported by the panel and the container then the container should be
/// added to a recycle pool keyed on the recycle key returned from
/// <see cref="NeedsContainer(object, int, out object?)"/>. It is assumed that recycled
/// containers will not be removed from the panel but instead hidden from view using
/// e.g. `container.IsVisible = false`.
/// - If recycling is not supported then the container should be removed from the panel.
///
/// When recycling an unrealized container, the following process should be followed:
///
/// - An element should be taken from the recycle pool.
/// - <see cref="NeedsContainer(object, int, out object?)"/> should be called to determine
/// whether the item needs a container, and if so, the recycle key.
/// - A container should be taken from the recycle pool keyed on the returned recycle key.
/// - The container should be made visible.
/// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
/// container.
@ -55,28 +65,43 @@ namespace Avalonia.Controls.Generators
internal ItemContainerGenerator(ItemsControl owner) => _owner = owner;
/// <summary>
/// Creates a new container control.
/// Determines whether the specified item needs to be wrapped in a container control.
/// </summary>
/// <returns>The newly created container control.</returns>
/// <remarks>
/// Before calling this method, <see cref="IsItemItsOwnContainer(Control)"/> should be
/// called to determine whether the item itself should be used as a container. After
/// calling this method, <see cref="PrepareItemContainer(Control, object, int)"/> should
/// be called to prepare the container to display the specified item.
/// </remarks>
public Control CreateContainer() => _owner.CreateContainerForItemOverride();
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item.</param>
/// <param name="recycleKey">
/// When the method returns, contains a key that can be used to locate a previously
/// recycled container of the correct type, or null if the item cannot be recycled.
/// </param>
/// <returns>
/// true if the item needs a container; otherwise false if the item can itself be used
/// as a container.
/// </returns>
public bool NeedsContainer(object? item, int index, out object? recycleKey) =>
_owner.NeedsContainerOverride(item, index, out recycleKey);
/// <summary>
/// Determines whether the specified item is (or is eligible to be) its own container.
/// Creates a new container control.
/// </summary>
/// <param name="container">The item.</param>
/// <returns>true if the item is its own container, otherwise false.</returns>
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item.</param>
/// <param name="recycleKey">
/// The recycle key returned from <see cref="NeedsContainer(object, int, out object?)"/>
/// </param>
/// <returns>The newly created container control.</returns>
/// <remarks>
/// Whereas in WPF/UWP, non-control items can be their own container, in Avalonia only
/// control items may be; the caller is responsible for checking if each item is a control
/// and calling this method before creating a new container.
/// Before calling this method, <see cref="NeedsContainer(object, int, out object?)"/>
/// should be called to determine whether the item itself should be used as a container.
/// After calling this method, <see cref="PrepareItemContainer(Control, object, int)"/>
/// must be called to prepare the container to display the specified item.
///
/// If the panel supports recycling then the returned recycle key should be stored alongside
/// the container and when container becomes eligible for recycling the container should
/// be placed in a recycle pool using this key. If the returned recycle key is null then
/// the container cannot be recycled.
/// </remarks>
public bool IsItemItsOwnContainer(Control container) => _owner.IsItemItsOwnContainerOverride(container);
public Control CreateContainer(object? item, int index, object? recycleKey)
=> _owner.CreateContainerForItemOverride(item, index, recycleKey);
/// <summary>
/// Prepares the specified element as the container for the corresponding item.
@ -85,10 +110,10 @@ namespace Avalonia.Controls.Generators
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item to display.</param>
/// <remarks>
/// If <see cref="IsItemItsOwnContainer(Control)"/> is true for an item, then this method
/// must only be called a single time, otherwise this method must be called after the
/// container is created, and each subsequent time the container is recycled to display a
/// new item.
/// If <see cref="NeedsContainer(object, int, out object?)"/> is false for an
/// item, then this method must only be called a single time; otherwise this method must
/// be called after the container is created, and each subsequent time the container is
/// recycled to display a new item.
/// </remarks>
public void PrepareItemContainer(Control container, object? item, int index) =>
_owner.PrepareItemContainer(container, item, index);
@ -104,8 +129,8 @@ namespace Avalonia.Controls.Generators
/// This method must be called when a container has been fully prepared and added
/// to the logical and visual trees, but may be called before a layout pass has completed.
/// It must be called regardless of the result of
/// <see cref="IsItemItsOwnContainer(Control)"/> but if that method returned true then
/// must be called only a single time.
/// <see cref="NeedsContainer(object, int, out object?)"/> but if that method returned
/// false then must be called only a single time.
/// </remarks>
public void ItemContainerPrepared(Control container, object? item, int index) =>
_owner.ItemContainerPrepared(container, item, index);
@ -128,7 +153,7 @@ namespace Avalonia.Controls.Generators
/// This method must be called when a container is unrealized. The container must have
/// already have been removed from the virtualizing panel's list of realized containers before
/// this method is called. This method must not be called if
/// <see cref="IsItemItsOwnContainer"/> returned true for the item.
/// <see cref="NeedsContainer(object, int, out object?)"/> returned false for the item.
/// </remarks>
public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container);

4
src/Avalonia.Controls/Image.cs

@ -91,9 +91,7 @@ namespace Avalonia.Controls
Rect sourceRect = new Rect(sourceSize)
.CenterRect(new Rect(destRect.Size / scale));
var interpolationMode = RenderOptions.GetBitmapInterpolationMode(this);
context.DrawImage(source, sourceRect, destRect, interpolationMode);
context.DrawImage(source, sourceRect, destRect);
}
}

58
src/Avalonia.Controls/ItemsControl.cs

@ -307,6 +307,12 @@ namespace Avalonia.Controls
set => SetValue(AreVerticalSnapPointsRegularProperty, value);
}
/// <summary>
/// Gets a default recycle key that can be used when an <see cref="ItemsControl"/> supports
/// a single container type.
/// </summary>
protected static object DefaultRecycleKey { get; } = new object();
/// <summary>
/// Returns the container for the item at the specified index.
/// </summary>
@ -362,7 +368,10 @@ namespace Avalonia.Controls
/// <summary>
/// Creates or a container that can be used to display an item.
/// </summary>
protected internal virtual Control CreateContainerForItemOverride() => new ContentPresenter();
protected internal virtual Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ContentPresenter();
}
/// <summary>
/// Prepares the specified element to display the specified item.
@ -495,11 +504,52 @@ namespace Avalonia.Controls
}
/// <summary>
/// Determines whether the specified item is (or is eligible to be) its own container.
/// Determines whether the specified item can be its own container.
/// </summary>
/// <param name="item">The item to check.</param>
/// <returns>true if the item is (or is eligible to be) its own container; otherwise, false.</returns>
protected internal virtual bool IsItemItsOwnContainerOverride(Control item) => true;
/// <param name="index">The index of the item.</param>
/// <param name="recycleKey">
/// When the method returns, contains a key that can be used to locate a previously
/// recycled container of the correct type, or null if the item cannot be recycled.
/// If the item is its own container then by definition it cannot be recycled, so
/// <paramref name="recycleKey"/> shoud be set to null.
/// </param>
/// <returns>
/// true if the item needs a container; otherwise false if the item can itself be used
/// as a container.
/// </returns>
protected internal virtual bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<Control>(item, out recycleKey);
}
/// <summary>
/// A default implementation of <see cref="NeedsContainerOverride(object, int, out object?)"/>
/// that returns true and sets the recycle key to <see cref="DefaultRecycleKey"/> if the item
/// is not a <typeparamref name="T"/> .
/// </summary>
/// <typeparam name="T">The container type.</typeparam>
/// <param name="item">The item.</param>
/// <param name="recycleKey">
/// When the method returns, contains <see cref="DefaultRecycleKey"/> if
/// <paramref name="item"/> is not of type <typeparamref name="T"/>; otherwise null.
/// </param>
/// <returns>
/// true if <paramref name="item"/> is of type <typeparamref name="T"/>; otherwise false.
/// </returns>
protected bool NeedsContainer<T>(object? item, out object? recycleKey) where T : Control
{
if (item is T)
{
recycleKey = null;
return false;
}
else
{
recycleKey = DefaultRecycleKey;
return true;
}
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)

11
src/Avalonia.Controls/ListBox.cs

@ -108,8 +108,15 @@ namespace Avalonia.Controls
/// </summary>
public void UnselectAll() => Selection.Clear();
protected internal override Control CreateContainerForItemOverride() => new ListBoxItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ListBoxItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ListBoxItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<ListBoxItem>(item, out recycleKey);
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)

18
src/Avalonia.Controls/MenuBase.cs

@ -133,8 +133,22 @@ namespace Avalonia.Controls
/// <inheritdoc/>
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
protected internal override Control CreateContainerForItemOverride() => new MenuItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new MenuItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
if (item is MenuItem or Separator)
{
recycleKey = null;
return false;
}
recycleKey = DefaultRecycleKey;
return true;
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)

18
src/Avalonia.Controls/MenuItem.cs

@ -339,8 +339,22 @@ namespace Avalonia.Controls
/// <inheritdoc/>
void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent));
protected internal override Control CreateContainerForItemOverride() => new MenuItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new MenuItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
if (item is MenuItem or Separator)
{
recycleKey = null;
return false;
}
recycleKey = DefaultRecycleKey;
return true;
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{

8
src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs

@ -113,14 +113,14 @@ namespace Avalonia.Controls.Presenters
var generator = itemsControl.ItemContainerGenerator;
Control container;
if (item is Control c && generator.IsItemItsOwnContainer(c))
if (generator.NeedsContainer(item, index, out var recycleKey))
{
container = c;
container.SetValue(ItemIsOwnContainerProperty, true);
container = generator.CreateContainer(item, index, recycleKey);
}
else
{
container = generator.CreateContainer();
container = (Control)item!;
container.SetValue(ItemIsOwnContainerProperty, true);
}
generator.PrepareItemContainer(container, item, index);

11
src/Avalonia.Controls/Primitives/TabStrip.cs

@ -16,8 +16,15 @@ namespace Avalonia.Controls.Primitives
ItemsPanelProperty.OverrideDefaultValue<TabStrip>(DefaultPanel);
}
protected internal override Control CreateContainerForItemOverride() => new TabStripItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabStripItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new TabStripItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<TabStripItem>(item, out recycleKey);
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)

15
src/Avalonia.Controls/Primitives/Thumb.cs

@ -4,7 +4,6 @@ using Avalonia.Controls.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
@ -47,6 +46,12 @@ namespace Avalonia.Controls.Primitives
remove { RemoveHandler(DragCompletedEvent, value); }
}
internal void AdjustDrag(Vector v)
{
if (_lastPoint.HasValue)
_lastPoint = _lastPoint.Value + v;
}
protected override AutomationPeer OnCreateAutomationPeer() => new ThumbAutomationPeer(this);
protected virtual void OnDragStarted(VectorEventArgs e)
@ -85,22 +90,20 @@ namespace Avalonia.Controls.Primitives
{
if (_lastPoint.HasValue)
{
var point = e.GetPosition(null);
var ev = new VectorEventArgs
{
RoutedEvent = DragDeltaEvent,
Vector = point - _lastPoint.Value,
Vector = e.GetPosition(this) - _lastPoint.Value,
};
RaiseEvent(ev);
_lastPoint = point;
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
e.Handled = true;
_lastPoint = e.GetPosition(null);
_lastPoint = e.GetPosition(this);
var ev = new VectorEventArgs
{
@ -123,7 +126,7 @@ namespace Avalonia.Controls.Primitives
var ev = new VectorEventArgs
{
RoutedEvent = DragCompletedEvent,
Vector = (Vector)e.GetPosition(null),
Vector = (Vector)e.GetPosition(this),
};
RaiseEvent(ev);

28
src/Avalonia.Controls/Primitives/Track.cs

@ -45,6 +45,8 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<bool> IgnoreThumbDragProperty =
AvaloniaProperty.Register<Track, bool>(nameof(IgnoreThumbDrag));
private Vector _lastDrag;
static Track()
{
ThumbProperty.Changed.AddClassHandler<Track>((x, e) => x.ThumbChanged(e));
@ -245,7 +247,10 @@ namespace Avalonia.Controls.Primitives
if (Thumb != null)
{
Thumb.Arrange(new Rect(offset, pieceSize));
var bounds = new Rect(offset, pieceSize);
var adjust = CalculateThumbAdjustment(Thumb, bounds);
Thumb.Arrange(bounds);
Thumb.AdjustDrag(adjust);
}
ThumbCenterOffset = offset.Y + (thumbLength * 0.5);
@ -277,12 +282,16 @@ namespace Avalonia.Controls.Primitives
if (Thumb != null)
{
Thumb.Arrange(new Rect(offset, pieceSize));
var bounds = new Rect(offset, pieceSize);
var adjust = CalculateThumbAdjustment(Thumb, bounds);
Thumb.Arrange(bounds);
Thumb.AdjustDrag(adjust);
}
ThumbCenterOffset = offset.X + (thumbLength * 0.5);
}
_lastDrag = default;
return arrangeSize;
}
@ -296,6 +305,12 @@ namespace Avalonia.Controls.Primitives
}
}
private Vector CalculateThumbAdjustment(Thumb thumb, Rect newThumbBounds)
{
var thumbDelta = newThumbBounds.Position - thumb.Bounds.Position;
return _lastDrag - thumbDelta;
}
private static void CoerceLength(ref double componentLength, double trackLength)
{
if (componentLength < 0)
@ -440,10 +455,17 @@ namespace Avalonia.Controls.Primitives
if (IgnoreThumbDrag)
return;
var value = Value;
var delta = ValueFromDistance(e.Vector.X, e.Vector.Y);
var factor = e.Vector / delta;
SetCurrentValue(ValueProperty, MathUtilities.Clamp(
Value + ValueFromDistance(e.Vector.X, e.Vector.Y),
value + delta,
Minimum,
Maximum));
// Record the part of the drag that actually had effect as the last drag delta.
_lastDrag = (Value - value) * factor;
}
private void ShowChildren(bool visible)

11
src/Avalonia.Controls/TabControl.cs

@ -148,8 +148,15 @@ namespace Avalonia.Controls
return RegisterContentPresenter(presenter);
}
protected internal override Control CreateContainerForItemOverride() => new TabItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new TabItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<TabItem>(item, out recycleKey);
}
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{

11
src/Avalonia.Controls/TreeView.cs

@ -486,8 +486,15 @@ namespace Avalonia.Controls
return (false, null);
}
protected internal override Control CreateContainerForItemOverride() => new TreeViewItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TreeViewItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new TreeViewItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<TreeViewItem>(item, out recycleKey);
}
protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index)
{

10
src/Avalonia.Controls/TreeViewItem.cs

@ -91,14 +91,14 @@ namespace Avalonia.Controls
internal TreeView? TreeViewOwner => _treeView;
protected internal override Control CreateContainerForItemOverride()
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return EnsureTreeView().CreateContainerForItemOverride();
return EnsureTreeView().CreateContainerForItemOverride(item, index, recycleKey);
}
protected internal override bool IsItemItsOwnContainerOverride(Control item)
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return EnsureTreeView().IsItemItsOwnContainerOverride(item);
return EnsureTreeView().NeedsContainerOverride(item, index, out recycleKey);
}
protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index)
@ -165,7 +165,7 @@ namespace Avalonia.Controls
{
Key.Left => ApplyToItemOrRecursivelyIfCtrl(FocusAwareCollapseItem, e.KeyModifiers),
Key.Right => ApplyToItemOrRecursivelyIfCtrl(ExpandItem, e.KeyModifiers),
Key.Enter or Key.Space => ApplyToItemOrRecursivelyIfCtrl(IsExpanded ? CollapseItem : ExpandItem, e.KeyModifiers),
Key.Enter => ApplyToItemOrRecursivelyIfCtrl(IsExpanded ? CollapseItem : ExpandItem, e.KeyModifiers),
// do not handle CTRL with numpad keys
Key.Subtract => FocusAwareCollapseItem,

3
src/Avalonia.Controls/Utils/StringUtils.cs

@ -1,3 +1,4 @@
using System;
using System.Globalization;
using Avalonia.Media.TextFormatting.Unicode;
@ -122,6 +123,8 @@ namespace Avalonia.Controls.Utils
{
return 0;
}
cursor = Math.Min(cursor, text.Length);
int begin;
int i;

97
src/Avalonia.Controls/VirtualizingCarouselPanel.cs

@ -15,13 +15,14 @@ namespace Avalonia.Controls
/// </summary>
public class VirtualizingCarouselPanel : VirtualizingPanel, ILogicalScrollable
{
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
AvaloniaProperty.RegisterAttached<VirtualizingCarouselPanel, Control, bool>("ItemIsOwnContainer");
private static readonly AttachedProperty<object?> RecycleKeyProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
private static readonly object s_itemIsItsOwnContainer = new object();
private Size _extent;
private Vector _offset;
private Size _viewport;
private Stack<Control>? _recyclePool;
private Dictionary<object, Stack<Control>>? _recyclePool;
private Control? _realized;
private int _realizedIndex = -1;
private Control? _transitionFrom;
@ -172,7 +173,7 @@ namespace Avalonia.Controls
return null;
if (index == _realizedIndex)
return _realized;
if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty))
if (Items[index] is Control c && c.GetValue(RecycleKeyProperty) == s_itemIsItsOwnContainer)
return c;
return null;
}
@ -246,10 +247,27 @@ namespace Avalonia.Controls
private Control GetOrCreateElement(IReadOnlyList<object?> items, int index)
{
return GetRealizedElement(index) ??
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ??
CreateElement(items, index);
Debug.Assert(ItemContainerGenerator is not null);
var e = GetRealizedElement(index);
if (e is null)
{
var item = items[index];
var generator = ItemContainerGenerator!;
if (generator.NeedsContainer(item, index, out var recycleKey))
{
e = GetRecycledElement(item, index, recycleKey) ??
CreateElement(item, index, recycleKey);
}
else
{
e = GetItemAsOwnContainer(item, index);
}
}
return e;
}
private Control? GetRealizedElement(int index)
@ -257,44 +275,37 @@ namespace Avalonia.Controls
return _realizedIndex == index ? _realized : null;
}
private Control? GetItemIsOwnContainer(IReadOnlyList<object?> items, int index)
private Control GetItemAsOwnContainer(object? item, int index)
{
Debug.Assert(ItemContainerGenerator is not null);
var item = items[index];
var controlItem = (Control)item!;
var generator = ItemContainerGenerator!;
if (item is Control controlItem)
if (!controlItem.IsSet(RecycleKeyProperty))
{
var generator = ItemContainerGenerator;
if (controlItem.IsSet(ItemIsOwnContainerProperty))
{
controlItem.IsVisible = true;
return controlItem;
}
else if (generator.IsItemItsOwnContainer(controlItem))
{
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(ItemIsOwnContainerProperty, true);
generator.ItemContainerPrepared(controlItem, item, index);
return controlItem;
}
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer);
generator.ItemContainerPrepared(controlItem, item, index);
}
return null;
controlItem.IsVisible = true;
return controlItem;
}
private Control? GetRecycledElement(IReadOnlyList<object?> items, int index)
private Control? GetRecycledElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator;
var item = items[index];
if (recycleKey is null)
return null;
var generator = ItemContainerGenerator!;
if (_recyclePool?.Count > 0)
if (_recyclePool?.TryGetValue(recycleKey, out var recyclePool) == true && recyclePool.Count > 0)
{
var recycled = _recyclePool.Pop();
var recycled = recyclePool.Pop();
recycled.IsVisible = true;
generator.PrepareItemContainer(recycled, item, index);
generator.ItemContainerPrepared(recycled, item, index);
@ -304,14 +315,14 @@ namespace Avalonia.Controls
return null;
}
private Control CreateElement(IReadOnlyList<object?> items, int index)
private Control CreateElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator;
var item = items[index];
var container = generator.CreateContainer();
var generator = ItemContainerGenerator!;
var container = generator.CreateContainer(item, index, recycleKey);
container.SetValue(RecycleKeyProperty, recycleKey);
generator.PrepareItemContainer(container, item, index);
AddInternalChild(container);
generator.ItemContainerPrepared(container, item, index);
@ -323,7 +334,10 @@ namespace Avalonia.Controls
{
Debug.Assert(ItemContainerGenerator is not null);
if (element.IsSet(ItemIsOwnContainerProperty))
var recycleKey = element.GetValue(RecycleKeyProperty);
Debug.Assert(recycleKey is not null);
if (recycleKey == s_itemIsItsOwnContainer)
{
element.IsVisible = false;
}
@ -331,7 +345,14 @@ namespace Avalonia.Controls
{
ItemContainerGenerator.ClearItemContainer(element);
_recyclePool ??= new();
_recyclePool.Push(element);
if (!_recyclePool.TryGetValue(recycleKey, out var pool))
{
pool = new();
_recyclePool.Add(recycleKey, pool);
}
pool.Push(element);
element.IsVisible = false;
}
}

112
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -52,10 +52,11 @@ namespace Avalonia.Controls
nameof(VerticalSnapPointsChanged),
RoutingStrategies.Bubble);
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, bool>("ItemIsOwnContainer");
private static readonly AttachedProperty<object?> RecycleKeyProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0);
private static readonly object s_itemIsItsOwnContainer = new object();
private readonly Action<Control, int> _recycleElement;
private readonly Action<Control> _recycleElementOnItemRemoved;
private readonly Action<Control, int, int> _updateElementIndex;
@ -68,7 +69,7 @@ namespace Avalonia.Controls
private RealizedStackElements? _realizedElements;
private ScrollViewer? _scrollViewer;
private Rect _viewport = s_invalidViewport;
private Stack<Control>? _recyclePool;
private Dictionary<object, Stack<Control>>? _recyclePool;
private Control? _unrealizedFocusedElement;
private int _unrealizedFocusedIndex = -1;
@ -331,7 +332,7 @@ namespace Avalonia.Controls
return null;
if (_realizedElements?.GetElement(index) is { } realized)
return realized;
if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty))
if (Items[index] is Control c && c.GetValue(RecycleKeyProperty) == s_itemIsItsOwnContainer)
return c;
return null;
}
@ -492,8 +493,7 @@ namespace Avalonia.Controls
c = c?.GetVisualParent();
}
return viewport;
return viewport.Intersect(new Rect(0, 0, double.PositiveInfinity, double.PositiveInfinity));
}
private void RealizeElements(
@ -561,10 +561,26 @@ namespace Avalonia.Controls
private Control GetOrCreateElement(IReadOnlyList<object?> items, int index)
{
var e = GetRealizedElement(index) ??
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ??
CreateElement(items, index);
Debug.Assert(ItemContainerGenerator is not null);
var e = GetRealizedElement(index);
if (e is null)
{
var item = items[index];
var generator = ItemContainerGenerator!;
if (generator.NeedsContainer(item, index, out var recycleKey))
{
e = GetRecycledElement(item, index, recycleKey) ??
CreateElement(item, index, recycleKey);
}
else
{
e = GetItemAsOwnContainer(item, index);
}
}
return e;
}
@ -575,38 +591,33 @@ namespace Avalonia.Controls
return _realizedElements?.GetElement(index);
}
private Control? GetItemIsOwnContainer(IReadOnlyList<object?> items, int index)
private Control GetItemAsOwnContainer(object? item, int index)
{
var item = items[index];
Debug.Assert(ItemContainerGenerator is not null);
if (item is Control controlItem)
{
var generator = ItemContainerGenerator!;
var controlItem = (Control)item!;
var generator = ItemContainerGenerator!;
if (controlItem.IsSet(ItemIsOwnContainerProperty))
{
controlItem.IsVisible = true;
return controlItem;
}
else if (generator.IsItemItsOwnContainer(controlItem))
{
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(ItemIsOwnContainerProperty, true);
generator.ItemContainerPrepared(controlItem, item, index);
return controlItem;
}
if (!controlItem.IsSet(RecycleKeyProperty))
{
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer);
generator.ItemContainerPrepared(controlItem, item, index);
}
return null;
controlItem.IsVisible = true;
return controlItem;
}
private Control? GetRecycledElement(IReadOnlyList<object?> items, int index)
private Control? GetRecycledElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
if (recycleKey is null)
return null;
var generator = ItemContainerGenerator!;
var item = items[index];
if (_unrealizedFocusedIndex == index && _unrealizedFocusedElement is not null)
{
@ -617,9 +628,9 @@ namespace Avalonia.Controls
return element;
}
if (_recyclePool?.Count > 0)
if (_recyclePool?.TryGetValue(recycleKey, out var recyclePool) == true && recyclePool.Count > 0)
{
var recycled = _recyclePool.Pop();
var recycled = recyclePool.Pop();
recycled.IsVisible = true;
generator.PrepareItemContainer(recycled, item, index);
generator.ItemContainerPrepared(recycled, item, index);
@ -629,14 +640,14 @@ namespace Avalonia.Controls
return null;
}
private Control CreateElement(IReadOnlyList<object?> items, int index)
private Control CreateElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator!;
var item = items[index];
var container = generator.CreateContainer();
var container = generator.CreateContainer(item, index, recycleKey);
container.SetValue(RecycleKeyProperty, recycleKey);
generator.PrepareItemContainer(container, item, index);
AddInternalChild(container);
generator.ItemContainerPrepared(container, item, index);
@ -650,7 +661,10 @@ namespace Avalonia.Controls
_scrollViewer?.UnregisterAnchorCandidate(element);
if (element.IsSet(ItemIsOwnContainerProperty))
var recycleKey = element.GetValue(RecycleKeyProperty);
Debug.Assert(recycleKey is not null);
if (recycleKey == s_itemIsItsOwnContainer)
{
element.IsVisible = false;
}
@ -663,8 +677,7 @@ namespace Avalonia.Controls
else
{
ItemContainerGenerator!.ClearItemContainer(element);
_recyclePool ??= new();
_recyclePool.Push(element);
PushToRecyclePool(recycleKey, element);
element.IsVisible = false;
}
}
@ -673,19 +686,34 @@ namespace Avalonia.Controls
{
Debug.Assert(ItemContainerGenerator is not null);
if (element.IsSet(ItemIsOwnContainerProperty))
var recycleKey = element.GetValue(RecycleKeyProperty);
Debug.Assert(recycleKey is not null);
if (recycleKey == s_itemIsItsOwnContainer)
{
RemoveInternalChild(element);
}
else
{
ItemContainerGenerator!.ClearItemContainer(element);
_recyclePool ??= new();
_recyclePool.Push(element);
PushToRecyclePool(recycleKey, element);
element.IsVisible = false;
}
}
private void PushToRecyclePool(object recycleKey, Control element)
{
_recyclePool ??= new();
if (!_recyclePool.TryGetValue(recycleKey, out var pool))
{
pool = new();
_recyclePool.Add(recycleKey, pool);
}
pool.Push(element);
}
private void UpdateElementIndex(Control element, int oldIndex, int newIndex)
{
Debug.Assert(ItemContainerGenerator is not null);

1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@ -522,7 +522,6 @@ namespace Avalonia.Diagnostics.ViewModels
.GroupBy(x => x.Key)
.ToDictionary(x => x.Key, x => x.ToArray());
TreePage.PropertiesFilter.FilterString = string.Empty;
var view = new DataGridCollectionView(properties);
view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)));

4
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@ -237,9 +237,9 @@ namespace Avalonia.Diagnostics.Views
else
{
//TODO Use Dictionary.Remove(Key, out Value) in netstandard 2.1
if (_frozenPopupStates.ContainsKey(popup))
if (_frozenPopupStates.TryGetValue(popup, out var value))
{
_frozenPopupStates[popup].Dispose();
value.Dispose();
_frozenPopupStates.Remove(popup);
}
}

13
src/Avalonia.Native/AvaloniaNativeDragSource.cs

@ -4,7 +4,6 @@ using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Native.Interop;
namespace Avalonia.Native
@ -18,16 +17,6 @@ namespace Avalonia.Native
_factory = factory;
}
private static TopLevel FindRoot(object? element)
{
while (element is Interactive interactive && element is not Visual)
element = interactive.GetInteractiveParent();
if (element == null)
return null;
var visual = (Visual)element;
return TopLevel.GetTopLevel(visual);
}
class DndCallback : NativeCallbackBase, IAvnDndResultCallback
{
private TaskCompletionSource<DragDropEffects> _tcs;
@ -46,7 +35,7 @@ namespace Avalonia.Native
public Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects)
{
// Sanity check
var tl = FindRoot(triggerEvent.Source);
var tl = TopLevel.GetTopLevel(triggerEvent.Source as Visual);
var view = tl?.PlatformImpl as WindowBaseImpl;
if (view == null)
throw new ArgumentException();

9
src/Avalonia.Remote.Protocol/MetsysBson.cs

@ -715,7 +715,8 @@ namespace Metsys.Bson
public MagicProperty FindProperty(string name)
{
return _properties.ContainsKey(name) ? _properties[name] : null;
_properties.TryGetValue(name, out var property);
return property;
}
public static TypeHelper GetHelperForType(Type type)
@ -1196,7 +1197,9 @@ namespace Metsys.Bson
}
object container = null;
var property = typeHelper.FindProperty(name);
var propertyType = property != null ? property.Type : _typeMap.ContainsKey(storageType) ? _typeMap[storageType] : typeof(object);
var propertyType = property?.Type
?? (_typeMap.TryGetValue(storageType, out var type1) ? type1 : null)
?? typeof(object);
if (property != null && property.Setter == null)
{
container = property.Getter(instance);
@ -1588,7 +1591,7 @@ namespace Metsys.Bson.Configuration
{
return property;
}
return map.ContainsKey(property) ? map[property] : property;
return map.TryGetValue(property, out var value) ? value : property;
}
public void AddIgnore<T>(string name)

21
src/Browser/Avalonia.Browser/BrowserAppBuilder.cs

@ -11,6 +11,27 @@ public class BrowserPlatformOptions
/// If null, default path resolved depending on the backend (browser or blazor) is used.
/// </summary>
public Func<string, string>? FrameworkAssetPathResolver { get; set; }
/// <summary>
/// Defines if the service worker used by Avalonia should be registered.
/// If registered, service worker can work as a save file picker fallback on the browsers that don't support native implementation.
/// For more details, see https://github.com/jimmywarting/native-file-system-adapter#a-note-when-downloading-with-the-polyfilled-version.
/// </summary>
public bool RegisterAvaloniaServiceWorker { get; set; }
/// <summary>
/// If <see cref="RegisterAvaloniaServiceWorker"/> is enabled, it is possible to redefine scope for the worker.
/// By default, current domain root is used as a scope.
/// </summary>
public string? AvaloniaServiceWorkerScope { get; set; }
/// <summary>
/// Avalonia uses "native-file-system-adapter" polyfill for the file dialogs.
/// If native implementation is available, by default it is used.
/// This property forces polyfill to be always used.
/// For more details, see https://github.com/jimmywarting/native-file-system-adapter#a-note-when-downloading-with-the-polyfilled-version.
/// </summary>
public bool PreferFileDialogPolyfill { get; set; }
}
public static class BrowserAppBuilder

9
src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs

@ -25,6 +25,15 @@ internal static partial class AvaloniaModule
public static Task ImportStorage() => s_importStorage.Value;
public static string ResolveServiceWorkerPath()
{
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return options.FrameworkAssetPathResolver!("sw.js");
}
[JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)]
public static partial bool IsMobile();
[JSImport("registerServiceWorker", AvaloniaModule.MainModuleName)]
public static partial void RegisterServiceWorker(string path, string? scope);
}

6
src/Browser/Avalonia.Browser/Interop/StorageHelper.cs

@ -9,15 +9,15 @@ internal static partial class StorageHelper
public static partial bool HasNativeFilePicker();
[JSImport("StorageProvider.selectFolderDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn);
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn, bool preferPolyfill);
[JSImport("StorageProvider.openFileDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> OpenFileDialog(JSObject? startIn, bool multiple,
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption);
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption, bool preferPolyfill);
[JSImport("StorageProvider.saveFileDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> SaveFileDialog(JSObject? startIn, string? suggestedName,
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption);
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption, bool preferPolyfill);
[JSImport("StorageItem.createWellKnownDirectory", AvaloniaModule.StorageModuleName)]
public static partial JSObject CreateWellKnownDirectory(string wellKnownDirectory);

16
src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs

@ -6,20 +6,22 @@ using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Browser.Storage;
internal record FilePickerAcceptType(string Description, IReadOnlyDictionary<string, IReadOnlyList<string>> Accept);
internal class BrowserStorageProvider : IStorageProvider
{
internal const string PickerCancelMessage = "The user aborted a request";
internal const string NoPermissionsMessage = "Permissions denied";
public bool CanOpen => true;
public bool CanSave => StorageHelper.HasNativeFilePicker();
public bool CanSave => true;
public bool CanPickFolder => true;
private bool PreferPolyfill =>
AvaloniaLocator.Current.GetService<BrowserPlatformOptions>()?.PreferFileDialogPolyfill ?? false;
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
await AvaloniaModule.ImportStorage();
@ -29,7 +31,7 @@ internal class BrowserStorageProvider : IStorageProvider
try
{
using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, excludeAll);
using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, excludeAll, PreferPolyfill);
if (items is null)
{
return Array.Empty<IStorageFile>();
@ -63,7 +65,9 @@ internal class BrowserStorageProvider : IStorageProvider
try
{
var item = await StorageHelper.SaveFileDialog(startIn, options.SuggestedFileName, types, excludeAll);
var suggestedName =
StorageProviderHelpers.NameWithExtension(options.SuggestedFileName, options.DefaultExtension, null);
var item = await StorageHelper.SaveFileDialog(startIn, suggestedName, types, excludeAll, PreferPolyfill);
return item is not null ? new JSStorageFile(item) : null;
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))
@ -89,7 +93,7 @@ internal class BrowserStorageProvider : IStorageProvider
try
{
var item = await StorageHelper.SelectFolderDialog(startIn);
var item = await StorageHelper.SelectFolderDialog(startIn, PreferPolyfill);
return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty<IStorageFolder>();
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))

8
src/Browser/Avalonia.Browser/WindowingPlatform.cs

@ -1,5 +1,6 @@
using System;
using System.Threading;
using Avalonia.Browser.Interop;
using Avalonia.Browser.Skia;
using Avalonia.Input;
using Avalonia.Input.Platform;
@ -46,6 +47,13 @@ namespace Avalonia.Browser
.Bind<IPlatformGraphics>().ToConstant(new BrowserSkiaGraphics())
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
if (AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() is { } options
&& options.RegisterAvaloniaServiceWorker)
{
var swPath = AvaloniaModule.ResolveServiceWorkerPath();
AvaloniaModule.RegisterServiceWorker(swPath, options.AvaloniaServiceWorkerScope);
}
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)

3
src/Browser/Avalonia.Browser/webapp/build.js

@ -1,7 +1,8 @@
require("esbuild").build({
entryPoints: [
"./modules/avalonia.ts",
"./modules/storage.ts"
"./modules/storage.ts",
"./modules/sw.ts"
],
outdir: "../wwwroot",
bundle: true,

9
src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts

@ -7,6 +7,12 @@ import { NativeControlHost } from "./avalonia/nativeControlHost";
import { NavigationHelper } from "./avalonia/navigationHelper";
import { GeneralHelpers } from "./avalonia/generalHelpers";
async function registerServiceWorker(path: string, scope: string | undefined) {
if ("serviceWorker" in navigator) {
await globalThis.navigator.serviceWorker.register(path, scope ? { scope } : undefined);
}
}
export {
Caniuse,
Canvas,
@ -17,5 +23,6 @@ export {
StreamHelper,
NativeControlHost,
NavigationHelper,
GeneralHelpers
GeneralHelpers,
registerServiceWorker
};

7
src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts

@ -18,12 +18,7 @@ export class StreamHelper {
const array = new Uint8Array(span.byteLength);
span.copyTo(array);
const data = {
type: "write",
data: array
};
return await stream.write(data);
return await stream.write(array);
}
public static byteLength(stream: Blob) {

23
src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts

@ -1,6 +1,6 @@
import { avaloniaDb, fileBookmarksStore } from "./indexedDb";
import { StorageItem, StorageItems } from "./storageItem";
import { showOpenFilePicker, showDirectoryPicker, FileSystemFileHandle } from "native-file-system-adapter";
import { showOpenFilePicker, showDirectoryPicker, showSaveFilePicker, FileSystemFileHandle } from "native-file-system-adapter";
declare global {
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
@ -12,10 +12,12 @@ declare global {
export class StorageProvider {
public static async selectFolderDialog(
startIn: StorageItem | null): Promise<StorageItem> {
startIn: StorageItem | null,
preferPolyfill: boolean): Promise<StorageItem> {
// 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined)
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
_preferPolyfill: preferPolyfill
};
const handle = await showDirectoryPicker(options as any);
@ -24,12 +26,14 @@ export class StorageProvider {
public static async openFileDialog(
startIn: StorageItem | null, multiple: boolean,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItems> {
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean,
preferPolyfill: boolean): Promise<StorageItems> {
const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
multiple,
excludeAcceptAllOption,
types: (types ?? undefined)
types: (types ?? undefined),
_preferPolyfill: preferPolyfill
};
const handles = await showOpenFilePicker(options);
@ -38,16 +42,17 @@ export class StorageProvider {
public static async saveFileDialog(
startIn: StorageItem | null, suggestedName: string | null,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItem> {
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean,
preferPolyfill: boolean): Promise<StorageItem> {
const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
suggestedName: (suggestedName ?? undefined),
excludeAcceptAllOption,
types: (types ?? undefined)
types: (types ?? undefined),
_preferPolyfill: preferPolyfill
};
// Always prefer native save file picker, as polyfill solutions are not reliable.
const handle = await (globalThis as any).showSaveFilePicker(options);
const handle = await showSaveFilePicker(options);
return StorageItem.createFromHandle(handle);
}

78
src/Browser/Avalonia.Browser/webapp/modules/sw.ts

@ -0,0 +1,78 @@
const WRITE = 0;
const PULL = 0;
const ERROR = 1;
const ABORT = 1;
const CLOSE = 2;
class MessagePortSource implements UnderlyingSource {
private controller?: ReadableStreamController<any>;
constructor (private readonly port: MessagePort) {
this.port.onmessage = evt => this.onMessage(evt.data);
}
start (controller: ReadableStreamController<any>) {
this.controller = controller;
}
cancel (reason: Error) {
// Firefox can notify a cancel event, chrome can't
// https://bugs.chromium.org/p/chromium/issues/detail?id=638494
this.port.postMessage({ type: ERROR, reason: reason.message });
this.port.close();
}
onMessage (message: { type: number; chunk: Uint8Array; reason: any }) {
// enqueue() will call pull() if needed when there's no backpressure
if (!this.controller) {
return;
}
if (message.type === WRITE) {
this.controller.enqueue(message.chunk);
this.port.postMessage({ type: PULL });
}
if (message.type === ABORT) {
this.controller.error(message.reason);
this.port.close();
}
if (message.type === CLOSE) {
this.controller.close();
this.port.close();
}
}
}
self.addEventListener("install", () => {
(self as any).skipWaiting();
});
self.addEventListener("activate", event /* ExtendableEvent */ => {
(event as any).waitUntil((self as any).clients.claim());
});
const map = new Map();
// This should be called once per download
// Each event has a dataChannel that the data will be piped through
globalThis.addEventListener("message", evt => {
const data = evt.data;
if (data.url && data.readablePort) {
data.rs = new ReadableStream(
new MessagePortSource(evt.data.readablePort),
new CountQueuingStrategy({ highWaterMark: 4 })
);
map.set(data.url, data);
}
});
globalThis.addEventListener("fetch", evt => {
const url = (evt as any).request.url;
const data = map.get(url);
if (!data) return null;
map.delete(url);
(evt as any).respondWith(new Response(data.rs, {
headers: data.headers
}));
});
export {};

13
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -2,11 +2,9 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Numerics;
using System.Runtime.InteropServices;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
using Avalonia.Media.Imaging;
@ -127,7 +125,8 @@ namespace Avalonia.Headless
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
IReadOnlyList<GlyphInfo> glyphInfos,
Point baselineOrigin)
Point baselineOrigin,
Rect bounds)
{
return new HeadlessGlyphRunStub();
}
@ -138,6 +137,10 @@ namespace Avalonia.Headless
public Point BaselineOrigin => new Point(0, 8);
public IGlyphTypeface GlyphTypeface => new HeadlessGlyphTypefaceImpl();
public double FontRenderingEmSize => 12;
public void Dispose()
{
}
@ -374,6 +377,8 @@ namespace Avalonia.Headless
public Matrix Transform { get; set; }
public RenderOptions RenderOptions { get; set; }
public void Clear(Color color)
{
@ -451,7 +456,7 @@ namespace Avalonia.Headless
{
}
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
{
}

50
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs

@ -36,13 +36,29 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined");
}
if (valueNode.Manipulation is not XamlObjectInitializationNode
{
Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty
})
if (valueNode.Manipulation is not XamlObjectInitializationNode initializationNode)
{
throw new XamlDocumentParseException(context.CurrentDocument,
$"Source property must be set on the \"{nodeTypeName}\" node.", valueNode);
$"Invalid \"{nodeTypeName}\" node initialization.", valueNode);
}
var additionalProperties = new List<IXamlAstManipulationNode>();
if (initializationNode.Manipulation is not XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty)
{
if (initializationNode.Manipulation is XamlManipulationGroupNode manipulationGroup
&& manipulationGroup.Children.OfType<XamlPropertyAssignmentNode>()
.FirstOrDefault(p => p.Property.Name == "Source") is { } sourceProperty2)
{
sourceProperty = sourceProperty2;
// We need to copy some additional properties from ResourceInclude to ResourceDictionary except the Source one.
// If there is any missing properties, then XAML compiler will throw an error in the emitter code.
additionalProperties = manipulationGroup.Children.Where(c => c != sourceProperty2).ToList();
}
else
{
throw new XamlDocumentParseException(context.CurrentDocument,
$"Source property must be set on the \"{nodeTypeName}\" node.", valueNode);
}
}
var (assetPathUri, sourceUriNode) = ResolveSourceFromXamlInclude(context, nodeTypeName, sourceProperty, false);
@ -65,12 +81,12 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
{
if (targetDocument.BuildMethod is not null)
{
return FromMethod(context, targetDocument.BuildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
return FromMethod(context, targetDocument.BuildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
if (targetDocument.ClassType is not null)
{
return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
return context.ParseError(
@ -95,11 +111,11 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
var buildMethod = avaResType.FindMethod(m => m.Name == relativeName);
if (buildMethod is not null)
{
return FromMethod(context, buildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
return FromMethod(context, buildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
else if (assetAssembly.FindType(fullTypeName) is { } type)
{
return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
return context.ParseError(
@ -108,7 +124,8 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
}
private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlAstNode li,
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly)
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly,
IEnumerable<IXamlAstManipulationNode> manipulationNodes)
{
if (!expectedLoadedType.IsAssignableFrom(type))
{
@ -116,15 +133,17 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
$"Resource \"{assetPathUri}\" is defined as \"{type}\" type in the \"{assembly}\" assembly, but expected \"{expectedLoadedType}\".",
li, fallbackNode);
}
IXamlAstNode newObjNode = new XamlAstObjectNode(li, new XamlAstClrTypeReference(li, type, false));
((XamlAstObjectNode)newObjNode).Children.AddRange(manipulationNodes);
newObjNode = new AvaloniaXamlIlConstructorServiceProviderTransformer().Transform(context, newObjNode);
newObjNode = new ConstructableObjectTransformer().Transform(context, newObjNode);
return new NewObjectTransformer().Transform(context, newObjNode);
}
private static IXamlAstNode FromMethod(AstTransformationContext context, IXamlMethod method, IXamlAstNode li,
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly)
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly,
IEnumerable<IXamlAstManipulationNode> manipulationNodes)
{
if (!expectedLoadedType.IsAssignableFrom(method.ReturnType))
{
@ -134,8 +153,11 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
}
var sp = context.Configuration.TypeMappings.ServiceProvider;
return new XamlStaticOrTargetedReturnMethodCallNode(li, method,
new[] { new NewServiceProviderNode(sp, li) });
return new XamlValueWithManipulationNode(li,
new XamlStaticOrTargetedReturnMethodCallNode(li, method,
new[] { new NewServiceProviderNode(sp, li) }),
new XamlManipulationGroupNode(li, manipulationNodes));
}
internal static (string?, IXamlAstNode?) ResolveSourceFromXamlInclude(

10
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs

@ -10,7 +10,8 @@ internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransform
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
var type = context.GetAvaloniaTypes().IThemeVariantProvider;
var avTypes = context.GetAvaloniaTypes();
var type = avTypes.IThemeVariantProvider;
if (!(node is XamlAstObjectNode on
&& type.IsAssignableFrom(on.Type.GetClrType())))
return node;
@ -21,6 +22,13 @@ internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransform
if (keyDirective is null)
return node;
var themeDictionariesColl = avTypes.IDictionaryT.MakeGenericType(avTypes.ThemeVariant, avTypes.IThemeVariantProvider);
if (context.ParentNodes().FirstOrDefault() is not XamlAstXamlPropertyValueNode propertyValueNode
|| !themeDictionariesColl.IsAssignableFrom(propertyValueNode.Property.GetClrProperty().Getter.ReturnType))
{
return node;
}
var keyProp = type.Properties.First(p => p.Name == "Key");
on.Children.Add(new XamlAstXamlPropertyValueNode(keyDirective,
new XamlAstClrProperty(keyDirective, keyProp, context.Configuration),

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -65,6 +65,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType Int { get; }
public IXamlType Long { get; }
public IXamlType Uri { get; }
public IXamlType IDictionaryT { get; }
public IXamlType FontFamily { get; }
public IXamlConstructor FontFamilyConstructorUriName { get; }
public IXamlType Thickness { get; }
@ -194,6 +195,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
Int = cfg.TypeSystem.GetType("System.Int32");
Long = cfg.TypeSystem.GetType("System.Int64");
Uri = cfg.TypeSystem.GetType("System.Uri");
IDictionaryT = cfg.TypeSystem.GetType("System.Collections.Generic.IDictionary`2");
FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily");
FontFamilyConstructorUriName = FontFamily.GetConstructor(new List<IXamlType> { Uri, XamlIlTypes.String });
ThemeVariant = cfg.TypeSystem.GetType("Avalonia.Styling.ThemeVariant");

39
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -24,10 +24,8 @@ namespace Avalonia.Skia
private readonly Vector _dpi;
private readonly Stack<PaintWrapper> _maskStack = new();
private readonly Stack<double> _opacityStack = new();
private readonly Stack<BitmapBlendingMode> _blendingModeStack = new();
private readonly Matrix? _postTransform;
private double _currentOpacity = 1.0f;
private BitmapBlendingMode _currentBlendingMode = BitmapBlendingMode.SourceOver;
private readonly bool _canTextUseLcdRendering;
private Matrix _currentTransform;
private bool _disposed;
@ -168,6 +166,8 @@ namespace Avalonia.Skia
public SKCanvas Canvas { get; }
public SKSurface? Surface { get; }
public RenderOptions RenderOptions { get; set; }
private void CheckLease()
{
if (_leased)
@ -182,7 +182,7 @@ namespace Avalonia.Skia
}
/// <inheritdoc />
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
{
CheckLease();
var drawableImage = (IDrawableBitmapImpl)source.Item;
@ -191,8 +191,8 @@ namespace Avalonia.Skia
var paint = SKPaintCache.Shared.Get();
paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * (_useOpacitySaveLayer ? 1 : _currentOpacity)));
paint.FilterQuality = bitmapInterpolationMode.ToSKFilterQuality();
paint.BlendMode = _currentBlendingMode.ToSKBlendMode();
paint.FilterQuality = RenderOptions.BitmapInterpolationMode.ToSKFilterQuality();
paint.BlendMode = RenderOptions.BitmapBlendingMode.ToSKBlendMode();
drawableImage.Draw(this, s, d, paint);
SKPaintCache.Shared.ReturnReset(paint);
@ -203,7 +203,7 @@ namespace Avalonia.Skia
{
CheckLease();
PushOpacityMask(opacityMask, opacityMaskRect);
DrawBitmap(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default);
DrawBitmap(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect);
PopOpacityMask();
}
@ -519,7 +519,9 @@ namespace Avalonia.Skia
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.Item;
Canvas.DrawText(glyphRunImpl.TextBlob, (float)glyphRun.Item.BaselineOrigin.X,
var textBlob = glyphRunImpl.GetTextBlob(RenderOptions);
Canvas.DrawText(textBlob, (float)glyphRun.Item.BaselineOrigin.X,
(float)glyphRun.Item.BaselineOrigin.Y, paintWrapper.Paint);
}
}
@ -649,21 +651,6 @@ namespace Avalonia.Skia
Canvas.Restore();
}
/// <inheritdoc />
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
{
CheckLease();
_blendingModeStack.Push(_currentBlendingMode);
_currentBlendingMode = blendingMode;
}
/// <inheritdoc />
public void PopBitmapBlendMode()
{
CheckLease();
_currentBlendingMode = _blendingModeStack.Pop();
}
/// <inheritdoc />
public void PushOpacityMask(IBrush mask, Rect bounds)
{
@ -905,12 +892,14 @@ namespace Avalonia.Skia
context.Clear(Colors.Transparent);
context.PushClip(calc.IntermediateClip);
context.Transform = calc.IntermediateTransform;
context.RenderOptions = RenderOptions;
context.DrawBitmap(
RefCountable.CreateUnownedNotClonable(tileBrushImage),
1,
sourceRect,
targetRect,
tileBrush.BitmapInterpolationMode);
targetRect);
context.PopClip();
}
@ -1143,7 +1132,7 @@ namespace Avalonia.Skia
{
var paintWrapper = new PaintWrapper(paint);
paint.IsAntialias = true;
paint.IsAntialias = RenderOptions.EdgeMode != EdgeMode.Aliased;
double opacity = brush.Opacity * (_useOpacitySaveLayer ? 1 :_currentOpacity);

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save