Browse Source

Merge branch 'master' into features/MenuFlyout/ItemContainerTheme

pull/10020/head
Max Katz 4 years ago
committed by GitHub
parent
commit
caa29115b5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      samples/ControlCatalog/MainView.xaml
  2. 2
      samples/ControlCatalog/MainView.xaml.cs
  3. 17
      samples/ControlCatalog/Pages/DialogsPage.xaml
  4. 60
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  5. 35
      samples/ControlCatalog/Pages/GesturePage.cs
  6. 222
      samples/ControlCatalog/Pages/ScrollSnapPage.xaml
  7. 68
      samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs
  8. 7
      samples/ControlCatalog/Pages/ScrollViewerPage.xaml
  9. 8
      samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
  10. 10
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  11. 3
      src/Android/Avalonia.Android/IActivityResultHandler.cs
  12. 52
      src/Android/Avalonia.Android/Platform/PlatformSupport.cs
  13. 101
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  14. 124
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
  15. 26
      src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs
  16. 4
      src/Avalonia.Base/AttachedProperty.cs
  17. 16
      src/Avalonia.Base/AvaloniaObject.cs
  18. 8
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  19. 8
      src/Avalonia.Base/AvaloniaProperty.cs
  20. 13
      src/Avalonia.Base/DirectProperty.cs
  21. 6
      src/Avalonia.Base/DirectPropertyBase.cs
  22. 5
      src/Avalonia.Base/Input/DragEventArgs.cs
  23. 5
      src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs
  24. 6
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  25. 34
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  26. 4
      src/Avalonia.Base/Input/Gestures.cs
  27. 7
      src/Avalonia.Base/Input/GotFocusEventArgs.cs
  28. 11
      src/Avalonia.Base/Input/KeyEventArgs.cs
  29. 1
      src/Avalonia.Base/Input/KeyboardDevice.cs
  30. 8
      src/Avalonia.Base/Input/PointerDeltaEventArgs.cs
  31. 5
      src/Avalonia.Base/Input/PointerEventArgs.cs
  32. 8
      src/Avalonia.Base/Input/PointerWheelEventArgs.cs
  33. 2
      src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs
  34. 16
      src/Avalonia.Base/Input/ScrollGestureEventArgs.cs
  35. 3
      src/Avalonia.Base/Input/TappedEventArgs.cs
  36. 4
      src/Avalonia.Base/Input/TextInputEventArgs.cs
  37. 7
      src/Avalonia.Base/Input/VectorEventArgs.cs
  38. 2
      src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs
  39. 2
      src/Avalonia.Base/Logging/TraceLogSink.cs
  40. 2
      src/Avalonia.Base/Media/Brush.cs
  41. 37
      src/Avalonia.Base/Media/GlyphRun.cs
  42. 11
      src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs
  43. 2
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  44. 20
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  45. 154
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  46. 167
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  47. 12
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  48. 3
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  49. 71
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  50. 63
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  51. 103
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
  52. 16
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  53. 4
      src/Avalonia.Base/Platform/Storage/IStorageItem.cs
  54. 35
      src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
  55. 6
      src/Avalonia.Base/Platform/Storage/NameCollisionOption.cs
  56. 55
      src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
  57. 37
      src/Avalonia.Base/Platform/Storage/WellKnownFolder.cs
  58. 20
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  59. 4
      src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
  60. 8
      src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
  61. 6
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
  62. 4
      src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs
  63. 4
      src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs
  64. 6
      src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs
  65. 2
      src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs
  66. 18
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  67. 2
      src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs
  68. 208
      src/Avalonia.Base/StyledProperty.cs
  69. 250
      src/Avalonia.Base/StyledPropertyBase.cs
  70. 4
      src/Avalonia.Base/Styling/PropertySetterInstance.cs
  71. 3
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  72. 3
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  73. 3
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
  74. 3
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml
  75. 91
      src/Avalonia.Controls/ItemsControl.cs
  76. 11
      src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs
  77. 123
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  78. 368
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  79. 50
      src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs
  80. 23
      src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs
  81. 23
      src/Avalonia.Controls/Primitives/SnapPointsType.cs
  82. 81
      src/Avalonia.Controls/ScrollViewer.cs
  83. 191
      src/Avalonia.Controls/StackPanel.cs
  84. 24
      src/Avalonia.Controls/TopLevel.cs
  85. 234
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  86. 4
      src/Avalonia.Diagnostics/Diagnostics/Screenshots/BaseRenderToStreamHandler.cs
  87. 57
      src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs
  88. 5
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  89. 12
      src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs
  90. 4
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  91. 2
      src/Avalonia.Dialogs/ManagedStorageProvider.cs
  92. 6
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  93. 6
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  94. 12
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  95. 3
      src/Avalonia.Headless/IHeadlessWindow.cs
  96. 9
      src/Avalonia.Native/SystemDialogs.cs
  97. 2
      src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml
  98. 4
      src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
  99. 7
      src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml
  100. 2
      src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml

5
samples/ControlCatalog/MainView.xaml

@ -144,9 +144,12 @@
<TabItem Header="RelativePanel"> <TabItem Header="RelativePanel">
<pages:RelativePanelPage /> <pages:RelativePanelPage />
</TabItem> </TabItem>
<TabItem Header="ScrollViewer"> <TabItem Header="ScrollViewer">
<pages:ScrollViewerPage /> <pages:ScrollViewerPage />
</TabItem> </TabItem>
<TabItem Header="ScrollViewer Snapping">
<pages:ScrollSnapPage />
</TabItem>
<TabItem Header="Slider"> <TabItem Header="Slider">
<pages:SliderPage /> <pages:SliderPage />
</TabItem> </TabItem>

2
samples/ControlCatalog/MainView.xaml.cs

@ -60,7 +60,7 @@ namespace ControlCatalog
{ {
if (flowDirections.SelectedItem is FlowDirection flowDirection) if (flowDirections.SelectedItem is FlowDirection flowDirection)
{ {
this.FlowDirection = flowDirection; TopLevel.GetTopLevel(this).FlowDirection = flowDirection;
} }
}; };

17
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -1,6 +1,8 @@
<UserControl x:Class="ControlCatalog.Pages.DialogsPage" <UserControl x:Class="ControlCatalog.Pages.DialogsPage"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:storage="clr-namespace:Avalonia.Platform.Storage;assembly=Avalonia.Base"
xmlns:generic="clr-namespace:System.Collections.Generic;assembly=System.Collections">
<StackPanel Margin="4" <StackPanel Margin="4"
Orientation="Vertical" Orientation="Vertical"
Spacing="4"> Spacing="4">
@ -42,6 +44,19 @@
</StackPanel> </StackPanel>
</Expander> </Expander>
<AutoCompleteBox x:Name="CurrentFolderBox" Watermark="Write full path/uri or well known folder name">
<AutoCompleteBox.Items>
<generic:List x:TypeArguments="storage:WellKnownFolder">
<storage:WellKnownFolder>Desktop</storage:WellKnownFolder>
<storage:WellKnownFolder>Documents</storage:WellKnownFolder>
<storage:WellKnownFolder>Downloads</storage:WellKnownFolder>
<storage:WellKnownFolder>Pictures</storage:WellKnownFolder>
<storage:WellKnownFolder>Videos</storage:WellKnownFolder>
<storage:WellKnownFolder>Music</storage:WellKnownFolder>
</generic:List>
</AutoCompleteBox.Items>
</AutoCompleteBox>
<TextBlock x:Name="PickerLastResultsVisible" <TextBlock x:Name="PickerLastResultsVisible"
Classes="h2" Classes="h2"
IsVisible="False" IsVisible="False"

60
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -24,13 +24,38 @@ namespace ControlCatalog.Pages
{ {
this.InitializeComponent(); this.InitializeComponent();
IStorageFolder? lastSelectedDirectory = null;
bool ignoreTextChanged = false;
var results = this.Get<ItemsControl>("PickerLastResults"); var results = this.Get<ItemsControl>("PickerLastResults");
var resultsVisible = this.Get<TextBlock>("PickerLastResultsVisible"); var resultsVisible = this.Get<TextBlock>("PickerLastResultsVisible");
var bookmarkContainer = this.Get<TextBox>("BookmarkContainer"); var bookmarkContainer = this.Get<TextBox>("BookmarkContainer");
var openedFileContent = this.Get<TextBox>("OpenedFileContent"); var openedFileContent = this.Get<TextBox>("OpenedFileContent");
var openMultiple = this.Get<CheckBox>("OpenMultiple"); var openMultiple = this.Get<CheckBox>("OpenMultiple");
var currentFolderBox = this.Get<AutoCompleteBox>("CurrentFolderBox");
currentFolderBox.TextChanged += async (sender, args) =>
{
if (ignoreTextChanged) return;
if (Enum.TryParse<WellKnownFolder>(currentFolderBox.Text, true, out var folderEnum))
{
lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolder(folderEnum);
}
else
{
if (!Uri.TryCreate(currentFolderBox.Text, UriKind.Absolute, out var folderLink))
{
Uri.TryCreate("file://" + currentFolderBox.Text, UriKind.Absolute, out folderLink);
}
if (folderLink is not null)
{
lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPath(folderLink);
}
}
};
IStorageFolder? lastSelectedDirectory = null;
List<FileDialogFilter> GetFilters() List<FileDialogFilter> GetFilters()
{ {
@ -84,7 +109,7 @@ namespace ControlCatalog.Pages
{ {
Title = "Open multiple files", Title = "Open multiple files",
Filters = GetFilters(), Filters = GetFilters(),
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null, Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
AllowMultiple = true AllowMultiple = true
}.ShowAsync(GetWindow()); }.ShowAsync(GetWindow());
results.Items = result; results.Items = result;
@ -97,7 +122,7 @@ namespace ControlCatalog.Pages
{ {
Title = "Save file", Title = "Save file",
Filters = filters, Filters = filters,
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null, Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
DefaultExtension = filters?.Any() == true ? "txt" : null, DefaultExtension = filters?.Any() == true ? "txt" : null,
InitialFileName = "test.txt" InitialFileName = "test.txt"
}.ShowAsync(GetWindow()); }.ShowAsync(GetWindow());
@ -109,7 +134,7 @@ namespace ControlCatalog.Pages
var result = await new OpenFolderDialog() var result = await new OpenFolderDialog()
{ {
Title = "Select folder", Title = "Select folder",
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
}.ShowAsync(GetWindow()); }.ShowAsync(GetWindow());
if (string.IsNullOrEmpty(result)) if (string.IsNullOrEmpty(result))
{ {
@ -117,7 +142,7 @@ namespace ControlCatalog.Pages
} }
else else
{ {
lastSelectedDirectory = new BclStorageFolder(new System.IO.DirectoryInfo(result)); SetFolder(await GetStorageProvider().TryGetFolderFromPath(result));
results.Items = new[] { result }; results.Items = new[] { result };
resultsVisible.IsVisible = true; resultsVisible.IsVisible = true;
} }
@ -127,7 +152,7 @@ namespace ControlCatalog.Pages
var result = await new OpenFileDialog() var result = await new OpenFileDialog()
{ {
Title = "Select both", Title = "Select both",
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null, Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
AllowMultiple = true AllowMultiple = true
}.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions }.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions
{ {
@ -210,7 +235,7 @@ namespace ControlCatalog.Pages
#endif #endif
await reader.WriteLineAsync(openedFileContent.Text); await reader.WriteLineAsync(openedFileContent.Text);
lastSelectedDirectory = await file.GetParentAsync(); SetFolder(await file.GetParentAsync());
} }
await SetPickerResult(file is null ? null : new[] { file }); await SetPickerResult(file is null ? null : new[] { file });
@ -226,7 +251,7 @@ namespace ControlCatalog.Pages
await SetPickerResult(folders); await SetPickerResult(folders);
lastSelectedDirectory = folders.FirstOrDefault(); SetFolder(folders.FirstOrDefault());
}; };
this.Get<Button>("OpenFileFromBookmark").Click += async delegate this.Get<Button>("OpenFileFromBookmark").Click += async delegate
{ {
@ -243,10 +268,16 @@ namespace ControlCatalog.Pages
: null; : null;
await SetPickerResult(folder is null ? null : new[] { folder }); await SetPickerResult(folder is null ? null : new[] { folder });
SetFolder(folder);
lastSelectedDirectory = folder;
}; };
void SetFolder(IStorageFolder? folder)
{
ignoreTextChanged = true;
lastSelectedDirectory = folder;
currentFolderBox.Text = folder?.Path.LocalPath;
ignoreTextChanged = false;
}
async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items) async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items)
{ {
items ??= Array.Empty<IStorageItem>(); items ??= Array.Empty<IStorageItem>();
@ -297,10 +328,11 @@ namespace ControlCatalog.Pages
openedFileContent.Text = resultText; openedFileContent.Text = resultText;
lastSelectedDirectory = await item.GetParentAsync(); var parent = await item.GetParentAsync();
if (lastSelectedDirectory is not null) SetFolder(parent);
if (parent is not null)
{ {
mappedResults.Add(FullPathOrName(lastSelectedDirectory)); mappedResults.Add(FullPathOrName(parent));
} }
foreach (var selectedItem in items) foreach (var selectedItem in items)
@ -393,7 +425,7 @@ CanPickFolder: {storageProvider.CanPickFolder}";
private static string FullPathOrName(IStorageItem? item) private static string FullPathOrName(IStorageItem? item)
{ {
if (item is null) return "(null)"; if (item is null) return "(null)";
return item.TryGetUri(out var uri) ? uri.ToString() : item.Name; return item.Path is { IsAbsoluteUri: true } path ? path.ToString() : item.Name;
} }
Window GetWindow() => TopLevel.GetTopLevel(this) as Window ?? throw new NullReferenceException("Invalid Owner"); Window GetWindow() => TopLevel.GetTopLevel(this) as Window ?? throw new NullReferenceException("Invalid Owner");

35
samples/ControlCatalog/Pages/GesturePage.cs

@ -6,6 +6,7 @@ using Avalonia.Input;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Utilities;
namespace ControlCatalog.Pages namespace ControlCatalog.Pages
{ {
@ -53,6 +54,7 @@ namespace ControlCatalog.Pages
{ {
_currentScale = 1; _currentScale = 1;
compositionVisual.Scale = new Vector3(1,1,1); compositionVisual.Scale = new Vector3(1,1,1);
compositionVisual.Offset = default;
image.InvalidateMeasure(); image.InvalidateMeasure();
} }
}; };
@ -100,13 +102,19 @@ namespace ControlCatalog.Pages
{ {
InitComposition(control!); InitComposition(control!);
isZooming = true;
if(compositionVisual != null) if(compositionVisual != null)
{ {
var scale = _currentScale * (float)e.Scale; var scale = _currentScale * (float)e.Scale;
if (scale <= 1)
{
scale = 1;
compositionVisual.Offset = default;
}
compositionVisual.Scale = new(scale, scale, 1); compositionVisual.Scale = new(scale, scale, 1);
e.Handled = true;
} }
}); });
@ -114,8 +122,6 @@ namespace ControlCatalog.Pages
{ {
InitComposition(control!); InitComposition(control!);
isZooming = false;
if (compositionVisual != null) if (compositionVisual != null)
{ {
_currentScale = compositionVisual.Scale.X; _currentScale = compositionVisual.Scale.X;
@ -126,11 +132,19 @@ namespace ControlCatalog.Pages
{ {
InitComposition(control!); InitComposition(control!);
if (compositionVisual != null && !isZooming) if (compositionVisual != null && _currentScale != 1)
{ {
currentOffset -= new Vector3((float)e.Delta.X, (float)e.Delta.Y, 0); currentOffset += new Vector3((float)e.Delta.X, (float)e.Delta.Y, 0);
var currentSize = control.Bounds.Size * _currentScale;
currentOffset = new Vector3((float)MathUtilities.Clamp(currentOffset.X, 0, currentSize.Width - control.Bounds.Width),
(float)MathUtilities.Clamp(currentOffset.Y, 0, currentSize.Height - control.Bounds.Height),
0);
compositionVisual.Offset = currentOffset; compositionVisual.Offset = currentOffset * -1;
e.Handled = true;
} }
}); });
} }
@ -173,6 +187,8 @@ namespace ControlCatalog.Pages
if (ballCompositionVisual != null) if (ballCompositionVisual != null)
{ {
ballCompositionVisual.Offset = defaultOffset + new System.Numerics.Vector3((float)e.Delta.X * 0.4f, (float)e.Delta.Y * 0.4f, 0) * (inverse ? -1 : 1); ballCompositionVisual.Offset = defaultOffset + new System.Numerics.Vector3((float)e.Delta.X * 0.4f, (float)e.Delta.Y * 0.4f, 0) * (inverse ? -1 : 1);
e.Handled = true;
} }
}); });
@ -187,11 +203,6 @@ namespace ControlCatalog.Pages
void InitComposition(Control control) void InitComposition(Control control)
{ {
if (ballCompositionVisual != null)
{
return;
}
ballCompositionVisual = ElementComposition.GetElementVisual(ball); ballCompositionVisual = ElementComposition.GetElementVisual(ball);
if (ballCompositionVisual != null) if (ballCompositionVisual != null)

222
samples/ControlCatalog/Pages/ScrollSnapPage.xaml

@ -0,0 +1,222 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DesignHeight="800"
d:DesignWidth="400"
x:Class="ControlCatalog.Pages.ScrollSnapPage"
xmlns:pages="using:ControlCatalog.Pages"
x:DataType="pages:ScrollSnapPageViewModel">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock TextWrapping="Wrap"
Classes="h2">Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen, or using the pointer wheel.</TextBlock>
<Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto">
<StackPanel Orientation="Horizontal"
Spacing="4">
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Snap Point Type" />
<ComboBox Items="{Binding AvailableSnapPointsType}"
SelectedItem="{Binding SnapPointsType}" />
</StackPanel>
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Snap Point Alignment" />
<ComboBox Items="{Binding AvailableSnapPointsAlignment}"
SelectedItem="{Binding SnapPointsAlignment}" />
</StackPanel>
<ToggleSwitch IsChecked="{Binding AreSnapPointsRegular}"
OffContent="No"
OnContent="Yes"
Content="Are Snap Points regular?" />
</StackPanel>
<TextBlock TextWrapping="Wrap"
Grid.Row="1"
Margin="0,10"
Classes="h2">Vertical Snapping</TextBlock>
<Border
BorderBrush="Green"
BorderThickness="1"
Padding="0"
Grid.Row="2"
Margin="10, 5">
<ScrollViewer x:Name="VerticalSnapsScrollViewer"
VerticalSnapPointsType="{Binding SnapPointsType}"
VerticalSnapPointsAlignment="{Binding SnapPointsAlignment}"
HorizontalAlignment="Stretch"
Height="350"
HorizontalScrollBarVisibility="Disabled">
<StackPanel AreVerticalSnapPointsRegular="{Binding AreSnapPointsRegular}"
Orientation="Vertical"
HorizontalAlignment="Stretch">
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 1"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 2"/>
</Border>
<Border Padding="5, 20"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 3"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 4"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 5"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 6"/>
</Border>
<Border Padding="5,8"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 7"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 8"/>
</Border>
<Border Padding="5,4"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 9"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 20"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 11"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 12"/>
</Border>
</StackPanel>
</ScrollViewer>
</Border>
<TextBlock TextWrapping="Wrap"
Grid.Row="3"
Margin="0,10"
Classes="h2">Horizontal Snapping</TextBlock>
<Border
BorderBrush="Green"
BorderThickness="1"
Padding="0"
Grid.Row="4"
Margin="10, 10">
<ScrollViewer x:Name="HorizontalSnapsScrollViewer"
HorizontalSnapPointsType="{Binding SnapPointsType}"
HorizontalSnapPointsAlignment="{Binding SnapPointsAlignment}"
HorizontalAlignment="Stretch"
Height="350"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled">
<StackPanel AreHorizontalSnapPointsRegular="{Binding AreSnapPointsRegular}"
Orientation="Horizontal"
HorizontalAlignment="Stretch">
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 1"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 2"/>
</Border>
<Border Padding="5, 20"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 3"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 4"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 5"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 6"/>
</Border>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</StackPanel>
</UserControl>

68
samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs

@ -0,0 +1,68 @@
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Markup.Xaml;
using MiniMvvm;
namespace ControlCatalog.Pages
{
public class ScrollSnapPageViewModel : ViewModelBase
{
private SnapPointsType _snapPointsType;
private SnapPointsAlignment _snapPointsAlignment;
private bool _areSnapPointsRegular;
public ScrollSnapPageViewModel()
{
AvailableSnapPointsType = new List<SnapPointsType>()
{
SnapPointsType.None,
SnapPointsType.Mandatory,
SnapPointsType.MandatorySingle
};
AvailableSnapPointsAlignment = new List<SnapPointsAlignment>()
{
SnapPointsAlignment.Near,
SnapPointsAlignment.Center,
SnapPointsAlignment.Far,
};
}
public bool AreSnapPointsRegular
{
get => _areSnapPointsRegular;
set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value);
}
public SnapPointsType SnapPointsType
{
get => _snapPointsType;
set => this.RaiseAndSetIfChanged(ref _snapPointsType, value);
}
public SnapPointsAlignment SnapPointsAlignment
{
get => _snapPointsAlignment;
set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value);
}
public List<SnapPointsType> AvailableSnapPointsType { get; }
public List<SnapPointsAlignment> AvailableSnapPointsAlignment { get; }
}
public class ScrollSnapPage : UserControl
{
public ScrollSnapPage()
{
this.InitializeComponent();
DataContext = new ScrollSnapPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

7
samples/ControlCatalog/Pages/ScrollViewerPage.xaml

@ -3,12 +3,13 @@
xmlns:pages="using:ControlCatalog.Pages" xmlns:pages="using:ControlCatalog.Pages"
x:Class="ControlCatalog.Pages.ScrollViewerPage" x:Class="ControlCatalog.Pages.ScrollViewerPage"
x:DataType="pages:ScrollViewerPageViewModel"> x:DataType="pages:ScrollViewerPageViewModel">
<StackPanel Orientation="Vertical" Spacing="4"> <StackPanel Orientation="Vertical" Spacing="20">
<TextBlock Classes="h2">Allows for horizontal and vertical content scrolling.</TextBlock> <TextBlock TextWrapping="Wrap" Classes="h2">Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling.</TextBlock>
<Grid ColumnDefinitions="Auto, *"> <Grid ColumnDefinitions="Auto, *">
<StackPanel Orientation="Vertical" Spacing="4"> <StackPanel Orientation="Vertical" Spacing="4">
<ToggleSwitch IsChecked="{Binding AllowAutoHide}" Content="Allow auto hide" /> <ToggleSwitch IsChecked="{Binding AllowAutoHide}" Content="Allow auto hide" />
<ToggleSwitch IsChecked="{Binding EnableInertia}" Content="Enable Inertia" />
<StackPanel Orientation="Vertical" Spacing="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Text="Horizontal Scroll" /> <TextBlock Text="Horizontal Scroll" />
@ -24,6 +25,7 @@
<ScrollViewer x:Name="ScrollViewer" <ScrollViewer x:Name="ScrollViewer"
Grid.Column="1" Grid.Column="1"
Width="400" Height="400" Width="400" Height="400"
IsScrollInertiaEnabled="{Binding EnableInertia}"
AllowAutoHide="{Binding AllowAutoHide}" AllowAutoHide="{Binding AllowAutoHide}"
HorizontalScrollBarVisibility="{Binding HorizontalScrollVisibility}" HorizontalScrollBarVisibility="{Binding HorizontalScrollVisibility}"
VerticalScrollBarVisibility="{Binding VerticalScrollVisibility}"> VerticalScrollBarVisibility="{Binding VerticalScrollVisibility}">
@ -31,6 +33,5 @@
Source="/Assets/delicate-arch-896885_640.jpg" /> Source="/Assets/delicate-arch-896885_640.jpg" />
</ScrollViewer> </ScrollViewer>
</Grid> </Grid>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

8
samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs

@ -9,6 +9,7 @@ namespace ControlCatalog.Pages
public class ScrollViewerPageViewModel : ViewModelBase public class ScrollViewerPageViewModel : ViewModelBase
{ {
private bool _allowAutoHide; private bool _allowAutoHide;
private bool _enableInertia;
private ScrollBarVisibility _horizontalScrollVisibility; private ScrollBarVisibility _horizontalScrollVisibility;
private ScrollBarVisibility _verticalScrollVisibility; private ScrollBarVisibility _verticalScrollVisibility;
@ -25,6 +26,7 @@ namespace ControlCatalog.Pages
HorizontalScrollVisibility = ScrollBarVisibility.Auto; HorizontalScrollVisibility = ScrollBarVisibility.Auto;
VerticalScrollVisibility = ScrollBarVisibility.Auto; VerticalScrollVisibility = ScrollBarVisibility.Auto;
AllowAutoHide = true; AllowAutoHide = true;
EnableInertia = true;
} }
public bool AllowAutoHide public bool AllowAutoHide
@ -33,6 +35,12 @@ namespace ControlCatalog.Pages
set => this.RaiseAndSetIfChanged(ref _allowAutoHide, value); set => this.RaiseAndSetIfChanged(ref _allowAutoHide, value);
} }
public bool EnableInertia
{
get => _enableInertia;
set => this.RaiseAndSetIfChanged(ref _enableInertia, value);
}
public ScrollBarVisibility HorizontalScrollVisibility public ScrollBarVisibility HorizontalScrollVisibility
{ {
get => _horizontalScrollVisibility; get => _horizontalScrollVisibility;

10
src/Android/Avalonia.Android/AvaloniaMainActivity.cs

@ -1,6 +1,8 @@
using System; using System;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Content.PM;
using Android.Content.Res;
using Android.OS; using Android.OS;
using Android.Runtime; using Android.Runtime;
using Android.Views; using Android.Views;
@ -13,6 +15,7 @@ namespace Avalonia.Android
internal static object ViewContent; internal static object ViewContent;
public Action<int, Result, Intent> ActivityResult { get; set; } public Action<int, Result, Intent> ActivityResult { get; set; }
public Action<int, string[], Permission[]> RequestPermissionsResult { get; set; }
internal AvaloniaView View; internal AvaloniaView View;
private GlobalLayoutListener _listener; private GlobalLayoutListener _listener;
@ -82,6 +85,13 @@ namespace Avalonia.Android
ActivityResult?.Invoke(requestCode, resultCode, data); ActivityResult?.Invoke(requestCode, resultCode, data);
} }
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
{
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
RequestPermissionsResult?.Invoke(requestCode, permissions, grantResults);
}
class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener
{ {
private AvaloniaView _view; private AvaloniaView _view;

3
src/Android/Avalonia.Android/IActivityResultHandler.cs

@ -1,11 +1,14 @@
using System; using System;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Content.PM;
namespace Avalonia.Android namespace Avalonia.Android
{ {
public interface IActivityResultHandler public interface IActivityResultHandler
{ {
public Action<int, Result, Intent> ActivityResult { get; set; } public Action<int, Result, Intent> ActivityResult { get; set; }
public Action<int, string[], Permission[]> RequestPermissionsResult { get; set; }
} }
} }

52
src/Android/Avalonia.Android/Platform/PlatformSupport.cs

@ -0,0 +1,52 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Content.PM;
namespace Avalonia.Android.Platform;
internal static class PlatformSupport
{
private static int s_lastRequestCode = 20000;
public static int GetNextRequestCode() => s_lastRequestCode++;
public static async Task<bool> CheckPermission(this Activity activity, string permission)
{
if (activity is not IActivityResultHandler mainActivity)
{
throw new InvalidOperationException("Main activity must implement IActivityResultHandler interface.");
}
if (!OperatingSystem.IsAndroidVersionAtLeast(23))
{
return true;
}
if (activity.CheckSelfPermission(permission) == Permission.Granted)
{
return true;
}
var currentRequestCode = GetNextRequestCode();
var tcs = new TaskCompletionSource<bool>();
mainActivity.RequestPermissionsResult += RequestPermissionsResult;
activity.RequestPermissions(new [] { permission }, currentRequestCode);
return await tcs.Task;
void RequestPermissionsResult(int requestCode, string[] arg2, Permission[] arg3)
{
if (currentRequestCode != requestCode)
{
return;
}
mainActivity.RequestPermissionsResult -= RequestPermissionsResult;
_ = tcs.TrySetResult(arg3.All(p => p == Permission.Granted));
}
}
}

101
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs

@ -2,10 +2,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Android;
using Android.App;
using Android.Content; using Android.Content;
using Android.Provider; using Android.Provider;
using Avalonia.Logging; using Avalonia.Logging;
@ -19,41 +20,48 @@ namespace Avalonia.Android.Platform.Storage;
internal abstract class AndroidStorageItem : IStorageBookmarkItem internal abstract class AndroidStorageItem : IStorageBookmarkItem
{ {
private Context? _context; private Activity? _activity;
private readonly bool _needsExternalFilesPermission;
protected AndroidStorageItem(Context context, AndroidUri uri) protected AndroidStorageItem(Activity activity, AndroidUri uri, bool needsExternalFilesPermission)
{ {
_context = context; _activity = activity;
_needsExternalFilesPermission = needsExternalFilesPermission;
Uri = uri; Uri = uri;
} }
internal AndroidUri Uri { get; } internal AndroidUri Uri { get; }
protected Activity Activity => _activity ?? throw new ObjectDisposedException(nameof(AndroidStorageItem));
protected Context Context => _context ?? throw new ObjectDisposedException(nameof(AndroidStorageItem)); public virtual string Name => GetColumnValue(Activity, Uri, MediaStore.IMediaColumns.DisplayName)
public string Name => GetColumnValue(Context, Uri, MediaStore.IMediaColumns.DisplayName)
?? Uri.PathSegments?.LastOrDefault() ?? string.Empty; ?? Uri.PathSegments?.LastOrDefault() ?? string.Empty;
public Uri Path => new(Uri.ToString()!);
public bool CanBookmark => true; public bool CanBookmark => true;
public Task<string?> SaveBookmarkAsync() public async Task<string?> SaveBookmarkAsync()
{ {
Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); if (!await EnsureExternalFilesPermission(false))
return Task.FromResult(Uri.ToString()); {
} return null;
}
public Task ReleaseBookmarkAsync() Activity.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
{ return Uri.ToString();
Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
return Task.CompletedTask;
} }
public bool TryGetUri([NotNullWhen(true)] out Uri? uri) public async Task ReleaseBookmarkAsync()
{ {
uri = new Uri(Uri.ToString()!); if (!await EnsureExternalFilesPermission(false))
return true; {
} return;
}
Activity.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
}
public abstract Task<StorageItemProperties> GetBasicPropertiesAsync(); public abstract Task<StorageItemProperties> GetBasicPropertiesAsync();
protected string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null) protected string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null)
@ -77,29 +85,44 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
return null; return null;
} }
public Task<IStorageFolder?> GetParentAsync() public async Task<IStorageFolder?> GetParentAsync()
{ {
if (!await EnsureExternalFilesPermission(false))
{
return null;
}
using var javaFile = new JavaFile(Uri.Path!); using var javaFile = new JavaFile(Uri.Path!);
// Java file represents files AND directories. Don't be confused. // Java file represents files AND directories. Don't be confused.
if (javaFile.ParentFile is {} parentFile if (javaFile.ParentFile is {} parentFile
&& AndroidUri.FromFile(parentFile) is {} androidUri) && AndroidUri.FromFile(parentFile) is {} androidUri)
{ {
return Task.FromResult<IStorageFolder?>(new AndroidStorageFolder(Context, androidUri)); return new AndroidStorageFolder(Activity, androidUri, false);
} }
return Task.FromResult<IStorageFolder?>(null); return null;
} }
protected async Task<bool> EnsureExternalFilesPermission(bool write)
{
if (!_needsExternalFilesPermission)
{
return true;
}
return await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage);
}
public void Dispose() public void Dispose()
{ {
_context = null; _activity = null;
} }
} }
internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
{ {
public AndroidStorageFolder(Context context, AndroidUri uri) : base(context, uri) public AndroidStorageFolder(Activity activity, AndroidUri uri, bool needsExternalFilesPermission) : base(activity, uri, needsExternalFilesPermission)
{ {
} }
@ -110,6 +133,11 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar
public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync() public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
{ {
if (!await EnsureExternalFilesPermission(false))
{
return Array.Empty<IStorageItem>();
}
using var javaFile = new JavaFile(Uri.Path!); using var javaFile = new JavaFile(Uri.Path!);
// Java file represents files AND directories. Don't be confused. // Java file represents files AND directories. Don't be confused.
@ -124,8 +152,8 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar
.Where(t => t.uri is not null) .Where(t => t.uri is not null)
.Select(t => t.file switch .Select(t => t.file switch
{ {
{ IsFile: true } => (IStorageItem)new AndroidStorageFile(Context, t.uri!), { IsFile: true } => (IStorageItem)new AndroidStorageFile(Activity, t.uri!),
{ IsDirectory: true } => new AndroidStorageFolder(Context, t.uri!), { IsDirectory: true } => new AndroidStorageFolder(Activity, t.uri!, false),
_ => null _ => null
}) })
.Where(i => i is not null) .Where(i => i is not null)
@ -133,9 +161,20 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar
} }
} }
internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder
{
public WellKnownAndroidStorageFolder(Activity activity, string identifier, AndroidUri uri, bool needsExternalFilesPermission)
: base(activity, uri, needsExternalFilesPermission)
{
Name = identifier;
}
public override string Name { get; }
}
internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile
{ {
public AndroidStorageFile(Context context, AndroidUri uri) : base(context, uri) public AndroidStorageFile(Activity activity, AndroidUri uri) : base(activity, uri, false)
{ {
} }
@ -143,10 +182,10 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
public bool CanOpenWrite => true; public bool CanOpenWrite => true;
public Task<Stream> OpenReadAsync() => Task.FromResult(OpenContentStream(Context, Uri, false) public Task<Stream> OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false)
?? throw new InvalidOperationException("Failed to open content stream")); ?? throw new InvalidOperationException("Failed to open content stream"));
public Task<Stream> OpenWriteAsync() => Task.FromResult(OpenContentStream(Context, Uri, true) public Task<Stream> OpenWriteAsync() => Task.FromResult(OpenContentStream(Activity, Uri, true)
?? throw new InvalidOperationException("Failed to open content stream")); ?? throw new InvalidOperationException("Failed to open content stream"));
private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput) private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput)
@ -210,7 +249,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
MediaStore.IMediaColumns.Size, MediaStore.IMediaColumns.DateAdded, MediaStore.IMediaColumns.Size, MediaStore.IMediaColumns.DateAdded,
MediaStore.IMediaColumns.DateModified MediaStore.IMediaColumns.DateModified
}; };
using var cursor = Context.ContentResolver!.Query(Uri, projection, null, null, null); using var cursor = Activity.ContentResolver!.Query(Uri, projection, null, null, null);
if (cursor?.MoveToFirst() == true) if (cursor?.MoveToFirst() == true)
{ {

124
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs

@ -4,18 +4,21 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Android;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Provider; using Android.Provider;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Java.Lang;
using AndroidUri = Android.Net.Uri; using AndroidUri = Android.Net.Uri;
using Exception = System.Exception;
using JavaFile = Java.IO.File;
namespace Avalonia.Android.Platform.Storage; namespace Avalonia.Android.Platform.Storage;
internal class AndroidStorageProvider : IStorageProvider internal class AndroidStorageProvider : IStorageProvider
{ {
private readonly Activity _activity; private readonly Activity _activity;
private int _lastRequestCode = 20000;
public AndroidStorageProvider(Activity activity) public AndroidStorageProvider(Activity activity)
{ {
@ -31,7 +34,108 @@ internal class AndroidStorageProvider : IStorageProvider
public Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark) public Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{ {
var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark)); var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark));
return Task.FromResult<IStorageBookmarkFolder?>(new AndroidStorageFolder(_activity, uri)); return Task.FromResult<IStorageBookmarkFolder?>(new AndroidStorageFolder(_activity, uri, false));
}
public async Task<IStorageFile?> TryGetFileFromPath(Uri filePath)
{
if (filePath is null)
{
throw new ArgumentNullException(nameof(filePath));
}
if (filePath is not { IsAbsoluteUri: true, Scheme: "file" or "content" })
{
throw new ArgumentException("File path is expected to be an absolute link with \"file\" or \"content\" scheme.");
}
var androidUri = AndroidUri.Parse(filePath.ToString());
if (androidUri?.Path is not {} androidUriPath)
{
return null;
}
var hasPerms = await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage);
if (!hasPerms)
{
throw new SecurityException("Application doesn't have ReadExternalStorage permission. Make sure android manifest has this permission defined and user allowed it.");
}
var javaFile = new JavaFile(androidUriPath);
if (javaFile.Exists() && javaFile.IsFile)
{
return null;
}
return new AndroidStorageFile(_activity, androidUri);
}
public async Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath)
{
if (folderPath is null)
{
throw new ArgumentNullException(nameof(folderPath));
}
if (folderPath is not { IsAbsoluteUri: true, Scheme: "file" or "content" })
{
throw new ArgumentException("Folder path is expected to be an absolute link with \"file\" or \"content\" scheme.");
}
var androidUri = AndroidUri.Parse(folderPath.ToString());
if (androidUri?.Path is not {} androidUriPath)
{
return null;
}
var hasPerms = await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage);
if (!hasPerms)
{
throw new SecurityException("Application doesn't have ReadExternalStorage permission. Make sure android manifest has this permission defined and user allowed it.");
}
var javaFile = new JavaFile(androidUriPath);
if (javaFile.Exists() && javaFile.IsDirectory)
{
return null;
}
return new AndroidStorageFolder(_activity, androidUri, false);
}
public Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder)
{
var dirCode = wellKnownFolder switch
{
WellKnownFolder.Desktop => null,
WellKnownFolder.Documents => global::Android.OS.Environment.DirectoryDocuments,
WellKnownFolder.Downloads => global::Android.OS.Environment.DirectoryDownloads,
WellKnownFolder.Music => global::Android.OS.Environment.DirectoryMusic,
WellKnownFolder.Pictures => global::Android.OS.Environment.DirectoryPictures,
WellKnownFolder.Videos => global::Android.OS.Environment.DirectoryMovies,
_ => throw new ArgumentOutOfRangeException(nameof(wellKnownFolder), wellKnownFolder, null)
};
if (dirCode is null)
{
return Task.FromResult<IStorageFolder?>(null);
}
var dir = _activity.GetExternalFilesDir(dirCode);
if (dir is null || !dir.Exists())
{
return Task.FromResult<IStorageFolder?>(null);
}
var uri = AndroidUri.FromFile(dir);
if (uri is null)
{
return Task.FromResult<IStorageFolder?>(null);
}
// To make TryGetWellKnownFolder API easier to use, we don't check for the permissions.
// It will work with file picker activities, but it will fail on any direct access to the folder, like getting list of children.
// We pass "needsExternalFilesPermission" parameter here, so folder itself can check for permissions on any FS access.
return Task.FromResult<IStorageFolder?>(new WellKnownAndroidStorageFolder(_activity, dirCode, uri, true));
} }
public Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark) public Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
@ -110,19 +214,21 @@ internal class AndroidStorageProvider : IStorageProvider
var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select folder"); var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select folder");
var uris = await StartActivity(pickerIntent, false); var uris = await StartActivity(pickerIntent, false);
return uris.Select(u => new AndroidStorageFolder(_activity, u)).ToArray(); return uris.Select(u => new AndroidStorageFolder(_activity, u, false)).ToArray();
} }
private async Task<List<AndroidUri>> StartActivity(Intent? pickerIntent, bool singleResult) private async Task<List<AndroidUri>> StartActivity(Intent? pickerIntent, bool singleResult)
{ {
var resultList = new List<AndroidUri>(1); var resultList = new List<AndroidUri>(1);
var tcs = new TaskCompletionSource<Intent?>(); var tcs = new TaskCompletionSource<Intent?>();
var currentRequestCode = _lastRequestCode++; var currentRequestCode = PlatformSupport.GetNextRequestCode();
if (_activity is IActivityResultHandler mainActivity) if (!(_activity is IActivityResultHandler mainActivity))
{ {
mainActivity.ActivityResult += OnActivityResult; throw new InvalidOperationException("Main activity must implement IActivityResultHandler interface.");
} }
mainActivity.ActivityResult += OnActivityResult;
_activity.StartActivityForResult(pickerIntent, currentRequestCode); _activity.StartActivityForResult(pickerIntent, currentRequestCode);
var result = await tcs.Task; var result = await tcs.Task;
@ -161,11 +267,7 @@ internal class AndroidStorageProvider : IStorageProvider
return; return;
} }
mainActivity.ActivityResult -= OnActivityResult;
if (_activity is IActivityResultHandler mainActivity)
{
mainActivity.ActivityResult -= OnActivityResult;
}
_ = tcs.TrySetResult(resultCode == Result.Ok ? data : null); _ = tcs.TrySetResult(resultCode == Result.Ok ? data : null);
} }

26
src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs

@ -4,6 +4,7 @@ using System.Collections.Generic;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Media.Transformation;
#nullable enable #nullable enable
@ -30,7 +31,7 @@ namespace Avalonia.Animation.Animators
return new ImmutableRadialGradientBrush( return new ImmutableRadialGradientBrush(
InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity),
oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, InterpolateTransform(progress, oldValue.Transform, newValue.Transform),
s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin), s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin),
oldValue.SpreadMethod, oldValue.SpreadMethod,
s_relativePointAnimator.Interpolate(progress, oldRadial.Center, newRadial.Center), s_relativePointAnimator.Interpolate(progress, oldRadial.Center, newRadial.Center),
@ -41,7 +42,7 @@ namespace Avalonia.Animation.Animators
return new ImmutableConicGradientBrush( return new ImmutableConicGradientBrush(
InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity),
oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, InterpolateTransform(progress, oldValue.Transform, newValue.Transform),
s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin), s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin),
oldValue.SpreadMethod, oldValue.SpreadMethod,
s_relativePointAnimator.Interpolate(progress, oldConic.Center, newConic.Center), s_relativePointAnimator.Interpolate(progress, oldConic.Center, newConic.Center),
@ -51,7 +52,7 @@ namespace Avalonia.Animation.Animators
return new ImmutableLinearGradientBrush( return new ImmutableLinearGradientBrush(
InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity),
oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, InterpolateTransform(progress, oldValue.Transform, newValue.Transform),
s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin), s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin),
oldValue.SpreadMethod, oldValue.SpreadMethod,
s_relativePointAnimator.Interpolate(progress, oldLinear.StartPoint, newLinear.StartPoint), s_relativePointAnimator.Interpolate(progress, oldLinear.StartPoint, newLinear.StartPoint),
@ -72,6 +73,25 @@ namespace Avalonia.Animation.Animators
return control.Bind((AvaloniaProperty<IBrush?>)Property, instance, BindingPriority.Animation); return control.Bind((AvaloniaProperty<IBrush?>)Property, instance, BindingPriority.Animation);
} }
private static ImmutableTransform? InterpolateTransform(double progress,
ITransform? oldTransform, ITransform? newTransform)
{
if (oldTransform is TransformOperations oldTransformOperations
&& newTransform is TransformOperations newTransformOperations)
{
return new ImmutableTransform(TransformOperations
.Interpolate(oldTransformOperations, newTransformOperations, progress).Value);
}
if (oldTransform is not null)
{
return new ImmutableTransform(oldTransform.Value);
}
return null;
}
private static IReadOnlyList<ImmutableGradientStop> InterpolateStops(double progress, IReadOnlyList<IGradientStop> oldValue, IReadOnlyList<IGradientStop> newValue) private static IReadOnlyList<ImmutableGradientStop> InterpolateStops(double progress, IReadOnlyList<IGradientStop> oldValue, IReadOnlyList<IGradientStop> newValue)
{ {
var resultCount = Math.Max(oldValue.Count, newValue.Count); var resultCount = Math.Max(oldValue.Count, newValue.Count);

4
src/Avalonia.Base/AttachedProperty.cs

@ -24,11 +24,9 @@ namespace Avalonia
Func<TValue, bool>? validate = null) Func<TValue, bool>? validate = null)
: base(name, ownerType, metadata, inherits, validate) : base(name, ownerType, metadata, inherits, validate)
{ {
IsAttached = true;
} }
/// <inheritdoc/>
public override bool IsAttached => true;
/// <summary> /// <summary>
/// Attaches the property as a non-attached property on the specified type. /// Attaches the property as a non-attached property on the specified type.
/// </summary> /// </summary>

16
src/Avalonia.Base/AvaloniaObject.cs

@ -132,7 +132,7 @@ namespace Avalonia
switch (property) switch (property)
{ {
case StyledPropertyBase<T> styled: case StyledProperty<T> styled:
ClearValue(styled); ClearValue(styled);
break; break;
case DirectPropertyBase<T> direct: case DirectPropertyBase<T> direct:
@ -147,7 +147,7 @@ namespace Avalonia
/// Clears a <see cref="AvaloniaProperty"/>'s local value. /// Clears a <see cref="AvaloniaProperty"/>'s local value.
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
public void ClearValue<T>(StyledPropertyBase<T> property) public void ClearValue<T>(StyledProperty<T> property)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess(); VerifyAccess();
@ -220,7 +220,7 @@ namespace Avalonia
/// <typeparam name="T">The type of the property.</typeparam> /// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <returns>The value.</returns> /// <returns>The value.</returns>
public T GetValue<T>(StyledPropertyBase<T> property) public T GetValue<T>(StyledProperty<T> property)
{ {
_ = property ?? throw new ArgumentNullException(nameof(property)); _ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess(); VerifyAccess();
@ -243,7 +243,7 @@ namespace Avalonia
} }
/// <inheritdoc/> /// <inheritdoc/>
public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property) public Optional<T> GetBaseValue<T>(StyledProperty<T> property)
{ {
_ = property ?? throw new ArgumentNullException(nameof(property)); _ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess(); VerifyAccess();
@ -309,7 +309,7 @@ namespace Avalonia
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null. /// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns> /// </returns>
public IDisposable? SetValue<T>( public IDisposable? SetValue<T>(
StyledPropertyBase<T> property, StyledProperty<T> property,
T value, T value,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
@ -373,7 +373,7 @@ namespace Avalonia
/// A disposable which can be used to terminate the binding. /// A disposable which can be used to terminate the binding.
/// </returns> /// </returns>
public IDisposable Bind<T>( public IDisposable Bind<T>(
StyledPropertyBase<T> property, StyledProperty<T> property,
IObservable<object?> source, IObservable<object?> source,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
@ -396,7 +396,7 @@ namespace Avalonia
/// A disposable which can be used to terminate the binding. /// A disposable which can be used to terminate the binding.
/// </returns> /// </returns>
public IDisposable Bind<T>( public IDisposable Bind<T>(
StyledPropertyBase<T> property, StyledProperty<T> property,
IObservable<T> source, IObservable<T> source,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
@ -419,7 +419,7 @@ namespace Avalonia
/// A disposable which can be used to terminate the binding. /// A disposable which can be used to terminate the binding.
/// </returns> /// </returns>
public IDisposable Bind<T>( public IDisposable Bind<T>(
StyledPropertyBase<T> property, StyledProperty<T> property,
IObservable<BindingValue<T>> source, IObservable<BindingValue<T>> source,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {

8
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -146,7 +146,7 @@ namespace Avalonia
return property switch return property switch
{ {
StyledPropertyBase<T> styled => target.Bind(styled, source, priority), StyledProperty<T> styled => target.Bind(styled, source, priority),
DirectPropertyBase<T> direct => target.Bind(direct, source), DirectPropertyBase<T> direct => target.Bind(direct, source),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."),
}; };
@ -170,7 +170,7 @@ namespace Avalonia
{ {
return property switch return property switch
{ {
StyledPropertyBase<T> styled => target.Bind(styled, source, priority), StyledProperty<T> styled => target.Bind(styled, source, priority),
DirectPropertyBase<T> direct => target.Bind(direct, source), DirectPropertyBase<T> direct => target.Bind(direct, source),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."),
}; };
@ -231,7 +231,7 @@ namespace Avalonia
return property switch return property switch
{ {
StyledPropertyBase<T> styled => target.GetValue(styled), StyledProperty<T> styled => target.GetValue(styled),
DirectPropertyBase<T> direct => target.GetValue(direct), DirectPropertyBase<T> direct => target.GetValue(direct),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
}; };
@ -280,7 +280,7 @@ namespace Avalonia
return property switch return property switch
{ {
StyledPropertyBase<T> styled => target.GetBaseValue(styled), StyledProperty<T> styled => target.GetBaseValue(styled),
DirectPropertyBase<T> direct => target.GetValue(direct), DirectPropertyBase<T> direct => target.GetValue(direct),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
}; };

8
src/Avalonia.Base/AvaloniaProperty.cs

@ -107,22 +107,22 @@ namespace Avalonia
/// <summary> /// <summary>
/// Gets a value indicating whether the property inherits its value. /// Gets a value indicating whether the property inherits its value.
/// </summary> /// </summary>
public virtual bool Inherits => false; public bool Inherits { get; private protected set; }
/// <summary> /// <summary>
/// Gets a value indicating whether this is an attached property. /// Gets a value indicating whether this is an attached property.
/// </summary> /// </summary>
public virtual bool IsAttached => false; public bool IsAttached { get; private protected set; }
/// <summary> /// <summary>
/// Gets a value indicating whether this is a direct property. /// Gets a value indicating whether this is a direct property.
/// </summary> /// </summary>
public virtual bool IsDirect => false; public bool IsDirect { get; private protected set; }
/// <summary> /// <summary>
/// Gets a value indicating whether this is a readonly property. /// Gets a value indicating whether this is a readonly property.
/// </summary> /// </summary>
public virtual bool IsReadOnly => false; public bool IsReadOnly { get; private protected set; }
/// <summary> /// <summary>
/// Gets an observable that is fired when this property changes on any /// Gets an observable that is fired when this property changes on any

13
src/Avalonia.Base/DirectProperty.cs

@ -33,6 +33,8 @@ namespace Avalonia
{ {
Getter = getter ?? throw new ArgumentNullException(nameof(getter)); Getter = getter ?? throw new ArgumentNullException(nameof(getter));
Setter = setter; Setter = setter;
IsDirect = true;
IsReadOnly = setter is null;
} }
/// <summary> /// <summary>
@ -51,17 +53,10 @@ namespace Avalonia
{ {
Getter = getter ?? throw new ArgumentNullException(nameof(getter)); Getter = getter ?? throw new ArgumentNullException(nameof(getter));
Setter = setter; Setter = setter;
IsDirect = true;
IsReadOnly = setter is null;
} }
/// <inheritdoc/>
public override bool IsDirect => true;
/// <inheritdoc/>
public override bool IsReadOnly => Setter == null;
/// <inheritdoc/>
public override Type Owner => typeof(TOwner);
/// <summary> /// <summary>
/// Gets the getter function. /// Gets the getter function.
/// </summary> /// </summary>

6
src/Avalonia.Base/DirectPropertyBase.cs

@ -1,8 +1,6 @@
using System; using System;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.PropertyStore; using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Styling;
namespace Avalonia namespace Avalonia
{ {
@ -28,6 +26,7 @@ namespace Avalonia
AvaloniaPropertyMetadata metadata) AvaloniaPropertyMetadata metadata)
: base(name, ownerType, metadata) : base(name, ownerType, metadata)
{ {
Owner = ownerType;
} }
/// <summary> /// <summary>
@ -42,12 +41,13 @@ namespace Avalonia
AvaloniaPropertyMetadata metadata) AvaloniaPropertyMetadata metadata)
: base(source, ownerType, metadata) : base(source, ownerType, metadata)
{ {
Owner = ownerType;
} }
/// <summary> /// <summary>
/// Gets the type that registered the property. /// Gets the type that registered the property.
/// </summary> /// </summary>
public abstract Type Owner { get; } public Type Owner { get; }
/// <summary> /// <summary>
/// Gets the value of the property on the instance. /// Gets the value of the property on the instance.

5
src/Avalonia.Base/Input/DragEventArgs.cs

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
@ -32,7 +33,9 @@ namespace Avalonia.Input
return point; return point;
} }
internal DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers) [Unstable]
[Obsolete("This constructor might be removed in 12.0. For unit testing, consider using DragDrop.DoDragDrop or IHeadlessWindow.DragDrop.")]
public DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
: base(routedEvent) : base(routedEvent)
{ {
Data = data; Data = data;

5
src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs

@ -57,7 +57,10 @@ namespace Avalonia.Input
var scale = distance / _initialDistance; var scale = distance / _initialDistance;
_target?.RaiseEvent(new PinchEventArgs(scale, _origin)); var pinchEventArgs = new PinchEventArgs(scale, _origin);
_target?.RaiseEvent(pinchEventArgs);
e.Handled = pinchEventArgs.Handled;
} }
} }
} }

6
src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics;
using Avalonia.Input.GestureRecognizers; using Avalonia.Input.GestureRecognizers;
namespace Avalonia.Input namespace Avalonia.Input
@ -88,7 +89,10 @@ namespace Avalonia.Input
} }
_pullInProgress = true; _pullInProgress = true;
_target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection)); var pullEventArgs = new PullGestureEventArgs(_gestureId, delta, PullDirection);
_target?.RaiseEvent(pullEventArgs);
e.Handled = pullEventArgs.Handled;
} }
} }

34
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -8,6 +8,10 @@ namespace Avalonia.Input.GestureRecognizers
: StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise : StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise
IGestureRecognizer IGestureRecognizer
{ {
// Pixels per second speed that is considered to be the stop of inertial scroll
internal const double InertialScrollSpeedEnd = 5;
public const double InertialResistance = 0.15;
private bool _scrolling; private bool _scrolling;
private Point _trackedRootPoint; private Point _trackedRootPoint;
private IPointer? _tracking; private IPointer? _tracking;
@ -23,7 +27,8 @@ namespace Avalonia.Input.GestureRecognizers
// Movement per second // Movement per second
private Vector _inertia; private Vector _inertia;
private ulong? _lastMoveTimestamp; private ulong? _lastMoveTimestamp;
private bool _isScrollInertiaEnabled;
/// <summary> /// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property. /// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary> /// </summary>
@ -42,6 +47,15 @@ namespace Avalonia.Input.GestureRecognizers
o => o.CanVerticallyScroll, o => o.CanVerticallyScroll,
(o, v) => o.CanVerticallyScroll = v); (o, v) => o.CanVerticallyScroll = v);
/// <summary>
/// Defines the <see cref="IsScrollInertiaEnabled"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, bool> IsScrollInertiaEnabledProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
nameof(IsScrollInertiaEnabled),
o => o.IsScrollInertiaEnabled,
(o, v) => o.IsScrollInertiaEnabled = v);
/// <summary> /// <summary>
/// Defines the <see cref="ScrollStartDistance"/> property. /// Defines the <see cref="ScrollStartDistance"/> property.
/// </summary> /// </summary>
@ -68,6 +82,15 @@ namespace Avalonia.Input.GestureRecognizers
get => _canVerticallyScroll; get => _canVerticallyScroll;
set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
} }
/// <summary>
/// Gets or sets whether the gesture should include inertia in it's behavior.
/// </summary>
public bool IsScrollInertiaEnabled
{
get => _isScrollInertiaEnabled;
set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value);
}
/// <summary> /// <summary>
/// Gets or sets a value indicating the distance the pointer moves before scrolling is started /// Gets or sets a value indicating the distance the pointer moves before scrolling is started
@ -97,9 +120,6 @@ namespace Avalonia.Input.GestureRecognizers
} }
} }
// Pixels per second speed that is considered to be the stop of inertial scroll
private const double InertialScrollSpeedEnd = 5;
public void PointerMoved(PointerEventArgs e) public void PointerMoved(PointerEventArgs e)
{ {
if (e.Pointer == _tracking) if (e.Pointer == _tracking)
@ -168,7 +188,8 @@ namespace Avalonia.Input.GestureRecognizers
if (_inertia == default if (_inertia == default
|| e.Timestamp == 0 || e.Timestamp == 0
|| _lastMoveTimestamp == 0 || _lastMoveTimestamp == 0
|| e.Timestamp - _lastMoveTimestamp > 200) || e.Timestamp - _lastMoveTimestamp > 200
|| !IsScrollInertiaEnabled)
EndGesture(); EndGesture();
else else
{ {
@ -176,6 +197,7 @@ namespace Avalonia.Input.GestureRecognizers
var savedGestureId = _gestureId; var savedGestureId = _gestureId;
var st = Stopwatch.StartNew(); var st = Stopwatch.StartNew();
var lastTime = TimeSpan.Zero; var lastTime = TimeSpan.Zero;
_target!.RaiseEvent(new ScrollGestureInertiaStartingEventArgs(_gestureId, _inertia));
DispatcherTimer.Run(() => DispatcherTimer.Run(() =>
{ {
// Another gesture has started, finish the current one // Another gesture has started, finish the current one
@ -187,7 +209,7 @@ namespace Avalonia.Input.GestureRecognizers
var elapsedSinceLastTick = st.Elapsed - lastTime; var elapsedSinceLastTick = st.Elapsed - lastTime;
lastTime = st.Elapsed; lastTime = st.Elapsed;
var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds); var speed = _inertia * Math.Pow(InertialResistance, st.Elapsed.TotalSeconds);
var distance = speed * elapsedSinceLastTick.TotalSeconds; var distance = speed * elapsedSinceLastTick.TotalSeconds;
var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance); var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance);
_target!.RaiseEvent(scrollGestureEventArgs); _target!.RaiseEvent(scrollGestureEventArgs);

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

@ -45,6 +45,10 @@ namespace Avalonia.Input
RoutedEvent.Register<ScrollGestureEventArgs>( RoutedEvent.Register<ScrollGestureEventArgs>(
"ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures)); "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<ScrollGestureInertiaStartingEventArgs> ScrollGestureInertiaStartingEvent =
RoutedEvent.Register<ScrollGestureInertiaStartingEventArgs>(
"ScrollGestureInertiaStarting", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<ScrollGestureEndedEventArgs> ScrollGestureEndedEvent = public static readonly RoutedEvent<ScrollGestureEndedEventArgs> ScrollGestureEndedEvent =
RoutedEvent.Register<ScrollGestureEndedEventArgs>( RoutedEvent.Register<ScrollGestureEndedEventArgs>(
"ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));

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

@ -7,19 +7,18 @@ namespace Avalonia.Input
/// </summary> /// </summary>
public class GotFocusEventArgs : RoutedEventArgs public class GotFocusEventArgs : RoutedEventArgs
{ {
internal GotFocusEventArgs() public GotFocusEventArgs() : base(InputElement.GotFocusEvent)
{ {
} }
/// <summary> /// <summary>
/// Gets or sets a value indicating how the change in focus occurred. /// Gets or sets a value indicating how the change in focus occurred.
/// </summary> /// </summary>
public NavigationMethod NavigationMethod { get; set; } public NavigationMethod NavigationMethod { get; init; }
/// <summary> /// <summary>
/// Gets or sets any key modifiers active at the time of focus. /// Gets or sets any key modifiers active at the time of focus.
/// </summary> /// </summary>
public KeyModifiers KeyModifiers { get; set; } public KeyModifiers KeyModifiers { get; init; }
} }
} }

11
src/Avalonia.Base/Input/KeyEventArgs.cs

@ -5,15 +5,10 @@ namespace Avalonia.Input
{ {
public class KeyEventArgs : RoutedEventArgs public class KeyEventArgs : RoutedEventArgs
{ {
public KeyEventArgs() public IKeyboardDevice? Device { get; init; }
{
} public Key Key { get; init; }
public IKeyboardDevice? Device { get; set; } public KeyModifiers KeyModifiers { get; init; }
public Key Key { get; set; }
public KeyModifiers KeyModifiers { get; set; }
} }
} }

1
src/Avalonia.Base/Input/KeyboardDevice.cs

@ -156,7 +156,6 @@ namespace Avalonia.Input
interactive?.RaiseEvent(new GotFocusEventArgs interactive?.RaiseEvent(new GotFocusEventArgs
{ {
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = method, NavigationMethod = method,
KeyModifiers = keyModifiers, KeyModifiers = keyModifiers,
}); });

8
src/Avalonia.Base/Input/PointerDeltaEventArgs.cs

@ -1,13 +1,17 @@
using System;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
{ {
public class PointerDeltaEventArgs : PointerEventArgs public class PointerDeltaEventArgs : PointerEventArgs
{ {
public Vector Delta { get; set; } public Vector Delta { get; }
internal PointerDeltaEventArgs(RoutedEvent routedEvent, object? source, [Unstable]
[Obsolete("This constructor might be removed in 12.0.")]
public PointerDeltaEventArgs(RoutedEvent routedEvent, object? source,
IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta) PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: base(routedEvent, source, pointer, rootVisual, rootVisualPosition, : base(routedEvent, source, pointer, rootVisual, rootVisualPosition,

5
src/Avalonia.Base/Input/PointerEventArgs.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
@ -13,7 +14,9 @@ namespace Avalonia.Input
private readonly PointerPointProperties _properties; private readonly PointerPointProperties _properties;
private readonly Lazy<IReadOnlyList<RawPointerPoint>?>? _previousPoints; private readonly Lazy<IReadOnlyList<RawPointerPoint>?>? _previousPoints;
internal PointerEventArgs(RoutedEvent routedEvent, [Unstable]
[Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")]
public PointerEventArgs(RoutedEvent routedEvent,
object? source, object? source,
IPointer pointer, IPointer pointer,
Visual? rootVisual, Point rootVisualPosition, Visual? rootVisual, Point rootVisualPosition,

8
src/Avalonia.Base/Input/PointerWheelEventArgs.cs

@ -1,13 +1,17 @@
using System;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
{ {
public class PointerWheelEventArgs : PointerEventArgs public class PointerWheelEventArgs : PointerEventArgs
{ {
public Vector Delta { get; set; } public Vector Delta { get; }
internal PointerWheelEventArgs(object source, IPointer pointer, Visual rootVisual, [Unstable]
[Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow.MouseWheel.")]
public PointerWheelEventArgs(object source, IPointer pointer, Visual rootVisual,
Point rootVisualPosition, ulong timestamp, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta) PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition,

2
src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs

@ -12,6 +12,6 @@ namespace Avalonia.Input.Raw
Text = text; Text = text;
} }
public string Text { get; set; } public string Text { get; }
} }
} }

16
src/Avalonia.Base/Input/ScrollGestureEventArgs.cs

@ -14,7 +14,7 @@ namespace Avalonia.Input
public static int GetNextFreeId() => _nextId++; public static int GetNextFreeId() => _nextId++;
internal ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent) public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent)
{ {
Id = id; Id = id;
Delta = delta; Delta = delta;
@ -25,9 +25,21 @@ namespace Avalonia.Input
{ {
public int Id { get; } public int Id { get; }
internal ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent) public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent)
{ {
Id = id; Id = id;
} }
} }
public class ScrollGestureInertiaStartingEventArgs : RoutedEventArgs
{
public int Id { get; }
public Vector Inertia { get; }
internal ScrollGestureInertiaStartingEventArgs(int id, Vector inertia) : base(Gestures.ScrollGestureInertiaStartingEvent)
{
Id = id;
Inertia = inertia;
}
}
} }

3
src/Avalonia.Base/Input/TappedEventArgs.cs

@ -1,3 +1,4 @@
using System;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -7,7 +8,7 @@ namespace Avalonia.Input
{ {
private readonly PointerEventArgs lastPointerEventArgs; private readonly PointerEventArgs lastPointerEventArgs;
internal TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs) public TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs)
: base(routedEvent) : base(routedEvent)
{ {
this.lastPointerEventArgs = lastPointerEventArgs; this.lastPointerEventArgs = lastPointerEventArgs;

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

@ -4,10 +4,6 @@ namespace Avalonia.Input
{ {
public class TextInputEventArgs : RoutedEventArgs public class TextInputEventArgs : RoutedEventArgs
{ {
public TextInputEventArgs()
{
}
public IKeyboardDevice? Device { get; set; } public IKeyboardDevice? Device { get; set; }
public string? Text { get; set; } public string? Text { get; set; }

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

@ -5,11 +5,6 @@ namespace Avalonia.Input
{ {
public class VectorEventArgs : RoutedEventArgs public class VectorEventArgs : RoutedEventArgs
{ {
internal VectorEventArgs() public Vector Vector { get; init; }
{
}
public Vector Vector { get; set; }
} }
} }

2
src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs

@ -7,7 +7,7 @@ namespace Avalonia.Layout
/// </summary> /// </summary>
public class EffectiveViewportChangedEventArgs : EventArgs public class EffectiveViewportChangedEventArgs : EventArgs
{ {
internal EffectiveViewportChangedEventArgs(Rect effectiveViewport) public EffectiveViewportChangedEventArgs(Rect effectiveViewport)
{ {
EffectiveViewport = effectiveViewport; EffectiveViewport = effectiveViewport;
} }

2
src/Avalonia.Base/Logging/TraceLogSink.cs

@ -141,7 +141,7 @@ namespace Avalonia.Logging
result.Append(')'); result.Append(')');
} }
return result.ToString(); return StringBuilderCache.GetStringAndRelease(result);
} }
} }
} }

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

@ -11,7 +11,7 @@ namespace Avalonia.Media
/// Describes how an area is painted. /// Describes how an area is painted.
/// </summary> /// </summary>
[TypeConverter(typeof(BrushConverter))] [TypeConverter(typeof(BrushConverter))]
public abstract class Brush : Animatable public abstract class Brush : Animatable, IBrush
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Opacity"/> property. /// Defines the <see cref="Opacity"/> property.

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

@ -13,14 +13,22 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public sealed class GlyphRun : IDisposable public sealed class GlyphRun : IDisposable
{ {
private readonly static IPlatformRenderInterface s_renderInterface;
private IRef<IGlyphRunImpl>? _platformImpl; private IRef<IGlyphRunImpl>? _platformImpl;
private double _fontRenderingEmSize; private double _fontRenderingEmSize;
private int _biDiLevel; private int _biDiLevel;
private GlyphRunMetrics? _glyphRunMetrics; private GlyphRunMetrics? _glyphRunMetrics;
private ReadOnlyMemory<char> _characters; private ReadOnlyMemory<char> _characters;
private IReadOnlyList<GlyphInfo> _glyphInfos; private IReadOnlyList<GlyphInfo> _glyphInfos;
private Point? _baselineOrigin;
private bool _hasOneCharPerCluster; // if true, character index and cluster are similar private bool _hasOneCharPerCluster; // if true, character index and cluster are similar
static GlyphRun()
{
s_renderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class. /// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
/// </summary> /// </summary>
@ -28,15 +36,17 @@ namespace Avalonia.Media
/// <param name="fontRenderingEmSize">The rendering em size.</param> /// <param name="fontRenderingEmSize">The rendering em size.</param>
/// <param name="characters">The characters.</param> /// <param name="characters">The characters.</param>
/// <param name="glyphIndices">The glyph indices.</param> /// <param name="glyphIndices">The glyph indices.</param>
/// <param name="baselineOrigin">The baseline origin of the run.</param>
/// <param name="biDiLevel">The bidi level.</param> /// <param name="biDiLevel">The bidi level.</param>
public GlyphRun( public GlyphRun(
IGlyphTypeface glyphTypeface, IGlyphTypeface glyphTypeface,
double fontRenderingEmSize, double fontRenderingEmSize,
ReadOnlyMemory<char> characters, ReadOnlyMemory<char> characters,
IReadOnlyList<ushort> glyphIndices, IReadOnlyList<ushort> glyphIndices,
Point? baselineOrigin = null,
int biDiLevel = 0) int biDiLevel = 0)
: this(glyphTypeface, fontRenderingEmSize, characters, : this(glyphTypeface, fontRenderingEmSize, characters,
CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), biDiLevel) CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), baselineOrigin, biDiLevel)
{ {
_hasOneCharPerCluster = true; _hasOneCharPerCluster = true;
} }
@ -48,12 +58,14 @@ namespace Avalonia.Media
/// <param name="fontRenderingEmSize">The rendering em size.</param> /// <param name="fontRenderingEmSize">The rendering em size.</param>
/// <param name="characters">The characters.</param> /// <param name="characters">The characters.</param>
/// <param name="glyphInfos">The list of glyphs used.</param> /// <param name="glyphInfos">The list of glyphs used.</param>
/// <param name="baselineOrigin">The baseline origin of the run.</param>
/// <param name="biDiLevel">The bidi level.</param> /// <param name="biDiLevel">The bidi level.</param>
public GlyphRun( public GlyphRun(
IGlyphTypeface glyphTypeface, IGlyphTypeface glyphTypeface,
double fontRenderingEmSize, double fontRenderingEmSize,
ReadOnlyMemory<char> characters, ReadOnlyMemory<char> characters,
IReadOnlyList<GlyphInfo> glyphInfos, IReadOnlyList<GlyphInfo> glyphInfos,
Point? baselineOrigin = null,
int biDiLevel = 0) int biDiLevel = 0)
{ {
GlyphTypeface = glyphTypeface; GlyphTypeface = glyphTypeface;
@ -64,6 +76,8 @@ namespace Avalonia.Media
_glyphInfos = glyphInfos; _glyphInfos = glyphInfos;
_baselineOrigin = baselineOrigin;
_biDiLevel = biDiLevel; _biDiLevel = biDiLevel;
} }
@ -72,6 +86,7 @@ namespace Avalonia.Media
_glyphInfos = Array.Empty<GlyphInfo>(); _glyphInfos = Array.Empty<GlyphInfo>();
GlyphTypeface = Typeface.Default.GlyphTypeface; GlyphTypeface = Typeface.Default.GlyphTypeface;
_platformImpl = platformImpl; _platformImpl = platformImpl;
_baselineOrigin = platformImpl.Item.BaselineOrigin;
} }
private static IReadOnlyList<GlyphInfo> CreateGlyphInfos(IReadOnlyList<ushort> glyphIndices, private static IReadOnlyList<GlyphInfo> CreateGlyphInfos(IReadOnlyList<ushort> glyphIndices,
@ -147,9 +162,13 @@ namespace Avalonia.Media
=> _glyphRunMetrics ??= CreateGlyphRunMetrics(); => _glyphRunMetrics ??= CreateGlyphRunMetrics();
/// <summary> /// <summary>
/// Gets the baseline origin of the<see cref="GlyphRun"/>. /// Gets or sets the baseline origin of the<see cref="GlyphRun"/>.
/// </summary> /// </summary>
public Point BaselineOrigin => PlatformImpl.Item.BaselineOrigin; public Point BaselineOrigin
{
get => _baselineOrigin ?? default;
set => Set(ref _baselineOrigin, value);
}
/// <summary> /// <summary>
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>. /// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
@ -204,9 +223,7 @@ namespace Avalonia.Media
/// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns> /// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns>
public Geometry BuildGeometry() public Geometry BuildGeometry()
{ {
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>(); var geometryImpl = s_renderInterface.BuildGlyphRunGeometry(this);
var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this);
return new PlatformGeometry(geometryImpl); return new PlatformGeometry(geometryImpl);
} }
@ -802,9 +819,11 @@ namespace Avalonia.Media
private IRef<IGlyphRunImpl> CreateGlyphRunImpl() private IRef<IGlyphRunImpl> CreateGlyphRunImpl()
{ {
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>(); var platformImpl = s_renderInterface.CreateGlyphRun(
GlyphTypeface,
var platformImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos); FontRenderingEmSize,
GlyphInfos,
_baselineOrigin ?? new Point(0, -GlyphTypeface.Metrics.Ascent * Scale));
_platformImpl = RefCountable.Create(platformImpl); _platformImpl = RefCountable.Create(platformImpl);

11
src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs

@ -93,16 +93,19 @@ namespace Avalonia.Media.TextFormatting
[Conditional("DEBUG")] [Conditional("DEBUG")]
public void VerifyAllReturned() public void VerifyAllReturned()
{ {
if (_pendingReturnCount > 0) var pendingReturnCount = _pendingReturnCount;
_pendingReturnCount = 0;
if (pendingReturnCount > 0)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"{_pendingReturnCount} RentedList<{typeof(T).Name} haven't been returned to the pool!"); $"{pendingReturnCount} RentedList<{typeof(T).Name}> haven't been returned to the pool!");
} }
if (_pendingReturnCount < 0) if (pendingReturnCount < 0)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"{-_pendingReturnCount} RentedList<{typeof(T).Name} extra lists have been returned to the pool!"); $"{-pendingReturnCount} RentedList<{typeof(T).Name}> extra lists have been returned to the pool!");
} }
} }
} }

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

@ -185,7 +185,7 @@ namespace Avalonia.Media.TextFormatting
ShapedBuffer.FontRenderingEmSize, ShapedBuffer.FontRenderingEmSize,
Text, Text,
ShapedBuffer, ShapedBuffer,
BidiLevel); biDiLevel: BidiLevel);
} }
public void Dispose() public void Dispose()

20
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@ -113,14 +113,18 @@ namespace Avalonia.Media.TextFormatting
var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool); var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool);
var collapsedRuns = new TextRun[preSplitRuns.Count + 1]; try
preSplitRuns.CopyTo(collapsedRuns); {
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; var collapsedRuns = new TextRun[preSplitRuns.Count + 1];
preSplitRuns.CopyTo(collapsedRuns);
objectPool.TextRunLists.Return(ref preSplitRuns); collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
objectPool.TextRunLists.Return(ref postSplitRuns); return collapsedRuns;
}
return collapsedRuns; finally
{
objectPool.TextRunLists.Return(ref preSplitRuns);
objectPool.TextRunLists.Return(ref postSplitRuns);
}
} }
} }
} }

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

@ -32,58 +32,64 @@ namespace Avalonia.Media.TextFormatting
var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool,
out var textEndOfLine, out var textSourceLength); out var textEndOfLine, out var textSourceLength);
RentedList<TextRun>? shapedTextRuns; RentedList<TextRun>? shapedTextRuns = null;
if (previousLineBreak?.RemainingRuns is { } remainingRuns) try
{ {
resolvedFlowDirection = previousLineBreak.FlowDirection; if (previousLineBreak?.RemainingRuns is { } remainingRuns)
textRuns = remainingRuns;
nextLineBreak = previousLineBreak;
shapedTextRuns = null;
}
else
{
shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, out resolvedFlowDirection);
textRuns = shapedTextRuns;
if (nextLineBreak == null && textEndOfLine != null)
{ {
nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); resolvedFlowDirection = previousLineBreak.FlowDirection;
textRuns = remainingRuns;
nextLineBreak = previousLineBreak;
shapedTextRuns = null;
} }
} else
{
shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager,
out resolvedFlowDirection);
textRuns = shapedTextRuns;
TextLineImpl textLine; if (nextLineBreak == null && textEndOfLine != null)
{
nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
}
}
switch (textWrapping) TextLineImpl textLine;
{
case TextWrapping.NoWrap: switch (textWrapping)
{ {
// perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class case TextWrapping.NoWrap:
// which already uses an array: ToArray() won't ever be called in this case {
var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class
// which already uses an array: ToArray() won't ever be called in this case
var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength, textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
textLine.FinalizeLine(); textLine.FinalizeLine();
break; break;
} }
case TextWrapping.WrapWithOverflow: case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap: case TextWrapping.Wrap:
{ {
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager); paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager);
break; break;
}
default:
throw new ArgumentOutOfRangeException(nameof(textWrapping));
} }
default:
throw new ArgumentOutOfRangeException(nameof(textWrapping));
}
objectPool.TextRunLists.Return(ref shapedTextRuns);
objectPool.TextRunLists.Return(ref fetchedRuns);
return textLine; return textLine;
}
finally
{
objectPool.TextRunLists.Return(ref shapedTextRuns);
objectPool.TextRunLists.Return(ref fetchedRuns);
}
} }
/// <summary> /// <summary>
@ -224,23 +230,26 @@ namespace Avalonia.Media.TextFormatting
(resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft; (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
var processedRuns = objectPool.TextRunLists.Rent(); var processedRuns = objectPool.TextRunLists.Rent();
var groupedRuns = objectPool.UnshapedTextRunLists.Rent();
CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns); try
{
CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns);
bidiData.Reset(); bidiData.Reset();
bidiAlgorithm.Reset(); bidiAlgorithm.Reset();
var groupedRuns = objectPool.UnshapedTextRunLists.Rent();
var textShaper = TextShaper.Current;
for (var index = 0; index < processedRuns.Count; index++) var textShaper = TextShaper.Current;
{
var currentRun = processedRuns[index];
switch (currentRun) for (var index = 0; index < processedRuns.Count; index++)
{ {
case UnshapedTextRun shapeableRun: var currentRun = processedRuns[index];
switch (currentRun)
{ {
case UnshapedTextRun shapeableRun:
{
groupedRuns.Clear(); groupedRuns.Clear();
groupedRuns.Add(shapeableRun); groupedRuns.Add(shapeableRun);
@ -277,17 +286,20 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
default: default:
{ {
shapedRuns.Add(currentRun); shapedRuns.Add(currentRun);
break; break;
} }
}
} }
} }
finally
objectPool.TextRunLists.Return(ref processedRuns); {
objectPool.UnshapedTextRunLists.Return(ref groupedRuns); objectPool.TextRunLists.Return(ref processedRuns);
objectPool.UnshapedTextRunLists.Return(ref groupedRuns);
}
return shapedRuns; return shapedRuns;
} }
@ -805,25 +817,29 @@ namespace Avalonia.Media.TextFormatting
var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool); var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool);
var textLineBreak = postSplitRuns?.Count > 0 ? try
new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
null;
if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null)
{ {
textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); var textLineBreak = postSplitRuns?.Count > 0 ?
} new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
null;
var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null)
paragraphWidth, paragraphProperties, resolvedFlowDirection, {
textLineBreak); textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
}
textLine.FinalizeLine();
objectPool.TextRunLists.Return(ref preSplitRuns); var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
objectPool.TextRunLists.Return(ref postSplitRuns); paragraphWidth, paragraphProperties, resolvedFlowDirection,
textLineBreak);
return textLine; textLine.FinalizeLine();
return textLine;
}
finally
{
objectPool.TextRunLists.Return(ref preSplitRuns);
objectPool.TextRunLists.Return(ref postSplitRuns);
}
} }
private struct TextRunEnumerator private struct TextRunEnumerator

167
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -441,128 +441,133 @@ namespace Avalonia.Media.TextFormatting
var textLines = objectPool.TextLines.Rent(); var textLines = objectPool.TextLines.Rent();
double left = double.PositiveInfinity, width = 0.0, height = 0.0; try
{
_textSourceLength = 0; double left = double.PositiveInfinity, width = 0.0, height = 0.0;
TextLine? previousLine = null; _textSourceLength = 0;
var textFormatter = TextFormatter.Current; TextLine? previousLine = null;
while (true) var textFormatter = TextFormatter.Current;
{
var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties,
previousLine?.TextLineBreak);
if (textLine.Length == 0) while (true)
{ {
if (previousLine != null && previousLine.NewLineLength > 0) var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
if (textLine.Length == 0)
{ {
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, if (previousLine != null && previousLine.NewLineLength > 0)
_paragraphProperties, fontManager); {
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth,
_paragraphProperties, fontManager);
textLines.Add(emptyTextLine); textLines.Add(emptyTextLine);
UpdateBounds(emptyTextLine, ref left, ref width, ref height); UpdateBounds(emptyTextLine, ref left, ref width, ref height);
} }
break; break;
} }
_textSourceLength += textLine.Length; _textSourceLength += textLine.Length;
//Fulfill max height constraint //Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight) if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight)
{ && height + textLine.Height > MaxHeight)
if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{ {
var collapsedLine = if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
previousLine.Collapse(GetCollapsingProperties(MaxWidth)); {
var collapsedLine =
previousLine.Collapse(GetCollapsingProperties(MaxWidth));
textLines[textLines.Count - 1] = collapsedLine; textLines[textLines.Count - 1] = collapsedLine;
} }
break; break;
} }
var hasOverflowed = textLine.HasOverflowed; var hasOverflowed = textLine.HasOverflowed;
if (hasOverflowed && _textTrimming != TextTrimming.None) if (hasOverflowed && _textTrimming != TextTrimming.None)
{ {
textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth)); textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth));
} }
textLines.Add(textLine); textLines.Add(textLine);
UpdateBounds(textLine, ref left, ref width, ref height); UpdateBounds(textLine, ref left, ref width, ref height);
previousLine = textLine; previousLine = textLine;
//Fulfill max lines constraint //Fulfill max lines constraint
if (MaxLines > 0 && textLines.Count >= MaxLines) if (MaxLines > 0 && textLines.Count >= MaxLines)
{
if(textLine.TextLineBreak?.RemainingRuns is not null)
{ {
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); if (textLine.TextLineBreak?.RemainingRuns is not null)
{
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
}
break;
} }
break; if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
{
break;
}
} }
if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph) //Make sure the TextLayout always contains at least on empty line
if (textLines.Count == 0)
{ {
break; var textLine =
} TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager);
}
//Make sure the TextLayout always contains at least on empty line textLines.Add(textLine);
if (textLines.Count == 0)
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager);
textLines.Add(textLine);
UpdateBounds(textLine, ref left, ref width, ref height);
}
Bounds = new Rect(left, 0, width, height); UpdateBounds(textLine, ref left, ref width, ref height);
}
if (_paragraphProperties.TextAlignment == TextAlignment.Justify) Bounds = new Rect(left, 0, width, height);
{
var whitespaceWidth = 0d;
for (var i = 0; i < textLines.Count; i++) if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
{ {
var line = textLines[i]; var whitespaceWidth = 0d;
var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
if (lineWhitespaceWidth > whitespaceWidth) for (var i = 0; i < textLines.Count; i++)
{ {
whitespaceWidth = lineWhitespaceWidth; var line = textLines[i];
} var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
}
var justificationWidth = width - whitespaceWidth; if (lineWhitespaceWidth > whitespaceWidth)
{
whitespaceWidth = lineWhitespaceWidth;
}
}
if (justificationWidth > 0) var justificationWidth = width - whitespaceWidth;
{
var justificationProperties = new InterWordJustification(justificationWidth);
for (var i = 0; i < textLines.Count - 1; i++) if (justificationWidth > 0)
{ {
var line = textLines[i]; var justificationProperties = new InterWordJustification(justificationWidth);
line.Justify(justificationProperties); for (var i = 0; i < textLines.Count - 1; i++)
{
var line = textLines[i];
line.Justify(justificationProperties);
}
} }
} }
}
var result = textLines.ToArray(); return textLines.ToArray();
}
objectPool.TextLines.Return(ref textLines); finally
objectPool.VerifyAllReturned(); {
objectPool.TextLines.Return(ref textLines);
return result; objectPool.VerifyAllReturned();
}
} }
/// <summary> /// <summary>

12
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@ -86,7 +86,6 @@ namespace Avalonia.Media.TextFormatting
RentedList<TextRun>? rentedPreSplitRuns = null; RentedList<TextRun>? rentedPreSplitRuns = null;
RentedList<TextRun>? rentedPostSplitRuns = null; RentedList<TextRun>? rentedPostSplitRuns = null;
TextRun[]? results;
try try
{ {
@ -113,9 +112,7 @@ namespace Avalonia.Media.TextFormatting
if (measuredLength <= _prefixLength || effectivePostSplitRuns is null) if (measuredLength <= _prefixLength || effectivePostSplitRuns is null)
{ {
results = collapsedRuns.ToArray(); return collapsedRuns.ToArray();
objectPool.TextRunLists.Return(ref collapsedRuns);
return results;
} }
var availableSuffixWidth = availableWidth; var availableSuffixWidth = availableWidth;
@ -157,16 +154,15 @@ namespace Avalonia.Media.TextFormatting
} }
} }
} }
return collapsedRuns.ToArray();
} }
finally finally
{ {
objectPool.TextRunLists.Return(ref rentedPreSplitRuns); objectPool.TextRunLists.Return(ref rentedPreSplitRuns);
objectPool.TextRunLists.Return(ref rentedPostSplitRuns); objectPool.TextRunLists.Return(ref rentedPostSplitRuns);
objectPool.TextRunLists.Return(ref collapsedRuns);
} }
results = collapsedRuns.ToArray();
objectPool.TextRunLists.Return(ref collapsedRuns);
return results;
} }
return new TextRun[] { shapedSymbol }; return new TextRun[] { shapedSymbol };

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

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

71
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs

@ -1,50 +1,65 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Security; using System.Security;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage.FileIO; namespace Avalonia.Platform.Storage.FileIO;
[Unstable] internal class BclStorageFile : IStorageBookmarkFile
public class BclStorageFile : IStorageBookmarkFile
{ {
private readonly FileInfo _fileInfo;
public BclStorageFile(string fileName) public BclStorageFile(string fileName)
{ {
_fileInfo = new FileInfo(fileName); FileInfo = new FileInfo(fileName);
} }
public BclStorageFile(FileInfo fileInfo) public BclStorageFile(FileInfo fileInfo)
{ {
_fileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo)); FileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo));
} }
public FileInfo FileInfo { get; }
public bool CanOpenRead => true; public bool CanOpenRead => true;
public bool CanOpenWrite => true; public bool CanOpenWrite => true;
public string Name => _fileInfo.Name; public string Name => FileInfo.Name;
public virtual bool CanBookmark => true; public virtual bool CanBookmark => true;
public Uri Path
{
get
{
try
{
if (FileInfo.Directory is not null)
{
return StorageProviderHelpers.FilePathToUri(FileInfo.FullName);
}
}
catch (SecurityException)
{
}
return new Uri(FileInfo.Name, UriKind.Relative);
}
}
public Task<StorageItemProperties> GetBasicPropertiesAsync() public Task<StorageItemProperties> GetBasicPropertiesAsync()
{ {
if (_fileInfo.Exists) if (FileInfo.Exists)
{ {
return Task.FromResult(new StorageItemProperties( return Task.FromResult(new StorageItemProperties(
(ulong)_fileInfo.Length, (ulong)FileInfo.Length,
_fileInfo.CreationTimeUtc, FileInfo.CreationTimeUtc,
_fileInfo.LastAccessTimeUtc)); FileInfo.LastAccessTimeUtc));
} }
return Task.FromResult(new StorageItemProperties()); return Task.FromResult(new StorageItemProperties());
} }
public Task<IStorageFolder?> GetParentAsync() public Task<IStorageFolder?> GetParentAsync()
{ {
if (_fileInfo.Directory is { } directory) if (FileInfo.Directory is { } directory)
{ {
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory)); return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
} }
@ -53,17 +68,17 @@ public class BclStorageFile : IStorageBookmarkFile
public Task<Stream> OpenReadAsync() public Task<Stream> OpenReadAsync()
{ {
return Task.FromResult<Stream>(_fileInfo.OpenRead()); return Task.FromResult<Stream>(FileInfo.OpenRead());
} }
public Task<Stream> OpenWriteAsync() public Task<Stream> OpenWriteAsync()
{ {
return Task.FromResult<Stream>(_fileInfo.OpenWrite()); return Task.FromResult<Stream>(FileInfo.OpenWrite());
} }
public virtual Task<string?> SaveBookmarkAsync() public virtual Task<string?> SaveBookmarkAsync()
{ {
return Task.FromResult<string?>(_fileInfo.FullName); return Task.FromResult<string?>(FileInfo.FullName);
} }
public Task ReleaseBookmarkAsync() public Task ReleaseBookmarkAsync()
@ -72,28 +87,6 @@ public class BclStorageFile : IStorageBookmarkFile
return Task.CompletedTask; return Task.CompletedTask;
} }
public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
{
try
{
if (_fileInfo.Directory is not null)
{
uri = Path.IsPathRooted(_fileInfo.FullName) ?
new Uri(new Uri("file://"), _fileInfo.FullName) :
new Uri(_fileInfo.FullName, UriKind.Relative);
return true;
}
uri = null;
return false;
}
catch (SecurityException)
{
uri = null;
return false;
}
}
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
} }

63
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs

@ -1,23 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security; using System.Security;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage.FileIO; namespace Avalonia.Platform.Storage.FileIO;
[Unstable] internal class BclStorageFolder : IStorageBookmarkFolder
public class BclStorageFolder : IStorageBookmarkFolder
{ {
private readonly DirectoryInfo _directoryInfo;
public BclStorageFolder(string path) public BclStorageFolder(string path)
{ {
_directoryInfo = new DirectoryInfo(path); DirectoryInfo = new DirectoryInfo(path);
if (!_directoryInfo.Exists) if (!DirectoryInfo.Exists)
{ {
throw new ArgumentException("Directory must exist"); throw new ArgumentException("Directory must exist");
} }
@ -25,29 +20,46 @@ public class BclStorageFolder : IStorageBookmarkFolder
public BclStorageFolder(DirectoryInfo directoryInfo) public BclStorageFolder(DirectoryInfo directoryInfo)
{ {
_directoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo)); DirectoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo));
if (!_directoryInfo.Exists) if (!DirectoryInfo.Exists)
{ {
throw new ArgumentException("Directory must exist", nameof(directoryInfo)); throw new ArgumentException("Directory must exist", nameof(directoryInfo));
} }
} }
public string Name => _directoryInfo.Name; public string Name => DirectoryInfo.Name;
public DirectoryInfo DirectoryInfo { get; }
public bool CanBookmark => true; public bool CanBookmark => true;
public Uri Path
{
get
{
try
{
return StorageProviderHelpers.FilePathToUri(DirectoryInfo.FullName);
}
catch (SecurityException)
{
return new Uri(DirectoryInfo.Name, UriKind.Relative);
}
}
}
public Task<StorageItemProperties> GetBasicPropertiesAsync() public Task<StorageItemProperties> GetBasicPropertiesAsync()
{ {
var props = new StorageItemProperties( var props = new StorageItemProperties(
null, null,
_directoryInfo.CreationTimeUtc, DirectoryInfo.CreationTimeUtc,
_directoryInfo.LastAccessTimeUtc); DirectoryInfo.LastAccessTimeUtc);
return Task.FromResult(props); return Task.FromResult(props);
} }
public Task<IStorageFolder?> GetParentAsync() public Task<IStorageFolder?> GetParentAsync()
{ {
if (_directoryInfo.Parent is { } directory) if (DirectoryInfo.Parent is { } directory)
{ {
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory)); return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
} }
@ -56,9 +68,9 @@ public class BclStorageFolder : IStorageBookmarkFolder
public Task<IReadOnlyList<IStorageItem>> GetItemsAsync() public Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
{ {
var items = _directoryInfo.GetDirectories() var items = DirectoryInfo.GetDirectories()
.Select(d => (IStorageItem)new BclStorageFolder(d)) .Select(d => (IStorageItem)new BclStorageFolder(d))
.Concat(_directoryInfo.GetFiles().Select(f => new BclStorageFile(f))) .Concat(DirectoryInfo.GetFiles().Select(f => new BclStorageFile(f)))
.ToArray(); .ToArray();
return Task.FromResult<IReadOnlyList<IStorageItem>>(items); return Task.FromResult<IReadOnlyList<IStorageItem>>(items);
@ -66,7 +78,7 @@ public class BclStorageFolder : IStorageBookmarkFolder
public virtual Task<string?> SaveBookmarkAsync() public virtual Task<string?> SaveBookmarkAsync()
{ {
return Task.FromResult<string?>(_directoryInfo.FullName); return Task.FromResult<string?>(DirectoryInfo.FullName);
} }
public Task ReleaseBookmarkAsync() public Task ReleaseBookmarkAsync()
@ -75,23 +87,6 @@ public class BclStorageFolder : IStorageBookmarkFolder
return Task.CompletedTask; return Task.CompletedTask;
} }
public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
{
try
{
uri = Path.IsPathRooted(_directoryInfo.FullName) ?
new Uri(new Uri("file://"), _directoryInfo.FullName) :
new Uri(_directoryInfo.FullName, UriKind.Relative);
return true;
}
catch (SecurityException)
{
uri = null;
return false;
}
}
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
} }

103
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs

@ -1,12 +1,13 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Metadata; using Avalonia.Compatibility;
namespace Avalonia.Platform.Storage.FileIO; namespace Avalonia.Platform.Storage.FileIO;
[Unstable] internal abstract class BclStorageProvider : IStorageProvider
public abstract class BclStorageProvider : IStorageProvider
{ {
public abstract bool CanOpen { get; } public abstract bool CanOpen { get; }
public abstract Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options); public abstract Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options);
@ -32,4 +33,98 @@ public abstract class BclStorageProvider : IStorageProvider
? Task.FromResult<IStorageBookmarkFolder?>(new BclStorageFolder(folder)) ? Task.FromResult<IStorageBookmarkFolder?>(new BclStorageFolder(folder))
: Task.FromResult<IStorageBookmarkFolder?>(null); : Task.FromResult<IStorageBookmarkFolder?>(null);
} }
public virtual Task<IStorageFile?> TryGetFileFromPath(Uri filePath)
{
if (filePath.IsAbsoluteUri)
{
var file = new FileInfo(filePath.LocalPath);
if (file.Exists)
{
return Task.FromResult<IStorageFile?>(new BclStorageFile(file));
}
}
return Task.FromResult<IStorageFile?>(null);
}
public virtual Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath)
{
if (folderPath.IsAbsoluteUri)
{
var directory = new DirectoryInfo(folderPath.LocalPath);
if (directory.Exists)
{
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
}
}
return Task.FromResult<IStorageFolder?>(null);
}
public virtual Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder)
{
// Note, this BCL API returns different values depending on the .NET version.
// We should also document it.
// https://github.com/dotnet/docs/issues/31423
// For pre-breaking change values see table:
// https://johnkoerner.com/csharp/special-folder-values-on-windows-versus-mac/
var folderPath = wellKnownFolder switch
{
WellKnownFolder.Desktop => GetFromSpecialFolder(Environment.SpecialFolder.Desktop),
WellKnownFolder.Documents => GetFromSpecialFolder(Environment.SpecialFolder.MyDocuments),
WellKnownFolder.Downloads => GetDownloadsWellKnownFolder(),
WellKnownFolder.Music => GetFromSpecialFolder(Environment.SpecialFolder.MyMusic),
WellKnownFolder.Pictures => GetFromSpecialFolder(Environment.SpecialFolder.MyPictures),
WellKnownFolder.Videos => GetFromSpecialFolder(Environment.SpecialFolder.MyVideos),
_ => throw new ArgumentOutOfRangeException(nameof(wellKnownFolder), wellKnownFolder, null)
};
if (folderPath is null)
{
return Task.FromResult<IStorageFolder?>(null);
}
var directory = new DirectoryInfo(folderPath);
if (!directory.Exists)
{
return Task.FromResult<IStorageFolder?>(null);
}
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
string GetFromSpecialFolder(Environment.SpecialFolder folder) =>
Environment.GetFolderPath(folder, Environment.SpecialFolderOption.Create);
}
// TODO, replace with https://github.com/dotnet/runtime/issues/70484 when implemented.
// Normally we want to avoid platform specific code in the Avalonia.Base assembly.
protected static string? GetDownloadsWellKnownFolder()
{
if (OperatingSystemEx.IsWindows())
{
return Environment.OSVersion.Version.Major < 6 ? null :
SHGetKnownFolderPath(s_folderDownloads, 0, IntPtr.Zero);
}
if (OperatingSystemEx.IsLinux())
{
var envDir = Environment.GetEnvironmentVariable("XDG_DOWNLOAD_DIR");
if (envDir != null && Directory.Exists(envDir))
{
return envDir;
}
}
if (OperatingSystemEx.IsLinux() || OperatingSystemEx.IsMacOS())
{
return "~/Downloads";
}
return null;
}
private static readonly Guid s_folderDownloads = new Guid("374DE290-123F-4565-9164-39C4925E467B");
[DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)]
private static extern string SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid id, int flags, IntPtr token);
} }

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

@ -1,13 +1,23 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Avalonia.Metadata; using System.Text;
namespace Avalonia.Platform.Storage.FileIO; namespace Avalonia.Platform.Storage.FileIO;
[Unstable] internal static class StorageProviderHelpers
public static class StorageProviderHelpers
{ {
public static Uri FilePathToUri(string path)
{
var uriPath = new StringBuilder(path)
.Replace("%", $"%{(int)'%':X2}")
.Replace("[", $"%{(int)'[':X2}")
.Replace("]", $"%{(int)']':X2}")
.ToString();
return new UriBuilder("file", string.Empty) { Path = uriPath }.Uri;
}
public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter) public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter)
{ {
var name = Path.GetFileName(path); var name = Path.GetFileName(path);

4
src/Avalonia.Base/Platform/Storage/IStorageItem.cs

@ -20,13 +20,13 @@ public interface IStorageItem : IDisposable
string Name { get; } string Name { get; }
/// <summary> /// <summary>
/// Gets the full file-system path of the item, if the item has a path. /// Gets the file-system path of the item.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Android backend might return file path with "content:" scheme. /// Android backend might return file path with "content:" scheme.
/// Browser and iOS backends might return relative uris. /// Browser and iOS backends might return relative uris.
/// </remarks> /// </remarks>
bool TryGetUri([NotNullWhen(true)] out Uri? uri); Uri Path { get; }
/// <summary> /// <summary>
/// Gets the basic properties of the current item. /// Gets the basic properties of the current item.

35
src/Avalonia.Base/Platform/Storage/IStorageProvider.cs

@ -1,4 +1,6 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Runtime.Versioning;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -53,4 +55,35 @@ public interface IStorageProvider
/// <param name="bookmark">Bookmark ID.</param> /// <param name="bookmark">Bookmark ID.</param>
/// <returns>Bookmarked folder or null if OS denied request.</returns> /// <returns>Bookmarked folder or null if OS denied request.</returns>
Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark); Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark);
/// <summary>
/// Attempts to read file from the file-system by its path.
/// </summary>
/// <param name="filePath">The path of the item to retrieve in Uri format.</param>
/// <remarks>
/// Uri path is usually expected to be an absolute path with "file" scheme.
/// But it can be an uri with "content" scheme on the Android.
/// It also might ask user for the permission, and throw an exception if it was denied.
/// </remarks>
/// <returns>File or null if it doesn't exist.</returns>
Task<IStorageFile?> TryGetFileFromPath(Uri filePath);
/// <summary>
/// Attempts to read folder from the file-system by its path.
/// </summary>
/// <param name="folderPath">The path of the folder to retrieve in Uri format.</param>
/// <remarks>
/// Uri path is usually expected to be an absolute path with "file" scheme.
/// But it can be an uri with "content" scheme on the Android.
/// It also might ask user for the permission, and throw an exception if it was denied.
/// </remarks>
/// <returns>Folder or null if it doesn't exist.</returns>
Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath);
/// <summary>
/// Attempts to read folder from the file-system by its path
/// </summary>
/// <param name="wellKnownFolder">Well known folder identifier.</param>
/// <returns>Folder or null if it doesn't exist.</returns>
Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder);
} }

6
src/Avalonia.Base/Platform/Storage/NameCollisionOption.cs

@ -0,0 +1,6 @@
namespace Avalonia.Platform.Storage;
public class NameCollisionOption
{
}

55
src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs

@ -0,0 +1,55 @@
using System.Threading.Tasks;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Group of public extensions for <see cref="IStorageProvider"/> class.
/// </summary>
public static class StorageProviderExtensions
{
/// <inheritdoc cref="IStorageProvider.TryGetFileFromPath"/>
public static Task<IStorageFile?> TryGetFileFromPath(this IStorageProvider provider, string filePath)
{
return provider.TryGetFileFromPath(StorageProviderHelpers.FilePathToUri(filePath));
}
/// <inheritdoc cref="IStorageProvider.TryGetFolderFromPath"/>
public static Task<IStorageFolder?> TryGetFolderFromPath(this IStorageProvider provider, string folderPath)
{
return provider.TryGetFolderFromPath(StorageProviderHelpers.FilePathToUri(folderPath));
}
internal static string? TryGetFullPath(this IStorageFolder folder)
{
// We can avoid double escaping of the path by checking for BclStorageFolder.
// Ideally, `folder.Path.LocalPath` should also work, as that's only available way for the users.
if (folder is BclStorageFolder storageFolder)
{
return storageFolder.DirectoryInfo.FullName;
}
if (folder.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath)
{
return absolutePath.LocalPath;
}
// android "content:", browser and ios relative links go here.
return null;
}
internal static string? TryGetFullPath(this IStorageFile file)
{
if (file is BclStorageFile storageFolder)
{
return storageFolder.FileInfo.FullName;
}
if (file.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath)
{
return absolutePath.LocalPath;
}
return null;
}
}

37
src/Avalonia.Base/Platform/Storage/WellKnownFolder.cs

@ -0,0 +1,37 @@
namespace Avalonia.Platform.Storage;
/// <summary>
/// Specifies enumerated constants used to retrieve directory paths to system well known folders.
/// </summary>
public enum WellKnownFolder
{
/// <summary>
/// Current user desktop folder.
/// </summary>
Desktop,
/// <summary>
/// Current user documents folder.
/// </summary>
Documents,
/// <summary>
/// Current user downloads folder.
/// </summary>
Downloads,
/// <summary>
/// Current user music folder
/// </summary>
Music,
/// <summary>
/// Current user pictures folder
/// </summary>
Pictures,
/// <summary>
/// Current user videos folder
/// </summary>
Videos,
}

20
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@ -19,7 +19,7 @@ namespace Avalonia.PropertyStore
private T? _baseValue; private T? _baseValue;
private UncommonFields? _uncommon; private UncommonFields? _uncommon;
public EffectiveValue(AvaloniaObject owner, StyledPropertyBase<T> property) public EffectiveValue(AvaloniaObject owner, StyledProperty<T> property)
{ {
Priority = BindingPriority.Unset; Priority = BindingPriority.Unset;
BasePriority = BindingPriority.Unset; BasePriority = BindingPriority.Unset;
@ -57,12 +57,12 @@ namespace Avalonia.PropertyStore
Debug.Assert(priority != BindingPriority.LocalValue); Debug.Assert(priority != BindingPriority.LocalValue);
UpdateValueEntry(value, priority); UpdateValueEntry(value, priority);
SetAndRaiseCore(owner, (StyledPropertyBase<T>)value.Property, GetValue(value), priority); SetAndRaiseCore(owner, (StyledProperty<T>)value.Property, GetValue(value), priority);
} }
public void SetLocalValueAndRaise( public void SetLocalValueAndRaise(
ValueStore owner, ValueStore owner,
StyledPropertyBase<T> property, StyledProperty<T> property,
T value) T value)
{ {
SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue); SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue);
@ -82,7 +82,7 @@ namespace Avalonia.PropertyStore
{ {
Debug.Assert(oldValue is not null || newValue is not null); Debug.Assert(oldValue is not null || newValue is not null);
var p = (StyledPropertyBase<T>)property; var p = (StyledProperty<T>)property;
var o = oldValue is not null ? ((EffectiveValue<T>)oldValue).Value : _metadata.DefaultValue; var o = oldValue is not null ? ((EffectiveValue<T>)oldValue).Value : _metadata.DefaultValue;
var n = newValue is not null ? ((EffectiveValue<T>)newValue).Value : _metadata.DefaultValue; var n = newValue is not null ? ((EffectiveValue<T>)newValue).Value : _metadata.DefaultValue;
var priority = newValue is not null ? BindingPriority.Inherited : BindingPriority.Unset; var priority = newValue is not null ? BindingPriority.Inherited : BindingPriority.Unset;
@ -98,7 +98,7 @@ namespace Avalonia.PropertyStore
Debug.Assert(Priority != BindingPriority.Animation); Debug.Assert(Priority != BindingPriority.Animation);
Debug.Assert(BasePriority != BindingPriority.Unset); Debug.Assert(BasePriority != BindingPriority.Unset);
UpdateValueEntry(null, BindingPriority.Animation); UpdateValueEntry(null, BindingPriority.Animation);
SetAndRaiseCore(owner, (StyledPropertyBase<T>)property, _baseValue!, BasePriority); SetAndRaiseCore(owner, (StyledProperty<T>)property, _baseValue!, BasePriority);
} }
public override void CoerceValue(ValueStore owner, AvaloniaProperty property) public override void CoerceValue(ValueStore owner, AvaloniaProperty property)
@ -107,7 +107,7 @@ namespace Avalonia.PropertyStore
return; return;
SetAndRaiseCore( SetAndRaiseCore(
owner, owner,
(StyledPropertyBase<T>)property, (StyledProperty<T>)property,
_uncommon._uncoercedValue!, _uncommon._uncoercedValue!,
Priority, Priority,
_uncommon._uncoercedBaseValue!, _uncommon._uncoercedBaseValue!,
@ -117,10 +117,10 @@ namespace Avalonia.PropertyStore
public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property) public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property)
{ {
UnsubscribeValueEntries(); UnsubscribeValueEntries();
DisposeAndRaiseUnset(owner, (StyledPropertyBase<T>)property); DisposeAndRaiseUnset(owner, (StyledProperty<T>)property);
} }
public void DisposeAndRaiseUnset(ValueStore owner, StyledPropertyBase<T> property) public void DisposeAndRaiseUnset(ValueStore owner, StyledProperty<T> property)
{ {
BindingPriority priority; BindingPriority priority;
T oldValue; T oldValue;
@ -156,7 +156,7 @@ namespace Avalonia.PropertyStore
private void SetAndRaiseCore( private void SetAndRaiseCore(
ValueStore owner, ValueStore owner,
StyledPropertyBase<T> property, StyledProperty<T> property,
T value, T value,
BindingPriority priority) BindingPriority priority)
{ {
@ -203,7 +203,7 @@ namespace Avalonia.PropertyStore
private void SetAndRaiseCore( private void SetAndRaiseCore(
ValueStore owner, ValueStore owner,
StyledPropertyBase<T> property, StyledProperty<T> property,
T value, T value,
BindingPriority priority, BindingPriority priority,
T baseValue, T baseValue,

4
src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs

@ -9,7 +9,7 @@ namespace Avalonia.PropertyStore
public ImmediateValueEntry( public ImmediateValueEntry(
ImmediateValueFrame owner, ImmediateValueFrame owner,
StyledPropertyBase<T> property, StyledProperty<T> property,
T value) T value)
{ {
_owner = owner; _owner = owner;
@ -17,7 +17,7 @@ namespace Avalonia.PropertyStore
Property = property; Property = property;
} }
public StyledPropertyBase<T> Property { get; } public StyledProperty<T> Property { get; }
public bool HasValue => true; public bool HasValue => true;
AvaloniaProperty IValueEntry.Property => Property; AvaloniaProperty IValueEntry.Property => Property;

8
src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs

@ -15,7 +15,7 @@ namespace Avalonia.PropertyStore
} }
public TypedBindingEntry<T> AddBinding<T>( public TypedBindingEntry<T> AddBinding<T>(
StyledPropertyBase<T> property, StyledProperty<T> property,
IObservable<BindingValue<T>> source) IObservable<BindingValue<T>> source)
{ {
var e = new TypedBindingEntry<T>(this, property, source); var e = new TypedBindingEntry<T>(this, property, source);
@ -24,7 +24,7 @@ namespace Avalonia.PropertyStore
} }
public TypedBindingEntry<T> AddBinding<T>( public TypedBindingEntry<T> AddBinding<T>(
StyledPropertyBase<T> property, StyledProperty<T> property,
IObservable<T> source) IObservable<T> source)
{ {
var e = new TypedBindingEntry<T>(this, property, source); var e = new TypedBindingEntry<T>(this, property, source);
@ -33,7 +33,7 @@ namespace Avalonia.PropertyStore
} }
public SourceUntypedBindingEntry<T> AddBinding<T>( public SourceUntypedBindingEntry<T> AddBinding<T>(
StyledPropertyBase<T> property, StyledProperty<T> property,
IObservable<object?> source) IObservable<object?> source)
{ {
var e = new SourceUntypedBindingEntry<T>(this, property, source); var e = new SourceUntypedBindingEntry<T>(this, property, source);
@ -41,7 +41,7 @@ namespace Avalonia.PropertyStore
return e; return e;
} }
public ImmediateValueEntry<T> AddValue<T>(StyledPropertyBase<T> property, T value) public ImmediateValueEntry<T> AddValue<T>(StyledProperty<T> property, T value)
{ {
var e = new ImmediateValueEntry<T>(this, property, value); var e = new ImmediateValueEntry<T>(this, property, value);
Add(e); Add(e);

6
src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs

@ -11,13 +11,13 @@ namespace Avalonia.PropertyStore
private readonly ValueStore _owner; private readonly ValueStore _owner;
private IDisposable? _subscription; private IDisposable? _subscription;
public LocalValueBindingObserver(ValueStore owner, StyledPropertyBase<T> property) public LocalValueBindingObserver(ValueStore owner, StyledProperty<T> property)
{ {
_owner = owner; _owner = owner;
Property = property; Property = property;
} }
public StyledPropertyBase<T> Property { get;} public StyledProperty<T> Property { get;}
public void Start(IObservable<T> source) public void Start(IObservable<T> source)
{ {
@ -41,7 +41,7 @@ namespace Avalonia.PropertyStore
public void OnNext(T value) public void OnNext(T value)
{ {
static void Execute(ValueStore owner, StyledPropertyBase<T> property, T value) static void Execute(ValueStore owner, StyledProperty<T> property, T value)
{ {
if (property.ValidateValue?.Invoke(value) != false) if (property.ValidateValue?.Invoke(value) != false)
owner.SetValue(property, value, BindingPriority.LocalValue); owner.SetValue(property, value, BindingPriority.LocalValue);

4
src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs

@ -11,13 +11,13 @@ namespace Avalonia.PropertyStore
private readonly ValueStore _owner; private readonly ValueStore _owner;
private IDisposable? _subscription; private IDisposable? _subscription;
public LocalValueUntypedBindingObserver(ValueStore owner, StyledPropertyBase<T> property) public LocalValueUntypedBindingObserver(ValueStore owner, StyledProperty<T> property)
{ {
_owner = owner; _owner = owner;
Property = property; Property = property;
} }
public StyledPropertyBase<T> Property { get; } public StyledProperty<T> Property { get; }
public void Start(IObservable<object?> source) public void Start(IObservable<object?> source)
{ {

4
src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs

@ -13,14 +13,14 @@ namespace Avalonia.PropertyStore
public SourceUntypedBindingEntry( public SourceUntypedBindingEntry(
ValueFrame frame, ValueFrame frame,
StyledPropertyBase<TTarget> property, StyledProperty<TTarget> property,
IObservable<object?> source) IObservable<object?> source)
: base(frame, property, source) : base(frame, property, source)
{ {
_validate = property.ValidateValue; _validate = property.ValidateValue;
} }
public new StyledPropertyBase<TTarget> Property => (StyledPropertyBase<TTarget>)base.Property; public new StyledProperty<TTarget> Property => (StyledProperty<TTarget>)base.Property;
protected override BindingValue<TTarget> ConvertAndValidate(object? value) protected override BindingValue<TTarget> ConvertAndValidate(object? value)
{ {

6
src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs

@ -11,7 +11,7 @@ namespace Avalonia.PropertyStore
{ {
public TypedBindingEntry( public TypedBindingEntry(
ValueFrame frame, ValueFrame frame,
StyledPropertyBase<T> property, StyledProperty<T> property,
IObservable<T> source) IObservable<T> source)
: base(frame, property, source) : base(frame, property, source)
{ {
@ -19,13 +19,13 @@ namespace Avalonia.PropertyStore
public TypedBindingEntry( public TypedBindingEntry(
ValueFrame frame, ValueFrame frame,
StyledPropertyBase<T> property, StyledProperty<T> property,
IObservable<BindingValue<T>> source) IObservable<BindingValue<T>> source)
: base(frame, property, source) : base(frame, property, source)
{ {
} }
public new StyledPropertyBase<T> Property => (StyledPropertyBase<T>)base.Property; public new StyledProperty<T> Property => (StyledProperty<T>)base.Property;
protected override BindingValue<T> ConvertAndValidate(T value) protected override BindingValue<T> ConvertAndValidate(T value)
{ {

2
src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs

@ -26,7 +26,7 @@ namespace Avalonia.PropertyStore
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
public static bool TryConvertAndValidate<T>( public static bool TryConvertAndValidate<T>(
StyledPropertyBase<T> property, StyledProperty<T> property,
object? value, object? value,
[MaybeNullWhen(false)] out T result) [MaybeNullWhen(false)] out T result)
{ {

18
src/Avalonia.Base/PropertyStore/ValueStore.cs

@ -43,7 +43,7 @@ namespace Avalonia.PropertyStore
} }
public IDisposable AddBinding<T>( public IDisposable AddBinding<T>(
StyledPropertyBase<T> property, StyledProperty<T> property,
IObservable<BindingValue<T>> source, IObservable<BindingValue<T>> source,
BindingPriority priority) BindingPriority priority)
{ {
@ -71,7 +71,7 @@ namespace Avalonia.PropertyStore
} }
public IDisposable AddBinding<T>( public IDisposable AddBinding<T>(
StyledPropertyBase<T> property, StyledProperty<T> property,
IObservable<T> source, IObservable<T> source,
BindingPriority priority) BindingPriority priority)
{ {
@ -99,7 +99,7 @@ namespace Avalonia.PropertyStore
} }
public IDisposable AddBinding<T>( public IDisposable AddBinding<T>(
StyledPropertyBase<T> property, StyledProperty<T> property,
IObservable<object?> source, IObservable<object?> source,
BindingPriority priority) BindingPriority priority)
{ {
@ -165,7 +165,7 @@ namespace Avalonia.PropertyStore
} }
} }
public IDisposable? SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority) public IDisposable? SetValue<T>(StyledProperty<T> property, T value, BindingPriority priority)
{ {
if (property.ValidateValue?.Invoke(value) == false) if (property.ValidateValue?.Invoke(value) == false)
{ {
@ -219,7 +219,7 @@ namespace Avalonia.PropertyStore
return GetDefaultValue(property); return GetDefaultValue(property);
} }
public T GetValue<T>(StyledPropertyBase<T> property) public T GetValue<T>(StyledProperty<T> property)
{ {
if (_effectiveValues.TryGetValue(property, out var v)) if (_effectiveValues.TryGetValue(property, out var v))
return ((EffectiveValue<T>)v).Value; return ((EffectiveValue<T>)v).Value;
@ -248,7 +248,7 @@ namespace Avalonia.PropertyStore
v.CoerceValue(this, property); v.CoerceValue(this, property);
} }
public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property) public Optional<T> GetBaseValue<T>(StyledProperty<T> property)
{ {
if (TryGetEffectiveValue(property, out var v) && if (TryGetEffectiveValue(property, out var v) &&
((EffectiveValue<T>)v).TryGetBaseValue(out var baseValue)) ((EffectiveValue<T>)v).TryGetBaseValue(out var baseValue))
@ -450,7 +450,7 @@ namespace Avalonia.PropertyStore
/// <param name="oldValue">The old value of the property.</param> /// <param name="oldValue">The old value of the property.</param>
/// <param name="value">The effective value instance.</param> /// <param name="value">The effective value instance.</param>
public void OnInheritedEffectiveValueChanged<T>( public void OnInheritedEffectiveValueChanged<T>(
StyledPropertyBase<T> property, StyledProperty<T> property,
T oldValue, T oldValue,
EffectiveValue<T> value) EffectiveValue<T> value)
{ {
@ -475,7 +475,7 @@ namespace Avalonia.PropertyStore
/// </summary> /// </summary>
/// <param name="property">The property whose value changed.</param> /// <param name="property">The property whose value changed.</param>
/// <param name="oldValue">The old value of the property.</param> /// <param name="oldValue">The old value of the property.</param>
public void OnInheritedEffectiveValueDisposed<T>(StyledPropertyBase<T> property, T oldValue) public void OnInheritedEffectiveValueDisposed<T>(StyledProperty<T> property, T oldValue)
{ {
Debug.Assert(property.Inherits); Debug.Assert(property.Inherits);
@ -520,7 +520,7 @@ namespace Avalonia.PropertyStore
/// <param name="oldValue">The old value of the property.</param> /// <param name="oldValue">The old value of the property.</param>
/// <param name="newValue">The new value of the property.</param> /// <param name="newValue">The new value of the property.</param>
public void OnAncestorInheritedValueChanged<T>( public void OnAncestorInheritedValueChanged<T>(
StyledPropertyBase<T> property, StyledProperty<T> property,
T oldValue, T oldValue,
T newValue) T newValue)
{ {

2
src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs

@ -12,7 +12,7 @@ namespace Avalonia.Rendering
/// </summary> /// </summary>
/// <param name="root">The render root that has been updated.</param> /// <param name="root">The render root that has been updated.</param>
/// <param name="dirtyRect">The updated area.</param> /// <param name="dirtyRect">The updated area.</param>
internal SceneInvalidatedEventArgs( public SceneInvalidatedEventArgs(
IRenderRoot root, IRenderRoot root,
Rect dirtyRect) Rect dirtyRect)
{ {

208
src/Avalonia.Base/StyledProperty.cs

@ -1,14 +1,18 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
{ {
/// <summary> /// <summary>
/// A styled avalonia property. /// A styled avalonia property.
/// </summary> /// </summary>
public class StyledProperty<TValue> : StyledPropertyBase<TValue> public class StyledProperty<TValue> : AvaloniaProperty<TValue>, IStyledPropertyAccessor
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StyledPropertyBase{T}"/> class. /// Initializes a new instance of the <see cref="StyledProperty{T}"/> class.
/// </summary> /// </summary>
/// <param name="name">The name of the property.</param> /// <param name="name">The name of the property.</param>
/// <param name="ownerType">The type of the class that registers the property.</param> /// <param name="ownerType">The type of the class that registers the property.</param>
@ -23,20 +27,30 @@ namespace Avalonia
bool inherits = false, bool inherits = false,
Func<TValue, bool>? validate = null, Func<TValue, bool>? validate = null,
Action<AvaloniaObject, bool>? notifying = null) Action<AvaloniaObject, bool>? notifying = null)
: base(name, ownerType, metadata, inherits, validate, notifying) : base(name, ownerType, metadata, notifying)
{ {
Inherits = inherits;
ValidateValue = validate;
HasCoercion |= metadata.CoerceValue != null;
if (validate?.Invoke(metadata.DefaultValue) == false)
{
throw new ArgumentException(
$"'{metadata.DefaultValue}' is not a valid default value for '{name}'.");
}
} }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StyledPropertyBase{T}"/> class. /// Gets the value validation callback for the property.
/// </summary> /// </summary>
/// <param name="source">The property to add the owner to.</param> public Func<TValue, bool>? ValidateValue { get; }
/// <param name="ownerType">The type of the class that registers the property.</param>
internal StyledProperty(StyledPropertyBase<TValue> source, Type ownerType) /// <summary>
: base(source, ownerType) /// Gets a value indicating whether this property has any value coercion callbacks defined
{ /// in its metadata.
} /// </summary>
internal bool HasCoercion { get; private set; }
/// <summary> /// <summary>
/// Registers the property on another type. /// Registers the property on another type.
/// </summary> /// </summary>
@ -47,5 +61,177 @@ namespace Avalonia
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this); AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this);
return this; return this;
} }
public TValue CoerceValue(AvaloniaObject instance, TValue baseValue)
{
var metadata = GetMetadata(instance.GetType());
if (metadata.CoerceValue != null)
{
return metadata.CoerceValue.Invoke(instance, baseValue);
}
return baseValue;
}
/// <summary>
/// Gets the default value for the property on the specified type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>The default value.</returns>
public TValue GetDefaultValue(Type type)
{
return GetMetadata(type).DefaultValue;
}
/// <summary>
/// Gets the property metadata for the specified type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>
/// The property metadata.
/// </returns>
public new StyledPropertyMetadata<TValue> GetMetadata(Type type)
{
_ = type ?? throw new ArgumentNullException(nameof(type));
return (StyledPropertyMetadata<TValue>)base.GetMetadata(type);
}
/// <summary>
/// Overrides the default value for the property on the specified type.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="defaultValue">The default value.</param>
public void OverrideDefaultValue<T>(TValue defaultValue) where T : AvaloniaObject
{
OverrideDefaultValue(typeof(T), defaultValue);
}
/// <summary>
/// Overrides the default value for the property on the specified type.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="defaultValue">The default value.</param>
public void OverrideDefaultValue(Type type, TValue defaultValue)
{
OverrideMetadata(type, new StyledPropertyMetadata<TValue>(defaultValue));
}
/// <summary>
/// Overrides the metadata for the property on the specified type.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="metadata">The metadata.</param>
public void OverrideMetadata<T>(StyledPropertyMetadata<TValue> metadata) where T : AvaloniaObject
{
base.OverrideMetadata(typeof(T), metadata);
}
/// <summary>
/// Overrides the metadata for the property on the specified type.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="metadata">The metadata.</param>
public void OverrideMetadata(Type type, StyledPropertyMetadata<TValue> metadata)
{
if (ValidateValue != null)
{
if (!ValidateValue(metadata.DefaultValue))
{
throw new ArgumentException(
$"'{metadata.DefaultValue}' is not a valid default value for '{Name}'.");
}
}
HasCoercion |= metadata.CoerceValue != null;
base.OverrideMetadata(type, metadata);
}
/// <summary>
/// Gets the string representation of the property.
/// </summary>
/// <returns>The property's string representation.</returns>
public override string ToString()
{
return Name;
}
/// <inheritdoc/>
object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
bool IStyledPropertyAccessor.ValidateValue(object? value)
{
if (value is null && !typeof(TValue).IsValueType)
return ValidateValue?.Invoke(default!) ?? true;
if (value is TValue typed)
return ValidateValue?.Invoke(typed) ?? true;
return false;
}
internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o)
{
return new EffectiveValue<TValue>(o, this);
}
/// <inheritdoc/>
internal override void RouteClearValue(AvaloniaObject o)
{
o.ClearValue<TValue>(this);
}
/// <inheritdoc/>
internal override object? RouteGetValue(AvaloniaObject o)
{
return o.GetValue<TValue>(this);
}
/// <inheritdoc/>
internal override object? RouteGetBaseValue(AvaloniaObject o)
{
var value = o.GetBaseValue<TValue>(this);
return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue;
}
/// <inheritdoc/>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
internal override IDisposable? RouteSetValue(
AvaloniaObject target,
object? value,
BindingPriority priority)
{
if (value == BindingOperations.DoNothing)
{
return null;
}
else if (value == UnsetValue)
{
target.ClearValue(this);
return null;
}
else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted))
{
return target.SetValue<TValue>(this, (TValue)converted!, priority);
}
else
{
var type = value?.GetType().FullName ?? "(null)";
throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})");
}
}
internal override IDisposable RouteBind(
AvaloniaObject target,
IObservable<object?> source,
BindingPriority priority)
{
return target.Bind<TValue>(this, source, priority);
}
private object? GetDefaultBoxedValue(Type type)
{
_ = type ?? throw new ArgumentNullException(nameof(type));
return GetMetadata(type).DefaultValue;
}
} }
} }

250
src/Avalonia.Base/StyledPropertyBase.cs

@ -1,250 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Styling;
using Avalonia.Utilities;
namespace Avalonia
{
/// <summary>
/// Base class for styled properties.
/// </summary>
public abstract class StyledPropertyBase<TValue> : AvaloniaProperty<TValue>, IStyledPropertyAccessor
{
private readonly bool _inherits;
/// <summary>
/// Initializes a new instance of the <see cref="StyledPropertyBase{T}"/> class.
/// </summary>
/// <param name="name">The name of the property.</param>
/// <param name="ownerType">The type of the class that registers the property.</param>
/// <param name="metadata">The property metadata.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="validate">A value validation callback.</param>
/// <param name="notifying">A <see cref="AvaloniaProperty.Notifying"/> callback.</param>
protected StyledPropertyBase(
string name,
Type ownerType,
StyledPropertyMetadata<TValue> metadata,
bool inherits = false,
Func<TValue, bool>? validate = null,
Action<AvaloniaObject, bool>? notifying = null)
: base(name, ownerType, metadata, notifying)
{
_inherits = inherits;
ValidateValue = validate;
HasCoercion |= metadata.CoerceValue != null;
if (validate?.Invoke(metadata.DefaultValue) == false)
{
throw new ArgumentException(
$"'{metadata.DefaultValue}' is not a valid default value for '{name}'.");
}
}
/// <summary>
/// Initializes a new instance of the <see cref="StyledPropertyBase{T}"/> class.
/// </summary>
/// <param name="source">The property to add the owner to.</param>
/// <param name="ownerType">The type of the class that registers the property.</param>
protected StyledPropertyBase(StyledPropertyBase<TValue> source, Type ownerType)
: base(source, ownerType, null)
{
_inherits = source.Inherits;
}
/// <summary>
/// Gets a value indicating whether the property inherits its value.
/// </summary>
/// <value>
/// A value indicating whether the property inherits its value.
/// </value>
public override bool Inherits => _inherits;
/// <summary>
/// Gets the value validation callback for the property.
/// </summary>
public Func<TValue, bool>? ValidateValue { get; }
/// <summary>
/// Gets a value indicating whether this property has any value coercion callbacks defined
/// in its metadata.
/// </summary>
internal bool HasCoercion { get; private set; }
public TValue CoerceValue(AvaloniaObject instance, TValue baseValue)
{
var metadata = GetMetadata(instance.GetType());
if (metadata.CoerceValue != null)
{
return metadata.CoerceValue.Invoke(instance, baseValue);
}
return baseValue;
}
/// <summary>
/// Gets the default value for the property on the specified type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>The default value.</returns>
public TValue GetDefaultValue(Type type)
{
return GetMetadata(type).DefaultValue;
}
/// <summary>
/// Gets the property metadata for the specified type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>
/// The property metadata.
/// </returns>
public new StyledPropertyMetadata<TValue> GetMetadata(Type type)
{
_ = type ?? throw new ArgumentNullException(nameof(type));
return (StyledPropertyMetadata<TValue>)base.GetMetadata(type);
}
/// <summary>
/// Overrides the default value for the property on the specified type.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="defaultValue">The default value.</param>
public void OverrideDefaultValue<T>(TValue defaultValue) where T : AvaloniaObject
{
OverrideDefaultValue(typeof(T), defaultValue);
}
/// <summary>
/// Overrides the default value for the property on the specified type.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="defaultValue">The default value.</param>
public void OverrideDefaultValue(Type type, TValue defaultValue)
{
OverrideMetadata(type, new StyledPropertyMetadata<TValue>(defaultValue));
}
/// <summary>
/// Overrides the metadata for the property on the specified type.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="metadata">The metadata.</param>
public void OverrideMetadata<T>(StyledPropertyMetadata<TValue> metadata) where T : AvaloniaObject
{
base.OverrideMetadata(typeof(T), metadata);
}
/// <summary>
/// Overrides the metadata for the property on the specified type.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="metadata">The metadata.</param>
public void OverrideMetadata(Type type, StyledPropertyMetadata<TValue> metadata)
{
if (ValidateValue != null)
{
if (!ValidateValue(metadata.DefaultValue))
{
throw new ArgumentException(
$"'{metadata.DefaultValue}' is not a valid default value for '{Name}'.");
}
}
HasCoercion |= metadata.CoerceValue != null;
base.OverrideMetadata(type, metadata);
}
/// <summary>
/// Gets the string representation of the property.
/// </summary>
/// <returns>The property's string representation.</returns>
public override string ToString()
{
return Name;
}
/// <inheritdoc/>
object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
bool IStyledPropertyAccessor.ValidateValue(object? value)
{
if (value is null && !typeof(TValue).IsValueType)
return ValidateValue?.Invoke(default!) ?? true;
if (value is TValue typed)
return ValidateValue?.Invoke(typed) ?? true;
return false;
}
internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o)
{
return new EffectiveValue<TValue>(o, this);
}
/// <inheritdoc/>
internal override void RouteClearValue(AvaloniaObject o)
{
o.ClearValue<TValue>(this);
}
/// <inheritdoc/>
internal override object? RouteGetValue(AvaloniaObject o)
{
return o.GetValue<TValue>(this);
}
/// <inheritdoc/>
internal override object? RouteGetBaseValue(AvaloniaObject o)
{
var value = o.GetBaseValue<TValue>(this);
return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue;
}
/// <inheritdoc/>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
internal override IDisposable? RouteSetValue(
AvaloniaObject target,
object? value,
BindingPriority priority)
{
if (value == BindingOperations.DoNothing)
{
return null;
}
else if (value == UnsetValue)
{
target.ClearValue(this);
return null;
}
else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted))
{
return target.SetValue<TValue>(this, (TValue)converted!, priority);
}
else
{
var type = value?.GetType().FullName ?? "(null)";
throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})");
}
}
internal override IDisposable RouteBind(
AvaloniaObject target,
IObservable<object?> source,
BindingPriority priority)
{
return target.Bind<TValue>(this, source, priority);
}
private object? GetDefaultBoxedValue(Type type)
{
_ = type ?? throw new ArgumentNullException(nameof(type));
return GetMetadata(type).DefaultValue;
}
}
}

4
src/Avalonia.Base/Styling/PropertySetterInstance.cs

@ -14,7 +14,7 @@ namespace Avalonia.Styling
ISetterInstance ISetterInstance
{ {
private readonly StyledElement _target; private readonly StyledElement _target;
private readonly StyledPropertyBase<T>? _styledProperty; private readonly StyledProperty<T>? _styledProperty;
private readonly DirectPropertyBase<T>? _directProperty; private readonly DirectPropertyBase<T>? _directProperty;
private readonly T _value; private readonly T _value;
private IDisposable? _subscription; private IDisposable? _subscription;
@ -22,7 +22,7 @@ namespace Avalonia.Styling
public PropertySetterInstance( public PropertySetterInstance(
StyledElement target, StyledElement target,
StyledPropertyBase<T> property, StyledProperty<T> property,
T value) T value)
{ {
_target = target; _target = target;

3
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

@ -245,11 +245,12 @@
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</Border> </Border>
<!-- Color updated in code-behind --> <!-- Color updated in code-behind -->
<!-- Max length must include an optional '#' prefix (#FFFFFFFF) -->
<TextBox x:Name="PART_HexTextBox" <TextBox x:Name="PART_HexTextBox"
Grid.Column="1" Grid.Column="1"
AutomationProperties.Name="Hexadecimal Color" AutomationProperties.Name="Hexadecimal Color"
Height="32" Height="32"
MaxLength="8" MaxLength="9"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
CornerRadius="0,4,4,0" /> CornerRadius="0,4,4,0" />
</Grid> </Grid>

3
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@ -494,11 +494,12 @@
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</Border> </Border>
<!-- Color updated in code-behind --> <!-- Color updated in code-behind -->
<!-- Max length must include an optional '#' prefix (#FFFFFFFF) -->
<TextBox x:Name="PART_HexTextBox" <TextBox x:Name="PART_HexTextBox"
Grid.Column="1" Grid.Column="1"
AutomationProperties.Name="Hexadecimal Color" AutomationProperties.Name="Hexadecimal Color"
Height="32" Height="32"
MaxLength="8" MaxLength="9"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
CornerRadius="0,4,4,0" /> CornerRadius="0,4,4,0" />
</Grid> </Grid>

3
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml

@ -245,11 +245,12 @@
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</Border> </Border>
<!-- Color updated in code-behind --> <!-- Color updated in code-behind -->
<!-- Max length must include an optional '#' prefix (#FFFFFFFF) -->
<TextBox x:Name="PART_HexTextBox" <TextBox x:Name="PART_HexTextBox"
Grid.Column="1" Grid.Column="1"
AutomationProperties.Name="Hexadecimal Color" AutomationProperties.Name="Hexadecimal Color"
Height="32" Height="32"
MaxLength="8" MaxLength="9"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
CornerRadius="0,0,0,0" /> CornerRadius="0,0,0,0" />
</Grid> </Grid>

3
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml

@ -456,11 +456,12 @@
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</Border> </Border>
<!-- Color updated in code-behind --> <!-- Color updated in code-behind -->
<!-- Max length must include an optional '#' prefix (#FFFFFFFF) -->
<TextBox x:Name="PART_HexTextBox" <TextBox x:Name="PART_HexTextBox"
Grid.Column="1" Grid.Column="1"
AutomationProperties.Name="Hexadecimal Color" AutomationProperties.Name="Hexadecimal Color"
Height="32" Height="32"
MaxLength="8" MaxLength="9"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
CornerRadius="0,0,0,0" /> CornerRadius="0,0,0,0" />
</Grid> </Grid>

91
src/Avalonia.Controls/ItemsControl.cs

@ -12,6 +12,8 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Styling; using Avalonia.Styling;
@ -23,7 +25,7 @@ namespace Avalonia.Controls
/// Displays a collection of items. /// Displays a collection of items.
/// </summary> /// </summary>
[PseudoClasses(":empty", ":singleitem")] [PseudoClasses(":empty", ":singleitem")]
public class ItemsControl : TemplatedControl, IChildIndexProvider public class ItemsControl : TemplatedControl, IChildIndexProvider, IScrollSnapPointsInfo
{ {
/// <summary> /// <summary>
/// The default value for the <see cref="ItemsPanel"/> property. /// The default value for the <see cref="ItemsPanel"/> property.
@ -72,6 +74,18 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty = public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding)); AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding));
/// <summary>
/// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
/// </summary>
public static readonly StyledProperty<bool> AreHorizontalSnapPointsRegularProperty =
AvaloniaProperty.Register<ItemsControl, bool>(nameof(AreHorizontalSnapPointsRegular));
/// <summary>
/// Defines the <see cref="AreVerticalSnapPointsRegular"/> property.
/// </summary>
public static readonly StyledProperty<bool> AreVerticalSnapPointsRegularProperty =
AvaloniaProperty.Register<ItemsControl, bool>(nameof(AreVerticalSnapPointsRegular));
/// <summary> /// <summary>
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item. /// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
@ -91,6 +105,8 @@ namespace Avalonia.Controls
private IDataTemplate? _displayMemberItemTemplate; private IDataTemplate? _displayMemberItemTemplate;
private Tuple<int, Control>? _containerBeingPrepared; private Tuple<int, Control>? _containerBeingPrepared;
private ScrollViewer? _scrollViewer; private ScrollViewer? _scrollViewer;
private ItemsPresenter? _itemsPresenter;
private IScrollSnapPointsInfo? _scrolSnapPointInfo;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ItemsControl"/> class. /// Initializes a new instance of the <see cref="ItemsControl"/> class.
@ -203,6 +219,63 @@ namespace Avalonia.Controls
remove => _childIndexChanged -= value; remove => _childIndexChanged -= value;
} }
public event EventHandler<RoutedEventArgs> HorizontalSnapPointsChanged
{
add
{
if (_itemsPresenter != null)
{
_itemsPresenter.HorizontalSnapPointsChanged += value;
}
}
remove
{
if (_itemsPresenter != null)
{
_itemsPresenter.HorizontalSnapPointsChanged -= value;
}
}
}
public event EventHandler<RoutedEventArgs> VerticalSnapPointsChanged
{
add
{
if (_itemsPresenter != null)
{
_itemsPresenter.VerticalSnapPointsChanged += value;
}
}
remove
{
if (_itemsPresenter != null)
{
_itemsPresenter.VerticalSnapPointsChanged -= value;
}
}
}
/// <summary>
/// Gets or sets whether the horizontal snap points for the <see cref="ItemsControl"/> are equidistant from each other.
/// </summary>
public bool AreHorizontalSnapPointsRegular
{
get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
}
/// <summary>
/// Gets or sets whether the vertical snap points for the <see cref="ItemsControl"/> are equidistant from each other.
/// </summary>
public bool AreVerticalSnapPointsRegular
{
get { return GetValue(AreVerticalSnapPointsRegularProperty); }
set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
}
/// <summary> /// <summary>
/// Returns the container for the item at the specified index. /// Returns the container for the item at the specified index.
/// </summary> /// </summary>
@ -255,7 +328,6 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public IEnumerable<Control> GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty<Control>(); public IEnumerable<Control> GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty<Control>();
/// <summary> /// <summary>
/// Creates or a container that can be used to display an item. /// Creates or a container that can be used to display an item.
/// </summary> /// </summary>
@ -355,6 +427,9 @@ namespace Avalonia.Controls
{ {
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
_scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer"); _scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
_itemsPresenter = e.NameScope.Find<ItemsPresenter>("PART_ItemsPresenter");
_scrolSnapPointInfo = _itemsPresenter as IScrollSnapPointsInfo;
} }
/// <summary> /// <summary>
@ -671,5 +746,17 @@ namespace Avalonia.Controls
count = ItemsView.Count; count = ItemsView.Count;
return true; return true;
} }
public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment)
{
return _itemsPresenter?.GetIrregularSnapPoints(orientation, snapPointsAlignment) ?? new List<double>();
}
public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset)
{
offset = 0;
return _itemsPresenter?.GetRegularSnapPoints(orientation, snapPointsAlignment, out offset) ?? 0;
}
} }
} }

11
src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Platform.Storage;
#nullable enable #nullable enable
@ -26,9 +27,7 @@ namespace Avalonia.Controls.Platform
var files = await filePicker.OpenFilePickerAsync(options); var files = await filePicker.OpenFilePickerAsync(options);
return files return files
.Select(file => file.TryGetUri(out var fullPath) .Select(file => file.TryGetFullPath() ?? file.Name)
? fullPath.LocalPath
: file.Name)
.ToArray(); .ToArray();
} }
else if (dialog is SaveFileDialog saveDialog) else if (dialog is SaveFileDialog saveDialog)
@ -47,9 +46,7 @@ namespace Avalonia.Controls.Platform
return null; return null;
} }
var filePath = file.TryGetUri(out var fullPath) var filePath = file.TryGetFullPath() ?? file.Name;
? fullPath.LocalPath
: file.Name;
return new[] { filePath }; return new[] { filePath };
} }
return null; return null;
@ -67,7 +64,7 @@ namespace Avalonia.Controls.Platform
var folders = await filePicker.OpenFolderPickerAsync(options); var folders = await filePicker.OpenFolderPickerAsync(options);
return folders return folders
.Select(f => f.TryGetUri(out var uri) ? uri.LocalPath : null) .Select(folder => folder.TryGetFullPath() ?? folder.Name)
.FirstOrDefault(u => u is not null); .FirstOrDefault(u => u is not null);
} }
} }

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

@ -3,13 +3,15 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
/// <summary> /// <summary>
/// Presents items inside an <see cref="Avalonia.Controls.ItemsControl"/>. /// Presents items inside an <see cref="Avalonia.Controls.ItemsControl"/>.
/// </summary> /// </summary>
public class ItemsPresenter : Control, ILogicalScrollable public class ItemsPresenter : Control, ILogicalScrollable, IScrollSnapPointsInfo
{ {
/// <summary> /// <summary>
/// Defines the <see cref="ItemsPanel"/> property. /// Defines the <see cref="ItemsPanel"/> property.
@ -19,8 +21,37 @@ namespace Avalonia.Controls.Presenters
private PanelContainerGenerator? _generator; private PanelContainerGenerator? _generator;
private ILogicalScrollable? _logicalScrollable; private ILogicalScrollable? _logicalScrollable;
private IScrollSnapPointsInfo? _scrollSnapPointsInfo;
private EventHandler? _scrollInvalidated; private EventHandler? _scrollInvalidated;
/// <summary>
/// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
/// </summary>
public static readonly StyledProperty<bool> AreHorizontalSnapPointsRegularProperty =
AvaloniaProperty.Register<ItemsControl, bool>(nameof(AreHorizontalSnapPointsRegular));
/// <summary>
/// Defines the <see cref="AreVerticalSnapPointsRegular"/> property.
/// </summary>
public static readonly StyledProperty<bool> AreVerticalSnapPointsRegularProperty =
AvaloniaProperty.Register<ItemsControl, bool>(nameof(AreVerticalSnapPointsRegular));
/// <summary>
/// Defines the <see cref="HorizontalSnapPointsChanged"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> HorizontalSnapPointsChangedEvent =
RoutedEvent.Register<StackPanel, RoutedEventArgs>(
nameof(HorizontalSnapPointsChanged),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="VerticalSnapPointsChanged"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> VerticalSnapPointsChangedEvent =
RoutedEvent.Register<StackPanel, RoutedEventArgs>(
nameof(VerticalSnapPointsChanged),
RoutingStrategies.Bubble);
static ItemsPresenter() static ItemsPresenter()
{ {
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue( KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
@ -83,12 +114,48 @@ namespace Avalonia.Controls.Presenters
} }
} }
/// <summary>
/// Occurs when the measurements for horizontal snap points change.
/// </summary>
public event EventHandler<RoutedEventArgs>? HorizontalSnapPointsChanged
{
add => AddHandler(HorizontalSnapPointsChangedEvent, value);
remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value);
}
/// <summary>
/// Occurs when the measurements for vertical snap points change.
/// </summary>
public event EventHandler<RoutedEventArgs>? VerticalSnapPointsChanged
{
add => AddHandler(VerticalSnapPointsChangedEvent, value);
remove => RemoveHandler(VerticalSnapPointsChangedEvent, value);
}
bool ILogicalScrollable.IsLogicalScrollEnabled => _logicalScrollable?.IsLogicalScrollEnabled ?? false; bool ILogicalScrollable.IsLogicalScrollEnabled => _logicalScrollable?.IsLogicalScrollEnabled ?? false;
Size ILogicalScrollable.ScrollSize => _logicalScrollable?.ScrollSize ?? default; Size ILogicalScrollable.ScrollSize => _logicalScrollable?.ScrollSize ?? default;
Size ILogicalScrollable.PageScrollSize => _logicalScrollable?.PageScrollSize ?? default; Size ILogicalScrollable.PageScrollSize => _logicalScrollable?.PageScrollSize ?? default;
Size IScrollable.Extent => _logicalScrollable?.Extent ?? default; Size IScrollable.Extent => _logicalScrollable?.Extent ?? default;
Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default; Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default;
/// <summary>
/// Gets or sets whether the horizontal snap points for the <see cref="ItemsControl"/> are equidistant from each other.
/// </summary>
public bool AreHorizontalSnapPointsRegular
{
get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
}
/// <summary>
/// Gets or sets whether the vertical snap points for the <see cref="ItemsControl"/> are equidistant from each other.
/// </summary>
public bool AreVerticalSnapPointsRegular
{
get { return GetValue(AreVerticalSnapPointsRegularProperty); }
set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
}
public override sealed void ApplyTemplate() public override sealed void ApplyTemplate()
{ {
if (Panel is null && ItemsControl is not null) if (Panel is null && ItemsControl is not null)
@ -100,14 +167,36 @@ namespace Avalonia.Controls.Presenters
Panel = ItemsPanel.Build(); Panel = ItemsPanel.Build();
Panel.SetValue(TemplatedParentProperty, TemplatedParent); Panel.SetValue(TemplatedParentProperty, TemplatedParent);
_scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo;
LogicalChildren.Add(Panel); LogicalChildren.Add(Panel);
VisualChildren.Add(Panel); VisualChildren.Add(Panel);
if (_scrollSnapPointsInfo != null)
{
_scrollSnapPointsInfo.AreVerticalSnapPointsRegular = AreVerticalSnapPointsRegular;
_scrollSnapPointsInfo.AreHorizontalSnapPointsRegular = AreHorizontalSnapPointsRegular;
}
if (Panel is VirtualizingPanel v) if (Panel is VirtualizingPanel v)
v.Attach(ItemsControl); v.Attach(ItemsControl);
else else
CreateSimplePanelGenerator(); CreateSimplePanelGenerator();
if(Panel is IScrollSnapPointsInfo scrollSnapPointsInfo)
{
scrollSnapPointsInfo.VerticalSnapPointsChanged += (s, e) =>
{
e.RoutedEvent = VerticalSnapPointsChangedEvent;
RaiseEvent(e);
};
scrollSnapPointsInfo.HorizontalSnapPointsChanged += (s, e) =>
{
e.RoutedEvent = HorizontalSnapPointsChangedEvent;
RaiseEvent(e);
};
}
_logicalScrollable = Panel as ILogicalScrollable; _logicalScrollable = Panel as ILogicalScrollable;
if (_logicalScrollable is not null) if (_logicalScrollable is not null)
@ -151,6 +240,16 @@ namespace Avalonia.Controls.Presenters
ResetState(); ResetState();
InvalidateMeasure(); InvalidateMeasure();
} }
else if(change.Property == AreHorizontalSnapPointsRegularProperty)
{
if (_scrollSnapPointsInfo != null)
_scrollSnapPointsInfo.AreHorizontalSnapPointsRegular = AreHorizontalSnapPointsRegular;
}
else if (change.Property == AreVerticalSnapPointsRegularProperty)
{
if (_scrollSnapPointsInfo != null)
_scrollSnapPointsInfo.AreVerticalSnapPointsRegular = AreVerticalSnapPointsRegular;
}
} }
internal void Refresh() internal void Refresh()
@ -204,5 +303,27 @@ namespace Avalonia.Controls.Presenters
} }
private void OnLogicalScrollInvalidated(object? sender, EventArgs e) => _scrollInvalidated?.Invoke(this, e); private void OnLogicalScrollInvalidated(object? sender, EventArgs e) => _scrollInvalidated?.Invoke(this, e);
public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment)
{
if(Panel is IScrollSnapPointsInfo scrollSnapPointsInfo)
{
return scrollSnapPointsInfo.GetIrregularSnapPoints(orientation, snapPointsAlignment);
}
return new List<double>();
}
public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset)
{
if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo)
{
return scrollSnapPointsInfo.GetRegularSnapPoints(orientation, snapPointsAlignment, out offset);
}
offset = 0;
return 0;
}
} }
} }

368
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -14,6 +15,7 @@ namespace Avalonia.Controls.Presenters
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider
{ {
private const double EdgeDetectionTolerance = 0.1; private const double EdgeDetectionTolerance = 0.1;
private const int ProximityPoints = 10;
/// <summary> /// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property. /// Defines the <see cref="CanHorizontallyScroll"/> property.
@ -57,6 +59,30 @@ namespace Avalonia.Controls.Presenters
o => o.Viewport, o => o.Viewport,
(o, v) => o.Viewport = v); (o, v) => o.Viewport = v);
/// <summary>
/// Defines the <see cref="HorizontalSnapPointsType"/> property.
/// </summary>
public static readonly StyledProperty<SnapPointsType> HorizontalSnapPointsTypeProperty =
ScrollViewer.HorizontalSnapPointsTypeProperty.AddOwner<ScrollContentPresenter>();
/// <summary>
/// Defines the <see cref="VerticalSnapPointsType"/> property.
/// </summary>
public static readonly StyledProperty<SnapPointsType> VerticalSnapPointsTypeProperty =
ScrollViewer.VerticalSnapPointsTypeProperty.AddOwner<ScrollContentPresenter>();
/// <summary>
/// Defines the <see cref="HorizontalSnapPointsAlignment"/> property.
/// </summary>
public static readonly StyledProperty<SnapPointsAlignment> HorizontalSnapPointsAlignmentProperty =
ScrollViewer.HorizontalSnapPointsAlignmentProperty.AddOwner<ScrollContentPresenter>();
/// <summary>
/// Defines the <see cref="VerticalSnapPointsAlignment"/> property.
/// </summary>
public static readonly StyledProperty<SnapPointsAlignment> VerticalSnapPointsAlignmentProperty =
ScrollViewer.VerticalSnapPointsAlignmentProperty.AddOwner<ScrollContentPresenter>();
/// <summary> /// <summary>
/// Defines the <see cref="IsScrollChainingEnabled"/> property. /// Defines the <see cref="IsScrollChainingEnabled"/> property.
/// </summary> /// </summary>
@ -71,10 +97,19 @@ namespace Avalonia.Controls.Presenters
private IDisposable? _logicalScrollSubscription; private IDisposable? _logicalScrollSubscription;
private Size _viewport; private Size _viewport;
private Dictionary<int, Vector>? _activeLogicalGestureScrolls; private Dictionary<int, Vector>? _activeLogicalGestureScrolls;
private Dictionary<int, Vector>? _scrollGestureSnapPoints;
private List<Control>? _anchorCandidates; private List<Control>? _anchorCandidates;
private Control? _anchorElement; private Control? _anchorElement;
private Rect _anchorElementBounds; private Rect _anchorElementBounds;
private bool _isAnchorElementDirty; private bool _isAnchorElementDirty;
private bool _areVerticalSnapPointsRegular;
private bool _areHorizontalSnapPointsRegular;
private IReadOnlyList<double>? _horizontalSnapPoints;
private double _horizontalSnapPoint;
private IReadOnlyList<double>? _verticalSnapPoints;
private double _verticalSnapPoint;
private double _verticalSnapPointOffset;
private double _horizontalSnapPointOffset;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class. /// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -93,6 +128,7 @@ namespace Avalonia.Controls.Presenters
AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture); AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture);
AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded);
AddHandler(Gestures.ScrollGestureInertiaStartingEvent, OnScrollGestureInertiaStartingEnded);
this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
} }
@ -142,6 +178,42 @@ namespace Avalonia.Controls.Presenters
private set { SetAndRaise(ViewportProperty, ref _viewport, value); } private set { SetAndRaise(ViewportProperty, ref _viewport, value); }
} }
/// <summary>
/// Gets or sets how scroll gesture reacts to the snap points along the horizontal axis.
/// </summary>
public SnapPointsType HorizontalSnapPointsType
{
get => GetValue(HorizontalSnapPointsTypeProperty);
set => SetValue(HorizontalSnapPointsTypeProperty, value);
}
/// <summary>
/// Gets or sets how scroll gesture reacts to the snap points along the vertical axis.
/// </summary>
public SnapPointsType VerticalSnapPointsType
{
get => GetValue(VerticalSnapPointsTypeProperty);
set => SetValue(VerticalSnapPointsTypeProperty, value);
}
/// <summary>
/// Gets or sets how the existing snap points are horizontally aligned versus the initial viewport.
/// </summary>
public SnapPointsAlignment HorizontalSnapPointsAlignment
{
get => GetValue(HorizontalSnapPointsAlignmentProperty);
set => SetValue(HorizontalSnapPointsAlignmentProperty, value);
}
/// <summary>
/// Gets or sets how the existing snap points are vertically aligned versus the initial viewport.
/// </summary>
public SnapPointsAlignment VerticalSnapPointsAlignment
{
get => GetValue(VerticalSnapPointsAlignmentProperty);
set => SetValue(VerticalSnapPointsAlignmentProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets if scroll chaining is enabled. The default value is true. /// Gets or sets if scroll chaining is enabled. The default value is true.
/// </summary> /// </summary>
@ -424,6 +496,25 @@ namespace Avalonia.Controls.Presenters
} }
Vector newOffset = new Vector(x, y); Vector newOffset = new Vector(x, y);
if (_scrollGestureSnapPoints?.TryGetValue(e.Id, out var snapPoint) == true)
{
double xOffset = x;
double yOffset = y;
if (HorizontalSnapPointsType != SnapPointsType.None)
{
xOffset = delta.X < 0 ? Math.Max(snapPoint.X, newOffset.X) : Math.Min(snapPoint.X, newOffset.X);
}
if (VerticalSnapPointsType != SnapPointsType.None)
{
yOffset = delta.Y < 0 ? Math.Max(snapPoint.Y, newOffset.Y) : Math.Min(snapPoint.Y, newOffset.Y);
}
newOffset = new Vector(xOffset, yOffset);
}
bool offsetChanged = newOffset != Offset; bool offsetChanged = newOffset != Offset;
Offset = newOffset; Offset = newOffset;
@ -434,7 +525,65 @@ namespace Avalonia.Controls.Presenters
} }
private void OnScrollGestureEnded(object? sender, ScrollGestureEndedEventArgs e) private void OnScrollGestureEnded(object? sender, ScrollGestureEndedEventArgs e)
=> _activeLogicalGestureScrolls?.Remove(e.Id); {
_activeLogicalGestureScrolls?.Remove(e.Id);
_scrollGestureSnapPoints?.Remove(e.Id);
Offset = SnapOffset(Offset);
}
private void OnScrollGestureInertiaStartingEnded(object? sender, ScrollGestureInertiaStartingEventArgs e)
{
if (Content is not IScrollSnapPointsInfo)
return;
if (_scrollGestureSnapPoints == null)
_scrollGestureSnapPoints = new Dictionary<int, Vector>();
var offset = Offset;
if (HorizontalSnapPointsType != SnapPointsType.None && VerticalSnapPointsType != SnapPointsType.None)
{
return;
}
double xDistance = 0;
double yDistance = 0;
if (HorizontalSnapPointsType != SnapPointsType.None)
{
xDistance = HorizontalSnapPointsType == SnapPointsType.Mandatory ? GetDistance(e.Inertia.X) : 0;
}
if (VerticalSnapPointsType != SnapPointsType.None)
{
yDistance = VerticalSnapPointsType == SnapPointsType.Mandatory ? GetDistance(e.Inertia.Y) : 0;
}
offset = new Vector(offset.X + xDistance, offset.Y + yDistance);
System.Diagnostics.Debug.WriteLine($"{offset}");
_scrollGestureSnapPoints.Add(e.Id, SnapOffset(offset));
double GetDistance(double speed)
{
var time = Math.Log(ScrollGestureRecognizer.InertialScrollSpeedEnd / Math.Abs(speed)) / Math.Log(ScrollGestureRecognizer.InertialResistance);
double timeElapsed = 0, distance = 0, step = 0;
while (timeElapsed <= time)
{
double s = speed * Math.Pow(ScrollGestureRecognizer.InertialResistance, timeElapsed);
distance += (s * step);
timeElapsed += 0.016f;
step = 0.016f;
}
return distance;
}
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnPointerWheelChanged(PointerWheelEventArgs e) protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
@ -458,6 +607,30 @@ namespace Avalonia.Controls.Presenters
if (Extent.Height > Viewport.Height) if (Extent.Height > Viewport.Height)
{ {
double height = isLogical ? scrollable!.ScrollSize.Height : 50; double height = isLogical ? scrollable!.ScrollSize.Height : 50;
if(VerticalSnapPointsType == SnapPointsType.MandatorySingle && Content is IScrollSnapPointsInfo)
{
if(_areVerticalSnapPointsRegular)
{
height = _verticalSnapPoint;
}
else if(_verticalSnapPoints != null)
{
double yOffset = Offset.Y;
switch (VerticalSnapPointsAlignment)
{
case SnapPointsAlignment.Center:
yOffset += Viewport.Height / 2;
break;
case SnapPointsAlignment.Far:
yOffset += Viewport.Height;
break;
}
var snapPoint = FindNearestSnapPoint(_verticalSnapPoints, yOffset, out var lowerSnapPoint);
height = snapPoint - lowerSnapPoint;
}
}
y += -delta.Y * height; y += -delta.Y * height;
y = Math.Max(y, 0); y = Math.Max(y, 0);
y = Math.Min(y, Extent.Height - Viewport.Height); y = Math.Min(y, Extent.Height - Viewport.Height);
@ -466,12 +639,37 @@ namespace Avalonia.Controls.Presenters
if (Extent.Width > Viewport.Width) if (Extent.Width > Viewport.Width)
{ {
double width = isLogical ? scrollable!.ScrollSize.Width : 50; double width = isLogical ? scrollable!.ScrollSize.Width : 50;
if (HorizontalSnapPointsType == SnapPointsType.MandatorySingle && Content is IScrollSnapPointsInfo)
{
if (_areHorizontalSnapPointsRegular)
{
width = _horizontalSnapPoint;
}
else if(_horizontalSnapPoints != null)
{
double xOffset = Offset.X;
switch (VerticalSnapPointsAlignment)
{
case SnapPointsAlignment.Center:
xOffset += Viewport.Width / 2;
break;
case SnapPointsAlignment.Far:
xOffset += Viewport.Width;
break;
}
var snapPoint = FindNearestSnapPoint(_horizontalSnapPoints, xOffset, out var lowerSnapPoint);
width = snapPoint - lowerSnapPoint;
}
}
x += -delta.X * width; x += -delta.X * width;
x = Math.Max(x, 0); x = Math.Max(x, 0);
x = Math.Min(x, Extent.Width - Viewport.Width); x = Math.Min(x, Extent.Width - Viewport.Width);
} }
Vector newOffset = new Vector(x, y); Vector newOffset = SnapOffset(new Vector(x, y));
bool offsetChanged = newOffset != Offset; bool offsetChanged = newOffset != Offset;
Offset = newOffset; Offset = newOffset;
@ -485,10 +683,36 @@ namespace Avalonia.Controls.Presenters
{ {
InvalidateArrange(); InvalidateArrange();
} }
else if (change.Property == ContentProperty)
{
if (change.OldValue is IScrollSnapPointsInfo oldSnapPointsInfo)
{
oldSnapPointsInfo.VerticalSnapPointsChanged -= ScrollSnapPointsInfoSnapPointsChanged;
oldSnapPointsInfo.HorizontalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
}
if (Content is IScrollSnapPointsInfo scrollSnapPointsInfo)
{
scrollSnapPointsInfo.VerticalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
scrollSnapPointsInfo.HorizontalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
}
UpdateSnapPoints();
}
else if (change.Property == HorizontalSnapPointsAlignmentProperty ||
change.Property == VerticalSnapPointsAlignmentProperty)
{
UpdateSnapPoints();
}
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
} }
private void ScrollSnapPointsInfoSnapPointsChanged(object? sender, Interactivity.RoutedEventArgs e)
{
UpdateSnapPoints();
}
private void BringIntoViewRequested(object? sender, RequestBringIntoViewEventArgs e) private void BringIntoViewRequested(object? sender, RequestBringIntoViewEventArgs e)
{ {
if (e.TargetObject is not null) if (e.TargetObject is not null)
@ -635,5 +859,145 @@ namespace Avalonia.Controls.Presenters
bounds = p.HasValue ? new Rect(p.Value, control.Bounds.Size) : default; bounds = p.HasValue ? new Rect(p.Value, control.Bounds.Size) : default;
return p.HasValue; return p.HasValue;
} }
private void UpdateSnapPoints()
{
if (Content is IScrollSnapPointsInfo scrollSnapPointsInfo)
{
_areVerticalSnapPointsRegular = scrollSnapPointsInfo.AreVerticalSnapPointsRegular;
_areHorizontalSnapPointsRegular = scrollSnapPointsInfo.AreHorizontalSnapPointsRegular;
if (!_areVerticalSnapPointsRegular)
{
_verticalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment);
}
else
{
_verticalSnapPoints = new List<double>();
_verticalSnapPoint = scrollSnapPointsInfo.GetRegularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment, out _verticalSnapPointOffset);
}
if (!_areHorizontalSnapPointsRegular)
{
_horizontalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Layout.Orientation.Horizontal, HorizontalSnapPointsAlignment);
}
else
{
_horizontalSnapPoints = new List<double>();
_horizontalSnapPoint = scrollSnapPointsInfo.GetRegularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment, out _horizontalSnapPointOffset);
}
}
else
{
_horizontalSnapPoints = new List<double>();
_verticalSnapPoints = new List<double>();
}
}
private Vector SnapOffset(Vector offset)
{
if(Content is not IScrollSnapPointsInfo)
return offset;
var diff = GetAlignedDiff();
if (VerticalSnapPointsType != SnapPointsType.None)
{
offset = new Vector(offset.X, offset.Y + diff.Y);
double nearestSnapPoint = offset.Y;
if (_areVerticalSnapPointsRegular)
{
var minSnapPoint = (int)(offset.Y / _verticalSnapPoint) * _verticalSnapPoint + _verticalSnapPointOffset;
var maxSnapPoint = minSnapPoint + _verticalSnapPoint;
var midPoint = (minSnapPoint + maxSnapPoint) / 2;
nearestSnapPoint = offset.Y < midPoint ? minSnapPoint : maxSnapPoint;
}
else if (_verticalSnapPoints != null && _verticalSnapPoints.Count > 0)
{
var higherSnapPoint = FindNearestSnapPoint(_verticalSnapPoints, offset.Y, out var lowerSnapPoint);
var midPoint = (lowerSnapPoint + higherSnapPoint) / 2;
nearestSnapPoint = offset.Y < midPoint ? lowerSnapPoint : higherSnapPoint;
}
offset = new Vector(offset.X, nearestSnapPoint - diff.Y);
}
if (HorizontalSnapPointsType != SnapPointsType.None)
{
offset = new Vector(offset.X + diff.X, offset.Y);
double nearestSnapPoint = offset.X;
if (_areHorizontalSnapPointsRegular)
{
var minSnapPoint = (int)(offset.X / _horizontalSnapPoint) * _horizontalSnapPoint + _horizontalSnapPointOffset;
var maxSnapPoint = minSnapPoint + _horizontalSnapPoint;
var midPoint = (minSnapPoint + maxSnapPoint) / 2;
nearestSnapPoint = offset.X < midPoint ? minSnapPoint : maxSnapPoint;
}
else if (_horizontalSnapPoints != null && _horizontalSnapPoints.Count > 0)
{
var higherSnapPoint = FindNearestSnapPoint(_horizontalSnapPoints, offset.X, out var lowerSnapPoint);
var midPoint = (lowerSnapPoint + higherSnapPoint) / 2;
nearestSnapPoint = offset.X < midPoint ? lowerSnapPoint : higherSnapPoint;
}
offset = new Vector(nearestSnapPoint - diff.X, offset.Y);
}
Vector GetAlignedDiff()
{
var vector = offset;
switch (VerticalSnapPointsAlignment)
{
case SnapPointsAlignment.Center:
vector += new Vector(0, Viewport.Height / 2);
break;
case SnapPointsAlignment.Far:
vector += new Vector(0, Viewport.Height);
break;
}
switch (HorizontalSnapPointsAlignment)
{
case SnapPointsAlignment.Center:
vector += new Vector(Viewport.Width / 2, 0);
break;
case SnapPointsAlignment.Far:
vector += new Vector(Viewport.Width, 0);
break;
}
return vector - offset;
}
return offset;
}
private static double FindNearestSnapPoint(IReadOnlyList<double> snapPoints, double value, out double lowerSnapPoint)
{
var point = snapPoints.BinarySearch(value, Comparer<double>.Default);
if (point < 0)
{
point = ~point;
lowerSnapPoint = snapPoints[Math.Max(0, point - 1)];
}
else
{
lowerSnapPoint = snapPoints[point];
point += 1;
}
return snapPoints[Math.Min(point, snapPoints.Count - 1)];
}
} }
} }

50
src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Describes snap point behavior for objects that contain and present items.
/// </summary>
public interface IScrollSnapPointsInfo
{
/// <summary>
/// Gets or sets a value that indicates whether the horizontal snap points for the container are equidistant from each other.
/// </summary>
bool AreHorizontalSnapPointsRegular { get; set; }
/// <summary>
/// Gets or sets a value that indicates whether the vertical snap points for the container are equidistant from each other.
/// </summary>
bool AreVerticalSnapPointsRegular { get; set; }
/// <summary>
/// Returns the set of distances between irregular snap points for a specified orientation and alignment.
/// </summary>
/// <param name="orientation">The orientation for the desired snap point set.</param>
/// <param name="snapPointsAlignment">The alignment to use when applying the snap points.</param>
/// <returns>The read-only collection of snap point distances. Returns an empty collection when no snap points are present.</returns>
IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment);
/// <summary>
/// Gets the distance between regular snap points for a specified orientation and alignment.
/// </summary>
/// <param name="orientation">The orientation for the desired snap point set.</param>
/// <param name="snapPointsAlignment">The alignment to use when applying the snap points.</param>
/// <param name="offset">Out parameter. The offset of the first snap point.</param>
/// <returns>The distance between the equidistant snap points. Returns 0 when no snap points are present.</returns>
double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset);
/// <summary>
/// Occurs when the measurements for horizontal snap points change.
/// </summary>
event EventHandler<RoutedEventArgs> HorizontalSnapPointsChanged;
/// <summary>
/// Occurs when the measurements for vertical snap points change.
/// </summary>
event EventHandler<RoutedEventArgs> VerticalSnapPointsChanged;
}
}

23
src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs

@ -0,0 +1,23 @@
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Specify options for snap point alignment relative to an edge. Which edge depends on the orientation of the object where the alignment is applied
/// </summary>
public enum SnapPointsAlignment
{
/// <summary>
/// Use snap points grouped closer to the orientation edge.
/// </summary>
Near,
/// <summary>
/// Use snap points that are centered in the orientation.
/// </summary>
Center,
/// <summary>
/// Use snap points grouped farther from the orientation edge.
/// </summary>
Far
}
}

23
src/Avalonia.Controls/Primitives/SnapPointsType.cs

@ -0,0 +1,23 @@
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Specify how panning snap points are processed for gesture input.
/// </summary>
public enum SnapPointsType
{
/// <summary>
/// No snapping behavior.
/// </summary>
None,
/// <summary>
/// Content always stops at the snap point closest to where inertia would naturally stop along the direction of inertia.
/// </summary>
Mandatory,
/// <summary>
/// Content always stops at the snap point closest to the release point along the direction of inertia.
/// </summary>
MandatorySingle
}
}

81
src/Avalonia.Controls/ScrollViewer.cs

@ -151,6 +151,34 @@ namespace Avalonia.Controls
o => o.VerticalScrollBarValue, o => o.VerticalScrollBarValue,
(o, v) => o.VerticalScrollBarValue = v); (o, v) => o.VerticalScrollBarValue = v);
/// <summary>
/// Defines the <see cref="HorizontalSnapPointsType"/> property.
/// </summary>
public static readonly StyledProperty<SnapPointsType> HorizontalSnapPointsTypeProperty =
AvaloniaProperty.Register<ScrollViewer, SnapPointsType>(
nameof(HorizontalSnapPointsType));
/// <summary>
/// Defines the <see cref="VerticalSnapPointsType"/> property.
/// </summary>
public static readonly StyledProperty<SnapPointsType> VerticalSnapPointsTypeProperty =
AvaloniaProperty.Register<ScrollViewer, SnapPointsType>(
nameof(VerticalSnapPointsType));
/// <summary>
/// Defines the <see cref="HorizontalSnapPointsAlignment"/> property.
/// </summary>
public static readonly AttachedProperty<SnapPointsAlignment> HorizontalSnapPointsAlignmentProperty =
AvaloniaProperty.RegisterAttached<ScrollViewer, Control, SnapPointsAlignment>(
nameof(HorizontalSnapPointsAlignment));
/// <summary>
/// Defines the <see cref="VerticalSnapPointsAlignment"/> property.
/// </summary>
public static readonly AttachedProperty<SnapPointsAlignment> VerticalSnapPointsAlignmentProperty =
AvaloniaProperty.RegisterAttached<ScrollViewer, Control, SnapPointsAlignment>(
nameof(VerticalSnapPointsAlignment));
/// <summary> /// <summary>
/// Defines the VerticalScrollBarViewportSize property. /// Defines the VerticalScrollBarViewportSize property.
/// </summary> /// </summary>
@ -193,6 +221,14 @@ namespace Avalonia.Controls
nameof(IsScrollChainingEnabled), nameof(IsScrollChainingEnabled),
defaultValue: true); defaultValue: true);
/// <summary>
/// Defines the <see cref="IsScrollInertiaEnabled"/> property.
/// </summary>
public static readonly AttachedProperty<bool> IsScrollInertiaEnabledProperty =
AvaloniaProperty.RegisterAttached<ScrollViewer, Control, bool>(
nameof(IsScrollInertiaEnabled),
defaultValue: true);
/// <summary> /// <summary>
/// Defines the <see cref="ScrollChanged"/> event. /// Defines the <see cref="ScrollChanged"/> event.
/// </summary> /// </summary>
@ -421,6 +457,42 @@ namespace Avalonia.Controls
private set => SetAndRaise(ScrollBar.IsExpandedProperty, ref _isExpanded, value); private set => SetAndRaise(ScrollBar.IsExpandedProperty, ref _isExpanded, value);
} }
/// <summary>
/// Gets or sets how scroll gesture reacts to the snap points along the horizontal axis.
/// </summary>
public SnapPointsType HorizontalSnapPointsType
{
get => GetValue(HorizontalSnapPointsTypeProperty);
set => SetValue(HorizontalSnapPointsTypeProperty, value);
}
/// <summary>
/// Gets or sets how scroll gesture reacts to the snap points along the vertical axis.
/// </summary>
public SnapPointsType VerticalSnapPointsType
{
get => GetValue(VerticalSnapPointsTypeProperty);
set => SetValue(VerticalSnapPointsTypeProperty, value);
}
/// <summary>
/// Gets or sets how the existing snap points are horizontally aligned versus the initial viewport.
/// </summary>
public SnapPointsAlignment HorizontalSnapPointsAlignment
{
get => GetValue(HorizontalSnapPointsAlignmentProperty);
set => SetValue(HorizontalSnapPointsAlignmentProperty, value);
}
/// <summary>
/// Gets or sets how the existing snap points are vertically aligned versus the initial viewport.
/// </summary>
public SnapPointsAlignment VerticalSnapPointsAlignment
{
get => GetValue(VerticalSnapPointsAlignmentProperty);
set => SetValue(VerticalSnapPointsAlignmentProperty, value);
}
/// <summary> /// <summary>
/// Gets a value that indicates whether scrollbars can hide itself when user is not interacting with it. /// Gets a value that indicates whether scrollbars can hide itself when user is not interacting with it.
/// </summary> /// </summary>
@ -444,6 +516,15 @@ namespace Avalonia.Controls
set => SetValue(IsScrollChainingEnabledProperty, value); set => SetValue(IsScrollChainingEnabledProperty, value);
} }
/// <summary>
/// Gets or sets whether scroll gestures should include inertia in their behavior and value.
/// </summary>
public bool IsScrollInertiaEnabled
{
get => GetValue(IsScrollInertiaEnabledProperty);
set => SetValue(IsScrollInertiaEnabledProperty, value);
}
/// <summary> /// <summary>
/// Scrolls the content up one line. /// Scrolls the content up one line.
/// </summary> /// </summary>

191
src/Avalonia.Controls/StackPanel.cs

@ -4,7 +4,11 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -12,7 +16,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A panel which lays out its children horizontally or vertically. /// A panel which lays out its children horizontally or vertically.
/// </summary> /// </summary>
public class StackPanel : Panel, INavigableContainer public class StackPanel : Panel, INavigableContainer, IScrollSnapPointsInfo
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Spacing"/> property. /// Defines the <see cref="Spacing"/> property.
@ -26,6 +30,34 @@ namespace Avalonia.Controls
public static readonly StyledProperty<Orientation> OrientationProperty = public static readonly StyledProperty<Orientation> OrientationProperty =
StackLayout.OrientationProperty.AddOwner<StackPanel>(); StackLayout.OrientationProperty.AddOwner<StackPanel>();
/// <summary>
/// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
/// </summary>
public static readonly StyledProperty<bool> AreHorizontalSnapPointsRegularProperty =
AvaloniaProperty.Register<StackPanel, bool>(nameof(AreHorizontalSnapPointsRegular));
/// <summary>
/// Defines the <see cref="AreVerticalSnapPointsRegular"/> property.
/// </summary>
public static readonly StyledProperty<bool> AreVerticalSnapPointsRegularProperty =
AvaloniaProperty.Register<StackPanel, bool>(nameof(AreVerticalSnapPointsRegular));
/// <summary>
/// Defines the <see cref="HorizontalSnapPointsChanged"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> HorizontalSnapPointsChangedEvent =
RoutedEvent.Register<StackPanel, RoutedEventArgs>(
nameof(HorizontalSnapPointsChanged),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="VerticalSnapPointsChanged"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> VerticalSnapPointsChangedEvent =
RoutedEvent.Register<StackPanel, RoutedEventArgs>(
nameof(VerticalSnapPointsChanged),
RoutingStrategies.Bubble);
/// <summary> /// <summary>
/// Initializes static members of the <see cref="StackPanel"/> class. /// Initializes static members of the <see cref="StackPanel"/> class.
/// </summary> /// </summary>
@ -53,6 +85,42 @@ namespace Avalonia.Controls
set { SetValue(OrientationProperty, value); } set { SetValue(OrientationProperty, value); }
} }
/// <summary>
/// Occurs when the measurements for horizontal snap points change.
/// </summary>
public event EventHandler<RoutedEventArgs>? HorizontalSnapPointsChanged
{
add => AddHandler(HorizontalSnapPointsChangedEvent, value);
remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value);
}
/// <summary>
/// Occurs when the measurements for vertical snap points change.
/// </summary>
public event EventHandler<RoutedEventArgs>? VerticalSnapPointsChanged
{
add => AddHandler(VerticalSnapPointsChangedEvent, value);
remove => RemoveHandler(VerticalSnapPointsChangedEvent, value);
}
/// <summary>
/// Gets or sets whether the horizontal snap points for the <see cref="StackPanel"/> are equidistant from each other.
/// </summary>
public bool AreHorizontalSnapPointsRegular
{
get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
}
/// <summary>
/// Gets or sets whether the vertical snap points for the <see cref="StackPanel"/> are equidistant from each other.
/// </summary>
public bool AreVerticalSnapPointsRegular
{
get { return GetValue(AreVerticalSnapPointsRegularProperty); }
set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
}
/// <summary> /// <summary>
/// Gets the next control in the specified direction. /// Gets the next control in the specified direction.
/// </summary> /// </summary>
@ -274,6 +342,8 @@ namespace Avalonia.Controls
ArrangeChild(child, rcChild, finalSize, Orientation); ArrangeChild(child, rcChild, finalSize, Orientation);
} }
RaiseEvent(new RoutedEventArgs(Orientation == Orientation.Horizontal ? HorizontalSnapPointsChangedEvent : VerticalSnapPointsChangedEvent));
return finalSize; return finalSize;
} }
@ -285,5 +355,124 @@ namespace Avalonia.Controls
{ {
child.Arrange(rect); child.Arrange(rect);
} }
/// <inheritdoc/>
public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment)
{
var snapPoints = new List<double>();
switch (orientation)
{
case Orientation.Horizontal:
if (AreHorizontalSnapPointsRegular)
throw new InvalidOperationException();
if (Orientation == Orientation.Horizontal)
{
foreach(var child in VisualChildren)
{
double snapPoint = 0;
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Near:
snapPoint = child.Bounds.Left;
break;
case SnapPointsAlignment.Center:
snapPoint = child.Bounds.Center.X;
break;
case SnapPointsAlignment.Far:
snapPoint = child.Bounds.Right;
break;
}
snapPoints.Add(snapPoint);
}
}
break;
case Orientation.Vertical:
if (AreVerticalSnapPointsRegular)
throw new InvalidOperationException();
if (Orientation == Orientation.Vertical)
{
foreach (var child in VisualChildren)
{
double snapPoint = 0;
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Near:
snapPoint = child.Bounds.Top;
break;
case SnapPointsAlignment.Center:
snapPoint = child.Bounds.Center.Y;
break;
case SnapPointsAlignment.Far:
snapPoint = child.Bounds.Bottom;
break;
}
snapPoints.Add(snapPoint);
}
}
break;
}
return snapPoints;
}
/// <inheritdoc/>
public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset)
{
offset = 0f;
var firstChild = VisualChildren.FirstOrDefault();
if(firstChild == null)
{
return 0;
}
double snapPoint = 0;
switch (Orientation)
{
case Orientation.Horizontal:
if (!AreHorizontalSnapPointsRegular)
throw new InvalidOperationException();
snapPoint = firstChild.Bounds.Width;
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Near:
offset = firstChild.Bounds.Left;
break;
case SnapPointsAlignment.Center:
offset = firstChild.Bounds.Center.X;
break;
case SnapPointsAlignment.Far:
offset = firstChild.Bounds.Right;
break;
}
break;
case Orientation.Vertical:
if (!AreVerticalSnapPointsRegular)
throw new InvalidOperationException();
snapPoint = firstChild.Bounds.Height;
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Near:
offset = firstChild.Bounds.Top;
break;
case SnapPointsAlignment.Center:
offset = firstChild.Bounds.Center.Y;
break;
case SnapPointsAlignment.Far:
offset = firstChild.Bounds.Bottom;
break;
}
break;
}
return snapPoint + Spacing;
}
} }
} }

24
src/Avalonia.Controls/TopLevel.cs

@ -581,12 +581,21 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
private void HandleInput(RawInputEventArgs e) private void HandleInput(RawInputEventArgs e)
{ {
if (e is RawPointerEventArgs pointerArgs) if (PlatformImpl != null)
{ {
pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position); if (e is RawPointerEventArgs pointerArgs)
} {
pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position);
}
_inputManager?.ProcessInput(e); _inputManager?.ProcessInput(e);
}
else
{
Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(
this,
"PlatformImpl is null, couldn't handle input.");
}
} }
private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e) private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e)
@ -606,6 +615,13 @@ namespace Avalonia.Controls
KeyboardDevice.Instance?.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None); KeyboardDevice.Instance?.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
} }
protected override bool BypassFlowDirectionPolicies => true;
public override void InvalidateMirrorTransform()
{
// Do nothing becuase TopLevel should't apply MirrorTransform on himself.
}
ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => ITextInputMethodImpl? ITextInputMethodRoot.InputMethod =>
(PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod; (PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod;
} }

234
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -3,8 +3,11 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reflection;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -14,7 +17,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Arranges and virtualizes content on a single line that is oriented either horizontally or vertically. /// Arranges and virtualizes content on a single line that is oriented either horizontally or vertically.
/// </summary> /// </summary>
public class VirtualizingStackPanel : VirtualizingPanel public class VirtualizingStackPanel : VirtualizingPanel, IScrollSnapPointsInfo
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Orientation"/> property. /// Defines the <see cref="Orientation"/> property.
@ -22,6 +25,34 @@ namespace Avalonia.Controls
public static readonly StyledProperty<Orientation> OrientationProperty = public static readonly StyledProperty<Orientation> OrientationProperty =
StackLayout.OrientationProperty.AddOwner<VirtualizingStackPanel>(); StackLayout.OrientationProperty.AddOwner<VirtualizingStackPanel>();
/// <summary>
/// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
/// </summary>
public static readonly StyledProperty<bool> AreHorizontalSnapPointsRegularProperty =
AvaloniaProperty.Register<VirtualizingStackPanel, bool>(nameof(AreHorizontalSnapPointsRegular));
/// <summary>
/// Defines the <see cref="AreVerticalSnapPointsRegular"/> property.
/// </summary>
public static readonly StyledProperty<bool> AreVerticalSnapPointsRegularProperty =
AvaloniaProperty.Register<VirtualizingStackPanel, bool>(nameof(AreVerticalSnapPointsRegular));
/// <summary>
/// Defines the <see cref="HorizontalSnapPointsChanged"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> HorizontalSnapPointsChangedEvent =
RoutedEvent.Register<VirtualizingStackPanel, RoutedEventArgs>(
nameof(HorizontalSnapPointsChanged),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="VerticalSnapPointsChanged"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> VerticalSnapPointsChangedEvent =
RoutedEvent.Register<VirtualizingStackPanel, RoutedEventArgs>(
nameof(VerticalSnapPointsChanged),
RoutingStrategies.Bubble);
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty = private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, bool>("ItemIsOwnContainer"); AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, bool>("ItemIsOwnContainer");
@ -62,6 +93,42 @@ namespace Avalonia.Controls
set => SetValue(OrientationProperty, value); set => SetValue(OrientationProperty, value);
} }
/// <summary>
/// Occurs when the measurements for horizontal snap points change.
/// </summary>
public event EventHandler<RoutedEventArgs>? HorizontalSnapPointsChanged
{
add => AddHandler(HorizontalSnapPointsChangedEvent, value);
remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value);
}
/// <summary>
/// Occurs when the measurements for vertical snap points change.
/// </summary>
public event EventHandler<RoutedEventArgs>? VerticalSnapPointsChanged
{
add => AddHandler(VerticalSnapPointsChangedEvent, value);
remove => RemoveHandler(VerticalSnapPointsChangedEvent, value);
}
/// <summary>
/// Gets or sets whether the horizontal snap points for the <see cref="VirtualizingStackPanel"/> are equidistant from each other.
/// </summary>
public bool AreHorizontalSnapPointsRegular
{
get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
}
/// <summary>
/// Gets or sets whether the vertical snap points for the <see cref="VirtualizingStackPanel"/> are equidistant from each other.
/// </summary>
public bool AreVerticalSnapPointsRegular
{
get { return GetValue(AreVerticalSnapPointsRegularProperty); }
set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
}
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
if (!IsEffectivelyVisible) if (!IsEffectivelyVisible)
@ -145,6 +212,8 @@ namespace Avalonia.Controls
finally finally
{ {
_isInLayout = false; _isInLayout = false;
RaiseEvent(new RoutedEventArgs(Orientation == Orientation.Horizontal ? HorizontalSnapPointsChangedEvent : VerticalSnapPointsChangedEvent));
} }
} }
@ -226,7 +295,7 @@ namespace Avalonia.Controls
{ {
if (toIndex < 0) if (toIndex < 0)
toIndex = count - 1; toIndex = count - 1;
else if (toIndex >= count - 1) else if (toIndex >= count)
toIndex = 0; toIndex = 0;
} }
@ -622,6 +691,167 @@ namespace Avalonia.Controls
Invalidate(c); Invalidate(c);
} }
/// <inheritdoc/>
public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment)
{
var snapPoints = new List<double>();
switch (orientation)
{
case Orientation.Horizontal:
if (AreHorizontalSnapPointsRegular)
throw new InvalidOperationException();
if (Orientation == Orientation.Horizontal)
{
var averageElementSize = EstimateElementSizeU();
double snapPoint = 0;
for (var i = 0; i < Items.Count; i++)
{
var container = ContainerFromIndex(i);
if (container != null)
{
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Near:
snapPoint = container.Bounds.Left;
break;
case SnapPointsAlignment.Center:
snapPoint = container.Bounds.Center.X;
break;
case SnapPointsAlignment.Far:
snapPoint = container.Bounds.Right;
break;
}
}
else
{
if (snapPoint == 0)
{
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Center:
snapPoint = averageElementSize / 2;
break;
case SnapPointsAlignment.Far:
snapPoint = averageElementSize;
break;
}
}
else
snapPoint += averageElementSize;
}
snapPoints.Add(snapPoint);
}
}
break;
case Orientation.Vertical:
if (AreVerticalSnapPointsRegular)
throw new InvalidOperationException();
if (Orientation == Orientation.Vertical)
{
var averageElementSize = EstimateElementSizeU();
double snapPoint = 0;
for (var i = 0; i < Items.Count; i++)
{
var container = ContainerFromIndex(i);
if (container != null)
{
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Near:
snapPoint = container.Bounds.Top;
break;
case SnapPointsAlignment.Center:
snapPoint = container.Bounds.Center.Y;
break;
case SnapPointsAlignment.Far:
snapPoint = container.Bounds.Bottom;
break;
}
}
else
{
if (snapPoint == 0)
{
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Center:
snapPoint = averageElementSize / 2;
break;
case SnapPointsAlignment.Far:
snapPoint = averageElementSize;
break;
}
}
else
snapPoint += averageElementSize;
}
snapPoints.Add(snapPoint);
}
}
break;
}
return snapPoints;
}
/// <inheritdoc/>
public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset)
{
offset = 0f;
var firstRealizedChild = _realizedElements?.Elements.FirstOrDefault();
if (firstRealizedChild == null)
{
return 0;
}
double snapPoint = 0;
switch (Orientation)
{
case Orientation.Horizontal:
if (!AreHorizontalSnapPointsRegular)
throw new InvalidOperationException();
snapPoint = firstRealizedChild.Bounds.Width;
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Near:
offset = 0;
break;
case SnapPointsAlignment.Center:
offset = (firstRealizedChild.Bounds.Right - firstRealizedChild.Bounds.Left) / 2;
break;
case SnapPointsAlignment.Far:
offset = firstRealizedChild.Bounds.Width;
break;
}
break;
case Orientation.Vertical:
if (!AreVerticalSnapPointsRegular)
throw new InvalidOperationException();
snapPoint = firstRealizedChild.Bounds.Height;
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Near:
offset = 0;
break;
case SnapPointsAlignment.Center:
offset = (firstRealizedChild.Bounds.Bottom - firstRealizedChild.Bounds.Top) / 2;
break;
case SnapPointsAlignment.Far:
offset = firstRealizedChild.Bounds.Height;
break;
}
break;
}
return snapPoint;
}
/// <summary> /// <summary>
/// Stores the realized element state for a <see cref="VirtualizingStackPanel"/>. /// Stores the realized element state for a <see cref="VirtualizingStackPanel"/>.
/// </summary> /// </summary>

4
src/Avalonia.Diagnostics/Diagnostics/Screenshots/BaseRenderToStreamHandler.cs

@ -17,7 +17,11 @@ namespace Avalonia.Diagnostics.Screenshots
public async Task Take(Control control) public async Task Take(Control control)
{ {
#if NET6_0_OR_GREATER
await using var output = await GetStream(control);
#else
using var output = await GetStream(control); using var output = await GetStream(control);
#endif
if (output is { }) if (output is { })
{ {
control.RenderTo(output); control.RenderTo(output);

57
src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs

@ -14,57 +14,54 @@ namespace Avalonia.Diagnostics.Screenshots
/// </summary> /// </summary>
public sealed class FilePickerHandler : BaseRenderToStreamHandler public sealed class FilePickerHandler : BaseRenderToStreamHandler
{ {
private readonly string _title;
private readonly string _screenshotRoot;
/// <summary> /// <summary>
/// Instance FilePickerHandler /// Instance FilePickerHandler
/// </summary> /// </summary>
public FilePickerHandler() public FilePickerHandler() : this(null, null)
{ {
} }
/// <summary> /// <summary>
/// Instance FilePickerHandler with specificated parameter /// Instance FilePickerHandler with specificated parameter
/// </summary> /// </summary>
/// <param name="title">SaveFilePicker Title</param> /// <param name="title">SaveFilePicker Title</param>
/// <param name="screenshotRoot"></param> /// <param name="screenshotRoot"></param>
public FilePickerHandler(string? title public FilePickerHandler(
, string? screenshotRoot = default string? title,
) string? screenshotRoot = default)
{ {
if (title is { }) _title = title ?? "Save Screenshot to ...";
Title = title; _screenshotRoot = screenshotRoot ?? Conventions.DefaultScreenshotsRoot;
if (screenshotRoot is { })
ScreenshotsRoot = screenshotRoot;
} }
/// <summary>
/// Get the root folder where screeshots well be stored.
/// The default root folder is [Environment.SpecialFolder.MyPictures]/Screenshots.
/// </summary>
public string ScreenshotsRoot { get; }
= Conventions.DefaultScreenshotsRoot;
/// <summary> private static TopLevel GetTopLevel(Control control)
/// SaveFilePicker Title
/// </summary>
public string Title { get; } = "Save Screenshot to ...";
static Window GetWindow(Control control)
{ {
var window = control.VisualRoot as Window; // If possible, use devtools main window.
var app = Application.Current; TopLevel? devToolsTopLevel = null;
if (app?.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime desktop) if (Application.Current?.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime desktop)
{ {
window = desktop.Windows.FirstOrDefault(w => w is Views.MainWindow); devToolsTopLevel = desktop.Windows.FirstOrDefault(w => w is Views.MainWindow);
} }
return window!;
return devToolsTopLevel ?? TopLevel.GetTopLevel(control)
?? throw new InvalidOperationException("No TopLevel is available.");
} }
protected override async Task<Stream?> GetStream(Control control) protected override async Task<Stream?> GetStream(Control control)
{ {
var result = await GetWindow(control).StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions var storageProvider = GetTopLevel(control).StorageProvider;
var defaultFolder = await storageProvider.TryGetFolderFromPath(_screenshotRoot)
?? await storageProvider.TryGetWellKnownFolder(WellKnownFolder.Pictures);
var result = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{ {
SuggestedStartLocation = new BclStorageFolder(new DirectoryInfo(ScreenshotsRoot)), SuggestedStartLocation = defaultFolder,
Title = Title, Title = _title,
FileTypeChoices = new FilePickerFileType[] { new FilePickerFileType("PNG") { Patterns = new string[] { "*.png" } } }, FileTypeChoices = new [] { FilePickerFileTypes.ImagePng },
DefaultExtension = ".png" DefaultExtension = ".png"
}); });
if (result is null) if (result is null)
@ -73,7 +70,7 @@ namespace Avalonia.Diagnostics.Screenshots
} }
if (!result.CanOpenWrite) if (!result.CanOpenWrite)
{ {
throw new InvalidOperationException("ReadOnly file was opened"); throw new InvalidOperationException("Read-only file was selected.");
} }
return await result.OpenWriteAsync(); return await result.OpenWriteAsync();

5
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -14,6 +14,11 @@
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" /> <ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<!-- For managed dialogs dev testing -->
<InternalsVisibleTo Include="ControlCatalog, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
<Import Project="..\..\build\ApiDiff.props" /> <Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />

12
src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs

@ -260,17 +260,7 @@ namespace Avalonia.Dialogs.Internal
public void Navigate(IStorageFolder path, string initialSelectionName = null) public void Navigate(IStorageFolder path, string initialSelectionName = null)
{ {
string fullDirectoryPath; var fullDirectoryPath = path?.TryGetFullPath() ?? Directory.GetCurrentDirectory();
if (path?.TryGetUri(out var fullDirectoryUri) == true
&& fullDirectoryUri.IsAbsoluteUri)
{
fullDirectoryPath = fullDirectoryUri.LocalPath;
}
else
{
fullDirectoryPath = Directory.GetCurrentDirectory();
}
Navigate(fullDirectoryPath, initialSelectionName); Navigate(fullDirectoryPath, initialSelectionName);
} }

4
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@ -51,9 +51,7 @@ namespace Avalonia.Dialogs
var files = await impl.OpenFilePickerAsync(dialog.ToFilePickerOpenOptions()); var files = await impl.OpenFilePickerAsync(dialog.ToFilePickerOpenOptions());
return files return files
.Select(file => file.TryGetUri(out var fullPath) .Select(file => file.TryGetFullPath() ?? file.Name)
? fullPath.LocalPath
: file.Name)
.ToArray(); .ToArray();
} }
} }

2
src/Avalonia.Dialogs/ManagedStorageProvider.cs

@ -11,7 +11,7 @@ using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Dialogs; namespace Avalonia.Dialogs;
public class ManagedStorageProvider<T> : BclStorageProvider where T : Window, new() internal class ManagedStorageProvider<T> : BclStorageProvider where T : Window, new()
{ {
private readonly Window _parent; private readonly Window _parent;
private readonly ManagedFileDialogOptions _managedOptions; private readonly ManagedFileDialogOptions _managedOptions;

6
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -88,8 +88,8 @@ namespace Avalonia.FreeDesktop
if (options.SuggestedFileName is { } currentName) if (options.SuggestedFileName is { } currentName)
chooserOptions.Add("current_name", currentName); chooserOptions.Add("current_name", currentName);
if (options.SuggestedStartLocation?.TryGetUri(out var currentFolder) == true) if (options.SuggestedStartLocation?.TryGetFullPath() is { } folderPath)
chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(currentFolder.ToString())); chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(folderPath));
objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
var request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath); var request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath);
@ -131,7 +131,7 @@ namespace Avalonia.FreeDesktop
.Where(Directory.Exists) .Where(Directory.Exists)
.Select(path => new BclStorageFolder(new DirectoryInfo(path))).ToList(); .Select(path => new BclStorageFolder(new DirectoryInfo(path))).ToList();
} }
private static (string name, (uint style, string extension)[])[] ParseFilters(IReadOnlyList<FilePickerFileType>? fileTypes) private static (string name, (uint style, string extension)[])[] ParseFilters(IReadOnlyList<FilePickerFileType>? fileTypes)
{ {
// Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])] // Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]

6
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -120,7 +120,11 @@ namespace Avalonia.Headless
return new HeadlessGeometryStub(new Rect(glyphRun.Size)); return new HeadlessGeometryStub(new Rect(glyphRun.Size));
} }
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos) public IGlyphRunImpl CreateGlyphRun(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
IReadOnlyList<GlyphInfo> glyphInfos,
Point baselineOrigin)
{ {
return new HeadlessGlyphRunStub(); return new HeadlessGlyphRunStub();
} }

12
src/Avalonia.Headless/HeadlessWindowImpl.cs

@ -284,6 +284,18 @@ namespace Avalonia.Headless
button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp, button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp,
point, modifiers)); point, modifiers));
} }
void IHeadlessWindow.MouseWheel(Point point, Vector delta, RawInputModifiers modifiers)
{
Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, InputRoot,
point, delta, modifiers));
}
void IHeadlessWindow.DragDrop(Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers)
{
var device = AvaloniaLocator.Current.GetRequiredService<IDragDropDevice>();
Input?.Invoke(new RawDragEvent(device, type, InputRoot, point, data, effects, modifiers));
}
void IWindowImpl.Move(PixelPoint point) void IWindowImpl.Move(PixelPoint point)
{ {

3
src/Avalonia.Headless/IHeadlessWindow.cs

@ -1,4 +1,5 @@
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -13,5 +14,7 @@ namespace Avalonia.Headless
void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None);
void MouseMove(Point point, RawInputModifiers modifiers = RawInputModifiers.None); void MouseMove(Point point, RawInputModifiers modifiers = RawInputModifiers.None);
void MouseUp(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); void MouseUp(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None);
void MouseWheel(Point point, Vector delta, RawInputModifiers modifiers = RawInputModifiers.None);
void DragDrop(Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers);
} }
} }

9
src/Avalonia.Native/SystemDialogs.cs

@ -33,8 +33,7 @@ namespace Avalonia.Native
{ {
using var events = new SystemDialogEvents(); using var events = new SystemDialogEvents();
var suggestedDirectory = options.SuggestedStartLocation?.TryGetUri(out var suggestedDirectoryTmp) == true var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty;
? suggestedDirectoryTmp.LocalPath : string.Empty;
_native.OpenFileDialog((IAvnWindow)_window.Native, _native.OpenFileDialog((IAvnWindow)_window.Native,
events, events,
@ -54,8 +53,7 @@ namespace Avalonia.Native
{ {
using var events = new SystemDialogEvents(); using var events = new SystemDialogEvents();
var suggestedDirectory = options.SuggestedStartLocation?.TryGetUri(out var suggestedDirectoryTmp) == true var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty;
? suggestedDirectoryTmp.LocalPath : string.Empty;
_native.SaveFileDialog((IAvnWindow)_window.Native, _native.SaveFileDialog((IAvnWindow)_window.Native,
events, events,
@ -74,8 +72,7 @@ namespace Avalonia.Native
{ {
using var events = new SystemDialogEvents(); using var events = new SystemDialogEvents();
var suggestedDirectory = options.SuggestedStartLocation?.TryGetUri(out var suggestedDirectoryTmp) == true var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty;
? suggestedDirectoryTmp.LocalPath : string.Empty;
_native.SelectFolderDialog((IAvnWindow)_window.Native, events, options.AllowMultiple.AsComBool(), options.Title ?? "", suggestedDirectory); _native.SelectFolderDialog((IAvnWindow)_window.Native, events, options.AllowMultiple.AsComBool(), options.Title ?? "", suggestedDirectory);

2
src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml

@ -9,6 +9,8 @@
CornerRadius="{TemplateBinding CornerRadius}" CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}"> Padding="{TemplateBinding Padding}">
<ItemsPresenter Name="PART_ItemsPresenter" <ItemsPresenter Name="PART_ItemsPresenter"
AreVerticalSnapPointsRegular="{TemplateBinding AreVerticalSnapPointsRegular}"
AreHorizontalSnapPointsRegular="{TemplateBinding AreHorizontalSnapPointsRegular}"
ItemsPanel="{TemplateBinding ItemsPanel}"/> ItemsPanel="{TemplateBinding ItemsPanel}"/>
</Border> </Border>
</ControlTemplate> </ControlTemplate>

4
src/Avalonia.Themes.Fluent/Controls/ListBox.xaml

@ -29,11 +29,15 @@
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<ScrollViewer Name="PART_ScrollViewer" <ScrollViewer Name="PART_ScrollViewer"
VerticalSnapPointsType="{TemplateBinding (ScrollViewer.VerticalSnapPointsType)}"
HorizontalSnapPointsType="{TemplateBinding (ScrollViewer.HorizontalSnapPointsType)}"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}" IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<ItemsPresenter Name="PART_ItemsPresenter" <ItemsPresenter Name="PART_ItemsPresenter"
AreVerticalSnapPointsRegular="{TemplateBinding AreVerticalSnapPointsRegular}"
AreHorizontalSnapPointsRegular="{TemplateBinding AreHorizontalSnapPointsRegular}"
ItemsPanel="{TemplateBinding ItemsPanel}" ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{TemplateBinding Padding}"/> Margin="{TemplateBinding Padding}"/>
</ScrollViewer> </ScrollViewer>

7
src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml

@ -34,12 +34,17 @@
Content="{TemplateBinding Content}" Content="{TemplateBinding Content}"
Extent="{TemplateBinding Extent, Mode=TwoWay}" Extent="{TemplateBinding Extent, Mode=TwoWay}"
Padding="{TemplateBinding Padding}" Padding="{TemplateBinding Padding}"
HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}"
VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}"
HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}"
VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}"
Offset="{TemplateBinding Offset, Mode=TwoWay}" Offset="{TemplateBinding Offset, Mode=TwoWay}"
Viewport="{TemplateBinding Viewport, Mode=TwoWay}" Viewport="{TemplateBinding Viewport, Mode=TwoWay}"
IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}"> IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}">
<ScrollContentPresenter.GestureRecognizers> <ScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}" <ScrollGestureRecognizer CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" /> CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
IsScrollInertiaEnabled="{TemplateBinding IsScrollInertiaEnabled}"/>
</ScrollContentPresenter.GestureRecognizers> </ScrollContentPresenter.GestureRecognizers>
</ScrollContentPresenter> </ScrollContentPresenter>
<ScrollBar Name="PART_HorizontalScrollBar" <ScrollBar Name="PART_HorizontalScrollBar"

2
src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml

@ -10,6 +10,8 @@
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<ItemsPresenter Name="PART_ItemsPresenter" <ItemsPresenter Name="PART_ItemsPresenter"
AreVerticalSnapPointsRegular="{TemplateBinding AreVerticalSnapPointsRegular}"
AreHorizontalSnapPointsRegular="{TemplateBinding AreHorizontalSnapPointsRegular}"
ItemsPanel="{TemplateBinding ItemsPanel}" /> ItemsPanel="{TemplateBinding ItemsPanel}" />
</Border> </Border>
</ControlTemplate> </ControlTemplate>

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

Loading…
Cancel
Save