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">
<pages:RelativePanelPage />
</TabItem>
<TabItem Header="ScrollViewer">
<TabItem Header="ScrollViewer">
<pages:ScrollViewerPage />
</TabItem>
<TabItem Header="ScrollViewer Snapping">
<pages:ScrollSnapPage />
</TabItem>
<TabItem Header="Slider">
<pages:SliderPage />
</TabItem>

2
samples/ControlCatalog/MainView.xaml.cs

@ -60,7 +60,7 @@ namespace ControlCatalog
{
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"
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"
Orientation="Vertical"
Spacing="4">
@ -42,6 +44,19 @@
</StackPanel>
</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"
Classes="h2"
IsVisible="False"

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

@ -24,13 +24,38 @@ namespace ControlCatalog.Pages
{
this.InitializeComponent();
IStorageFolder? lastSelectedDirectory = null;
bool ignoreTextChanged = false;
var results = this.Get<ItemsControl>("PickerLastResults");
var resultsVisible = this.Get<TextBlock>("PickerLastResultsVisible");
var bookmarkContainer = this.Get<TextBox>("BookmarkContainer");
var openedFileContent = this.Get<TextBox>("OpenedFileContent");
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()
{
@ -84,7 +109,7 @@ namespace ControlCatalog.Pages
{
Title = "Open multiple files",
Filters = GetFilters(),
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
AllowMultiple = true
}.ShowAsync(GetWindow());
results.Items = result;
@ -97,7 +122,7 @@ namespace ControlCatalog.Pages
{
Title = "Save file",
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,
InitialFileName = "test.txt"
}.ShowAsync(GetWindow());
@ -109,7 +134,7 @@ namespace ControlCatalog.Pages
var result = await new OpenFolderDialog()
{
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());
if (string.IsNullOrEmpty(result))
{
@ -117,7 +142,7 @@ namespace ControlCatalog.Pages
}
else
{
lastSelectedDirectory = new BclStorageFolder(new System.IO.DirectoryInfo(result));
SetFolder(await GetStorageProvider().TryGetFolderFromPath(result));
results.Items = new[] { result };
resultsVisible.IsVisible = true;
}
@ -127,7 +152,7 @@ namespace ControlCatalog.Pages
var result = await new OpenFileDialog()
{
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
}.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions
{
@ -210,7 +235,7 @@ namespace ControlCatalog.Pages
#endif
await reader.WriteLineAsync(openedFileContent.Text);
lastSelectedDirectory = await file.GetParentAsync();
SetFolder(await file.GetParentAsync());
}
await SetPickerResult(file is null ? null : new[] { file });
@ -226,7 +251,7 @@ namespace ControlCatalog.Pages
await SetPickerResult(folders);
lastSelectedDirectory = folders.FirstOrDefault();
SetFolder(folders.FirstOrDefault());
};
this.Get<Button>("OpenFileFromBookmark").Click += async delegate
{
@ -243,10 +268,16 @@ namespace ControlCatalog.Pages
: null;
await SetPickerResult(folder is null ? null : new[] { folder });
lastSelectedDirectory = folder;
SetFolder(folder);
};
void SetFolder(IStorageFolder? folder)
{
ignoreTextChanged = true;
lastSelectedDirectory = folder;
currentFolderBox.Text = folder?.Path.LocalPath;
ignoreTextChanged = false;
}
async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items)
{
items ??= Array.Empty<IStorageItem>();
@ -297,10 +328,11 @@ namespace ControlCatalog.Pages
openedFileContent.Text = resultText;
lastSelectedDirectory = await item.GetParentAsync();
if (lastSelectedDirectory is not null)
var parent = await item.GetParentAsync();
SetFolder(parent);
if (parent is not null)
{
mappedResults.Add(FullPathOrName(lastSelectedDirectory));
mappedResults.Add(FullPathOrName(parent));
}
foreach (var selectedItem in items)
@ -393,7 +425,7 @@ CanPickFolder: {storageProvider.CanPickFolder}";
private static string FullPathOrName(IStorageItem? item)
{
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");

35
samples/ControlCatalog/Pages/GesturePage.cs

@ -6,6 +6,7 @@ using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Rendering.Composition;
using Avalonia.Utilities;
namespace ControlCatalog.Pages
{
@ -53,6 +54,7 @@ namespace ControlCatalog.Pages
{
_currentScale = 1;
compositionVisual.Scale = new Vector3(1,1,1);
compositionVisual.Offset = default;
image.InvalidateMeasure();
}
};
@ -100,13 +102,19 @@ namespace ControlCatalog.Pages
{
InitComposition(control!);
isZooming = true;
if(compositionVisual != null)
{
var scale = _currentScale * (float)e.Scale;
if (scale <= 1)
{
scale = 1;
compositionVisual.Offset = default;
}
compositionVisual.Scale = new(scale, scale, 1);
e.Handled = true;
}
});
@ -114,8 +122,6 @@ namespace ControlCatalog.Pages
{
InitComposition(control!);
isZooming = false;
if (compositionVisual != null)
{
_currentScale = compositionVisual.Scale.X;
@ -126,11 +132,19 @@ namespace ControlCatalog.Pages
{
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)
{
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)
{
if (ballCompositionVisual != null)
{
return;
}
ballCompositionVisual = ElementComposition.GetElementVisual(ball);
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"
x:Class="ControlCatalog.Pages.ScrollViewerPage"
x:DataType="pages:ScrollViewerPageViewModel">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h2">Allows for horizontal and vertical content scrolling.</TextBlock>
<StackPanel Orientation="Vertical" Spacing="20">
<TextBlock TextWrapping="Wrap" Classes="h2">Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling.</TextBlock>
<Grid ColumnDefinitions="Auto, *">
<StackPanel Orientation="Vertical" Spacing="4">
<ToggleSwitch IsChecked="{Binding AllowAutoHide}" Content="Allow auto hide" />
<ToggleSwitch IsChecked="{Binding EnableInertia}" Content="Enable Inertia" />
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Text="Horizontal Scroll" />
@ -24,6 +25,7 @@
<ScrollViewer x:Name="ScrollViewer"
Grid.Column="1"
Width="400" Height="400"
IsScrollInertiaEnabled="{Binding EnableInertia}"
AllowAutoHide="{Binding AllowAutoHide}"
HorizontalScrollBarVisibility="{Binding HorizontalScrollVisibility}"
VerticalScrollBarVisibility="{Binding VerticalScrollVisibility}">
@ -31,6 +33,5 @@
Source="/Assets/delicate-arch-896885_640.jpg" />
</ScrollViewer>
</Grid>
</StackPanel>
</UserControl>

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

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

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

@ -1,6 +1,8 @@
using System;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Content.Res;
using Android.OS;
using Android.Runtime;
using Android.Views;
@ -13,6 +15,7 @@ namespace Avalonia.Android
internal static object ViewContent;
public Action<int, Result, Intent> ActivityResult { get; set; }
public Action<int, string[], Permission[]> RequestPermissionsResult { get; set; }
internal AvaloniaView View;
private GlobalLayoutListener _listener;
@ -82,6 +85,13 @@ namespace Avalonia.Android
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
{
private AvaloniaView _view;

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

@ -1,11 +1,14 @@
using System;
using Android.App;
using Android.Content;
using Android.Content.PM;
namespace Avalonia.Android
{
public interface IActivityResultHandler
{
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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Android;
using Android.App;
using Android.Content;
using Android.Provider;
using Avalonia.Logging;
@ -19,41 +20,48 @@ namespace Avalonia.Android.Platform.Storage;
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;
}
internal AndroidUri Uri { get; }
protected Activity Activity => _activity ?? throw new ObjectDisposedException(nameof(AndroidStorageItem));
protected Context Context => _context ?? throw new ObjectDisposedException(nameof(AndroidStorageItem));
public string Name => GetColumnValue(Context, Uri, MediaStore.IMediaColumns.DisplayName)
public virtual string Name => GetColumnValue(Activity, Uri, MediaStore.IMediaColumns.DisplayName)
?? Uri.PathSegments?.LastOrDefault() ?? string.Empty;
public Uri Path => new(Uri.ToString()!);
public bool CanBookmark => true;
public Task<string?> SaveBookmarkAsync()
public async Task<string?> SaveBookmarkAsync()
{
Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
return Task.FromResult(Uri.ToString());
}
if (!await EnsureExternalFilesPermission(false))
{
return null;
}
public Task ReleaseBookmarkAsync()
{
Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
return Task.CompletedTask;
Activity.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
return Uri.ToString();
}
public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
public async Task ReleaseBookmarkAsync()
{
uri = new Uri(Uri.ToString()!);
return true;
}
if (!await EnsureExternalFilesPermission(false))
{
return;
}
Activity.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
}
public abstract Task<StorageItemProperties> GetBasicPropertiesAsync();
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;
}
public Task<IStorageFolder?> GetParentAsync()
public async Task<IStorageFolder?> GetParentAsync()
{
if (!await EnsureExternalFilesPermission(false))
{
return null;
}
using var javaFile = new JavaFile(Uri.Path!);
// Java file represents files AND directories. Don't be confused.
if (javaFile.ParentFile is {} parentFile
&& 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()
{
_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()
{
if (!await EnsureExternalFilesPermission(false))
{
return Array.Empty<IStorageItem>();
}
using var javaFile = new JavaFile(Uri.Path!);
// 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)
.Select(t => t.file switch
{
{ IsFile: true } => (IStorageItem)new AndroidStorageFile(Context, t.uri!),
{ IsDirectory: true } => new AndroidStorageFolder(Context, t.uri!),
{ IsFile: true } => (IStorageItem)new AndroidStorageFile(Activity, t.uri!),
{ IsDirectory: true } => new AndroidStorageFolder(Activity, t.uri!, false),
_ => 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
{
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 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"));
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"));
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.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)
{

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

@ -4,18 +4,21 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Android;
using Android.App;
using Android.Content;
using Android.Provider;
using Avalonia.Platform.Storage;
using Java.Lang;
using AndroidUri = Android.Net.Uri;
using Exception = System.Exception;
using JavaFile = Java.IO.File;
namespace Avalonia.Android.Platform.Storage;
internal class AndroidStorageProvider : IStorageProvider
{
private readonly Activity _activity;
private int _lastRequestCode = 20000;
public AndroidStorageProvider(Activity activity)
{
@ -31,7 +34,108 @@ internal class AndroidStorageProvider : IStorageProvider
public Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string 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)
@ -110,19 +214,21 @@ internal class AndroidStorageProvider : IStorageProvider
var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select folder");
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)
{
var resultList = new List<AndroidUri>(1);
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);
var result = await tcs.Task;
@ -161,11 +267,7 @@ internal class AndroidStorageProvider : IStorageProvider
return;
}
if (_activity is IActivityResultHandler mainActivity)
{
mainActivity.ActivityResult -= OnActivityResult;
}
mainActivity.ActivityResult -= OnActivityResult;
_ = 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.Media;
using Avalonia.Media.Immutable;
using Avalonia.Media.Transformation;
#nullable enable
@ -30,7 +31,7 @@ namespace Avalonia.Animation.Animators
return new ImmutableRadialGradientBrush(
InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
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),
oldValue.SpreadMethod,
s_relativePointAnimator.Interpolate(progress, oldRadial.Center, newRadial.Center),
@ -41,7 +42,7 @@ namespace Avalonia.Animation.Animators
return new ImmutableConicGradientBrush(
InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
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),
oldValue.SpreadMethod,
s_relativePointAnimator.Interpolate(progress, oldConic.Center, newConic.Center),
@ -51,7 +52,7 @@ namespace Avalonia.Animation.Animators
return new ImmutableLinearGradientBrush(
InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
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),
oldValue.SpreadMethod,
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);
}
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)
{
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)
: base(name, ownerType, metadata, inherits, validate)
{
IsAttached = true;
}
/// <inheritdoc/>
public override bool IsAttached => true;
/// <summary>
/// Attaches the property as a non-attached property on the specified type.
/// </summary>

16
src/Avalonia.Base/AvaloniaObject.cs

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

8
src/Avalonia.Base/AvaloniaObjectExtensions.cs

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

8
src/Avalonia.Base/AvaloniaProperty.cs

@ -107,22 +107,22 @@ namespace Avalonia
/// <summary>
/// Gets a value indicating whether the property inherits its value.
/// </summary>
public virtual bool Inherits => false;
public bool Inherits { get; private protected set; }
/// <summary>
/// Gets a value indicating whether this is an attached property.
/// </summary>
public virtual bool IsAttached => false;
public bool IsAttached { get; private protected set; }
/// <summary>
/// Gets a value indicating whether this is a direct property.
/// </summary>
public virtual bool IsDirect => false;
public bool IsDirect { get; private protected set; }
/// <summary>
/// Gets a value indicating whether this is a readonly property.
/// </summary>
public virtual bool IsReadOnly => false;
public bool IsReadOnly { get; private protected set; }
/// <summary>
/// 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));
Setter = setter;
IsDirect = true;
IsReadOnly = setter is null;
}
/// <summary>
@ -51,17 +53,10 @@ namespace Avalonia
{
Getter = getter ?? throw new ArgumentNullException(nameof(getter));
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>
/// Gets the getter function.
/// </summary>

6
src/Avalonia.Base/DirectPropertyBase.cs

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

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

@ -1,5 +1,6 @@
using System;
using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.VisualTree;
namespace Avalonia.Input
@ -32,7 +33,9 @@ namespace Avalonia.Input
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)
{
Data = data;

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

@ -57,7 +57,10 @@ namespace Avalonia.Input
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.Diagnostics;
using Avalonia.Input.GestureRecognizers;
namespace Avalonia.Input
@ -88,7 +89,10 @@ namespace Avalonia.Input
}
_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
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 Point _trackedRootPoint;
private IPointer? _tracking;
@ -23,7 +27,8 @@ namespace Avalonia.Input.GestureRecognizers
// Movement per second
private Vector _inertia;
private ulong? _lastMoveTimestamp;
private bool _isScrollInertiaEnabled;
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary>
@ -42,6 +47,15 @@ namespace Avalonia.Input.GestureRecognizers
o => o.CanVerticallyScroll,
(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>
/// Defines the <see cref="ScrollStartDistance"/> property.
/// </summary>
@ -68,6 +82,15 @@ namespace Avalonia.Input.GestureRecognizers
get => _canVerticallyScroll;
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>
/// 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)
{
if (e.Pointer == _tracking)
@ -168,7 +188,8 @@ namespace Avalonia.Input.GestureRecognizers
if (_inertia == default
|| e.Timestamp == 0
|| _lastMoveTimestamp == 0
|| e.Timestamp - _lastMoveTimestamp > 200)
|| e.Timestamp - _lastMoveTimestamp > 200
|| !IsScrollInertiaEnabled)
EndGesture();
else
{
@ -176,6 +197,7 @@ namespace Avalonia.Input.GestureRecognizers
var savedGestureId = _gestureId;
var st = Stopwatch.StartNew();
var lastTime = TimeSpan.Zero;
_target!.RaiseEvent(new ScrollGestureInertiaStartingEventArgs(_gestureId, _inertia));
DispatcherTimer.Run(() =>
{
// Another gesture has started, finish the current one
@ -187,7 +209,7 @@ namespace Avalonia.Input.GestureRecognizers
var elapsedSinceLastTick = st.Elapsed - lastTime;
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 scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance);
_target!.RaiseEvent(scrollGestureEventArgs);

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

@ -45,6 +45,10 @@ namespace Avalonia.Input
RoutedEvent.Register<ScrollGestureEventArgs>(
"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 =
RoutedEvent.Register<ScrollGestureEndedEventArgs>(
"ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));

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

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

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

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

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

@ -1,13 +1,17 @@
using System;
using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
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,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: base(routedEvent, source, pointer, rootVisual, rootVisualPosition,

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

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.VisualTree;
namespace Avalonia.Input
@ -13,7 +14,9 @@ namespace Avalonia.Input
private readonly PointerPointProperties _properties;
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,
IPointer pointer,
Visual? rootVisual, Point rootVisualPosition,

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

@ -1,13 +1,17 @@
using System;
using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
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,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: 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;
}
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++;
internal ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent)
public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent)
{
Id = id;
Delta = delta;
@ -25,9 +25,21 @@ namespace Avalonia.Input
{
public int Id { get; }
internal ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent)
public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent)
{
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.VisualTree;
@ -7,7 +8,7 @@ namespace Avalonia.Input
{
private readonly PointerEventArgs lastPointerEventArgs;
internal TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs)
public TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs)
: base(routedEvent)
{
this.lastPointerEventArgs = lastPointerEventArgs;

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

@ -4,10 +4,6 @@ namespace Avalonia.Input
{
public class TextInputEventArgs : RoutedEventArgs
{
public TextInputEventArgs()
{
}
public IKeyboardDevice? Device { 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
{
internal VectorEventArgs()
{
}
public Vector Vector { get; set; }
public Vector Vector { get; init; }
}
}

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

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

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

@ -141,7 +141,7 @@ namespace Avalonia.Logging
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.
/// </summary>
[TypeConverter(typeof(BrushConverter))]
public abstract class Brush : Animatable
public abstract class Brush : Animatable, IBrush
{
/// <summary>
/// Defines the <see cref="Opacity"/> property.

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

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

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

@ -93,16 +93,19 @@ namespace Avalonia.Media.TextFormatting
[Conditional("DEBUG")]
public void VerifyAllReturned()
{
if (_pendingReturnCount > 0)
var pendingReturnCount = _pendingReturnCount;
_pendingReturnCount = 0;
if (pendingReturnCount > 0)
{
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(
$"{-_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,
Text,
ShapedBuffer,
BidiLevel);
biDiLevel: BidiLevel);
}
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 collapsedRuns = new TextRun[preSplitRuns.Count + 1];
preSplitRuns.CopyTo(collapsedRuns);
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
objectPool.TextRunLists.Return(ref preSplitRuns);
objectPool.TextRunLists.Return(ref postSplitRuns);
return collapsedRuns;
try
{
var collapsedRuns = new TextRun[preSplitRuns.Count + 1];
preSplitRuns.CopyTo(collapsedRuns);
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
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,
out var textEndOfLine, out var textSourceLength);
RentedList<TextRun>? shapedTextRuns;
RentedList<TextRun>? shapedTextRuns = null;
if (previousLineBreak?.RemainingRuns is { } remainingRuns)
try
{
resolvedFlowDirection = previousLineBreak.FlowDirection;
textRuns = remainingRuns;
nextLineBreak = previousLineBreak;
shapedTextRuns = null;
}
else
{
shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, out resolvedFlowDirection);
textRuns = shapedTextRuns;
if (nextLineBreak == null && textEndOfLine != null)
if (previousLineBreak?.RemainingRuns is { } remainingRuns)
{
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)
{
case TextWrapping.NoWrap:
TextLineImpl textLine;
switch (textWrapping)
{
// 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();
case TextWrapping.NoWrap:
{
// 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,
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
textLine.FinalizeLine();
textLine.FinalizeLine();
break;
}
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager);
break;
break;
}
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager);
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>
@ -224,23 +230,26 @@ namespace Avalonia.Media.TextFormatting
(resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
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();
bidiAlgorithm.Reset();
bidiData.Reset();
bidiAlgorithm.Reset();
var groupedRuns = objectPool.UnshapedTextRunLists.Rent();
var textShaper = TextShaper.Current;
for (var index = 0; index < processedRuns.Count; index++)
{
var currentRun = processedRuns[index];
var textShaper = TextShaper.Current;
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.Add(shapeableRun);
@ -277,17 +286,20 @@ namespace Avalonia.Media.TextFormatting
break;
}
default:
default:
{
shapedRuns.Add(currentRun);
break;
}
}
}
}
objectPool.TextRunLists.Return(ref processedRuns);
objectPool.UnshapedTextRunLists.Return(ref groupedRuns);
finally
{
objectPool.TextRunLists.Return(ref processedRuns);
objectPool.UnshapedTextRunLists.Return(ref groupedRuns);
}
return shapedRuns;
}
@ -805,25 +817,29 @@ namespace Avalonia.Media.TextFormatting
var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool);
var textLineBreak = postSplitRuns?.Count > 0 ?
new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
null;
if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null)
try
{
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,
paragraphWidth, paragraphProperties, resolvedFlowDirection,
textLineBreak);
textLine.FinalizeLine();
if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null)
{
textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
}
objectPool.TextRunLists.Return(ref preSplitRuns);
objectPool.TextRunLists.Return(ref postSplitRuns);
var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection,
textLineBreak);
return textLine;
textLine.FinalizeLine();
return textLine;
}
finally
{
objectPool.TextRunLists.Return(ref preSplitRuns);
objectPool.TextRunLists.Return(ref postSplitRuns);
}
}
private struct TextRunEnumerator

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

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

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

@ -86,7 +86,6 @@ namespace Avalonia.Media.TextFormatting
RentedList<TextRun>? rentedPreSplitRuns = null;
RentedList<TextRun>? rentedPostSplitRuns = null;
TextRun[]? results;
try
{
@ -113,9 +112,7 @@ namespace Avalonia.Media.TextFormatting
if (measuredLength <= _prefixLength || effectivePostSplitRuns is null)
{
results = collapsedRuns.ToArray();
objectPool.TextRunLists.Return(ref collapsedRuns);
return results;
return collapsedRuns.ToArray();
}
var availableSuffixWidth = availableWidth;
@ -157,16 +154,15 @@ namespace Avalonia.Media.TextFormatting
}
}
}
return collapsedRuns.ToArray();
}
finally
{
objectPool.TextRunLists.Return(ref rentedPreSplitRuns);
objectPool.TextRunLists.Return(ref rentedPostSplitRuns);
objectPool.TextRunLists.Return(ref collapsedRuns);
}
results = collapsedRuns.ToArray();
objectPool.TextRunLists.Return(ref collapsedRuns);
return results;
}
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="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="glyphInfos">The list of glyphs.</param>
/// <param name="baselineOrigin">The baseline origin of the run. Can be null.</param>
/// <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>
/// 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.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage.FileIO;
[Unstable]
public class BclStorageFile : IStorageBookmarkFile
internal class BclStorageFile : IStorageBookmarkFile
{
private readonly FileInfo _fileInfo;
public BclStorageFile(string fileName)
{
_fileInfo = new FileInfo(fileName);
FileInfo = new FileInfo(fileName);
}
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 CanOpenWrite => true;
public string Name => _fileInfo.Name;
public string Name => FileInfo.Name;
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()
{
if (_fileInfo.Exists)
if (FileInfo.Exists)
{
return Task.FromResult(new StorageItemProperties(
(ulong)_fileInfo.Length,
_fileInfo.CreationTimeUtc,
_fileInfo.LastAccessTimeUtc));
(ulong)FileInfo.Length,
FileInfo.CreationTimeUtc,
FileInfo.LastAccessTimeUtc));
}
return Task.FromResult(new StorageItemProperties());
}
public Task<IStorageFolder?> GetParentAsync()
{
if (_fileInfo.Directory is { } directory)
if (FileInfo.Directory is { } directory)
{
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
}
@ -53,17 +68,17 @@ public class BclStorageFile : IStorageBookmarkFile
public Task<Stream> OpenReadAsync()
{
return Task.FromResult<Stream>(_fileInfo.OpenRead());
return Task.FromResult<Stream>(FileInfo.OpenRead());
}
public Task<Stream> OpenWriteAsync()
{
return Task.FromResult<Stream>(_fileInfo.OpenWrite());
return Task.FromResult<Stream>(FileInfo.OpenWrite());
}
public virtual Task<string?> SaveBookmarkAsync()
{
return Task.FromResult<string?>(_fileInfo.FullName);
return Task.FromResult<string?>(FileInfo.FullName);
}
public Task ReleaseBookmarkAsync()
@ -72,28 +87,6 @@ public class BclStorageFile : IStorageBookmarkFile
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)
{
}

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

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

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.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Metadata;
using Avalonia.Compatibility;
namespace Avalonia.Platform.Storage.FileIO;
[Unstable]
public abstract class BclStorageProvider : IStorageProvider
internal abstract class BclStorageProvider : IStorageProvider
{
public abstract bool CanOpen { get; }
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?>(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.IO;
using System.Linq;
using Avalonia.Metadata;
using System.Text;
namespace Avalonia.Platform.Storage.FileIO;
[Unstable]
public static class StorageProviderHelpers
internal 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)
{
var name = Path.GetFileName(path);

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

@ -20,13 +20,13 @@ public interface IStorageItem : IDisposable
string Name { get; }
/// <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>
/// <remarks>
/// Android backend might return file path with "content:" scheme.
/// Browser and iOS backends might return relative uris.
/// </remarks>
bool TryGetUri([NotNullWhen(true)] out Uri? uri);
Uri Path { get; }
/// <summary>
/// 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 Avalonia.Metadata;
@ -53,4 +55,35 @@ public interface IStorageProvider
/// <param name="bookmark">Bookmark ID.</param>
/// <returns>Bookmarked folder or null if OS denied request.</returns>
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 UncommonFields? _uncommon;
public EffectiveValue(AvaloniaObject owner, StyledPropertyBase<T> property)
public EffectiveValue(AvaloniaObject owner, StyledProperty<T> property)
{
Priority = BindingPriority.Unset;
BasePriority = BindingPriority.Unset;
@ -57,12 +57,12 @@ namespace Avalonia.PropertyStore
Debug.Assert(priority != BindingPriority.LocalValue);
UpdateValueEntry(value, priority);
SetAndRaiseCore(owner, (StyledPropertyBase<T>)value.Property, GetValue(value), priority);
SetAndRaiseCore(owner, (StyledProperty<T>)value.Property, GetValue(value), priority);
}
public void SetLocalValueAndRaise(
ValueStore owner,
StyledPropertyBase<T> property,
StyledProperty<T> property,
T value)
{
SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue);
@ -82,7 +82,7 @@ namespace Avalonia.PropertyStore
{
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 n = newValue is not null ? ((EffectiveValue<T>)newValue).Value : _metadata.DefaultValue;
var priority = newValue is not null ? BindingPriority.Inherited : BindingPriority.Unset;
@ -98,7 +98,7 @@ namespace Avalonia.PropertyStore
Debug.Assert(Priority != BindingPriority.Animation);
Debug.Assert(BasePriority != BindingPriority.Unset);
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)
@ -107,7 +107,7 @@ namespace Avalonia.PropertyStore
return;
SetAndRaiseCore(
owner,
(StyledPropertyBase<T>)property,
(StyledProperty<T>)property,
_uncommon._uncoercedValue!,
Priority,
_uncommon._uncoercedBaseValue!,
@ -117,10 +117,10 @@ namespace Avalonia.PropertyStore
public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property)
{
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;
T oldValue;
@ -156,7 +156,7 @@ namespace Avalonia.PropertyStore
private void SetAndRaiseCore(
ValueStore owner,
StyledPropertyBase<T> property,
StyledProperty<T> property,
T value,
BindingPriority priority)
{
@ -203,7 +203,7 @@ namespace Avalonia.PropertyStore
private void SetAndRaiseCore(
ValueStore owner,
StyledPropertyBase<T> property,
StyledProperty<T> property,
T value,
BindingPriority priority,
T baseValue,

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

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

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

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

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

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

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

@ -13,14 +13,14 @@ namespace Avalonia.PropertyStore
public SourceUntypedBindingEntry(
ValueFrame frame,
StyledPropertyBase<TTarget> property,
StyledProperty<TTarget> property,
IObservable<object?> source)
: base(frame, property, source)
{
_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)
{

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

@ -11,7 +11,7 @@ namespace Avalonia.PropertyStore
{
public TypedBindingEntry(
ValueFrame frame,
StyledPropertyBase<T> property,
StyledProperty<T> property,
IObservable<T> source)
: base(frame, property, source)
{
@ -19,13 +19,13 @@ namespace Avalonia.PropertyStore
public TypedBindingEntry(
ValueFrame frame,
StyledPropertyBase<T> property,
StyledProperty<T> property,
IObservable<BindingValue<T>> 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)
{

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

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

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

@ -43,7 +43,7 @@ namespace Avalonia.PropertyStore
}
public IDisposable AddBinding<T>(
StyledPropertyBase<T> property,
StyledProperty<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority)
{
@ -71,7 +71,7 @@ namespace Avalonia.PropertyStore
}
public IDisposable AddBinding<T>(
StyledPropertyBase<T> property,
StyledProperty<T> property,
IObservable<T> source,
BindingPriority priority)
{
@ -99,7 +99,7 @@ namespace Avalonia.PropertyStore
}
public IDisposable AddBinding<T>(
StyledPropertyBase<T> property,
StyledProperty<T> property,
IObservable<object?> source,
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)
{
@ -219,7 +219,7 @@ namespace Avalonia.PropertyStore
return GetDefaultValue(property);
}
public T GetValue<T>(StyledPropertyBase<T> property)
public T GetValue<T>(StyledProperty<T> property)
{
if (_effectiveValues.TryGetValue(property, out var v))
return ((EffectiveValue<T>)v).Value;
@ -248,7 +248,7 @@ namespace Avalonia.PropertyStore
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) &&
((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="value">The effective value instance.</param>
public void OnInheritedEffectiveValueChanged<T>(
StyledPropertyBase<T> property,
StyledProperty<T> property,
T oldValue,
EffectiveValue<T> value)
{
@ -475,7 +475,7 @@ namespace Avalonia.PropertyStore
/// </summary>
/// <param name="property">The property whose value changed.</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);
@ -520,7 +520,7 @@ namespace Avalonia.PropertyStore
/// <param name="oldValue">The old value of the property.</param>
/// <param name="newValue">The new value of the property.</param>
public void OnAncestorInheritedValueChanged<T>(
StyledPropertyBase<T> property,
StyledProperty<T> property,
T oldValue,
T newValue)
{

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

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

208
src/Avalonia.Base/StyledProperty.cs

@ -1,14 +1,18 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Utilities;
namespace Avalonia
{
/// <summary>
/// A styled avalonia property.
/// </summary>
public class StyledProperty<TValue> : StyledPropertyBase<TValue>
public class StyledProperty<TValue> : AvaloniaProperty<TValue>, IStyledPropertyAccessor
{
/// <summary>
/// Initializes a new instance of the <see cref="StyledPropertyBase{T}"/> class.
/// Initializes a new instance of the <see cref="StyledProperty{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>
@ -23,20 +27,30 @@ namespace Avalonia
bool inherits = false,
Func<TValue, bool>? validate = 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>
/// Initializes a new instance of the <see cref="StyledPropertyBase{T}"/> class.
/// Gets the value validation callback for the property.
/// </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>
internal StyledProperty(StyledPropertyBase<TValue> source, Type ownerType)
: base(source, ownerType)
{
}
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; }
/// <summary>
/// Registers the property on another type.
/// </summary>
@ -47,5 +61,177 @@ namespace Avalonia
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), 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
{
private readonly StyledElement _target;
private readonly StyledPropertyBase<T>? _styledProperty;
private readonly StyledProperty<T>? _styledProperty;
private readonly DirectPropertyBase<T>? _directProperty;
private readonly T _value;
private IDisposable? _subscription;
@ -22,7 +22,7 @@ namespace Avalonia.Styling
public PropertySetterInstance(
StyledElement target,
StyledPropertyBase<T> property,
StyledProperty<T> property,
T value)
{
_target = target;

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

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

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

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

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

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

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

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

91
src/Avalonia.Controls/ItemsControl.cs

@ -12,6 +12,8 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Styling;
@ -23,7 +25,7 @@ namespace Avalonia.Controls
/// Displays a collection of items.
/// </summary>
[PseudoClasses(":empty", ":singleitem")]
public class ItemsControl : TemplatedControl, IChildIndexProvider
public class ItemsControl : TemplatedControl, IChildIndexProvider, IScrollSnapPointsInfo
{
/// <summary>
/// The default value for the <see cref="ItemsPanel"/> property.
@ -72,6 +74,18 @@ namespace Avalonia.Controls
/// </summary>
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
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>
/// 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 Tuple<int, Control>? _containerBeingPrepared;
private ScrollViewer? _scrollViewer;
private ItemsPresenter? _itemsPresenter;
private IScrollSnapPointsInfo? _scrolSnapPointInfo;
/// <summary>
/// Initializes a new instance of the <see cref="ItemsControl"/> class.
@ -203,6 +219,63 @@ namespace Avalonia.Controls
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>
/// Returns the container for the item at the specified index.
/// </summary>
@ -255,7 +328,6 @@ namespace Avalonia.Controls
/// </summary>
public IEnumerable<Control> GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty<Control>();
/// <summary>
/// Creates or a container that can be used to display an item.
/// </summary>
@ -355,6 +427,9 @@ namespace Avalonia.Controls
{
base.OnApplyTemplate(e);
_scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
_itemsPresenter = e.NameScope.Find<ItemsPresenter>("PART_ItemsPresenter");
_scrolSnapPointInfo = _itemsPresenter as IScrollSnapPointsInfo;
}
/// <summary>
@ -671,5 +746,17 @@ namespace Avalonia.Controls
count = ItemsView.Count;
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.Linq;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
#nullable enable
@ -26,9 +27,7 @@ namespace Avalonia.Controls.Platform
var files = await filePicker.OpenFilePickerAsync(options);
return files
.Select(file => file.TryGetUri(out var fullPath)
? fullPath.LocalPath
: file.Name)
.Select(file => file.TryGetFullPath() ?? file.Name)
.ToArray();
}
else if (dialog is SaveFileDialog saveDialog)
@ -47,9 +46,7 @@ namespace Avalonia.Controls.Platform
return null;
}
var filePath = file.TryGetUri(out var fullPath)
? fullPath.LocalPath
: file.Name;
var filePath = file.TryGetFullPath() ?? file.Name;
return new[] { filePath };
}
return null;
@ -67,7 +64,7 @@ namespace Avalonia.Controls.Platform
var folders = await filePicker.OpenFolderPickerAsync(options);
return folders
.Select(f => f.TryGetUri(out var uri) ? uri.LocalPath : null)
.Select(folder => folder.TryGetFullPath() ?? folder.Name)
.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 Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Presents items inside an <see cref="Avalonia.Controls.ItemsControl"/>.
/// </summary>
public class ItemsPresenter : Control, ILogicalScrollable
public class ItemsPresenter : Control, ILogicalScrollable, IScrollSnapPointsInfo
{
/// <summary>
/// Defines the <see cref="ItemsPanel"/> property.
@ -19,8 +21,37 @@ namespace Avalonia.Controls.Presenters
private PanelContainerGenerator? _generator;
private ILogicalScrollable? _logicalScrollable;
private IScrollSnapPointsInfo? _scrollSnapPointsInfo;
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()
{
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;
Size ILogicalScrollable.ScrollSize => _logicalScrollable?.ScrollSize ?? default;
Size ILogicalScrollable.PageScrollSize => _logicalScrollable?.PageScrollSize ?? default;
Size IScrollable.Extent => _logicalScrollable?.Extent ?? 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()
{
if (Panel is null && ItemsControl is not null)
@ -100,14 +167,36 @@ namespace Avalonia.Controls.Presenters
Panel = ItemsPanel.Build();
Panel.SetValue(TemplatedParentProperty, TemplatedParent);
_scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo;
LogicalChildren.Add(Panel);
VisualChildren.Add(Panel);
if (_scrollSnapPointsInfo != null)
{
_scrollSnapPointsInfo.AreVerticalSnapPointsRegular = AreVerticalSnapPointsRegular;
_scrollSnapPointsInfo.AreHorizontalSnapPointsRegular = AreHorizontalSnapPointsRegular;
}
if (Panel is VirtualizingPanel v)
v.Attach(ItemsControl);
else
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;
if (_logicalScrollable is not null)
@ -151,6 +240,16 @@ namespace Avalonia.Controls.Presenters
ResetState();
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()
@ -204,5 +303,27 @@ namespace Avalonia.Controls.Presenters
}
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.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Utilities;
using Avalonia.VisualTree;
@ -14,6 +15,7 @@ namespace Avalonia.Controls.Presenters
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider
{
private const double EdgeDetectionTolerance = 0.1;
private const int ProximityPoints = 10;
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
@ -57,6 +59,30 @@ namespace Avalonia.Controls.Presenters
o => o.Viewport,
(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>
/// Defines the <see cref="IsScrollChainingEnabled"/> property.
/// </summary>
@ -71,10 +97,19 @@ namespace Avalonia.Controls.Presenters
private IDisposable? _logicalScrollSubscription;
private Size _viewport;
private Dictionary<int, Vector>? _activeLogicalGestureScrolls;
private Dictionary<int, Vector>? _scrollGestureSnapPoints;
private List<Control>? _anchorCandidates;
private Control? _anchorElement;
private Rect _anchorElementBounds;
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>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -93,6 +128,7 @@ namespace Avalonia.Controls.Presenters
AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture);
AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded);
AddHandler(Gestures.ScrollGestureInertiaStartingEvent, OnScrollGestureInertiaStartingEnded);
this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
}
@ -142,6 +178,42 @@ namespace Avalonia.Controls.Presenters
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>
/// Gets or sets if scroll chaining is enabled. The default value is true.
/// </summary>
@ -424,6 +496,25 @@ namespace Avalonia.Controls.Presenters
}
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;
Offset = newOffset;
@ -434,7 +525,65 @@ namespace Avalonia.Controls.Presenters
}
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/>
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
@ -458,6 +607,30 @@ namespace Avalonia.Controls.Presenters
if (Extent.Height > Viewport.Height)
{
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 = Math.Max(y, 0);
y = Math.Min(y, Extent.Height - Viewport.Height);
@ -466,12 +639,37 @@ namespace Avalonia.Controls.Presenters
if (Extent.Width > Viewport.Width)
{
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 = Math.Max(x, 0);
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;
Offset = newOffset;
@ -485,10 +683,36 @@ namespace Avalonia.Controls.Presenters
{
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);
}
private void ScrollSnapPointsInfoSnapPointsChanged(object? sender, Interactivity.RoutedEventArgs e)
{
UpdateSnapPoints();
}
private void BringIntoViewRequested(object? sender, RequestBringIntoViewEventArgs e)
{
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;
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, 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>
/// Defines the VerticalScrollBarViewportSize property.
/// </summary>
@ -193,6 +221,14 @@ namespace Avalonia.Controls
nameof(IsScrollChainingEnabled),
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>
/// Defines the <see cref="ScrollChanged"/> event.
/// </summary>
@ -421,6 +457,42 @@ namespace Avalonia.Controls
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>
/// Gets a value that indicates whether scrollbars can hide itself when user is not interacting with it.
/// </summary>
@ -444,6 +516,15 @@ namespace Avalonia.Controls
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>
/// Scrolls the content up one line.
/// </summary>

191
src/Avalonia.Controls/StackPanel.cs

@ -4,7 +4,11 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace Avalonia.Controls
@ -12,7 +16,7 @@ namespace Avalonia.Controls
/// <summary>
/// A panel which lays out its children horizontally or vertically.
/// </summary>
public class StackPanel : Panel, INavigableContainer
public class StackPanel : Panel, INavigableContainer, IScrollSnapPointsInfo
{
/// <summary>
/// Defines the <see cref="Spacing"/> property.
@ -26,6 +30,34 @@ namespace Avalonia.Controls
public static readonly StyledProperty<Orientation> OrientationProperty =
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>
/// Initializes static members of the <see cref="StackPanel"/> class.
/// </summary>
@ -53,6 +85,42 @@ namespace Avalonia.Controls
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>
/// Gets the next control in the specified direction.
/// </summary>
@ -274,6 +342,8 @@ namespace Avalonia.Controls
ArrangeChild(child, rcChild, finalSize, Orientation);
}
RaiseEvent(new RoutedEventArgs(Orientation == Orientation.Horizontal ? HorizontalSnapPointsChangedEvent : VerticalSnapPointsChangedEvent));
return finalSize;
}
@ -285,5 +355,124 @@ namespace Avalonia.Controls
{
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>
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)
@ -606,6 +615,13 @@ namespace Avalonia.Controls
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 =>
(PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod;
}

234
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -3,8 +3,11 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Utilities;
using Avalonia.VisualTree;
@ -14,7 +17,7 @@ namespace Avalonia.Controls
/// <summary>
/// Arranges and virtualizes content on a single line that is oriented either horizontally or vertically.
/// </summary>
public class VirtualizingStackPanel : VirtualizingPanel
public class VirtualizingStackPanel : VirtualizingPanel, IScrollSnapPointsInfo
{
/// <summary>
/// Defines the <see cref="Orientation"/> property.
@ -22,6 +25,34 @@ namespace Avalonia.Controls
public static readonly StyledProperty<Orientation> OrientationProperty =
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 =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, bool>("ItemIsOwnContainer");
@ -62,6 +93,42 @@ namespace Avalonia.Controls
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)
{
if (!IsEffectivelyVisible)
@ -145,6 +212,8 @@ namespace Avalonia.Controls
finally
{
_isInLayout = false;
RaiseEvent(new RoutedEventArgs(Orientation == Orientation.Horizontal ? HorizontalSnapPointsChangedEvent : VerticalSnapPointsChangedEvent));
}
}
@ -226,7 +295,7 @@ namespace Avalonia.Controls
{
if (toIndex < 0)
toIndex = count - 1;
else if (toIndex >= count - 1)
else if (toIndex >= count)
toIndex = 0;
}
@ -622,6 +691,167 @@ namespace Avalonia.Controls
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>
/// Stores the realized element state for a <see cref="VirtualizingStackPanel"/>.
/// </summary>

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

@ -17,7 +17,11 @@ namespace Avalonia.Diagnostics.Screenshots
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);
#endif
if (output is { })
{
control.RenderTo(output);

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

@ -14,57 +14,54 @@ namespace Avalonia.Diagnostics.Screenshots
/// </summary>
public sealed class FilePickerHandler : BaseRenderToStreamHandler
{
private readonly string _title;
private readonly string _screenshotRoot;
/// <summary>
/// Instance FilePickerHandler
/// </summary>
public FilePickerHandler()
public FilePickerHandler() : this(null, null)
{
}
/// <summary>
/// Instance FilePickerHandler with specificated parameter
/// </summary>
/// <param name="title">SaveFilePicker Title</param>
/// <param name="screenshotRoot"></param>
public FilePickerHandler(string? title
, string? screenshotRoot = default
)
public FilePickerHandler(
string? title,
string? screenshotRoot = default)
{
if (title is { })
Title = title;
if (screenshotRoot is { })
ScreenshotsRoot = screenshotRoot;
_title = title ?? "Save Screenshot to ...";
_screenshotRoot = screenshotRoot ?? Conventions.DefaultScreenshotsRoot;
}
/// <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>
/// SaveFilePicker Title
/// </summary>
public string Title { get; } = "Save Screenshot to ...";
static Window GetWindow(Control control)
private static TopLevel GetTopLevel(Control control)
{
var window = control.VisualRoot as Window;
var app = Application.Current;
if (app?.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime desktop)
// If possible, use devtools main window.
TopLevel? devToolsTopLevel = null;
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)
{
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)),
Title = Title,
FileTypeChoices = new FilePickerFileType[] { new FilePickerFileType("PNG") { Patterns = new string[] { "*.png" } } },
SuggestedStartLocation = defaultFolder,
Title = _title,
FileTypeChoices = new [] { FilePickerFileTypes.ImagePng },
DefaultExtension = ".png"
});
if (result is null)
@ -73,7 +70,7 @@ namespace Avalonia.Diagnostics.Screenshots
}
if (!result.CanOpenWrite)
{
throw new InvalidOperationException("ReadOnly file was opened");
throw new InvalidOperationException("Read-only file was selected.");
}
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" />
</ItemGroup>
<ItemGroup>
<!-- For managed dialogs dev testing -->
<InternalsVisibleTo Include="ControlCatalog, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\DevAnalyzers.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)
{
string fullDirectoryPath;
if (path?.TryGetUri(out var fullDirectoryUri) == true
&& fullDirectoryUri.IsAbsoluteUri)
{
fullDirectoryPath = fullDirectoryUri.LocalPath;
}
else
{
fullDirectoryPath = Directory.GetCurrentDirectory();
}
var fullDirectoryPath = path?.TryGetFullPath() ?? Directory.GetCurrentDirectory();
Navigate(fullDirectoryPath, initialSelectionName);
}

4
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

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

2
src/Avalonia.Dialogs/ManagedStorageProvider.cs

@ -11,7 +11,7 @@ using Avalonia.Platform.Storage.FileIO;
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 ManagedFileDialogOptions _managedOptions;

6
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

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

12
src/Avalonia.Headless/HeadlessWindowImpl.cs

@ -284,6 +284,18 @@ namespace Avalonia.Headless
button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp,
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)
{

3
src/Avalonia.Headless/IHeadlessWindow.cs

@ -1,4 +1,5 @@
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Utilities;
@ -13,5 +14,7 @@ namespace Avalonia.Headless
void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None);
void MouseMove(Point point, 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();
var suggestedDirectory = options.SuggestedStartLocation?.TryGetUri(out var suggestedDirectoryTmp) == true
? suggestedDirectoryTmp.LocalPath : string.Empty;
var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty;
_native.OpenFileDialog((IAvnWindow)_window.Native,
events,
@ -54,8 +53,7 @@ namespace Avalonia.Native
{
using var events = new SystemDialogEvents();
var suggestedDirectory = options.SuggestedStartLocation?.TryGetUri(out var suggestedDirectoryTmp) == true
? suggestedDirectoryTmp.LocalPath : string.Empty;
var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty;
_native.SaveFileDialog((IAvnWindow)_window.Native,
events,
@ -74,8 +72,7 @@ namespace Avalonia.Native
{
using var events = new SystemDialogEvents();
var suggestedDirectory = options.SuggestedStartLocation?.TryGetUri(out var suggestedDirectoryTmp) == true
? suggestedDirectoryTmp.LocalPath : string.Empty;
var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty;
_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}"
Padding="{TemplateBinding Padding}">
<ItemsPresenter Name="PART_ItemsPresenter"
AreVerticalSnapPointsRegular="{TemplateBinding AreVerticalSnapPointsRegular}"
AreHorizontalSnapPointsRegular="{TemplateBinding AreHorizontalSnapPointsRegular}"
ItemsPanel="{TemplateBinding ItemsPanel}"/>
</Border>
</ControlTemplate>

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

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

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

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

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

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

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

Loading…
Cancel
Save