Browse Source

Merge branch 'master' into expander-isexpanded-styled-prop

pull/9979/head
robloo 4 years ago
committed by GitHub
parent
commit
f635f17f32
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  2. 5
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  3. 113
      native/Avalonia.Native/src/OSX/PlatformSettings.mm
  4. 4
      native/Avalonia.Native/src/OSX/WindowBaseImpl.h
  5. 20
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  6. 6
      native/Avalonia.Native/src/OSX/WindowImpl.h
  7. 18
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  8. 1
      native/Avalonia.Native/src/OSX/common.h
  9. 10
      native/Avalonia.Native/src/OSX/main.mm
  10. 73
      samples/ControlCatalog/MainView.xaml.cs
  11. 4
      samples/ControlCatalog/Pages/CarouselPage.xaml
  12. 2
      samples/ControlCatalog/Pages/DialogsPage.xaml
  13. 60
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  14. 2
      samples/ControlCatalog/Pages/TabControlPage.xaml
  15. 4
      samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
  16. 5
      samples/VirtualizationDemo/MainWindow.xaml
  17. 10
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  18. 2
      src/Android/Avalonia.Android/AndroidPlatform.cs
  19. 19
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  20. 16
      src/Android/Avalonia.Android/AvaloniaView.cs
  21. 14
      src/Android/Avalonia.Android/IAndroidNavigationService.cs
  22. 94
      src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs
  23. 28
      src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs
  24. 13
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  25. 2
      src/Avalonia.Base/Animation/PageSlide.cs
  26. 2
      src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs
  27. 5
      src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs
  28. 5
      src/Avalonia.Base/Interactivity/EventRoute.cs
  29. 7
      src/Avalonia.Base/Media/FormattedText.cs
  30. 93
      src/Avalonia.Base/Media/GlyphRun.cs
  31. 275
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  32. 115
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs
  33. 14
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  34. 6
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  35. 24
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  36. 16
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  37. 123
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  38. 4
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  39. 153
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  40. 21
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  41. 6
      src/Avalonia.Base/Media/TextFormatting/TextShaper.cs
  42. 2
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  43. 67
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  44. 6
      src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  45. 14
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  46. 14
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  47. 13
      src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  48. 44
      src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs
  49. 27
      src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
  50. 15
      src/Avalonia.Base/Platform/IPlatformSettings.cs
  51. 6
      src/Avalonia.Base/Platform/ITextShaperImpl.cs
  52. 68
      src/Avalonia.Base/Platform/PlatformColorValues.cs
  53. 18
      src/Avalonia.Base/Platform/SystemNavigationManager.cs
  54. 5
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  55. 5
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml
  56. 1
      src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs
  57. 52
      src/Avalonia.Controls/Carousel.cs
  58. 34
      src/Avalonia.Controls/ComboBox.cs
  59. 5
      src/Avalonia.Controls/ContextMenu.cs
  60. 2
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  61. 5
      src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs
  62. 114
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  63. 18
      src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs
  64. 49
      src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs
  65. 313
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  66. 102
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  67. 42
      src/Avalonia.Controls/Generators/ItemContainerInfo.cs
  68. 21
      src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs
  69. 105
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  70. 166
      src/Avalonia.Controls/Generators/TreeContainerIndex.cs
  71. 166
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  72. 19
      src/Avalonia.Controls/IVirtualizingController.cs
  73. 89
      src/Avalonia.Controls/IVirtualizingPanel.cs
  74. 18
      src/Avalonia.Controls/ItemVirtualizationMode.cs
  75. 532
      src/Avalonia.Controls/ItemsControl.cs
  76. 249
      src/Avalonia.Controls/ItemsSourceView.cs
  77. 25
      src/Avalonia.Controls/ListBox.cs
  78. 10
      src/Avalonia.Controls/Menu.cs
  79. 17
      src/Avalonia.Controls/MenuBase.cs
  80. 19
      src/Avalonia.Controls/MenuItem.cs
  81. 6
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  82. 276
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  83. 18
      src/Avalonia.Controls/Presenters/IItemsPresenter.cs
  84. 26
      src/Avalonia.Controls/Presenters/IItemsPresenterHost.cs
  85. 125
      src/Avalonia.Controls/Presenters/ItemContainerSync.cs
  86. 303
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  87. 106
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  88. 606
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  89. 223
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  90. 308
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  91. 160
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  92. 16
      src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
  93. 91
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  94. 9
      src/Avalonia.Controls/Primitives/TabStrip.cs
  95. 6
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
  96. 3
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  97. 1
      src/Avalonia.Controls/Selection/SelectionModel.cs
  98. 9
      src/Avalonia.Controls/Selection/SelectionNodeBase.cs
  99. 64
      src/Avalonia.Controls/TabControl.cs
  100. 15
      src/Avalonia.Controls/TextBlock.cs

4
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@ -50,6 +50,7 @@
BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; };
BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; };
ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; };
EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */ = {isa = PBXBuildFile; fileRef = EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -103,6 +104,7 @@
BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = "<group>"; };
BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = "<group>"; };
ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformSettings.mm; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -163,6 +165,7 @@
1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */,
37A517B22159597E00FBA241 /* Screens.mm */,
37C09D8721580FE4006A6758 /* SystemDialogs.mm */,
EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */,
AB7A61F02147C815003C5833 /* Products */,
AB661C1C2148230E00291242 /* Frameworks */,
18391676ECF0E983F4964357 /* WindowBaseImpl.mm */,
@ -299,6 +302,7 @@
1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */,
18391AC16726CBC45856233B /* AvnWindow.mm in Sources */,
18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */,
EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

5
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -44,7 +44,7 @@
-(bool) isDialog
{
return _parent->IsDialog();
return _parent->IsModal();
}
-(double) getExtendedTitleBarHeight
@ -281,6 +281,9 @@
- (void)windowDidBecomeKey:(NSNotification *_Nonnull)notification
{
if (_parent == nullptr)
return;
_parent->BringToFront();
dispatch_async(dispatch_get_main_queue(), ^{

113
native/Avalonia.Native/src/OSX/PlatformSettings.mm

@ -0,0 +1,113 @@
#include "common.h"
@interface CocoaThemeObserver : NSObject
-(id)initWithCallback:(IAvnActionCallback *)callback;
@end
class PlatformSettings : public ComSingleObject<IAvnPlatformSettings, &IID_IAvnPlatformSettings>
{
CocoaThemeObserver* observer;
public:
FORWARD_IUNKNOWN()
virtual AvnPlatformThemeVariant GetPlatformTheme() override
{
@autoreleasepool
{
if (@available(macOS 10.14, *))
{
if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAqua
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameVibrantLight) {
return AvnPlatformThemeVariant::Light;
} else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameDarkAqua
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameVibrantDark) {
return AvnPlatformThemeVariant::Dark;
} else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastAqua
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantLight) {
return AvnPlatformThemeVariant::HighContrastLight;
} else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastDarkAqua
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantDark) {
return AvnPlatformThemeVariant::HighContrastDark;
}
}
return AvnPlatformThemeVariant::Light;
}
}
virtual unsigned int GetAccentColor() override
{
@autoreleasepool
{
if (@available(macOS 10.14, *))
{
auto color = [NSColor controlAccentColor];
return to_argb(color);
}
else
{
return 0;
}
}
}
virtual void RegisterColorsChange(IAvnActionCallback *callback) override
{
if (@available(macOS 10.14, *))
{
observer = [[CocoaThemeObserver alloc] initWithCallback: callback];
[[NSApplication sharedApplication] addObserver:observer forKeyPath:@"effectiveAppearance" options:NSKeyValueObservingOptionNew context:nil];
}
}
private:
unsigned int to_argb(NSColor* color)
{
const CGFloat* components = CGColorGetComponents(color.CGColor);
unsigned int alpha = static_cast<unsigned int>(CGColorGetAlpha(color.CGColor) * 0xFF);
unsigned int red = static_cast<unsigned int>(components[0] * 0xFF);
unsigned int green = static_cast<unsigned int>(components[1] * 0xFF);
unsigned int blue = static_cast<unsigned int>(components[2] * 0xFF);
return (alpha << 24) + (red << 16) + (green << 8) + blue;
}
};
@implementation CocoaThemeObserver
{
ComPtr<IAvnActionCallback> _callback;
}
- (id) initWithCallback:(IAvnActionCallback *)callback{
self = [super init];
if (self) {
_callback = callback;
}
return self;
}
/*- (void)didChangeValueForKey:(NSString *)key {
if([key isEqualToString:@"effectiveAppearance"]) {
_callback->Run();
}
else {
[super didChangeValueForKey:key];
}
}*/
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if([keyPath isEqualToString:@"effectiveAppearance"]) {
_callback->Run();
} else {
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
@end
extern IAvnPlatformSettings* CreatePlatformSettings()
{
return new PlatformSettings();
}

4
native/Avalonia.Native/src/OSX/WindowBaseImpl.h

@ -92,11 +92,13 @@ BEGIN_INTERFACE_MAP()
virtual HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode) override;
virtual HRESULT SetFrameThemeVariant(AvnPlatformThemeVariant variant) override;
virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard *clipboard, IAvnDndResultCallback *cb,
void *sourceHandle) override;
virtual bool IsDialog();
virtual bool IsModal();
id<AvnWindowProtocol> GetWindowProtocol ();

20
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@ -498,6 +498,24 @@ HRESULT WindowBaseImpl::SetTransparencyMode(AvnWindowTransparencyMode mode) {
return S_OK;
}
HRESULT WindowBaseImpl::SetFrameThemeVariant(AvnPlatformThemeVariant variant) {
START_COM_CALL;
NSAppearanceName appearanceName;
if (@available(macOS 10.14, *))
{
appearanceName = variant == AvnPlatformThemeVariant::Dark ? NSAppearanceNameDarkAqua : NSAppearanceNameAqua;
}
else
{
appearanceName = variant == AvnPlatformThemeVariant::Dark ? NSAppearanceNameVibrantDark : NSAppearanceNameAqua;
}
[Window setAppearance: [NSAppearance appearanceNamed: appearanceName]];
return S_OK;
}
HRESULT WindowBaseImpl::BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard *clipboard, IAvnDndResultCallback *cb, void *sourceHandle) {
START_COM_CALL;
@ -542,7 +560,7 @@ HRESULT WindowBaseImpl::BeginDragAndDropOperation(AvnDragDropEffects effects, Av
return S_OK;
}
bool WindowBaseImpl::IsDialog() {
bool WindowBaseImpl::IsModal() {
return false;
}

6
native/Avalonia.Native/src/OSX/WindowImpl.h

@ -23,7 +23,7 @@ private:
NSRect _preZoomSize;
bool _transitioningWindowState;
bool _isClientAreaExtended;
bool _isDialog;
bool _isModal;
WindowImpl* _parent;
std::list<WindowImpl*> _children;
AvnExtendClientAreaChromeHints _extendClientHints;
@ -91,7 +91,9 @@ BEGIN_INTERFACE_MAP()
virtual HRESULT SetWindowState (AvnWindowState state) override;
virtual bool IsDialog() override;
virtual bool IsModal() override;
bool IsOwned();
virtual void BringToFront () override;

18
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -63,7 +63,7 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) {
START_COM_CALL;
@autoreleasepool {
_isDialog = isDialog || _parent != nullptr;
_isModal = isDialog;
WindowBaseImpl::Show(activate, isDialog);
@ -97,7 +97,7 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) {
_parent = cparent;
_isDialog = _parent != nullptr;
_isModal = _parent != nullptr;
if(_parent != nullptr && Window != nullptr){
// If one tries to show a child window with a minimized parent window, then the parent window will be
@ -123,7 +123,7 @@ void WindowImpl::BringToFront()
{
if ([Window isVisible] && ![Window isMiniaturized])
{
if(IsDialog())
if(IsModal())
{
Activate();
}
@ -150,7 +150,7 @@ bool WindowImpl::CanBecomeKeyWindow()
{
for(auto iterator = _children.begin(); iterator != _children.end(); iterator++)
{
if((*iterator)->IsDialog())
if((*iterator)->IsModal())
{
return false;
}
@ -569,8 +569,12 @@ HRESULT WindowImpl::SetWindowState(AvnWindowState state) {
}
}
bool WindowImpl::IsDialog() {
return _isDialog;
bool WindowImpl::IsModal() {
return _isModal;
}
bool WindowImpl::IsOwned() {
return _parent != nullptr;
}
NSWindowStyleMask WindowImpl::GetStyle() {
@ -599,7 +603,7 @@ NSWindowStyleMask WindowImpl::GetStyle() {
break;
}
if (!IsDialog()) {
if (!IsOwned()) {
s |= NSWindowStyleMaskMiniaturizable;
}

1
native/Avalonia.Native/src/OSX/common.h

@ -27,6 +27,7 @@ extern IAvnMenuItem* CreateAppMenuItem();
extern IAvnMenuItem* CreateAppMenuItemSeparator();
extern IAvnApplicationCommands* CreateApplicationCommands();
extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);
extern IAvnPlatformSettings* CreatePlatformSettings();
extern void SetAppMenu(IAvnMenu *menu);
extern void SetServicesMenu (IAvnMenu* menu);
extern IAvnMenu* GetAppMenu ();

10
native/Avalonia.Native/src/OSX/main.mm

@ -398,6 +398,16 @@ public:
}
}
virtual HRESULT CreatePlatformSettings (IAvnPlatformSettings** ppv) override
{
START_COM_CALL;
@autoreleasepool
{
*ppv = ::CreatePlatformSettings();
return S_OK;
}
}
};
extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative()

73
samples/ControlCatalog/MainView.xaml.cs

@ -3,9 +3,11 @@ using System.Collections;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.VisualTree;
using ControlCatalog.Models;
using ControlCatalog.Pages;
@ -14,10 +16,14 @@ namespace ControlCatalog
{
public class MainView : UserControl
{
private readonly IPlatformSettings _platformSettings;
public MainView()
{
AvaloniaXamlLoader.Load(this);
_platformSettings = AvaloniaLocator.Current.GetRequiredService<IPlatformSettings>();
PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues());
var sideBar = this.Get<TabControl>("Sidebar");
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
@ -37,6 +43,15 @@ namespace ControlCatalog
if (themes.SelectedItem is CatalogTheme theme)
{
App.SetThemeVariant(theme);
((TopLevel?)this.GetVisualRoot())?.PlatformImpl?.SetFrameThemeVariant(theme switch
{
CatalogTheme.FluentLight => PlatformThemeVariant.Light,
CatalogTheme.FluentDark => PlatformThemeVariant.Dark,
CatalogTheme.SimpleLight => PlatformThemeVariant.Light,
CatalogTheme.SimpleDark => PlatformThemeVariant.Dark,
_ => throw new ArgumentOutOfRangeException()
});
}
};
@ -89,6 +104,62 @@ namespace ControlCatalog
var decorations = this.Get<ComboBox>("Decorations");
if (VisualRoot is Window window)
decorations.SelectedIndex = (int)window.SystemDecorations;
_platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged;
PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues());
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
_platformSettings.ColorValuesChanged -= PlatformSettingsOnColorValuesChanged;
}
private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e)
{
var themes = this.Get<ComboBox>("Themes");
var currentTheme = (CatalogTheme?)themes.SelectedItem ?? CatalogTheme.FluentLight;
var newTheme = (currentTheme, e.ThemeVariant) switch
{
(CatalogTheme.FluentDark, PlatformThemeVariant.Light) => CatalogTheme.FluentLight,
(CatalogTheme.FluentLight, PlatformThemeVariant.Dark) => CatalogTheme.FluentDark,
(CatalogTheme.SimpleDark, PlatformThemeVariant.Light) => CatalogTheme.SimpleLight,
(CatalogTheme.SimpleLight, PlatformThemeVariant.Dark) => CatalogTheme.SimpleDark,
_ => currentTheme
};
themes.SelectedItem = newTheme;
Application.Current!.Resources["SystemAccentColor"] = e.AccentColor1;
Application.Current.Resources["SystemAccentColorDark1"] = ChangeColorLuminosity(e.AccentColor1, -0.3);
Application.Current.Resources["SystemAccentColorDark2"] = ChangeColorLuminosity(e.AccentColor1, -0.5);
Application.Current.Resources["SystemAccentColorDark3"] = ChangeColorLuminosity(e.AccentColor1, -0.7);
Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, -0.3);
Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, -0.5);
Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, -0.7);
static Color ChangeColorLuminosity(Color color, double luminosityFactor)
{
var red = (double)color.R;
var green = (double)color.G;
var blue = (double)color.B;
if (luminosityFactor < 0)
{
luminosityFactor = 1 + luminosityFactor;
red *= luminosityFactor;
green *= luminosityFactor;
blue *= luminosityFactor;
}
else if (luminosityFactor >= 0)
{
red = (255 - red) * luminosityFactor + red;
green = (255 - green) * luminosityFactor + green;
blue = (255 - blue) * luminosityFactor + blue;
}
return new Color(color.A, (byte)red, (byte)green, (byte)blue);
}
}
}
}

4
samples/ControlCatalog/Pages/CarouselPage.xaml

@ -12,7 +12,7 @@
</Button>
<Carousel Name="carousel" Grid.Column="1">
<Carousel.PageTransition>
<PageSlide Duration="0.25" Orientation="Vertical" />
<PageSlide Duration="0.25" Orientation="Horizontal" />
</Carousel.PageTransition>
<Image Source="/Assets/delicate-arch-896885_640.jpg"/>
<Image Source="/Assets/hirsch-899118_640.jpg"/>
@ -35,7 +35,7 @@
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock VerticalAlignment="Center">Orientation</TextBlock>
<ComboBox Name="orientation" SelectedIndex="1" VerticalAlignment="Center">
<ComboBox Name="orientation" SelectedIndex="0" VerticalAlignment="Center">
<ComboBoxItem>Horizontal</ComboBoxItem>
<ComboBoxItem>Vertical</ComboBoxItem>
</ComboBox>

2
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -46,7 +46,7 @@
Classes="h2"
IsVisible="False"
Text="Last picker results:" />
<ItemsPresenter x:Name="PickerLastResults" />
<ItemsControl x:Name="PickerLastResults" />
<TextBox Name="BookmarkContainer" Watermark="Bookmark" />
<TextBox Name="OpenedFileContent"

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

@ -24,7 +24,7 @@ namespace ControlCatalog.Pages
{
this.InitializeComponent();
var results = this.Get<ItemsPresenter>("PickerLastResults");
var results = this.Get<ItemsControl>("PickerLastResults");
var resultsVisible = this.Get<TextBlock>("PickerLastResultsVisible");
var bookmarkContainer = this.Get<TextBox>("BookmarkContainer");
var openedFileContent = this.Get<TextBox>("OpenedFileContent");
@ -36,18 +36,18 @@ namespace ControlCatalog.Pages
{
if (this.Get<CheckBox>("UseFilters").IsChecked != true)
return new List<FileDialogFilter>();
return new List<FileDialogFilter>
{
new FileDialogFilter
{
Name = "Text files (.txt)", Extensions = new List<string> {"txt"}
},
new FileDialogFilter
{
Name = "All files",
Extensions = new List<string> {"*"}
}
};
return new List<FileDialogFilter>
{
new FileDialogFilter
{
Name = "Text files (.txt)", Extensions = new List<string> {"txt"}
},
new FileDialogFilter
{
Name = "All files",
Extensions = new List<string> {"*"}
}
};
}
List<FilePickerFileType>? GetFileTypes()
@ -55,10 +55,10 @@ namespace ControlCatalog.Pages
if (this.Get<CheckBox>("UseFilters").IsChecked != true)
return null;
return new List<FilePickerFileType>
{
FilePickerFileTypes.All,
FilePickerFileTypes.TextPlain
};
{
FilePickerFileTypes.All,
FilePickerFileTypes.TextPlain
};
}
this.Get<Button>("OpenFile").Click += async delegate
@ -205,15 +205,15 @@ namespace ControlCatalog.Pages
await using var stream = await file.OpenWriteAsync();
await using var reader = new System.IO.StreamWriter(stream);
#else
using var stream = await file.OpenWriteAsync();
using var reader = new System.IO.StreamWriter(stream);
using var stream = await file.OpenWriteAsync();
using var reader = new System.IO.StreamWriter(stream);
#endif
await reader.WriteLineAsync(openedFileContent.Text);
lastSelectedDirectory = await file.GetParentAsync();
}
await SetPickerResult(file is null ? null : new [] {file});
await SetPickerResult(file is null ? null : new[] { file });
};
this.Get<Button>("OpenFolderPicker").Click += async delegate
{
@ -243,7 +243,7 @@ namespace ControlCatalog.Pages
: null;
await SetPickerResult(folder is null ? null : new[] { folder });
lastSelectedDirectory = folder;
};
@ -260,23 +260,23 @@ namespace ControlCatalog.Pages
var props = await item.GetBasicPropertiesAsync();
resultText += @$"Size: {props.Size}
DateCreated: {props.DateCreated}
DateModified: {props.DateModified}
CanBookmark: {item.CanBookmark}
";
DateCreated: {props.DateCreated}
DateModified: {props.DateModified}
CanBookmark: {item.CanBookmark}
";
if (item is IStorageFile file)
{
resultText += @$"
CanOpenRead: {file.CanOpenRead}
CanOpenWrite: {file.CanOpenWrite}
Content:
";
CanOpenRead: {file.CanOpenRead}
CanOpenWrite: {file.CanOpenWrite}
Content:
";
if (file.CanOpenRead)
{
#if NET6_0_OR_GREATER
await using var stream = await file.OpenReadAsync();
#else
using var stream = await file.OpenReadAsync();
using var stream = await file.OpenReadAsync();
#endif
using var reader = new System.IO.StreamReader(stream);

2
samples/ControlCatalog/Pages/TabControlPage.xaml

@ -53,7 +53,7 @@
<TabControl
Items="{Binding Tabs}"
Margin="0 16"
HeaderDisplayMemberBinding="{Binding Header, x:DataType=viewModels:TabControlPageViewModelItem}"
DisplayMemberBinding="{Binding Header, x:DataType=viewModels:TabControlPageViewModelItem}"
TabStripPlacement="{Binding TabPlacement}">
<TabControl.ContentTemplate>
<DataTemplate x:DataType="viewModels:TabControlPageViewModelItem">

4
samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml

@ -168,9 +168,7 @@
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<ItemsPresenter Name="PART_ItemsPresenter"
HorizontalAlignment="Stretch"
ItemTemplate="{TemplateBinding ItemTemplate}"
Items="{TemplateBinding Items}">
HorizontalAlignment="Stretch">
<ItemsPresenter.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel x:Name="HamburgerItemsPanel"

5
samples/VirtualizationDemo/MainWindow.xaml

@ -9,10 +9,8 @@
<DockPanel LastChildFill="True" Margin="16">
<StackPanel DockPanel.Dock="Right"
Margin="16 0 0 0"
MinWidth="150"
Width="150"
Spacing="4">
<ComboBox Items="{Binding VirtualizationModes}"
SelectedItem="{Binding VirtualizationMode}"/>
<ComboBox Items="{Binding Orientations}"
SelectedItem="{Binding Orientation}"/>
<TextBox Watermark="Item Count"
@ -49,7 +47,6 @@
Items="{Binding Items}"
Selection="{Binding Selection}"
SelectionMode="Multiple"
VirtualizationMode="{Binding VirtualizationMode}"
ScrollViewer.HorizontalScrollBarVisibility="{Binding HorizontalScrollBarVisibility, Mode=TwoWay}"
ScrollViewer.VerticalScrollBarVisibility="{Binding VerticalScrollBarVisibility, Mode=TwoWay}">
<ListBox.ItemsPanel>

10
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

@ -21,7 +21,6 @@ namespace VirtualizationDemo.ViewModels
private ScrollBarVisibility _horizontalScrollBarVisibility = ScrollBarVisibility.Auto;
private ScrollBarVisibility _verticalScrollBarVisibility = ScrollBarVisibility.Auto;
private Orientation _orientation = Orientation.Vertical;
private ItemVirtualizationMode _virtualizationMode = ItemVirtualizationMode.Simple;
public MainWindowViewModel()
{
@ -81,15 +80,6 @@ namespace VirtualizationDemo.ViewModels
public IEnumerable<ScrollBarVisibility> ScrollBarVisibilities =>
Enum.GetValues(typeof(ScrollBarVisibility)).Cast<ScrollBarVisibility>();
public ItemVirtualizationMode VirtualizationMode
{
get { return _virtualizationMode; }
set { this.RaiseAndSetIfChanged(ref _virtualizationMode, value); }
}
public IEnumerable<ItemVirtualizationMode> VirtualizationModes =>
Enum.GetValues(typeof(ItemVirtualizationMode)).Cast<ItemVirtualizationMode>();
public MiniCommand AddItemCommand { get; private set; }
public MiniCommand RecreateCommand { get; private set; }
public MiniCommand RemoveItemCommand { get; private set; }

2
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -43,7 +43,7 @@ namespace Avalonia.Android
.Bind<ICursorFactory>().ToTransient<CursorFactory>()
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IPlatformSettings>().ToSingleton<AndroidPlatformSettings>()
.Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())

19
src/Android/Avalonia.Android/AvaloniaMainActivity.cs

@ -1,18 +1,14 @@
using System;
using Android.App;
using Android.Content;
using Android.Content.Res;
using Android.OS;
using Android.Runtime;
using Android.Views;
using AndroidX.AppCompat.App;
using AndroidX.Lifecycle;
using AndroidRect = Android.Graphics.Rect;
namespace Avalonia.Android
{
public abstract class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler
public abstract class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler, IActivityNavigationService
{
internal static object ViewContent;
@ -56,9 +52,18 @@ namespace Avalonia.Android
}
}
public override void OnConfigurationChanged(Configuration newConfig)
public event EventHandler<AndroidBackRequestedEventArgs> BackRequested;
public override void OnBackPressed()
{
base.OnConfigurationChanged(newConfig);
var eventArgs = new AndroidBackRequestedEventArgs();
BackRequested?.Invoke(this, eventArgs);
if (!eventArgs.Handled)
{
base.OnBackPressed();
}
}
protected override void OnDestroy()

16
src/Android/Avalonia.Android/AvaloniaView.cs

@ -1,11 +1,14 @@
using System;
using Android.Content;
using Android.Content.Res;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Avalonia.Android.Platform;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
using Avalonia.Platform;
using Avalonia.Rendering;
namespace Avalonia.Android
@ -26,6 +29,7 @@ namespace Avalonia.Android
_root.Prepare();
this.SetBackgroundColor(global::Android.Graphics.Color.Transparent);
OnConfigurationChanged();
}
internal TopLevelImpl TopLevelImpl => _view;
@ -70,6 +74,18 @@ namespace Avalonia.Android
_timerSubscription?.Dispose();
}
}
protected override void OnConfigurationChanged(Configuration newConfig)
{
base.OnConfigurationChanged(newConfig);
OnConfigurationChanged();
}
private void OnConfigurationChanged()
{
var settings = AvaloniaLocator.Current.GetRequiredService<IPlatformSettings>() as AndroidPlatformSettings;
settings?.OnViewConfigurationChanged(Context);
}
class ViewImpl : TopLevelImpl
{

14
src/Android/Avalonia.Android/IAndroidNavigationService.cs

@ -0,0 +1,14 @@
using System;
namespace Avalonia.Android
{
public interface IActivityNavigationService
{
event EventHandler<AndroidBackRequestedEventArgs> BackRequested;
}
public class AndroidBackRequestedEventArgs : EventArgs
{
public bool Handled { get; set; }
}
}

94
src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs

@ -0,0 +1,94 @@
using System;
using Android;
using Android.Content;
using Android.Content.Res;
using Android.Graphics;
using Android.Provider;
using Android.Views.Accessibility;
using AndroidX.Core.Content.Resources;
using Avalonia.Media;
using Avalonia.Platform;
using Color = Avalonia.Media.Color;
namespace Avalonia.Android.Platform;
// TODO: ideally should be created per view/activity.
internal class AndroidPlatformSettings : DefaultPlatformSettings
{
private PlatformColorValues _latestValues;
public AndroidPlatformSettings()
{
_latestValues = base.GetColorValues();
}
public override PlatformColorValues GetColorValues()
{
return _latestValues;
}
internal void OnViewConfigurationChanged(Context context)
{
if (context.Resources?.Configuration is null)
{
return;
}
var systemTheme = (context.Resources.Configuration.UiMode & UiMode.NightMask) switch
{
UiMode.NightYes => PlatformThemeVariant.Dark,
UiMode.NightNo => PlatformThemeVariant.Light,
_ => throw new ArgumentOutOfRangeException()
};
if (OperatingSystem.IsAndroidVersionAtLeast(31))
{
// See https://developer.android.com/reference/android/R.color
var accent1 = context.Resources.GetColor(17170494, context.Theme); // Resource.Color.SystemAccent1500
var accent2 = context.Resources.GetColor(17170507, context.Theme); // Resource.Color.SystemAccent2500
var accent3 = context.Resources.GetColor(17170520, context.Theme); // Resource.Color.SystemAccent3500
_latestValues = new PlatformColorValues
{
ThemeVariant = systemTheme,
ContrastPreference = IsHighContrast(context),
AccentColor1 = new Color(accent1.A, accent1.R, accent1.G, accent1.B),
AccentColor2 = new Color(accent2.A, accent2.R, accent2.G, accent2.B),
AccentColor3 = new Color(accent3.A, accent3.R, accent3.G, accent3.B),
};
}
else if (OperatingSystem.IsAndroidVersionAtLeast(23))
{
// See https://developer.android.com/reference/android/R.attr
var array = context.Theme.ObtainStyledAttributes(new[] { 16843829 }); // Resource.Attribute.ColorAccent
var accent = array.GetColor(0, 0);
_latestValues = new PlatformColorValues
{
ThemeVariant = systemTheme,
ContrastPreference = IsHighContrast(context),
AccentColor1 = new Color(accent.A, accent.R, accent.G, accent.B)
};
array.Recycle();
}
else
{
_latestValues = _latestValues with { ThemeVariant = systemTheme };
}
OnColorValuesChanged(_latestValues);
}
private static ColorContrastPreference IsHighContrast(Context context)
{
try
{
return Settings.Secure.GetInt(context.ContentResolver, "high_text_contrast_enabled", 0) == 1
? ColorContrastPreference.High : ColorContrastPreference.NoPreference;
}
catch
{
return ColorContrastPreference.NoPreference;
}
}
}

28
src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs

@ -0,0 +1,28 @@
using System;
using Avalonia.Interactivity;
using Avalonia.Platform;
namespace Avalonia.Android.Platform
{
internal class AndroidSystemNavigationManager : ISystemNavigationManager
{
public event EventHandler<RoutedEventArgs> BackRequested;
public AndroidSystemNavigationManager(IActivityNavigationService? navigationService)
{
if(navigationService != null)
{
navigationService.BackRequested += OnBackRequested;
}
}
private void OnBackRequested(object sender, AndroidBackRequestedEventArgs e)
{
var routedEventArgs = new RoutedEventArgs();
BackRequested?.Invoke(this, routedEventArgs);
e.Handled = routedEventArgs.Handled;
}
}
}

13
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -26,12 +26,14 @@ using Avalonia.Rendering.Composition;
using Java.Lang;
using Math = System.Math;
using AndroidRect = Android.Graphics.Rect;
using Window = Android.Views.Window;
using Android.Graphics.Drawables;
namespace Avalonia.Android.Platform.SkiaPlatform
{
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo,
ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider
ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider,
ITopLevelWithSystemNavigationManager
{
private readonly IGlPlatformSurface _gl;
private readonly IFramebufferPlatformSurface _framebuffer;
@ -57,6 +59,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
NativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
StorageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context);
SystemNavigationManager = new AndroidSystemNavigationManager(avaloniaView.Context as IActivityNavigationService);
}
public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
@ -286,6 +290,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public WindowTransparencyLevel TransparencyLevel { get; private set; }
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
{
// TODO adjust status bar depending on full screen mode.
}
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle;
@ -300,6 +309,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IStorageProvider StorageProvider { get; }
public ISystemNavigationManager SystemNavigationManager { get; }
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{
if (TransparencyLevel != transparencyLevel)

2
src/Avalonia.Base/Animation/PageSlide.cs

@ -79,7 +79,6 @@ namespace Avalonia.Animation
var animation = new Animation
{
Easing = SlideOutEasing,
FillMode = FillMode.Forward,
Children =
{
new KeyFrame
@ -110,7 +109,6 @@ namespace Avalonia.Animation
to.IsVisible = true;
var animation = new Animation
{
FillMode = FillMode.Forward,
Easing = SlideInEasing,
Children =
{

2
src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs

@ -71,7 +71,6 @@ public class Rotate3DTransition: PageSlide
{
Easing = SlideOutEasing,
Duration = Duration,
FillMode = FillMode.Forward,
Children =
{
CreateKeyFrame(0d, 0d, 2),
@ -90,7 +89,6 @@ public class Rotate3DTransition: PageSlide
{
Easing = SlideInEasing,
Duration = Duration,
FillMode = FillMode.Forward,
Children =
{
CreateKeyFrame(0d, 90d * (forward ? 1 : -1), 1),

5
src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs

@ -81,6 +81,10 @@ namespace Avalonia.Input.Platform
{
new KeyGesture(Key.Apps)
};
Back = new List<KeyGesture>
{
new KeyGesture(Key.Left, KeyModifiers.Alt)
};
}
public KeyModifiers CommandModifiers { get; set; }
@ -101,5 +105,6 @@ namespace Avalonia.Input.Platform
public List<KeyGesture> MoveCursorToTheStartOfDocumentWithSelection { get; set; }
public List<KeyGesture> MoveCursorToTheEndOfDocumentWithSelection { get; set; }
public List<KeyGesture> OpenContextMenu { get; set; }
public List<KeyGesture> Back { get; set; }
}
}

5
src/Avalonia.Base/Interactivity/EventRoute.cs

@ -143,10 +143,7 @@ namespace Avalonia.Interactivity
// If we've got to a new control then call any RoutedEvent.Raised listeners.
if (entry.Target != lastTarget)
{
if (!e.Handled)
{
_event.InvokeRaised(entry.Target, e);
}
_event.InvokeRaised(entry.Target, e);
// If this is a direct event and we've already raised events then we're finished.
if (e.Route == RoutingStrategies.Direct && lastTarget is object)

7
src/Avalonia.Base/Media/FormattedText.cs

@ -1610,12 +1610,9 @@ namespace Avalonia.Media
var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex);
var text = _that._text.AsMemory(textSourceCharacterIndex, thatFormatRider.Length);
TextRunProperties properties = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
var textCharacters = new TextCharacters(_that._text, textSourceCharacterIndex, thatFormatRider.Length,
properties);
return textCharacters;
return new TextCharacters(text, properties);
}
}
}

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

@ -21,7 +21,7 @@ namespace Avalonia.Media
private Point? _baselineOrigin;
private GlyphRunMetrics? _glyphRunMetrics;
private IReadOnlyList<char> _characters;
private ReadOnlyMemory<char> _characters;
private IReadOnlyList<ushort> _glyphIndices;
private IReadOnlyList<double>? _glyphAdvances;
private IReadOnlyList<Vector>? _glyphOffsets;
@ -41,7 +41,7 @@ namespace Avalonia.Media
public GlyphRun(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
IReadOnlyList<char> characters,
ReadOnlyMemory<char> characters,
IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double>? glyphAdvances = null,
IReadOnlyList<Vector>? glyphOffsets = null,
@ -141,7 +141,7 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
/// </summary>
public IReadOnlyList<char> Characters
public ReadOnlyMemory<char> Characters
{
get => _characters;
set => Set(ref _characters, value);
@ -600,9 +600,9 @@ namespace Avalonia.Media
}
}
if (Characters != null)
if (!Characters.IsEmpty)
{
clusterLength = Characters.Count - characterLength;
clusterLength = Characters.Length - characterLength;
}
else
{
@ -653,10 +653,10 @@ namespace Avalonia.Media
}
else
{
if (Characters != null && Characters.Count > 0)
if (!Characters.IsEmpty)
{
firstCluster = 0;
lastCluster = Characters.Count - 1;
lastCluster = Characters.Length - 1;
}
}
@ -716,14 +716,15 @@ namespace Avalonia.Media
glyphCount = 0;
newLineLength = 0;
var trailingWhitespaceLength = 0;
var charactersSpan = _characters.Span;
if (Characters != null)
if (!charactersSpan.IsEmpty)
{
if (GlyphClusters == null)
{
for (var i = _characters.Count - 1; i >= 0;)
for (var i = charactersSpan.Length - 1; i >= 0;)
{
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count);
if (!codepoint.IsWhiteSpace)
{
@ -743,55 +744,52 @@ namespace Avalonia.Media
}
else
{
if (Characters.Count > 0)
var characterIndex = charactersSpan.Length - 1;
for (var i = GlyphClusters.Count - 1; i >= 0; i--)
{
var characterIndex = Characters.Count - 1;
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
for (var i = GlyphClusters.Count - 1; i >= 0; i--)
{
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
characterIndex -= characterLength;
characterIndex -= characterLength;
if (!codepoint.IsWhiteSpace)
{
break;
}
if (!codepoint.IsWhiteSpace)
{
break;
}
var clusterLength = 1;
var clusterLength = 1;
while (i - 1 >= 0)
{
var nextCluster = GlyphClusters[i - 1];
while (i - 1 >= 0)
if (currentCluster == nextCluster)
{
var nextCluster = GlyphClusters[i - 1];
clusterLength++;
i--;
if (currentCluster == nextCluster)
if(characterIndex >= 0)
{
clusterLength++;
i--;
if(characterIndex >= 0)
{
codepoint = Codepoint.ReadAt(_characters, characterIndex, out characterLength);
codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength);
characterIndex -= characterLength;
}
continue;
characterIndex -= characterLength;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
continue;
}
trailingWhitespaceLength += clusterLength;
break;
}
glyphCount++;
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
trailingWhitespaceLength += clusterLength;
glyphCount++;
}
}
}
@ -804,14 +802,15 @@ namespace Avalonia.Media
glyphCount = 0;
newLineLength = 0;
var trailingWhitespaceLength = 0;
var charactersSpan = Characters.Span;
if (Characters != null)
if (!charactersSpan.IsEmpty)
{
if (GlyphClusters == null)
{
for (var i = 0; i < Characters.Count;)
for (var i = 0; i < charactersSpan.Length;)
{
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count);
if (!codepoint.IsWhiteSpace)
{
@ -836,7 +835,7 @@ namespace Avalonia.Media
for (var i = 0; i < GlyphClusters.Count; i++)
{
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
characterIndex += characterLength;

275
src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs

@ -1,275 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
public readonly struct CharacterBufferRange : IReadOnlyList<char>
{
/// <summary>
/// Getting an empty character string
/// </summary>
public static CharacterBufferRange Empty => new CharacterBufferRange();
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from character array
/// </summary>
/// <param name="characterArray">character array</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
/// <param name="characterLength">character length</param>
public CharacterBufferRange(
char[] characterArray,
int offsetToFirstChar,
int characterLength
)
: this(
new CharacterBufferReference(characterArray, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from string
/// </summary>
/// <param name="characterString">character string</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
/// <param name="characterLength">character length</param>
public CharacterBufferRange(
string characterString,
int offsetToFirstChar,
int characterLength
)
: this(
new CharacterBufferReference(characterString, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from <see cref="CharacterBufferReference"/>
/// </summary>
/// <param name="characterBufferReference">character buffer reference</param>
/// <param name="characterLength">number of characters</param>
public CharacterBufferRange(
CharacterBufferReference characterBufferReference,
int characterLength
)
{
if (characterLength < 0)
{
throw new ArgumentOutOfRangeException("characterLength", "ParameterCannotBeNegative");
}
int maxLength = characterBufferReference.CharacterBuffer.Length > 0 ?
characterBufferReference.CharacterBuffer.Length - characterBufferReference.OffsetToFirstChar :
0;
if (characterLength > maxLength)
{
throw new ArgumentOutOfRangeException("characterLength", $"ParameterCannotBeGreaterThan {maxLength}");
}
CharacterBufferReference = characterBufferReference;
Length = characterLength;
}
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from part of another <see cref="CharacterBufferRange"/>
/// </summary>
internal CharacterBufferRange(
CharacterBufferRange characterBufferRange,
int offsetToFirstChar,
int characterLength
) :
this(
characterBufferRange.CharacterBuffer,
characterBufferRange.OffsetToFirstChar + offsetToFirstChar,
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from string
/// </summary>
internal CharacterBufferRange(
string charString
) :
this(
charString,
0,
charString.Length
)
{ }
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from memory buffer
/// </summary>
internal CharacterBufferRange(
ReadOnlyMemory<char> charBuffer,
int offsetToFirstChar,
int characterLength
) :
this(
new CharacterBufferReference(charBuffer, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> by extracting text info from a text run
/// </summary>
internal CharacterBufferRange(TextRun textRun)
{
CharacterBufferReference = textRun.CharacterBufferReference;
Length = textRun.Length;
}
public char this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
#if DEBUG
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
#endif
return CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index];
}
}
/// <summary>
/// Gets a reference to the character buffer
/// </summary>
public CharacterBufferReference CharacterBufferReference { get; }
/// <summary>
/// Gets the number of characters in text source character store
/// </summary>
public int Length { get; }
/// <summary>
/// Gets a span from the character buffer range
/// </summary>
public ReadOnlySpan<char> Span => CharacterBuffer.Span.Slice(OffsetToFirstChar, Length);
/// <summary>
/// Gets the character memory buffer
/// </summary>
internal ReadOnlyMemory<char> CharacterBuffer => CharacterBufferReference.CharacterBuffer;
/// <summary>
/// Gets the character offset relative to the beginning of buffer to
/// the first character of the run
/// </summary>
internal int OffsetToFirstChar => CharacterBufferReference.OffsetToFirstChar;
/// <summary>
/// Indicate whether the character buffer range is empty
/// </summary>
internal bool IsEmpty => CharacterBuffer.Length == 0 || Length <= 0;
internal CharacterBufferRange Take(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new CharacterBufferRange(CharacterBufferReference, length);
}
internal CharacterBufferRange Skip(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
if (length == Length)
{
return new CharacterBufferRange(new CharacterBufferReference(), 0);
}
var characterBufferReference = new CharacterBufferReference(CharacterBuffer, OffsetToFirstChar + length);
return new CharacterBufferRange(characterBufferReference, Length - length);
}
/// <summary>
/// Compute hash code
/// </summary>
public override int GetHashCode()
{
return CharacterBufferReference.GetHashCode() ^ Length;
}
/// <summary>
/// Test equality with the input object
/// </summary>
/// <param name="obj"> The object to test </param>
public override bool Equals(object? obj)
{
if (obj is CharacterBufferRange range)
{
return Equals(range);
}
return false;
}
/// <summary>
/// Test equality with the input CharacterBufferRange
/// </summary>
/// <param name="value"> The CharacterBufferRange value to test </param>
public bool Equals(CharacterBufferRange value)
{
return CharacterBufferReference.Equals(value.CharacterBufferReference)
&& Length == value.Length;
}
/// <summary>
/// Compare two CharacterBufferRange for equality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator ==(CharacterBufferRange left, CharacterBufferRange right)
{
return left.Equals(right);
}
/// <summary>
/// Compare two CharacterBufferRange for inequality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator !=(CharacterBufferRange left, CharacterBufferRange right)
{
return !(left == right);
}
int IReadOnlyCollection<char>.Count => Length;
public IEnumerator<char> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<char>(this);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

115
src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs

@ -1,115 +0,0 @@
using System;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Text character buffer reference
/// </summary>
public readonly struct CharacterBufferReference : IEquatable<CharacterBufferReference>
{
/// <summary>
/// Construct character buffer reference from character array
/// </summary>
/// <param name="characterArray">character array</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
public CharacterBufferReference(char[] characterArray, int offsetToFirstChar = 0)
: this(characterArray.AsMemory(), offsetToFirstChar)
{ }
/// <summary>
/// Construct character buffer reference from string
/// </summary>
/// <param name="characterString">character string</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
public CharacterBufferReference(string characterString, int offsetToFirstChar = 0)
: this(characterString.AsMemory(), offsetToFirstChar)
{ }
/// <summary>
/// Construct character buffer reference from memory buffer
/// </summary>
internal CharacterBufferReference(ReadOnlyMemory<char> characterBuffer, int offsetToFirstChar = 0)
{
if (offsetToFirstChar < 0)
{
throw new ArgumentOutOfRangeException("offsetToFirstChar", "ParameterCannotBeNegative");
}
// maximum offset is one less than CharacterBuffer.Count, except that zero is always a valid offset
// even in the case of an empty or null character buffer
var maxOffset = characterBuffer.Length == 0 ? 0 : Math.Max(0, characterBuffer.Length - 1);
if (offsetToFirstChar > maxOffset)
{
throw new ArgumentOutOfRangeException("offsetToFirstChar", $"ParameterCannotBeGreaterThan, {maxOffset}");
}
CharacterBuffer = characterBuffer;
OffsetToFirstChar = offsetToFirstChar;
}
/// <summary>
/// Gets the character memory buffer
/// </summary>
public ReadOnlyMemory<char> CharacterBuffer { get; }
/// <summary>
/// Gets the character offset relative to the beginning of buffer to
/// the first character of the run
/// </summary>
public int OffsetToFirstChar { get; }
/// <summary>
/// Compute hash code
/// </summary>
public override int GetHashCode()
{
return CharacterBuffer.IsEmpty ? 0 : CharacterBuffer.GetHashCode();
}
/// <summary>
/// Test equality with the input object
/// </summary>
/// <param name="obj"> The object to test. </param>
public override bool Equals(object? obj)
{
if (obj is CharacterBufferReference reference)
{
return Equals(reference);
}
return false;
}
/// <summary>
/// Test equality with the input CharacterBufferReference
/// </summary>
/// <param name="value"> The characterBufferReference value to test </param>
public bool Equals(CharacterBufferReference value)
{
return CharacterBuffer.Equals(value.CharacterBuffer);
}
/// <summary>
/// Compare two CharacterBufferReference for equality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator ==(CharacterBufferReference left, CharacterBufferReference right)
{
return left.Equals(right);
}
/// <summary>
/// Compare two CharacterBufferReference for inequality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator !=(CharacterBufferReference left, CharacterBufferReference right)
{
return !(left == right);
}
}
}

14
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

@ -7,14 +7,14 @@ namespace Avalonia.Media.TextFormatting
{
internal readonly struct FormattedTextSource : ITextSource
{
private readonly CharacterBufferRange _text;
private readonly string _text;
private readonly TextRunProperties _defaultProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textModifier;
public FormattedTextSource(string text, TextRunProperties defaultProperties,
IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{
_text = new CharacterBufferRange(text);
_text = text;
_defaultProperties = defaultProperties;
_textModifier = textModifier;
}
@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting
return null;
}
var runText = _text.Skip(textSourceIndex);
var runText = _text.AsSpan(textSourceIndex);
if (runText.IsEmpty)
{
@ -35,7 +35,7 @@ namespace Avalonia.Media.TextFormatting
var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier);
return new TextCharacters(runText.Take(textStyleRun.Length).CharacterBufferReference, textStyleRun.Length, textStyleRun.Value);
return new TextCharacters(_text.AsMemory(textSourceIndex, textStyleRun.Length), textStyleRun.Value);
}
/// <summary>
@ -48,7 +48,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>
/// The created text style run.
/// </returns>
private static ValueSpan<TextRunProperties> CreateTextStyleRun(CharacterBufferRange text, int firstTextSourceIndex,
private static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySpan<char> text, int firstTextSourceIndex,
TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{
if (textModifier == null || textModifier.Count == 0)
@ -122,7 +122,7 @@ namespace Avalonia.Media.TextFormatting
return new ValueSpan<TextRunProperties>(firstTextSourceIndex, length, currentProperties);
}
private static int CoerceLength(CharacterBufferRange text, int length)
private static int CoerceLength(ReadOnlySpan<char> text, int length)
{
var finalLength = 0;
@ -132,7 +132,7 @@ namespace Avalonia.Media.TextFormatting
{
var grapheme = graphemeEnumerator.Current;
finalLength += grapheme.Length;
finalLength += grapheme.Text.Length;
if (finalLength >= length)
{

6
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@ -50,14 +50,14 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in lineImpl.TextRuns)
{
var text = new CharacterBufferRange(textRun);
var text = textRun.Text;
if (text.IsEmpty)
{
continue;
}
var lineBreakEnumerator = new LineBreakEnumerator(text);
var lineBreakEnumerator = new LineBreakEnumerator(text.Span);
while (lineBreakEnumerator.MoveNext())
{
@ -84,7 +84,7 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in lineImpl.TextRuns)
{
var text = textRun.CharacterBufferReference.CharacterBuffer;
var text = textRun.Text;
if (text.IsEmpty)
{

24
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@ -9,21 +9,21 @@ namespace Avalonia.Media.TextFormatting
{
private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters();
private bool _bufferRented;
public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) :
this(characterBufferRange,
new ArraySlice<GlyphInfo>(ArrayPool<GlyphInfo>.Shared.Rent(bufferLength), 0, bufferLength),
glyphTypeface,
fontRenderingEmSize,
public ShapedBuffer(ReadOnlyMemory<char> text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) :
this(text,
new ArraySlice<GlyphInfo>(ArrayPool<GlyphInfo>.Shared.Rent(bufferLength), 0, bufferLength),
glyphTypeface,
fontRenderingEmSize,
bidiLevel)
{
_bufferRented = true;
Length = bufferLength;
}
internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
internal ShapedBuffer(ReadOnlyMemory<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
{
CharacterBufferRange = characterBufferRange;
Text = text;
GlyphInfos = glyphInfos;
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
@ -51,7 +51,7 @@ namespace Avalonia.Media.TextFormatting
public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos);
public CharacterBufferRange CharacterBufferRange { get; }
public ReadOnlyMemory<char> Text { get; }
/// <summary>
/// Finds a glyph index for given character index.
@ -113,7 +113,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The split result.</returns>
internal SplitResult<ShapedBuffer> Split(int length)
{
if (CharacterBufferRange.Length == length)
if (Text.Length == length)
{
return new SplitResult<ShapedBuffer>(this, null);
}
@ -125,10 +125,10 @@ namespace Avalonia.Media.TextFormatting
var glyphCount = FindGlyphIndex(start + length);
var first = new ShapedBuffer(CharacterBufferRange.Take(length),
var first = new ShapedBuffer(Text.Slice(0, length),
GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var second = new ShapedBuffer(CharacterBufferRange.Skip(length),
var second = new ShapedBuffer(Text.Slice(length),
GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
return new SplitResult<ShapedBuffer>(first, second);

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

@ -13,8 +13,6 @@ namespace Avalonia.Media.TextFormatting
public ShapedTextRun(ShapedBuffer shapedBuffer, TextRunProperties properties)
{
ShapedBuffer = shapedBuffer;
CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference;
Length = shapedBuffer.CharacterBufferRange.Length;
Properties = properties;
TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize);
}
@ -26,13 +24,15 @@ namespace Avalonia.Media.TextFormatting
public ShapedBuffer ShapedBuffer { get; }
/// <inheritdoc/>
public override CharacterBufferReference CharacterBufferReference { get; }
public override ReadOnlyMemory<char> Text
=> ShapedBuffer.Text;
/// <inheritdoc/>
public override TextRunProperties Properties { get; }
/// <inheritdoc/>
public override int Length { get; }
public override int Length
=> ShapedBuffer.Text.Length;
public TextMetrics TextMetrics { get; }
@ -113,6 +113,7 @@ namespace Avalonia.Media.TextFormatting
{
length = 0;
var currentWidth = 0.0;
var charactersSpan = GlyphRun.Characters.Span;
for (var i = 0; i < ShapedBuffer.Length; i++)
{
@ -123,7 +124,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
Codepoint.ReadAt(GlyphRun.Characters, length, out var count);
Codepoint.ReadAt(charactersSpan, length, out var count);
length += count;
currentWidth += advance;
@ -136,6 +137,7 @@ namespace Avalonia.Media.TextFormatting
{
length = 0;
width = 0;
var charactersSpan = GlyphRun.Characters.Span;
for (var i = ShapedBuffer.Length - 1; i >= 0; i--)
{
@ -146,7 +148,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
Codepoint.ReadAt(GlyphRun.Characters, length, out var count);
Codepoint.ReadAt(charactersSpan, length, out var count);
length += count;
width += advance;
@ -192,7 +194,7 @@ namespace Avalonia.Media.TextFormatting
return new GlyphRun(
ShapedBuffer.GlyphTypeface,
ShapedBuffer.FontRenderingEmSize,
new CharacterBufferRange(CharacterBufferReference, Length),
Text,
ShapedBuffer.GlyphIndices,
ShapedBuffer.GlyphAdvances,
ShapedBuffer.GlyphOffsets,

123
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -10,82 +10,34 @@ namespace Avalonia.Media.TextFormatting
public class TextCharacters : TextRun
{
/// <summary>
/// Construct a run of text content from character array
/// Constructs a run for text content from a string.
/// </summary>
public TextCharacters(
char[] characterArray,
int offsetToFirstChar,
int length,
TextRunProperties textRunProperties
) :
this(
new CharacterBufferReference(characterArray, offsetToFirstChar),
length,
textRunProperties
)
{ }
/// <summary>
/// Construct a run for text content from string
/// </summary>
public TextCharacters(
string characterString,
TextRunProperties textRunProperties
) :
this(
characterString,
0, // offsetToFirstChar
(characterString == null) ? 0 : characterString.Length,
textRunProperties
)
{ }
/// <summary>
/// Construct a run for text content from string
/// </summary>
public TextCharacters(
string characterString,
int offsetToFirstChar,
int length,
TextRunProperties textRunProperties
) :
this(
new CharacterBufferReference(characterString, offsetToFirstChar),
length,
textRunProperties
)
{ }
public TextCharacters(string text, TextRunProperties textRunProperties)
: this(text.AsMemory(), textRunProperties)
{
}
/// <summary>
/// Internal constructor of TextContent
/// Constructs a run for text content from a memory region.
/// </summary>
public TextCharacters(
CharacterBufferReference characterBufferReference,
int length,
TextRunProperties textRunProperties
)
public TextCharacters(ReadOnlyMemory<char> text, TextRunProperties textRunProperties)
{
if (length <= 0)
{
throw new ArgumentOutOfRangeException("length", "ParameterMustBeGreaterThanZero");
}
if (textRunProperties.FontRenderingEmSize <= 0)
{
throw new ArgumentOutOfRangeException("textRunProperties.FontRenderingEmSize", "ParameterMustBeGreaterThanZero");
throw new ArgumentOutOfRangeException(nameof(textRunProperties), textRunProperties.FontRenderingEmSize,
$"Invalid {nameof(TextRunProperties.FontRenderingEmSize)}");
}
CharacterBufferReference = characterBufferReference;
Length = length;
Text = text;
Properties = textRunProperties;
}
/// <inheritdoc />
public override int Length { get; }
public override int Length
=> Text.Length;
/// <inheritdoc />
public override CharacterBufferReference CharacterBufferReference { get; }
public override ReadOnlyMemory<char> Text { get; }
/// <inheritdoc />
public override TextRunProperties Properties { get; }
@ -94,17 +46,19 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of <see cref="UnshapedTextRun"/>.
/// </summary>
/// <returns>The shapeable text characters.</returns>
internal IReadOnlyList<UnshapedTextRun> GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties)
internal IReadOnlyList<UnshapedTextRun> GetShapeableCharacters(ReadOnlyMemory<char> text, sbyte biDiLevel,
ref TextRunProperties? previousProperties)
{
var shapeableCharacters = new List<UnshapedTextRun>(2);
var properties = Properties;
while (characterBufferRange.Length > 0)
while (!text.IsEmpty)
{
var shapeableRun = CreateShapeableRun(characterBufferRange, Properties, biDiLevel, ref previousProperties);
var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties);
shapeableCharacters.Add(shapeableRun);
characterBufferRange = characterBufferRange.Skip(shapeableRun.Length);
text = text.Slice(shapeableRun.Length);
previousProperties = shapeableRun.Properties;
}
@ -115,45 +69,46 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Creates a shapeable text run with unique properties.
/// </summary>
/// <param name="characterBufferRange">The character buffer range to create text runs from.</param>
/// <param name="text">The characters to create text runs from.</param>
/// <param name="defaultProperties">The default text run properties.</param>
/// <param name="biDiLevel">The bidi level of the run.</param>
/// <param name="previousProperties"></param>
/// <returns>A list of shapeable text runs.</returns>
private static UnshapedTextRun CreateShapeableRun(CharacterBufferRange characterBufferRange,
private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory<char> text,
TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
{
var defaultTypeface = defaultProperties.Typeface;
var currentTypeface = defaultTypeface;
var previousTypeface = previousProperties?.Typeface;
var textSpan = text.Span;
if (TryGetShapeableLength(characterBufferRange, currentTypeface, null, out var count, out var script))
if (TryGetShapeableLength(textSpan, currentTypeface, null, out var count, out var script))
{
if (script == Script.Common && previousTypeface is not null)
{
if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _))
if (TryGetShapeableLength(textSpan, previousTypeface.Value, null, out var fallbackCount, out _))
{
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, fallbackCount,
return new UnshapedTextRun(text.Slice(0, fallbackCount),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
}
}
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
if (previousTypeface is not null)
{
if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _))
if (TryGetShapeableLength(textSpan, previousTypeface.Value, defaultTypeface, out count, out _))
{
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count,
return new UnshapedTextRun(text.Slice(0, count),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
}
}
var codepoint = Codepoint.ReplacementCodepoint;
var codepointEnumerator = new CodepointEnumerator(characterBufferRange.Skip(count));
var codepointEnumerator = new CodepointEnumerator(text.Slice(count).Span);
while (codepointEnumerator.MoveNext())
{
@ -173,10 +128,10 @@ namespace Avalonia.Media.TextFormatting
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out currentTypeface);
if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _))
if (matchFound && TryGetShapeableLength(textSpan, currentTypeface, defaultTypeface, out count, out _))
{
//Fallback found
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
@ -185,7 +140,7 @@ namespace Avalonia.Media.TextFormatting
var glyphTypeface = currentTypeface.GlyphTypeface;
var enumerator = new GraphemeEnumerator(characterBufferRange);
var enumerator = new GraphemeEnumerator(textSpan);
while (enumerator.MoveNext())
{
@ -196,23 +151,23 @@ namespace Avalonia.Media.TextFormatting
break;
}
count += grapheme.Length;
count += grapheme.Text.Length;
}
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel);
return new UnshapedTextRun(text.Slice(0, count), defaultProperties, biDiLevel);
}
/// <summary>
/// Tries to get a shapeable length that is supported by the specified typeface.
/// </summary>
/// <param name="characterBufferRange">The character buffer range to shape.</param>
/// <param name="text">The characters to shape.</param>
/// <param name="typeface">The typeface that is used to find matching characters.</param>
/// <param name="defaultTypeface"></param>
/// <param name="length">The shapeable length.</param>
/// <param name="script"></param>
/// <returns></returns>
internal static bool TryGetShapeableLength(
CharacterBufferRange characterBufferRange,
ReadOnlySpan<char> text,
Typeface typeface,
Typeface? defaultTypeface,
out int length,
@ -221,7 +176,7 @@ namespace Avalonia.Media.TextFormatting
length = 0;
script = Script.Unknown;
if (characterBufferRange.Length == 0)
if (text.IsEmpty)
{
return false;
}
@ -229,7 +184,7 @@ namespace Avalonia.Media.TextFormatting
var font = typeface.GlyphTypeface;
var defaultFont = defaultTypeface?.GlyphTypeface;
var enumerator = new GraphemeEnumerator(characterBufferRange);
var enumerator = new GraphemeEnumerator(text);
while (enumerator.MoveNext())
{
@ -264,7 +219,7 @@ namespace Avalonia.Media.TextFormatting
}
}
length += currentGrapheme.Length;
length += currentGrapheme.Text.Length;
}
return length > 0;

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

@ -45,9 +45,7 @@ namespace Avalonia.Media.TextFormatting
{
var currentBreakPosition = 0;
var text = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
var lineBreaker = new LineBreakEnumerator(text);
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{

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

@ -1,5 +1,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
@ -162,17 +164,13 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in textRuns)
{
if (textRun.CharacterBufferReference.CharacterBuffer.Length == 0)
if (textRun.Text.IsEmpty)
{
var characterBuffer = new CharacterBufferReference(new char[textRun.Length]);
biDiData.Append(new CharacterBufferRange(characterBuffer, textRun.Length));
biDiData.Append(new char[textRun.Length]);
}
else
{
var text = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
biDiData.Append(text);
biDiData.Append(textRun.Text.Span);
}
}
@ -198,9 +196,7 @@ namespace Avalonia.Media.TextFormatting
case UnshapedTextRun shapeableRun:
{
var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun };
var characterBufferReference = currentRun.CharacterBufferReference;
var length = currentRun.Length;
var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar;
var text = shapeableRun.Text;
while (index + 1 < processedRuns.Count)
{
@ -209,23 +205,14 @@ namespace Avalonia.Media.TextFormatting
break;
}
if (shapeableRun.CanShapeTogether(nextRun))
if (shapeableRun.BidiLevel == nextRun.BidiLevel
&& TryJoinContiguousMemories(text, nextRun.Text, out var joinedText)
&& CanShapeTogether(shapeableRun.Properties, nextRun.Properties))
{
groupedRuns.Add(nextRun);
length += nextRun.Length;
if (offsetToFirstCharacter > nextRun.CharacterBufferReference.OffsetToFirstChar)
{
offsetToFirstCharacter = nextRun.CharacterBufferReference.OffsetToFirstChar;
}
characterBufferReference = new CharacterBufferReference(characterBufferReference.CharacterBuffer, offsetToFirstCharacter);
index++;
shapeableRun = nextRun;
text = joinedText;
continue;
}
@ -237,7 +224,7 @@ namespace Avalonia.Media.TextFormatting
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
shapedRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
break;
}
@ -253,12 +240,81 @@ namespace Avalonia.Media.TextFormatting
return shapedRuns;
}
/// <summary>
/// Tries to join two potnetially contiguous memory regions.
/// </summary>
/// <param name="x">The first memory region.</param>
/// <param name="y">The second memory region.</param>
/// <param name="joinedMemory">On success, a memory region representing the union of the two regions.</param>
/// <returns>true if the two regions were contigous; false otherwise.</returns>
private static bool TryJoinContiguousMemories(ReadOnlyMemory<char> x, ReadOnlyMemory<char> y,
out ReadOnlyMemory<char> joinedMemory)
{
if (MemoryMarshal.TryGetString(x, out var xString, out var xStart, out var xLength))
{
if (MemoryMarshal.TryGetString(y, out var yString, out var yStart, out var yLength)
&& ReferenceEquals(xString, yString)
&& TryGetContiguousStart(xStart, xLength, yStart, yLength, out var joinedStart))
{
joinedMemory = xString.AsMemory(joinedStart, xLength + yLength);
return true;
}
}
else if (MemoryMarshal.TryGetArray(x, out var xSegment))
{
if (MemoryMarshal.TryGetArray(y, out var ySegment)
&& ReferenceEquals(xSegment.Array, ySegment.Array)
&& TryGetContiguousStart(xSegment.Offset, xSegment.Count, ySegment.Offset, ySegment.Count, out var joinedStart))
{
joinedMemory = xSegment.Array.AsMemory(joinedStart, xSegment.Count + ySegment.Count);
return true;
}
}
else if (MemoryMarshal.TryGetMemoryManager(x, out MemoryManager<char>? xManager, out xStart, out xLength))
{
if (MemoryMarshal.TryGetMemoryManager(y, out MemoryManager<char>? yManager, out var yStart, out var yLength)
&& ReferenceEquals(xManager, yManager)
&& TryGetContiguousStart(xStart, xLength, yStart, yLength, out var joinedStart))
{
joinedMemory = xManager.Memory.Slice(joinedStart, xLength + yLength);
return true;
}
}
joinedMemory = default;
return false;
static bool TryGetContiguousStart(int xStart, int xLength, int yStart, int yLength, out int joinedStart)
{
var xRange = (Start: xStart, Length: xLength);
var yRange = (Start: yStart, Length: yLength);
var (firstRange, secondRange) = xStart <= yStart ? (xRange, yRange) : (yRange, xRange);
if (firstRange.Start + firstRange.Length == secondRange.Start)
{
joinedStart = firstRange.Start;
return true;
}
joinedStart = default;
return false;
}
}
private static bool CanShapeTogether(TextRunProperties x, TextRunProperties y)
=> MathUtilities.AreClose(x.FontRenderingEmSize, y.FontRenderingEmSize)
&& x.Typeface == y.Typeface
&& x.BaselineAlignment == y.BaselineAlignment;
private static IReadOnlyList<ShapedTextRun> ShapeTogether(
IReadOnlyList<UnshapedTextRun> textRuns, CharacterBufferReference text, int length, TextShaperOptions options)
IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text, TextShaperOptions options)
{
var shapedRuns = new List<ShapedTextRun>(textRuns.Count);
var shapedBuffer = TextShaper.Current.ShapeText(text, length, options);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
for (var i = 0; i < textRuns.Count; i++)
{
@ -294,7 +350,7 @@ namespace Avalonia.Media.TextFormatting
TextRunProperties? previousProperties = null;
TextCharacters? currentRun = null;
CharacterBufferRange runText = default;
ReadOnlyMemory<char> runText = default;
for (var i = 0; i < textCharacters.Count; i++)
{
@ -312,11 +368,12 @@ namespace Avalonia.Media.TextFormatting
continue;
}
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
runText = currentRun.Text;
var runTextSpan = runText.Span;
for (; j < runText.Length;)
for (; j < runTextSpan.Length;)
{
Codepoint.ReadAt(runText, j, out var count);
Codepoint.ReadAt(runTextSpan, j, out var count);
if (levelIndex + 1 == levels.Length)
{
@ -326,9 +383,9 @@ namespace Avalonia.Media.TextFormatting
levelIndex++;
j += count;
if (j == runText.Length)
if (j == runTextSpan.Length)
{
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties));
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties));
runLevel = levels[levelIndex];
@ -341,9 +398,10 @@ namespace Avalonia.Media.TextFormatting
}
// End of this run
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties));
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties));
runText = runText.Skip(j);
runText = runText.Slice(j);
runTextSpan = runText.Span;
j = 0;
@ -411,7 +469,7 @@ namespace Avalonia.Media.TextFormatting
{
if (TryGetLineBreak(textCharacters, out var runLineBreak))
{
var splitResult = new TextCharacters(textCharacters.CharacterBufferReference, runLineBreak.PositionWrap,
var splitResult = new TextCharacters(textCharacters.Text.Slice(0, runLineBreak.PositionWrap),
textCharacters.Properties);
textRuns.Add(splitResult);
@ -442,14 +500,14 @@ namespace Avalonia.Media.TextFormatting
{
lineBreak = default;
if (textRun.CharacterBufferReference.CharacterBuffer.IsEmpty)
var text = textRun.Text;
if (text.IsEmpty)
{
return false;
}
var characterBufferRange = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
var lineBreakEnumerator = new LineBreakEnumerator(characterBufferRange);
var lineBreakEnumerator = new LineBreakEnumerator(text.Span);
while (lineBreakEnumerator.MoveNext())
{
@ -541,8 +599,7 @@ namespace Avalonia.Media.TextFormatting
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
var characterBufferRange = new CharacterBufferRange(new CharacterBufferReference(s_empty), s_empty.Length);
var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection);
var textRuns = new List<DrawableTextRun> { new ShapedTextRun(shapedBuffer, properties) };
@ -589,10 +646,8 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextRun:
{
var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
var lineBreaker = new LineBreakEnumerator(runText);
{
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (lineBreaker.MoveNext())
{
@ -651,9 +706,7 @@ namespace Avalonia.Media.TextFormatting
currentRun = textRuns[index];
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
lineBreaker = new LineBreakEnumerator(runText);
lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
}
}
else
@ -771,9 +824,7 @@ namespace Avalonia.Media.TextFormatting
var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo);
var characterBuffer = textRun.CharacterBufferReference;
var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions);
var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions);
return new ShapedTextRun(shapedBuffer, textRun.Properties);
}

21
src/Avalonia.Base/Media/TextFormatting/TextRun.cs

@ -1,4 +1,5 @@
using System.Diagnostics;
using System;
using System.Diagnostics;
namespace Avalonia.Media.TextFormatting
{
@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Gets the text run's text.
/// </summary>
public virtual CharacterBufferReference CharacterBufferReference => default;
public virtual ReadOnlyMemory<char> Text => default;
/// <summary>
/// A set of properties shared by every characters in the run
@ -34,21 +35,7 @@ namespace Avalonia.Media.TextFormatting
_textRun = textRun;
}
public string Text
{
get
{
unsafe
{
var characterBuffer = new CharacterBufferRange(_textRun.CharacterBufferReference, _textRun.Length);
fixed (char* charsPtr = characterBuffer.Span)
{
return new string(charsPtr, 0, _textRun.Length);
}
}
}
}
public string Text => _textRun.Text.ToString();
public TextRunProperties? Properties => _textRun.Properties;
}

6
src/Avalonia.Base/Media/TextFormatting/TextShaper.cs

@ -40,14 +40,14 @@ namespace Avalonia.Media.TextFormatting
}
/// <inheritdoc cref="ITextShaperImpl.ShapeText"/>
public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options = default)
public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options = default)
{
return _platformImpl.ShapeText(text, length, options);
return _platformImpl.ShapeText(text, options);
}
public ShapedBuffer ShapeText(string text, TextShaperOptions options = default)
{
return ShapeText(new CharacterBufferReference(text), text.Length, options);
return ShapeText(text.AsMemory(), options);
}
}
}

2
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs

@ -64,7 +64,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Appends text to the bidi data.
/// </summary>
/// <param name="text">The text to process.</param>
public void Append(CharacterBufferRange text)
public void Append(ReadOnlySpan<char> text)
{
_classes.Add(text.Length);
_pairedBracketTypes.Add(text.Length);

67
src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs

@ -166,72 +166,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <param name="index">The index to read at.</param>
/// <param name="count">The count of character that were read.</param>
/// <returns></returns>
public static Codepoint ReadAt(IReadOnlyList<char> text, int index, out int count)
{
count = 1;
if (index >= text.Count)
{
return ReplacementCodepoint;
}
var code = text[index];
ushort hi, low;
//# High surrogate
if (0xD800 <= code && code <= 0xDBFF)
{
hi = code;
if (index + 1 == text.Count)
{
return ReplacementCodepoint;
}
low = text[index + 1];
if (0xDC00 <= low && low <= 0xDFFF)
{
count = 2;
return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
}
return ReplacementCodepoint;
}
//# Low surrogate
if (0xDC00 <= code && code <= 0xDFFF)
{
if (index == 0)
{
return ReplacementCodepoint;
}
hi = text[index - 1];
low = code;
if (0xD800 <= hi && hi <= 0xDBFF)
{
count = 2;
return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
}
return ReplacementCodepoint;
}
return new Codepoint(code);
}
/// <summary>
/// Reads the <see cref="Codepoint"/> at specified position.
/// </summary>
/// <param name="text">The buffer to read from.</param>
/// <param name="index">The index to read at.</param>
/// <param name="count">The count of character that were read.</param>
/// <returns></returns>
public static Codepoint ReadAt(CharacterBufferRange text, int index, out int count)
public static Codepoint ReadAt(ReadOnlySpan<char> text, int index, out int count)
{
count = 1;

6
src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs

@ -4,9 +4,9 @@ namespace Avalonia.Media.TextFormatting.Unicode
{
public ref struct CodepointEnumerator
{
private CharacterBufferRange _text;
private ReadOnlySpan<char> _text;
public CodepointEnumerator(CharacterBufferRange text)
public CodepointEnumerator(ReadOnlySpan<char> text)
{
_text = text;
Current = Codepoint.ReplacementCodepoint;
@ -32,7 +32,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
Current = Codepoint.ReadAt(_text, 0, out var count);
_text = _text.Skip(count);
_text = _text.Slice(count);
return true;
}

14
src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs

@ -7,11 +7,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary>
public readonly ref struct Grapheme
{
public Grapheme(Codepoint firstCodepoint, int offset, int length)
public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text)
{
FirstCodepoint = firstCodepoint;
Offset = offset;
Length = length;
Text = text;
}
/// <summary>
@ -20,13 +19,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
public Codepoint FirstCodepoint { get; }
/// <summary>
/// The Offset to the FirstCodepoint
/// The text of the grapheme cluster
/// </summary>
public int Offset { get; }
/// <summary>
/// The length of the grapheme cluster
/// </summary>
public int Length { get; }
public ReadOnlySpan<char> Text { get; }
}
}

14
src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs

@ -3,16 +3,16 @@
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System.Collections.Generic;
using System;
using System.Runtime.InteropServices;
namespace Avalonia.Media.TextFormatting.Unicode
{
public ref struct GraphemeEnumerator
{
private CharacterBufferRange _text;
private ReadOnlySpan<char> _text;
public GraphemeEnumerator(CharacterBufferRange text)
public GraphemeEnumerator(ReadOnlySpan<char> text)
{
_text = text;
Current = default;
@ -185,9 +185,9 @@ namespace Avalonia.Media.TextFormatting.Unicode
Return:
Current = new Grapheme(firstCodepoint, _text.OffsetToFirstChar, processor.CurrentCodeUnitOffset);
Current = new Grapheme(firstCodepoint, _text.Slice(0, processor.CurrentCodeUnitOffset));
_text = _text.Skip(processor.CurrentCodeUnitOffset);
_text = _text.Slice(processor.CurrentCodeUnitOffset);
return true; // rules GB2, GB999
}
@ -195,10 +195,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
[StructLayout(LayoutKind.Auto)]
private ref struct Processor
{
private readonly CharacterBufferRange _buffer;
private readonly ReadOnlySpan<char> _buffer;
private int _codeUnitLengthOfCurrentScalar;
internal Processor(CharacterBufferRange buffer)
internal Processor(ReadOnlySpan<char> buffer)
{
_buffer = buffer;
_codeUnitLengthOfCurrentScalar = 0;

13
src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

@ -3,7 +3,6 @@
// Ported from: https://github.com/SixLabors/Fonts/
using System;
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting.Unicode
{
@ -13,7 +12,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary>
public ref struct LineBreakEnumerator
{
private readonly IReadOnlyList<char> _text;
private readonly ReadOnlySpan<char> _text;
private int _position;
private int _lastPosition;
private LineBreakClass _currentClass;
@ -29,7 +28,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
private int _lb30a;
private bool _lb31;
public LineBreakEnumerator(IReadOnlyList<char> text)
public LineBreakEnumerator(ReadOnlySpan<char> text)
: this()
{
_text = text;
@ -63,7 +62,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
_lb30a = 0;
}
while (_position < _text.Count)
while (_position < _text.Length)
{
_lastPosition = _position;
var lastClass = _nextClass;
@ -93,11 +92,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
}
}
if (_position >= _text.Count)
if (_position >= _text.Length)
{
if (_lastPosition < _text.Count)
if (_lastPosition < _text.Length)
{
_lastPosition = _text.Count;
_lastPosition = _text.Length;
var required = false;

44
src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs

@ -1,4 +1,4 @@
using Avalonia.Utilities;
using System;
namespace Avalonia.Media.TextFormatting
{
@ -7,52 +7,20 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public sealed class UnshapedTextRun : TextRun
{
public UnshapedTextRun(CharacterBufferReference characterBufferReference, int length,
TextRunProperties properties, sbyte biDiLevel)
public UnshapedTextRun(ReadOnlyMemory<char> text, TextRunProperties properties, sbyte biDiLevel)
{
CharacterBufferReference = characterBufferReference;
Length = length;
Text = text;
Properties = properties;
BidiLevel = biDiLevel;
}
public override int Length { get; }
public override int Length
=> Text.Length;
public override CharacterBufferReference CharacterBufferReference { get; }
public override ReadOnlyMemory<char> Text { get; }
public override TextRunProperties Properties { get; }
public sbyte BidiLevel { get; }
public bool CanShapeTogether(UnshapedTextRun unshapedTextRun)
{
if (!CharacterBufferReference.Equals(unshapedTextRun.CharacterBufferReference))
{
return false;
}
if (BidiLevel != unshapedTextRun.BidiLevel)
{
return false;
}
if (!MathUtilities.AreClose(Properties.FontRenderingEmSize,
unshapedTextRun.Properties.FontRenderingEmSize))
{
return false;
}
if (Properties.Typeface != unshapedTextRun.Properties.Typeface)
{
return false;
}
if (Properties.BaselineAlignment != unshapedTextRun.Properties.BaselineAlignment)
{
return false;
}
return true;
}
}
}

27
src/Avalonia.Base/Platform/DefaultPlatformSettings.cs

@ -1,15 +1,15 @@
using System;
using Avalonia.Input;
using Avalonia.Media;
namespace Avalonia.Platform
{
/// <summary>
/// A default implementation of <see cref="IPlatformSettings"/> for platforms which don't have
/// an OS-specific implementation.
/// A default implementation of <see cref="IPlatformSettings"/> for platforms.
/// </summary>
public class DefaultPlatformSettings : IPlatformSettings
{
public Size GetTapSize(PointerType type)
public virtual Size GetTapSize(PointerType type)
{
return type switch
{
@ -17,7 +17,7 @@ namespace Avalonia.Platform
_ => new(4, 4),
};
}
public Size GetDoubleTapSize(PointerType type)
public virtual Size GetDoubleTapSize(PointerType type)
{
return type switch
{
@ -25,8 +25,23 @@ namespace Avalonia.Platform
_ => new(4, 4),
};
}
public TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(500);
public virtual TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(500);
public TimeSpan HoldWaitDuration { get; set; } = TimeSpan.FromMilliseconds(300);
public virtual TimeSpan HoldWaitDuration => TimeSpan.FromMilliseconds(300);
public virtual PlatformColorValues GetColorValues()
{
return new PlatformColorValues
{
ThemeVariant = PlatformThemeVariant.Light
};
}
public event EventHandler<PlatformColorValues>? ColorValuesChanged;
protected void OnColorValuesChanged(PlatformColorValues colorValues)
{
ColorValuesChanged?.Invoke(this, colorValues);
}
}
}

15
src/Avalonia.Base/Platform/IPlatformSettings.cs

@ -28,6 +28,19 @@ namespace Avalonia.Platform
/// </summary>
TimeSpan GetDoubleTapTime(PointerType type);
TimeSpan HoldWaitDuration { get; set; }
/// <summary>
/// Holding duration between pointer press and when event is fired.
/// </summary>
TimeSpan HoldWaitDuration { get; }
/// <summary>
/// Gets current system color values including dark mode and accent colors.
/// </summary>
PlatformColorValues GetColorValues();
/// <summary>
/// Raises when current system color values are changed. Including changing of a dark mode and accent colors.
/// </summary>
event EventHandler<PlatformColorValues>? ColorValuesChanged;
}
}

6
src/Avalonia.Base/Platform/ITextShaperImpl.cs

@ -1,4 +1,5 @@
using Avalonia.Media.TextFormatting;
using System;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
namespace Avalonia.Platform
@ -13,9 +14,8 @@ namespace Avalonia.Platform
/// Shapes the specified region within the text and returns a shaped buffer.
/// </summary>
/// <param name="text">The text buffer.</param>
/// <param name="length">The length of text.</param>
/// <param name="options">Text shaper options to customize the shaping process.</param>
/// <returns>A shaped glyph run.</returns>
ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options);
ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options);
}
}

68
src/Avalonia.Base/Platform/PlatformColorValues.cs

@ -0,0 +1,68 @@
using Avalonia.Media;
namespace Avalonia.Platform;
/// <summary>
/// System theme variant or mode.
/// </summary>
public enum PlatformThemeVariant
{
Light,
Dark
}
/// <summary>
/// System high contrast preference.
/// </summary>
public enum ColorContrastPreference
{
NoPreference,
High
}
/// <summary>
/// Information about current system color values, including information about dark mode and accent colors.
/// </summary>
public record PlatformColorValues
{
private static Color DefaultAccent => new(255, 0, 120, 215);
private Color _accentColor2, _accentColor3;
/// <summary>
/// System theme variant or mode.
/// </summary>
public PlatformThemeVariant ThemeVariant { get; init; }
/// <summary>
/// System high contrast preference.
/// </summary>
public ColorContrastPreference ContrastPreference { get; init; }
/// <summary>
/// Primary system accent color.
/// </summary>
public Color AccentColor1 { get; init; }
/// <summary>
/// Secondary system accent color. On some platforms can return the same value as <see cref="AccentColor1"/>.
/// </summary>
public Color AccentColor2
{
get => _accentColor2 != default ? _accentColor2 : AccentColor1;
init => _accentColor2 = value;
}
/// <summary>
/// Tertiary system accent color. On some platforms can return the same value as <see cref="AccentColor1"/>.
/// </summary>
public Color AccentColor3
{
get => _accentColor3 != default ? _accentColor3 : AccentColor1;
init => _accentColor3 = value;
}
public PlatformColorValues()
{
AccentColor1 = DefaultAccent;
}
}

18
src/Avalonia.Base/Platform/SystemNavigationManager.cs

@ -0,0 +1,18 @@
using System;
using Avalonia.Interactivity;
using Avalonia.Metadata;
namespace Avalonia.Platform
{
[Unstable]
public interface ITopLevelWithSystemNavigationManager
{
ISystemNavigationManager SystemNavigationManager { get; }
}
[Unstable]
public interface ISystemNavigationManager
{
public event EventHandler<RoutedEventArgs>? BackRequested;
}
}

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

@ -98,11 +98,8 @@
IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{TemplateBinding Padding}"
VirtualizationMode="{TemplateBinding VirtualizationMode}" />
Margin="{TemplateBinding Padding}"/>
</ScrollViewer>
</Border>
</ControlTemplate>

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

@ -98,11 +98,8 @@
IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{TemplateBinding Padding}"
VirtualizationMode="{TemplateBinding VirtualizationMode}" />
Margin="{TemplateBinding Padding}"/>
</ScrollViewer>
</Border>
</ControlTemplate>

1
src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs

@ -2,7 +2,6 @@
using Avalonia.Reactive;
using System;
using System.Collections.Generic;
using Avalonia.Reactive;
namespace Avalonia.Controls.Utils
{

52
src/Avalonia.Controls/Carousel.cs

@ -2,7 +2,6 @@ using Avalonia.Animation;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Input;
namespace Avalonia.Controls
{
@ -11,12 +10,6 @@ namespace Avalonia.Controls
/// </summary>
public class Carousel : SelectingItemsControl
{
/// <summary>
/// Defines the <see cref="IsVirtualized"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsVirtualizedProperty =
AvaloniaProperty.Register<Carousel, bool>(nameof(IsVirtualized), true);
/// <summary>
/// Defines the <see cref="PageTransition"/> property.
/// </summary>
@ -28,7 +21,9 @@ namespace Avalonia.Controls
/// <see cref="Carousel"/>.
/// </summary>
private static readonly ITemplate<Panel> PanelTemplate =
new FuncTemplate<Panel>(() => new Panel());
new FuncTemplate<Panel>(() => new VirtualizingCarouselPanel());
private IScrollable? _scroller;
/// <summary>
/// Initializes static members of the <see cref="Carousel"/> class.
@ -38,18 +33,6 @@ namespace Avalonia.Controls
SelectionModeProperty.OverrideDefaultValue<Carousel>(SelectionMode.AlwaysSelected);
ItemsPanelProperty.OverrideDefaultValue<Carousel>(PanelTemplate);
}
/// <summary>
/// Gets or sets a value indicating whether the items in the carousel are virtualized.
/// </summary>
/// <remarks>
/// When the carousel is virtualized, only the active page is held in memory.
/// </remarks>
public bool IsVirtualized
{
get { return GetValue(IsVirtualizedProperty); }
set { SetValue(IsVirtualizedProperty, value); }
}
/// <summary>
/// Gets or sets the transition to use when moving between pages.
@ -65,7 +48,7 @@ namespace Avalonia.Controls
/// </summary>
public void Next()
{
if (SelectedIndex < Items.Count() - 1)
if (SelectedIndex < ItemCount - 1)
{
++SelectedIndex;
}
@ -81,5 +64,32 @@ namespace Avalonia.Controls
--SelectedIndex;
}
}
protected override Size ArrangeOverride(Size finalSize)
{
var result = base.ArrangeOverride(finalSize);
if (_scroller is not null)
_scroller.Offset = new(SelectedIndex, 0);
return result;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_scroller = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == SelectedIndexProperty && _scroller is not null)
{
var value = change.GetNewValue<int>();
_scroller.Offset = new(value, 0);
}
}
}
}

34
src/Avalonia.Controls/ComboBox.cs

@ -53,12 +53,6 @@ namespace Avalonia.Controls
public static readonly DirectProperty<ComboBox, object?> SelectionBoxItemProperty =
AvaloniaProperty.RegisterDirect<ComboBox, object?>(nameof(SelectionBoxItem), o => o.SelectionBoxItem);
/// <summary>
/// Defines the <see cref="VirtualizationMode"/> property.
/// </summary>
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
ItemsPresenter.VirtualizationModeProperty.AddOwner<ComboBox>();
/// <summary>
/// Defines the <see cref="PlaceholderText"/> property.
/// </summary>
@ -153,15 +147,6 @@ namespace Avalonia.Controls
set => SetValue(PlaceholderForegroundProperty, value);
}
/// <summary>
/// Gets or sets the virtualization mode for the items.
/// </summary>
public ItemVirtualizationMode VirtualizationMode
{
get => GetValue(VirtualizationModeProperty);
set => SetValue(VirtualizationModeProperty, value);
}
/// <summary>
/// Gets or sets the horizontal alignment of the content within the control.
/// </summary>
@ -180,16 +165,6 @@ namespace Avalonia.Controls
set => SetValue(VerticalContentAlignmentProperty, value);
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<ComboBoxItem>(
this,
ComboBoxItem.ContentProperty,
ComboBoxItem.ContentTemplateProperty);
}
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
@ -202,6 +177,9 @@ namespace Avalonia.Controls
UpdateFlowDirection();
}
protected internal override Control CreateContainerForItemOverride() => new ComboBoxItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ComboBoxItem;
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{
@ -490,11 +468,11 @@ namespace Avalonia.Controls
private void SelectFocusedItem()
{
foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers)
foreach (var dropdownItem in GetRealizedContainers())
{
if (dropdownItem.ContainerControl.IsFocused)
if (dropdownItem.IsFocused)
{
SelectedIndex = dropdownItem.Index;
SelectedIndex = IndexFromContainer(dropdownItem);
break;
}
}

5
src/Avalonia.Controls/ContextMenu.cs

@ -316,11 +316,6 @@ namespace Avalonia.Controls
remove => _popupHostChangedHandler -= value;
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
private void Open(Control control, Control placementTarget, bool requestedByPointer)
{
if (IsOpen)

2
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@ -58,6 +58,8 @@ namespace Avalonia.Controls.Embedding.Offscreen
public Action<WindowTransparencyLevel>? TransparencyLevelChanged { get; set; }
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { }
/// <inheritdoc/>
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1);

5
src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs

@ -35,11 +35,6 @@ namespace Avalonia.Controls
throw new NotSupportedException("Use MenuFlyout.ShowAt(Control) instead");
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);

114
src/Avalonia.Controls/Generators/IItemContainerGenerator.cs

@ -1,114 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Styling;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for items and maintains a list of created containers.
/// </summary>
public interface IItemContainerGenerator
{
/// <summary>
/// Gets the currently realized containers.
/// </summary>
IEnumerable<ItemContainerInfo> Containers { get; }
/// <summary>
/// Gets or sets the theme to be applied to the items in the control.
/// </summary>
ControlTheme? ItemContainerTheme { get; set; }
/// <summary>
/// Gets or sets the data template used to display the items in the control.
/// </summary>
IDataTemplate? ItemTemplate { get; set; }
/// <summary>
/// Gets or sets the binding to use to bind to the member of an item used for displaying
/// </summary>
IBinding? DisplayMemberBinding { get; set; }
/// <summary>
/// Gets the ContainerType, or null if its an untyped ContainerGenerator.
/// </summary>
Type? ContainerType { get; }
/// <summary>
/// Signaled whenever new containers are materialized.
/// </summary>
event EventHandler<ItemContainerEventArgs>? Materialized;
/// <summary>
/// Event raised whenever containers are dematerialized.
/// </summary>
event EventHandler<ItemContainerEventArgs>? Dematerialized;
/// <summary>
/// Event raised whenever containers are recycled.
/// </summary>
event EventHandler<ItemContainerEventArgs>? Recycled;
/// <summary>
/// Creates a container control for an item.
/// </summary>
/// <param name="index">
/// The index of the item of data in the control's items.
/// </param>
/// <param name="item">The item.</param>
/// <returns>The created controls.</returns>
ItemContainerInfo Materialize(int index, object item);
/// <summary>
/// Removes a set of created containers.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item in the control's items.
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count);
/// <summary>
/// Inserts space for newly inserted containers in the index.
/// </summary>
/// <param name="index">The index at which space should be inserted.</param>
/// <param name="count">The number of blank spaces to create.</param>
void InsertSpace(int index, int count);
/// <summary>
/// Removes a set of created containers and updates the index of later containers to fill
/// the gap.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item in the control's items.
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count);
bool TryRecycle(int oldIndex, int newIndex, object item);
/// <summary>
/// Clears all created containers and returns the removed controls.
/// </summary>
/// <returns>The removed controls.</returns>
IEnumerable<ItemContainerInfo> Clear();
/// <summary>
/// Gets the container control representing the item with the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The container, or null if no container created.</returns>
Control? ContainerFromIndex(int index);
/// <summary>
/// Gets the index of the specified container control.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The index of the container, or -1 if not found.</returns>
int IndexFromContainer(Control? container);
}
}

18
src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs

@ -1,18 +0,0 @@
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for tree items and maintains a list of created containers.
/// </summary>
public interface ITreeItemContainerGenerator : IItemContainerGenerator
{
/// <summary>
/// Gets the container index for the tree.
/// </summary>
TreeContainerIndex? Index { get; }
/// <summary>
/// Updates the index based on the parent <see cref="TreeView"/>.
/// </summary>
void UpdateIndex();
}
}

49
src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs

@ -1,49 +0,0 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Provides details for the <see cref="IItemContainerGenerator.Materialized"/>
/// and <see cref="IItemContainerGenerator.Dematerialized"/> events.
/// </summary>
public class ItemContainerEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
/// </summary>
/// <param name="container">The container.</param>
public ItemContainerEventArgs(ItemContainerInfo container)
{
StartingIndex = container.Index;
Containers = new[] { container };
}
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
/// </summary>
/// <param name="startingIndex">The index of the first container in the source items.</param>
/// <param name="containers">The containers.</param>
/// <remarks>
/// TODO: Do we really need to pass in StartingIndex here? The ItemContainerInfo objects
/// have an index, and what happens if the contains passed in aren't sequential?
/// </remarks>
public ItemContainerEventArgs(
int startingIndex,
IList<ItemContainerInfo> containers)
{
StartingIndex = startingIndex;
Containers = containers;
}
/// <summary>
/// Gets the containers.
/// </summary>
public IList<ItemContainerInfo> Containers { get; }
/// <summary>
/// Gets the index of the first container in the source items.
/// </summary>
public int StartingIndex { get; }
}
}

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

@ -1,262 +1,113 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Styling;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for items and maintains a list of created containers.
/// Generates containers for an <see cref="ItemsControl"/>.
/// </summary>
public class ItemContainerGenerator : IItemContainerGenerator
/// <remarks>
/// When creating a container for an item from a <see cref="VirtualizingPanel"/>, the following
/// method order should be followed:
///
/// - <see cref="IsItemItsOwnContainer(Control)"/> should first be called if the item is
/// derived from the <see cref="Control"/> class. If this method returns true then the
/// item itself should be used as the container.
/// - If <see cref="IsItemItsOwnContainer(Control)"/> returns false then
/// <see cref="CreateContainer"/> should be called to create a new container.
/// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
/// container.
/// - The container should then be added to the panel using
/// <see cref="VirtualizingPanel.AddInternalChild(Control)"/>
/// - Finally, <see cref="ItemContainerPrepared(Control, object?, int)"/> should be called.
/// - When the item is ready to be recycled, <see cref="ClearItemContainer(Control)"/> should
/// be called if <see cref="IsItemItsOwnContainer(Control)"/> returned false.
///
/// NOTE: Although this class is similar to that found in WPF/UWP, in Avalonia this class only
/// concerns itself with generating and clearing item containers; it does not maintain a
/// record of the currently realized containers, that responsibility is delegated to the
/// items panel.
/// </remarks>
public class ItemContainerGenerator
{
private SortedDictionary<int, ItemContainerInfo> _containers = new SortedDictionary<int, ItemContainerInfo>();
private readonly ItemsControl _owner;
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
public ItemContainerGenerator(Control owner)
{
Owner = owner ?? throw new ArgumentNullException(nameof(owner));
}
/// <inheritdoc/>
public IEnumerable<ItemContainerInfo> Containers => _containers.Values;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs>? Materialized;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs>? Dematerialized;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs>? Recycled;
internal ItemContainerGenerator(ItemsControl owner) => _owner = owner;
/// <summary>
/// Gets or sets the theme to be applied to the items in the control.
/// Creates a new container control.
/// </summary>
public ControlTheme? ItemContainerTheme { get; set; }
/// <returns>The newly created container control.</returns>
/// <remarks>
/// Before calling this method, <see cref="IsItemItsOwnContainer(Control)"/> should be
/// called to determine whether the item itself should be used as a container. After
/// calling this method, <see cref="PrepareItemContainer(Control, object, int)"/> should
/// be called to prepare the container to display the specified item.
/// </remarks>
public Control CreateContainer() => _owner.CreateContainerForItemOverride();
/// <summary>
/// Gets or sets the data template used to display the items in the control.
/// Determines whether the specified item is (or is eligible to be) its own container.
/// </summary>
public IDataTemplate? ItemTemplate { get; set; }
/// <inheritdoc />
public IBinding? DisplayMemberBinding { get; set; }
/// <param name="container">The item.</param>
/// <returns>true if the item is its own container, otherwise false.</returns>
/// <remarks>
/// Whereas in WPF/UWP, non-control items can be their own container, in Avalonia only
/// control items may be; the caller is responsible for checking if each item is a control
/// and calling this method before creating a new container.
/// </remarks>
public bool IsItemItsOwnContainer(Control container) => _owner.IsItemItsOwnContainerOverride(container);
/// <summary>
/// Gets the owner control.
/// Prepares the specified element as the container for the corresponding item.
/// </summary>
public Control Owner { get; }
/// <inheritdoc/>
public virtual Type? ContainerType => null;
/// <inheritdoc/>
public ItemContainerInfo Materialize(int index, object item)
{
var container = new ItemContainerInfo(CreateContainer(item)!, item, index);
_containers.Add(container.Index, container);
Materialized?.Invoke(this, new ItemContainerEventArgs(container));
return container;
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
{
var result = new List<ItemContainerInfo>();
for (int i = startingIndex; i < startingIndex + count; ++i)
{
result.Add(_containers[i]);
_containers.Remove(i);
}
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
return result;
}
/// <inheritdoc/>
public virtual void InsertSpace(int index, int count)
{
if (count > 0)
{
var toMove = _containers.Where(x => x.Key >= index)
.OrderByDescending(x => x.Key)
.ToArray();
foreach (var i in toMove)
{
_containers.Remove(i.Key);
i.Value.Index += count;
_containers.Add(i.Value.Index, i.Value);
}
}
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
{
var result = new List<ItemContainerInfo>();
if (count > 0)
{
for (var i = startingIndex; i < startingIndex + count; ++i)
{
if (_containers.TryGetValue(i, out var found))
{
result.Add(found);
}
_containers.Remove(i);
}
var toMove = _containers.Where(x => x.Key >= startingIndex)
.OrderBy(x => x.Key).ToArray();
foreach (var i in toMove)
{
_containers.Remove(i.Key);
i.Value.Index -= count;
_containers.Add(i.Value.Index, i.Value);
}
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
if (toMove.Length > 0)
{
var containers = toMove.Select(x => x.Value).ToArray();
Recycled?.Invoke(this, new ItemContainerEventArgs(containers[0].Index, containers));
}
}
return result;
}
/// <inheritdoc/>
public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false;
/// <inheritdoc/>
public virtual IEnumerable<ItemContainerInfo> Clear()
{
var result = Containers.ToArray();
_containers.Clear();
if (result.Length > 0)
{
Dematerialized?.Invoke(this, new ItemContainerEventArgs(0, result));
}
return result;
}
/// <inheritdoc/>
public Control? ContainerFromIndex(int index)
{
ItemContainerInfo? result;
_containers.TryGetValue(index, out result);
return result?.ContainerControl;
}
/// <inheritdoc/>
public int IndexFromContainer(Control? container)
{
foreach (var i in _containers)
{
if (i.Value.ContainerControl == container)
{
return i.Key;
}
}
return -1;
}
/// <param name="container">The element that's used to display the specified item.</param>
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item to display.</param>
/// <remarks>
/// If <see cref="IsItemItsOwnContainer(Control)"/> is true for an item, then this method
/// only needs to be called a single time, otherwise this method should be called after the
/// container is created, and each subsequent time the container is recycled to display a
/// new item.
/// </remarks>
public void PrepareItemContainer(Control container, object? item, int index) =>
_owner.PrepareItemContainer(container, item, index);
/// <summary>
/// Creates the container for an item.
/// Notifies the <see cref="ItemsControl"/> that a container has been fully prepared to
/// display an item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>The created container control.</returns>
protected virtual Control? CreateContainer(object item)
{
var result = item as Control;
if (result == null)
{
result = new ContentPresenter();
if (DisplayMemberBinding is not null)
{
result.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
result.Bind(ContentPresenter.ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
result.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Style);
}
if (ItemTemplate != null)
{
result.SetValue(
ContentPresenter.ContentTemplateProperty,
ItemTemplate,
BindingPriority.Style);
}
}
if (ItemContainerTheme != null)
{
result.SetValue(
StyledElement.ThemeProperty,
ItemContainerTheme,
BindingPriority.Template);
}
return result;
}
/// <param name="container">The container control.</param>
/// <param name="item">The item being displayed.</param>
/// <param name="index">The index of the item being displayed.</param>
/// <remarks>
/// This method should be called when a container has been fully prepared and added
/// to the logical and visual trees, but may be called before a layout pass has completed.
/// It should be called regardless of the result of
/// <see cref="IsItemItsOwnContainer(Control)"/>.
/// </remarks>
public void ItemContainerPrepared(Control container, object? item, int index) =>
_owner.ItemContainerPrepared(container, item, index);
/// <summary>
/// Moves a container.
/// Called when the index for a container changes due to an insertion or removal in the
/// items collection.
/// </summary>
/// <param name="container">The container whose index changed.</param>
/// <param name="oldIndex">The old index.</param>
/// <param name="newIndex">The new index.</param>
/// <param name="item">The new item.</param>
/// <returns>The container info.</returns>
protected ItemContainerInfo MoveContainer(int oldIndex, int newIndex, object item)
{
var container = _containers[oldIndex];
container.Index = newIndex;
container.Item = item;
_containers.Remove(oldIndex);
_containers.Add(newIndex, container);
return container;
}
public void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex) =>
_owner.ItemContainerIndexChanged(container, oldIndex, newIndex);
/// <summary>
/// Gets all containers with an index that fall within a range.
/// Undoes the effects of the <see cref="PrepareItemContainer(Control, object, int)"/> method.
/// </summary>
/// <param name="index">The first index.</param>
/// <param name="count">The number of elements in the range.</param>
/// <returns>The containers.</returns>
protected IEnumerable<ItemContainerInfo> GetContainerRange(int index, int count)
{
return _containers.Where(x => x.Key >= index && x.Key < index + count).Select(x => x.Value);
}
/// <param name="container">The container control.</param>
public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container);
/// <summary>
/// Raises the <see cref="Recycled"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected void RaiseRecycled(ItemContainerEventArgs e)
{
Recycled?.Invoke(this, e);
}
[Obsolete("Use ItemsControl.ContainerFromIndex")]
public Control? ContainerFromIndex(int index) => _owner.ContainerFromIndex(index);
[Obsolete("Use ItemsControl.IndexFromContainer")]
public int IndexFromContainer(Control container) => _owner.IndexFromContainer(container);
}
}

102
src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs

@ -1,102 +0,0 @@
using System;
using Avalonia.Data;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for items and maintains a list of created containers.
/// </summary>
/// <typeparam name="T">The type of the container.</typeparam>
public class ItemContainerGenerator<T> : ItemContainerGenerator where T : Control, new()
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator{T}"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
/// <param name="contentProperty">The container's Content property.</param>
/// <param name="contentTemplateProperty">The container's ContentTemplate property.</param>
public ItemContainerGenerator(
Control owner,
AvaloniaProperty contentProperty,
AvaloniaProperty? contentTemplateProperty)
: base(owner)
{
ContentProperty = contentProperty ?? throw new ArgumentNullException(nameof(contentProperty));
ContentTemplateProperty = contentTemplateProperty;
}
/// <inheritdoc/>
public override Type ContainerType => typeof(T);
/// <summary>
/// Gets the container's Content property.
/// </summary>
protected AvaloniaProperty ContentProperty { get; }
/// <summary>
/// Gets the container's ContentTemplate property.
/// </summary>
protected AvaloniaProperty? ContentTemplateProperty { get; }
/// <inheritdoc/>
protected override Control? CreateContainer(object item)
{
var container = item as T;
if (container is null)
{
container = new T();
if (ContentTemplateProperty != null)
{
container.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style);
}
if (DisplayMemberBinding is not null)
{
container.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
container.Bind(ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
container.SetValue(ContentProperty, item, BindingPriority.Style);
}
if (!(item is Control))
{
container.DataContext = item;
}
}
if (ItemContainerTheme != null)
{
container.SetValue(StyledElement.ThemeProperty, ItemContainerTheme, BindingPriority.Style);
}
return container;
}
/// <inheritdoc/>
public override bool TryRecycle(int oldIndex, int newIndex, object item)
{
var container = ContainerFromIndex(oldIndex);
if (container == null)
{
throw new IndexOutOfRangeException("Could not recycle container: not materialized.");
}
container.SetValue(ContentProperty, item);
if (!(item is Control))
{
container.DataContext = item;
}
var info = MoveContainer(oldIndex, newIndex, item);
RaiseRecycled(new ItemContainerEventArgs(info));
return true;
}
}
}

42
src/Avalonia.Controls/Generators/ItemContainerInfo.cs

@ -1,42 +0,0 @@
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Holds information about an item container generated by an
/// <see cref="IItemContainerGenerator"/>.
/// </summary>
public class ItemContainerInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerInfo"/> class.
/// </summary>
/// <param name="container">The container control.</param>
/// <param name="item">The item that the container represents.</param>
/// <param name="index">
/// The index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </param>
public ItemContainerInfo(Control container, object item, int index)
{
ContainerControl = container;
Item = item;
Index = index;
}
/// <summary>
/// Gets the container control.
/// </summary>
/// <remarks>
/// This will be null if <see cref="Item"/> is null.
/// </remarks>
public Control ContainerControl { get; }
/// <summary>
/// Gets the item that the container represents.
/// </summary>
public object Item { get; internal set; }
/// <summary>
/// Gets the index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </summary>
public int Index { get; set; }
}
}

21
src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs

@ -1,21 +0,0 @@
namespace Avalonia.Controls.Generators
{
public class MenuItemContainerGenerator : ItemContainerGenerator<MenuItem>
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator{T}"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
public MenuItemContainerGenerator(Control owner)
: base(owner, MenuItem.HeaderProperty, null)
{
}
/// <inheritdoc/>
protected override Control? CreateContainer(object item)
{
var separator = item as Separator;
return separator != null ? separator : base.CreateContainer(item);
}
}
}

105
src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs

@ -1,105 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Generators
{
public class TabItemContainerGenerator : ItemContainerGenerator<TabItem>
{
public TabItemContainerGenerator(TabControl owner)
: base(owner, ContentControl.ContentProperty, ContentControl.ContentTemplateProperty)
{
Owner = owner;
}
public new TabControl Owner { get; }
protected override Control CreateContainer(object item)
{
var tabItem = (TabItem)base.CreateContainer(item)!;
tabItem.Bind(TabItem.TabStripPlacementProperty, new OwnerBinding<Dock>(
tabItem,
TabControl.TabStripPlacementProperty));
if (tabItem.HeaderTemplate == null)
{
tabItem.Bind(TabItem.HeaderTemplateProperty, new OwnerBinding<IDataTemplate?>(
tabItem,
TabControl.ItemTemplateProperty));
}
if (Owner.HeaderDisplayMemberBinding is not null)
{
tabItem.Bind(HeaderedContentControl.HeaderProperty, Owner.HeaderDisplayMemberBinding,
BindingPriority.Style);
}
if (tabItem.Header == null)
{
if (item is IHeadered headered)
{
tabItem.Header = headered.Header;
}
else
{
if (!(tabItem.DataContext is Control))
{
tabItem.Header = tabItem.DataContext;
}
}
}
if (!(tabItem.Content is Control))
{
tabItem.Bind(TabItem.ContentTemplateProperty, new OwnerBinding<IDataTemplate?>(
tabItem,
TabControl.ContentTemplateProperty));
}
return tabItem;
}
private class OwnerBinding<T> : SingleSubscriberObservableBase<T>
{
private readonly TabItem _item;
private readonly StyledProperty<T> _ownerProperty;
private IDisposable? _ownerSubscription;
private IDisposable? _propertySubscription;
public OwnerBinding(TabItem item, StyledProperty<T> ownerProperty)
{
_item = item;
_ownerProperty = ownerProperty;
}
protected override void Subscribed()
{
_ownerSubscription = ControlLocator.Track(_item, 0, typeof(TabControl)).Subscribe(OwnerChanged);
}
protected override void Unsubscribed()
{
_ownerSubscription?.Dispose();
_ownerSubscription = null;
}
private void OwnerChanged(ILogical? c)
{
_propertySubscription?.Dispose();
_propertySubscription = null;
if (c is TabControl tabControl)
{
_propertySubscription = tabControl.GetObservable(_ownerProperty)
.Subscribe(x => PublishNext(x));
}
}
}
}
}

166
src/Avalonia.Controls/Generators/TreeContainerIndex.cs

@ -1,166 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Maintains an index of all item containers currently materialized by a <see cref="TreeView"/>.
/// </summary>
/// <remarks>
/// Each <see cref="TreeViewItem"/> has its own <see cref="TreeItemContainerGenerator{T}"/>
/// that maintains the list of its direct children, but they also share an instance of this
/// class in their <see cref="TreeItemContainerGenerator{T}.Index"/> property which tracks
/// the containers materialized for the entire tree.
/// </remarks>
public class TreeContainerIndex
{
private readonly Dictionary<object, HashSet<Control>> _itemToContainerSet = new Dictionary<object, HashSet<Control>>();
private readonly Dictionary<object, Control> _itemToContainer = new Dictionary<object, Control>();
private readonly Dictionary<Control, object> _containerToItem = new Dictionary<Control, object>();
/// <summary>
/// Signaled whenever new containers are materialized.
/// </summary>
public event EventHandler<ItemContainerEventArgs>? Materialized;
/// <summary>
/// Event raised whenever containers are dematerialized.
/// </summary>
public event EventHandler<ItemContainerEventArgs>? Dematerialized;
/// <summary>
/// Gets the currently materialized containers.
/// </summary>
public IEnumerable<Control> Containers => _containerToItem.Keys;
/// <summary>
/// Gets the items of currently materialized containers.
/// </summary>
public IEnumerable<object> Items => _containerToItem.Values;
/// <summary>
/// Adds an entry to the index.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="container">The item container.</param>
public void Add(object item, Control container)
{
_itemToContainer[item] = container;
if (_itemToContainerSet.TryGetValue(item, out var set))
{
set.Add(container);
}
else
{
_itemToContainerSet.Add(item, new HashSet<Control> { container });
}
_containerToItem.Add(container, item);
Materialized?.Invoke(
this,
new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0)));
}
/// <summary>
/// Removes a container from private collections.
/// </summary>
/// <param name="container">The item container.</param>
/// <param name="item">The DataContext object</param>
private void RemoveContainer(Control container, object item)
{
if (_itemToContainerSet.TryGetValue(item, out var set))
{
set.Remove(container);
if (set.Count == 0)
{
_itemToContainerSet.Remove(item);
_itemToContainer.Remove(item);
}
else
{
_itemToContainer[item] = set.First();
}
}
}
/// <summary>
/// Removes a container from the index.
/// </summary>
/// <param name="container">The item container.</param>
public void Remove(Control container)
{
var item = _containerToItem[container];
_containerToItem.Remove(container);
RemoveContainer(container, item);
Dematerialized?.Invoke(
this,
new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0)));
}
/// <summary>
/// Removes a set of containers from the index.
/// </summary>
/// <param name="startingIndex">The index of the first item.</param>
/// <param name="containers">The item containers.</param>
public void Remove(int startingIndex, IEnumerable<ItemContainerInfo> containers)
{
foreach (var container in containers)
{
var item = _containerToItem[container.ContainerControl];
_containerToItem.Remove(container.ContainerControl);
RemoveContainer(container.ContainerControl, item);
}
Dematerialized?.Invoke(
this,
new ItemContainerEventArgs(startingIndex, containers.ToList()));
}
/// <summary>
/// Gets the container for an item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>The container, or null of not found.</returns>
public Control? ContainerFromItem(object item)
{
if (item != null)
{
_itemToContainer.TryGetValue(item, out var result);
if (result == null)
{
_itemToContainerSet.TryGetValue(item, out var set);
if (set?.Count > 0)
{
return set.FirstOrDefault();
}
}
return result;
}
return null;
}
/// <summary>
/// Gets the item for a container.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The item, or null of not found.</returns>
public object? ItemFromContainer(Control? container)
{
if (container != null)
{
_containerToItem.TryGetValue(container, out var result);
if (result != null)
{
_itemToContainer[result] = container;
}
return result;
}
return null;
}
}
}

166
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@ -1,164 +1,32 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for tree items and maintains a list of created containers.
/// </summary>
/// <typeparam name="T">The type of the container.</typeparam>
public class TreeItemContainerGenerator<T> : ItemContainerGenerator<T>, ITreeItemContainerGenerator
where T : Control, new()
public class TreeItemContainerGenerator : ItemContainerGenerator
{
private TreeView? _treeView;
/// <summary>
/// Initializes a new instance of the <see cref="TreeItemContainerGenerator{T}"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
/// <param name="contentProperty">The container's Content property.</param>
/// <param name="contentTemplateProperty">The container's ContentTemplate property.</param>
/// <param name="itemsProperty">The container's Items property.</param>
/// <param name="isExpandedProperty">The container's IsExpanded property.</param>
public TreeItemContainerGenerator(
Control owner,
AvaloniaProperty contentProperty,
AvaloniaProperty contentTemplateProperty,
AvaloniaProperty itemsProperty,
AvaloniaProperty isExpandedProperty)
: base(owner, contentProperty, contentTemplateProperty)
{
ItemsProperty = itemsProperty ?? throw new ArgumentNullException(nameof(itemsProperty));
IsExpandedProperty = isExpandedProperty ?? throw new ArgumentNullException(nameof(isExpandedProperty));
UpdateIndex();
}
/// <summary>
/// Gets the container index for the tree.
/// </summary>
public TreeContainerIndex? Index { get; private set; }
/// <summary>
/// Gets the item container's Items property.
/// </summary>
protected AvaloniaProperty ItemsProperty { get; }
/// <summary>
/// Gets the item container's IsExpanded property.
/// </summary>
protected AvaloniaProperty IsExpandedProperty { get; }
/// <inheritdoc/>
protected override Control? CreateContainer(object? item)
{
var container = item as T;
if (item == null)
{
return null;
}
else if (container != null)
{
Index?.Add(item, container);
return container;
}
else
{
var template = GetTreeDataTemplate(item, ItemTemplate);
var result = new T();
if (ItemContainerTheme != null)
{
result.SetValue(Control.ThemeProperty, ItemContainerTheme, BindingPriority.Style);
}
if (DisplayMemberBinding is not null)
{
result.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
result.Bind(ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style);
}
var itemsSelector = template.ItemsSelector(item);
if (itemsSelector != null)
{
BindingOperations.Apply(result, ItemsProperty, itemsSelector, null);
}
if (!(item is Control))
{
result.DataContext = item;
}
Index?.Add(item, result);
return result;
}
}
public override IEnumerable<ItemContainerInfo> Clear()
internal TreeItemContainerGenerator(TreeView owner)
: base(owner)
{
var items = base.Clear();
Index?.Remove(0, items);
return items;
Index = new TreeContainerIndex(owner);
}
public override IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
{
Index?.Remove(startingIndex, GetContainerRange(startingIndex, count));
return base.Dematerialize(startingIndex, count);
}
public TreeContainerIndex Index { get; }
}
public override IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
{
Index?.Remove(startingIndex, GetContainerRange(startingIndex, count));
return base.RemoveRange(startingIndex, count);
}
public class TreeContainerIndex
{
private readonly TreeView _owner;
public override bool TryRecycle(int oldIndex, int newIndex, object item) => false;
internal TreeContainerIndex(TreeView owner) => _owner = owner;
public void UpdateIndex()
{
if (Owner is TreeView treeViewOwner && Index == null)
{
Index = new TreeContainerIndex();
_treeView = treeViewOwner;
}
else
{
var treeView = Owner.GetSelfAndLogicalAncestors().OfType<TreeView>().FirstOrDefault();
if (treeView != _treeView)
{
Clear();
Index = treeView?.ItemContainerGenerator?.Index;
_treeView = treeView;
}
}
}
[Obsolete("Use TreeView.GetRealizedTreeContainers")]
public IEnumerable<Control> Containers => _owner.GetRealizedTreeContainers();
class WrapperTreeDataTemplate : ITreeDataTemplate
{
private readonly IDataTemplate _inner;
public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner;
public Control? Build(object? param) => _inner.Build(param);
public bool Match(object? data) => _inner.Match(data);
public InstancedBinding? ItemsSelector(object item) => null;
}
[Obsolete("Use TreeView.TreeContainerFromItem")]
public Control? ContainerFromItem(object item) => _owner.TreeContainerFromItem(item);
private ITreeDataTemplate GetTreeDataTemplate(object item, IDataTemplate? primary)
{
var template = Owner.FindDataTemplate(item, primary) ?? FuncDataTemplate.Default;
var treeTemplate = template as ITreeDataTemplate ?? new WrapperTreeDataTemplate(template);
return treeTemplate;
}
[Obsolete("Use TreeView.TreeItemFromContainer")]
public object? ItemFromContainer(Control container) => _owner.TreeItemFromContainer(container);
}
}

19
src/Avalonia.Controls/IVirtualizingController.cs

@ -1,19 +0,0 @@
namespace Avalonia.Controls
{
/// <summary>
/// Interface implemented by controls that act as controllers for an
/// <see cref="IVirtualizingPanel"/>.
/// </summary>
public interface IVirtualizingController
{
/// <summary>
/// Called when the <see cref="IVirtualizingPanel"/>'s controls should be updated.
/// </summary>
/// <remarks>
/// The controller should respond to this method being called by either adding
/// children up until <see cref="IVirtualizingPanel.IsFull"/> becomes true or
/// removing <see cref="IVirtualizingPanel.OverflowCount"/> controls.
/// </remarks>
void UpdateControls();
}
}

89
src/Avalonia.Controls/IVirtualizingPanel.cs

@ -1,89 +0,0 @@
using Avalonia.Layout;
namespace Avalonia.Controls
{
/// <summary>
/// A panel that can be used to virtualize items.
/// </summary>
public interface IVirtualizingPanel
{
/// <summary>
/// Gets the children of the panel.
/// </summary>
Controls Children { get; }
/// <summary>
/// Gets or sets the controller for the virtualizing panel.
/// </summary>
/// <remarks>
/// A virtualizing controller is responsible for maintaining the controls in the virtualizing
/// panel. This property will be set by the controller when virtualization is initialized.
/// Note that this property may remain null if the panel is added to a control that does
/// not act as a virtualizing controller.
/// </remarks>
IVirtualizingController? Controller { get; set; }
/// <summary>
/// Gets a value indicating whether the panel is full.
/// </summary>
/// <remarks>
/// This property should return false until enough children are added to fill the space
/// passed into the last measure or arrange in the direction of scroll. It should be
/// updated immediately after a child is added or removed.
/// </remarks>
bool IsFull { get; }
/// <summary>
/// Gets the number of items that can be removed while keeping the panel full.
/// </summary>
/// <remarks>
/// This property should return the number of children that are completely out of the
/// panel's current bounds in the direction of scroll. It should be updated after an
/// arrange.
/// </remarks>
int OverflowCount { get; }
/// <summary>
/// Gets the direction of scroll.
/// </summary>
Orientation ScrollDirection { get; }
/// <summary>
/// Gets the average size of the materialized items in the direction of scroll.
/// </summary>
double AverageItemSize { get; }
/// <summary>
/// Gets or sets a size in pixels by which the content is overflowing the panel, in the
/// direction of scroll.
/// </summary>
/// <remarks>
/// This may be non-zero even when <see cref="OverflowCount"/> is zero if the last item
/// overflows the panel bounds.
/// </remarks>
double PixelOverflow { get; }
/// <summary>
/// Gets or sets the current pixel offset of the items in the direction of scroll.
/// </summary>
double PixelOffset { get; set; }
/// <summary>
/// Gets or sets the current scroll offset in the cross axis.
/// </summary>
double CrossAxisOffset { get; set; }
/// <summary>
/// Invalidates the measure of the control and forces a call to
/// <see cref="IVirtualizingController.UpdateControls"/> on the next measure.
/// </summary>
/// <remarks>
/// The implementation for this method should call
/// <see cref="Layoutable.InvalidateMeasure"/> and also ensure that the next call to
/// <see cref="Layoutable.Measure(Size)"/> calls
/// <see cref="IVirtualizingController.UpdateControls"/> on the next measure even if
/// the available size hasn't changed.
/// </remarks>
void ForceInvalidateMeasure();
}
}

18
src/Avalonia.Controls/ItemVirtualizationMode.cs

@ -1,18 +0,0 @@
namespace Avalonia.Controls
{
/// <summary>
/// Describes the item virtualization method to use for a list.
/// </summary>
public enum ItemVirtualizationMode
{
/// <summary>
/// Do not virtualize items.
/// </summary>
None,
/// <summary>
/// Virtualize items without smooth scrolling.
/// </summary>
Simple,
}
}

532
src/Avalonia.Controls/ItemsControl.cs

@ -2,21 +2,20 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Automation.Peers;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.VisualTree;
using Avalonia.Styling;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -24,7 +23,7 @@ namespace Avalonia.Controls
/// Displays a collection of items.
/// </summary>
[PseudoClasses(":empty", ":singleitem")]
public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener, IChildIndexProvider
public class ItemsControl : TemplatedControl, IChildIndexProvider
{
/// <summary>
/// The default value for the <see cref="ItemsPanel"/> property.
@ -62,6 +61,11 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate));
/// <summary>
/// Defines the <see cref="ItemsView"/> property.
/// </summary>
public static readonly DirectProperty<ItemsControl, ItemsSourceView> ItemsViewProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, ItemsSourceView>(nameof(ItemsView), o => o.ItemsView);
/// <summary>
/// Defines the <see cref="DisplayMemberBinding" /> property
@ -80,49 +84,32 @@ namespace Avalonia.Controls
}
private IEnumerable? _items = new AvaloniaList<object>();
private ItemsSourceView _itemsView;
private int _itemCount;
private IItemContainerGenerator? _itemContainerGenerator;
private ItemContainerGenerator? _itemContainerGenerator;
private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
/// <summary>
/// Initializes static members of the <see cref="ItemsControl"/> class.
/// </summary>
static ItemsControl()
{
ItemsProperty.Changed.AddClassHandler<ItemsControl>((x, e) => x.ItemsChanged(e));
ItemTemplateProperty.Changed.AddClassHandler<ItemsControl>((x, e) => x.ItemTemplateChanged(e));
}
private IDataTemplate? _displayMemberItemTemplate;
private Tuple<int, Control>? _containerBeingPrepared;
private ScrollViewer? _scrollViewer;
/// <summary>
/// Initializes a new instance of the <see cref="ItemsControl"/> class.
/// </summary>
public ItemsControl()
{
_itemsView = ItemsSourceView.GetOrCreate(_items);
_itemsView.PostCollectionChanged += ItemsCollectionChanged;
UpdatePseudoClasses(0);
SubscribeToItems(_items);
}
/// <summary>
/// Gets the <see cref="IItemContainerGenerator"/> for the control.
/// Gets the <see cref="ItemContainerGenerator"/> for the control.
/// </summary>
public IItemContainerGenerator ItemContainerGenerator
public ItemContainerGenerator ItemContainerGenerator
{
get
{
if (_itemContainerGenerator == null)
{
_itemContainerGenerator = CreateItemContainerGenerator();
_itemContainerGenerator.ItemContainerTheme = ItemContainerTheme;
_itemContainerGenerator.ItemTemplate = ItemTemplate;
_itemContainerGenerator.DisplayMemberBinding = DisplayMemberBinding;
_itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e);
_itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
_itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e);
}
return _itemContainerGenerator;
}
#pragma warning disable CS0612 // Type or member is obsolete
get => _itemContainerGenerator ??= CreateItemContainerGenerator();
#pragma warning restore CS0612 // Type or member is obsolete
}
/// <summary>
@ -174,10 +161,38 @@ namespace Avalonia.Controls
/// <summary>
/// Gets the items presenter control.
/// </summary>
public IItemsPresenter? Presenter
{
get;
protected set;
public ItemsPresenter? Presenter { get; private set; }
/// <summary>
/// Gets the <see cref="Panel"/> specified by <see cref="ItemsPanel"/>.
/// </summary>
public Panel? ItemsPanelRoot => Presenter?.Panel;
/// <summary>
/// Gets a standardized view over <see cref="Items"/>.
/// </summary>
/// <remarks>
/// The <see cref="Items"/> property may be an enumerable which does not implement
/// <see cref="IList"/> or may be null. This view can be used to provide a standardized
/// view of the current items regardless of the type of the concrete collection, and
/// without having to deal with null values.
/// </remarks>
public ItemsSourceView ItemsView
{
get => _itemsView;
private set
{
if (ReferenceEquals(_itemsView, value))
return;
var oldValue = _itemsView;
RemoveControlItemsFromLogicalChildren(_itemsView);
_itemsView.PostCollectionChanged -= ItemsCollectionChanged;
_itemsView = value;
_itemsView.PostCollectionChanged += ItemsCollectionChanged;
AddControlItemsToLogicalChildren(_itemsView);
RaisePropertyChanged(ItemsViewProperty, oldValue, _itemsView);
}
}
private protected bool WrapFocus { get; set; }
@ -188,144 +203,158 @@ namespace Avalonia.Controls
remove => _childIndexChanged -= value;
}
/// <inheritdoc/>
void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter)
{
if (Presenter is IChildIndexProvider oldInnerProvider)
{
oldInnerProvider.ChildIndexChanged -= PresenterChildIndexChanged;
}
Presenter = presenter;
ItemContainerGenerator?.Clear();
if (Presenter is IChildIndexProvider innerProvider)
{
innerProvider.ChildIndexChanged += PresenterChildIndexChanged;
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
}
}
/// <summary>
/// Returns the container for the item at the specified index.
/// </summary>
/// <param name="index">The index of the item to retrieve.</param>
/// <returns>
/// The container for the item at the specified index within the item collection, if the
/// item has a container; otherwise, null.
/// </returns>
public Control? ContainerFromIndex(int index) => Presenter?.ContainerFromIndex(index);
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
/// <summary>
/// Returns the container corresponding to the specified item.
/// </summary>
/// <param name="item">The item to retrieve the container for.</param>
/// <returns>
/// A container that corresponds to the specified item, if the item has a container and
/// exists in the collection; otherwise, null.
/// </returns>
public Control? ContainerFromItem(object item)
{
var index = ItemsView.IndexOf(item);
return index >= 0 ? ContainerFromIndex(index) : null;
}
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
}
/// <summary>
/// Returns the index to the item that has the specified, generated container.
/// </summary>
/// <param name="container">The generated container to retrieve the item index for.</param>
/// <returns>
/// The index to the item that corresponds to the specified generated container, or -1 if
/// <paramref name="container"/> is not found.
/// </returns>
public int IndexFromContainer(Control container) => Presenter?.IndexFromContainer(container) ?? -1;
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
/// <summary>
/// Returns the item that corresponds to the specified, generated container.
/// </summary>
/// <param name="container">The control that corresponds to the item to be returned.</param>
/// <returns>
/// The contained item, or the container if it does not contain an item.
/// </returns>
public object? ItemFromContainer(Control container)
{
ItemsCollectionChanged(sender, e);
var index = IndexFromContainer(container);
return index >= 0 && index < ItemsView.Count ? ItemsView[index] : null;
}
/// <summary>
/// Gets the item at the specified index in a collection.
/// Gets the currently realized containers.
/// </summary>
public IEnumerable<Control> GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty<Control>();
/// <summary>
/// Creates or a container that can be used to display an item.
/// </summary>
protected internal virtual Control CreateContainerForItemOverride() => new ContentPresenter();
/// <summary>
/// Prepares the specified element to display the specified item.
/// </summary>
/// <param name="items">The collection.</param>
/// <param name="index">The index.</param>
/// <returns>The item at the given index or null if the index is out of bounds.</returns>
protected static object? ElementAt(IEnumerable? items, int index)
/// <param name="container">The element that's used to display the specified item.</param>
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item to display.</param>
protected internal virtual void PrepareContainerForItemOverride(Control container, object? item, int index)
{
if (index != -1 && index < items.Count())
if (container == item)
return;
var itemTemplate = GetEffectiveItemTemplate();
if (container is HeaderedContentControl hcc)
{
return items!.ElementAt(index) ?? null;
hcc.Content = item;
if (item is IHeadered headered)
hcc.Header = headered.Header;
else if (item is not Visual)
hcc.Header = item;
if (itemTemplate is not null)
hcc.HeaderTemplate = itemTemplate;
}
else
else if (container is ContentControl cc)
{
return null;
cc.Content = item;
if (itemTemplate is not null)
cc.ContentTemplate = itemTemplate;
}
}
/// <summary>
/// Gets the index of an item in a collection.
/// </summary>
/// <param name="items">The collection.</param>
/// <param name="item">The item.</param>
/// <returns>The index of the item or -1 if the item was not found.</returns>
protected static int IndexOf(IEnumerable? items, object item)
{
if (items != null && item != null)
else if (container is ContentPresenter p)
{
p.Content = item;
if (itemTemplate is not null)
p.ContentTemplate = itemTemplate;
}
else if (container is ItemsControl ic)
{
var list = items as IList;
if (itemTemplate is not null)
ic.ItemTemplate = itemTemplate;
if (ItemContainerTheme is { } ict && !ict.IsSet(ItemContainerThemeProperty))
ic.ItemContainerTheme = ict;
}
if (list != null)
{
return list.IndexOf(item);
}
else
{
int index = 0;
// This condition is separate because HeaderedItemsControl needs to also run the
// ItemsControl preparation.
if (container is HeaderedItemsControl hic)
{
hic.Header = item;
hic.HeaderTemplate = itemTemplate;
foreach (var i in items)
{
if (Equals(i, item))
{
return index;
}
itemTemplate ??= hic.FindDataTemplate(item) ?? this.FindDataTemplate(item);
++index;
}
if (itemTemplate is ITreeDataTemplate treeTemplate)
{
if (item is not null && treeTemplate.ItemsSelector(item) is { } itemsBinding)
BindingOperations.Apply(hic, ItemsProperty, itemsBinding, null);
}
}
return -1;
}
/// <summary>
/// Creates the <see cref="ItemContainerGenerator"/> for the control.
/// Called when the index for a container changes due to an insertion or removal in the
/// items collection.
/// </summary>
/// <returns>
/// An <see cref="IItemContainerGenerator"/>.
/// </returns>
protected virtual IItemContainerGenerator CreateItemContainerGenerator()
/// <param name="container">The container whose index changed.</param>
/// <param name="oldIndex">The old index.</param>
/// <param name="newIndex">The new index.</param>
protected virtual void ContainerIndexChangedOverride(Control container, int oldIndex, int newIndex)
{
return new ItemContainerGenerator(this);
}
/// <summary>
/// Called when new containers are materialized for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// Undoes the effects of the <see cref="PrepareContainerForItemOverride(Control, object?, int)"/> method.
/// </summary>
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersMaterialized(ItemContainerEventArgs e)
/// <param name="container">The container element.</param>
protected internal virtual void ClearContainerForItemOverride(Control container)
{
foreach (var container in e.Containers)
{
// If the item is its own container, then it will be added to the logical tree when
// it was added to the Items collection.
if (container.ContainerControl != null && container.ContainerControl != container.Item)
{
LogicalChildren.Add(container.ContainerControl);
}
}
// Feels like we should be clearing the HeaderedItemsControl.Items binding here, but looking at
// the WPF source it seems that this isn't done there.
}
/// <summary>
/// Called when containers are dematerialized for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// Determines whether the specified item is (or is eligible to be) its own container.
/// </summary>
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersDematerialized(ItemContainerEventArgs e)
{
foreach (var container in e.Containers)
{
// If the item is its own container, then it will be removed from the logical tree
// when it is removed from the Items collection.
if (container.ContainerControl != container.Item)
{
LogicalChildren.Remove(container.ContainerControl);
}
}
}
/// <param name="item">The item to check.</param>
/// <returns>true if the item is (or is eligible to be) its own container; otherwise, false.</returns>
protected internal virtual bool IsItemItsOwnContainerOverride(Control item) => true;
/// <summary>
/// Called when containers are recycled for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// </summary>
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersRecycled(ItemContainerEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
}
/// <summary>
@ -381,51 +410,51 @@ namespace Avalonia.Controls
{
base.OnPropertyChanged(change);
if (change.Property == ItemCountProperty)
if (change.Property == ItemsProperty)
{
ItemsView = ItemsSourceView.GetOrCreate(change.GetNewValue<IEnumerable?>());
ItemCount = ItemsView.Count;
}
else if (change.Property == ItemCountProperty)
{
UpdatePseudoClasses(change.GetNewValue<int>());
}
else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
{
_itemContainerGenerator.ItemContainerTheme = change.GetNewValue<ControlTheme?>();
RefreshContainers();
}
}
/// <summary>
/// Called when the <see cref="Items"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldValue = e.OldValue as IEnumerable;
var newValue = e.NewValue as IEnumerable;
if (oldValue is INotifyCollectionChanged incc)
else if (change.Property == ItemTemplateProperty)
{
CollectionChangedEventManager.Instance.RemoveListener(incc, this);
if (change.NewValue is not null && DisplayMemberBinding is not null)
throw new InvalidOperationException("Cannot set both DisplayMemberBinding and ItemTemplate.");
RefreshContainers();
}
UpdateItemCount();
RemoveControlItemsFromLogicalChildren(oldValue);
AddControlItemsToLogicalChildren(newValue);
if (Presenter != null)
else if (change.Property == DisplayMemberBindingProperty)
{
Presenter.Items = newValue;
if (change.NewValue is not null && ItemTemplate is not null)
throw new InvalidOperationException("Cannot set both DisplayMemberBinding and ItemTemplate.");
_displayMemberItemTemplate = null;
RefreshContainers();
}
SubscribeToItems(newValue);
}
/// <summary>
/// Refreshes the containers displayed by the control.
/// </summary>
/// <remarks>
/// Causes all containers to be unrealized and re-realized.
/// </remarks>
protected void RefreshContainers() => Presenter?.Refresh();
/// <summary>
/// Called when the <see cref="INotifyCollectionChanged.CollectionChanged"/> event is
/// raised on <see cref="Items"/>.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
protected virtual void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateItemCount();
ItemCount = _itemsView.Count;
switch (e.Action)
{
@ -437,8 +466,76 @@ namespace Avalonia.Controls
RemoveControlItemsFromLogicalChildren(e.OldItems);
break;
}
}
/// <summary>
/// Creates the <see cref="ItemContainerGenerator"/>
/// </summary>
/// <remarks>
/// This method is only present for backwards compatibility with 0.10.x in order for
/// TreeView to be able to create a <see cref="TreeItemContainerGenerator"/>. Can be
/// removed in 12.0.
/// </remarks>
[Obsolete]
private protected virtual ItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator(this);
}
internal void AddLogicalChild(Control c) => LogicalChildren.Add(c);
internal void RemoveLogicalChild(Control c) => LogicalChildren.Remove(c);
/// <summary>
/// Called by <see cref="ItemsPresenter"/> to register with the <see cref="ItemsControl"/>.
/// </summary>
/// <param name="presenter">The items presenter.</param>
/// <remarks>
/// ItemsPresenters can be within nested templates or in popups and so are not necessarily
/// created immediately when the ItemsControl control's template is instantiated. Instead
/// they register themselves using this method.
/// </remarks>
internal void RegisterItemsPresenter(ItemsPresenter presenter)
{
Presenter = presenter;
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
}
internal void PrepareItemContainer(Control container, object? item, int index)
{
var itemContainerTheme = ItemContainerTheme;
if (itemContainerTheme is not null &&
!container.IsSet(ThemeProperty) &&
((IStyleable)container).StyleKey == itemContainerTheme.TargetType)
{
container.Theme = itemContainerTheme;
}
Presenter?.ItemsChanged(e);
if (item is not Control)
container.DataContext = item;
PrepareContainerForItemOverride(container, item, index);
}
internal void ItemContainerPrepared(Control container, object? item, int index)
{
_containerBeingPrepared = new(index, container);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container));
_containerBeingPrepared = null;
_scrollViewer?.RegisterAnchorCandidate(container);
}
internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex)
{
ContainerIndexChangedOverride(container, oldIndex, newIndex);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container));
}
internal void ClearItemContainer(Control container)
{
_scrollViewer?.UnregisterAnchorCandidate(container);
ClearContainerForItemOverride(container);
}
/// <summary>
@ -447,22 +544,22 @@ namespace Avalonia.Controls
/// <param name="items">The items.</param>
private void AddControlItemsToLogicalChildren(IEnumerable? items)
{
var toAdd = new List<ILogical>();
if (items is null)
return;
if (items != null)
List<ILogical>? toAdd = null;
foreach (var i in items)
{
foreach (var i in items)
if (i is Control control && !LogicalChildren.Contains(control))
{
var control = i as Control;
if (control != null && !LogicalChildren.Contains(control))
{
toAdd.Add(control);
}
toAdd ??= new();
toAdd.Add(control);
}
}
LogicalChildren.AddRange(toAdd);
if (toAdd is not null)
LogicalChildren.AddRange(toAdd);
}
/// <summary>
@ -471,63 +568,39 @@ namespace Avalonia.Controls
/// <param name="items">The items.</param>
private void RemoveControlItemsFromLogicalChildren(IEnumerable? items)
{
var toRemove = new List<ILogical>();
if (items is null)
return;
List<ILogical>? toRemove = null;
if (items != null)
foreach (var i in items)
{
foreach (var i in items)
if (i is Control control)
{
var control = i as Control;
if (control != null)
{
toRemove.Add(control);
}
toRemove ??= new();
toRemove.Add(control);
}
}
LogicalChildren.RemoveAll(toRemove);
if (toRemove is not null)
LogicalChildren.RemoveAll(toRemove);
}
/// <summary>
/// Subscribes to an <see cref="Items"/> collection.
/// </summary>
/// <param name="items">The items collection.</param>
private void SubscribeToItems(IEnumerable? items)
private IDataTemplate? GetEffectiveItemTemplate()
{
if (items is INotifyCollectionChanged incc)
{
CollectionChangedEventManager.Instance.AddListener(incc, this);
}
}
if (ItemTemplate is { } itemTemplate)
return itemTemplate;
/// <summary>
/// Called when the <see cref="ItemTemplate"/> changes.
/// </summary>
/// <param name="e">The event args.</param>
private void ItemTemplateChanged(AvaloniaPropertyChangedEventArgs e)
{
if (_itemContainerGenerator != null)
if (_displayMemberItemTemplate is null && DisplayMemberBinding is { } binding)
{
_itemContainerGenerator.ItemTemplate = (IDataTemplate?)e.NewValue;
// TODO: Rebuild the item containers.
_displayMemberItemTemplate = new FuncDataTemplate<object?>((_, _) =>
new TextBlock
{
[!TextBlock.TextProperty] = binding,
});
}
}
private void UpdateItemCount()
{
if (Items == null)
{
ItemCount = 0;
}
else if (Items is IList list)
{
ItemCount = list.Count;
}
else
{
ItemCount = Items.Count();
}
return _displayMemberItemTemplate;
}
private void UpdatePseudoClasses(int itemCount)
@ -585,26 +658,17 @@ namespace Avalonia.Controls
}
}
private void PresenterChildIndexChanged(object? sender, ChildIndexChangedEventArgs e)
{
_childIndexChanged?.Invoke(this, e);
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
return Presenter is IChildIndexProvider innerProvider
? innerProvider.GetChildIndex(child) : -1;
if (_containerBeingPrepared?.Item2 == child)
return _containerBeingPrepared.Item1;
return child is Control container ? IndexFromContainer(container) : -1;
}
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
if (Presenter is IChildIndexProvider presenter
&& presenter.TryGetTotalCount(out count))
{
return true;
}
count = ItemCount;
count = ItemsView.Count;
return true;
}
}

249
src/Avalonia.Controls/ItemsSourceView.cs

@ -13,40 +13,38 @@ using Avalonia.Controls.Utils;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a standardized view of the supported interactions between a given ItemsSource
/// object and an <see cref="ItemsRepeater"/> control.
/// Represents a standardized view of the supported interactions between an items collection
/// and an items control.
/// </summary>
/// <remarks>
/// Components written to work with ItemsRepeater should consume the
/// <see cref="ItemsRepeater.Items"/> via ItemsSourceView since this provides a normalized
/// view of the Items. That way, each component does not need to know if the source is an
/// IEnumerable, an IList, or something else.
/// </remarks>
public class ItemsSourceView : INotifyCollectionChanged, IDisposable
public class ItemsSourceView : IReadOnlyList<object?>,
INotifyCollectionChanged,
ICollectionChangedListener
{
/// <summary>
/// Gets an empty <see cref="ItemsSourceView"/>
/// </summary>
public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object>());
private IList? _inner;
private readonly IList _inner;
private NotifyCollectionChangedEventHandler? _collectionChanged;
private NotifyCollectionChangedEventHandler? _postCollectionChanged;
private bool _listening;
/// <summary>
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
/// </summary>
/// <param name="source">The data source.</param>
public ItemsSourceView(IEnumerable source)
private protected ItemsSourceView(IEnumerable source)
{
source = source ?? throw new ArgumentNullException(nameof(source));
_inner = source switch
{
ItemsSourceView _ => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
IList list => list,
INotifyCollectionChanged _ => throw new ArgumentException(
"Collection implements INotifyCollectionChanged by not IList.",
INotifyCollectionChanged => throw new ArgumentException(
"Collection implements INotifyCollectionChanged but not IList.",
nameof(source)),
IEnumerable<object> iObj => new List<object>(iObj),
null => throw new ArgumentNullException(nameof(source)),
_ => new List<object>(source.Cast<object>())
};
}
@ -56,26 +54,10 @@ namespace Avalonia.Controls
/// </summary>
public int Count => Inner.Count;
/// <summary>
/// Gets a value that indicates whether the items source can provide a unique key for each item.
/// </summary>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
public bool HasKeyIndexMapping => false;
/// <summary>
/// Gets the inner collection.
/// </summary>
public IList Inner
{
get
{
if (_inner is null)
ThrowDisposed();
return _inner!;
}
}
public IList Inner => _inner;
/// <summary>
/// Retrieves the item at the specified index.
@ -84,6 +66,14 @@ namespace Avalonia.Controls
/// <returns>The item.</returns>
public object? this[int index] => GetAt(index);
/// <summary>
/// Gets a value that indicates whether the items source can provide a unique key for each item.
/// </summary>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
internal bool HasKeyIndexMapping => false;
/// <summary>
/// Occurs when the collection has changed to indicate the reason for the change and which items changed.
/// </summary>
@ -91,34 +81,54 @@ namespace Avalonia.Controls
{
add
{
if (_collectionChanged is null && Inner is INotifyCollectionChanged incc)
{
incc.CollectionChanged += OnCollectionChanged;
}
AddListenerIfNecessary();
_collectionChanged += value;
}
remove
{
_collectionChanged -= value;
RemoveListenerIfNecessary();
}
}
if (_collectionChanged is null && Inner is INotifyCollectionChanged incc)
{
incc.CollectionChanged -= OnCollectionChanged;
}
/// <summary>
/// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/>
/// event handlers have been notified.
/// </summary>
internal event NotifyCollectionChangedEventHandler? PostCollectionChanged
{
add
{
AddListenerIfNecessary();
_postCollectionChanged += value;
}
remove
{
_postCollectionChanged -= value;
RemoveListenerIfNecessary();
}
}
/// <inheritdoc/>
public void Dispose()
private void AddListenerIfNecessary()
{
if (_inner is INotifyCollectionChanged incc)
if (!_listening)
{
incc.CollectionChanged -= OnCollectionChanged;
if (_inner is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.AddListener(incc, this);
_listening = true;
}
}
_inner = null;
private void RemoveListenerIfNecessary()
{
if (_listening && _collectionChanged is null && _postCollectionChanged is null)
{
if (_inner is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.RemoveListener(incc, this);
_listening = false;
}
}
/// <summary>
@ -128,80 +138,121 @@ namespace Avalonia.Controls
/// <returns>The item.</returns>
public object? GetAt(int index) => Inner[index];
/// <summary>
/// Determines the index of a specific item in the collection.
/// </summary>
/// <param name="item">The object to locate in the collection.</param>
/// <returns>The index of value if found in the list; otherwise, -1.</returns>
public int IndexOf(object? item) => Inner.IndexOf(item);
/// <summary>
/// Gets or creates an <see cref="ItemsSourceView"/> for the specified enumerable.
/// </summary>
/// <param name="items">The enumerable.</param>
/// <remarks>
/// This method handles the following three cases:
/// - If <paramref name="items"/> is null, returns <see cref="Empty"/>
/// - If <paramref name="items"/> is an <see cref="ItemsSourceView"/> returns the existing
/// <see cref="ItemsSourceView"/>
/// - Otherwise creates a new <see cref="ItemsSourceView"/>
/// </remarks>
public static ItemsSourceView GetOrCreate(IEnumerable? items)
{
if (items is ItemsSourceView isv)
return items switch
{
return isv;
}
else if (items is null)
{
return Empty;
}
else
{
return new ItemsSourceView(items);
}
ItemsSourceView isv => isv,
null => Empty,
_ => new ItemsSourceView(items)
};
}
/// <summary>
/// Retrieves the index of the item that has the specified unique identifier (key).
/// Gets or creates an <see cref="ItemsSourceView{T}"/> for the specified enumerable.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The key</returns>
/// <param name="items">The enumerable.</param>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// This method handles the following three cases:
/// - If <paramref name="items"/> is null, returns <see cref="Empty"/>
/// - If <paramref name="items"/> is an <see cref="ItemsSourceView"/> returns the existing
/// <see cref="ItemsSourceView"/>
/// - Otherwise creates a new <see cref="ItemsSourceView"/>
/// </remarks>
public string KeyFromIndex(int index)
public static ItemsSourceView<T> GetOrCreate<T>(IEnumerable? items)
{
throw new NotImplementedException();
return items switch
{
ItemsSourceView<T> isv => isv,
null => ItemsSourceView<T>.Empty,
_ => new ItemsSourceView<T>(items)
};
}
/// <summary>
/// Retrieves the unique identifier (key) for the item at the specified index.
/// Gets or creates an <see cref="ItemsSourceView{T}"/> for the specified enumerable.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The index.</returns>
/// <param name="items">The enumerable.</param>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// This method handles the following three cases:
/// - If <paramref name="items"/> is null, returns <see cref="Empty"/>
/// - If <paramref name="items"/> is an <see cref="ItemsSourceView"/> returns the existing
/// <see cref="ItemsSourceView"/>
/// - Otherwise creates a new <see cref="ItemsSourceView"/>
/// </remarks>
public int IndexFromKey(string key)
public static ItemsSourceView<T> GetOrCreate<T>(IEnumerable<T>? items)
{
throw new NotImplementedException();
return items switch
{
ItemsSourceView<T> isv => isv,
null => ItemsSourceView<T>.Empty,
_ => new ItemsSourceView<T>(items)
};
}
internal void AddListener(ICollectionChangedListener listener)
public IEnumerator<object?> GetEnumerator()
{
if (Inner is INotifyCollectionChanged incc)
static IEnumerator<object> EnumerateItems(IList list)
{
CollectionChangedEventManager.Instance.AddListener(incc, listener);
foreach (var o in list)
yield return o;
}
var inner = Inner;
return inner switch
{
IEnumerable<object> e => e.GetEnumerator(),
_ => EnumerateItems(inner),
};
}
internal void RemoveListener(ICollectionChangedListener listener)
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
if (Inner is INotifyCollectionChanged incc)
{
CollectionChangedEventManager.Instance.RemoveListener(incc, listener);
}
}
protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args)
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
_collectionChanged?.Invoke(this, args);
_collectionChanged?.Invoke(this, e);
}
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
OnItemsSourceChanged(e);
_postCollectionChanged?.Invoke(this, e);
}
private void ThrowDisposed() => throw new ObjectDisposedException(nameof(ItemsSourceView));
/// <summary>
/// Retrieves the index of the item that has the specified unique identifier (key).
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The key</returns>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
internal string KeyFromIndex(int index) => throw new NotImplementedException();
}
public class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
public sealed class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
{
/// <summary>
/// Gets an empty <see cref="ItemsSourceView"/>
@ -212,12 +263,12 @@ namespace Avalonia.Controls
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
/// </summary>
/// <param name="source">The data source.</param>
public ItemsSourceView(IEnumerable<T> source)
internal ItemsSourceView(IEnumerable<T> source)
: base(source)
{
}
private ItemsSourceView(IEnumerable source)
internal ItemsSourceView(IEnumerable source)
: base(source)
{
}
@ -227,9 +278,7 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
#pragma warning disable CS8603
public new T this[int index] => GetAt(index);
#pragma warning restore CS8603
/// <summary>
/// Retrieves the item at the specified index.
@ -238,23 +287,23 @@ namespace Avalonia.Controls
/// <returns>The item.</returns>
public new T GetAt(int index) => (T)Inner[index]!;
public IEnumerator<T> GetEnumerator() => Inner.Cast<T>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
public static new ItemsSourceView<T> GetOrCreate(IEnumerable? items)
public new IEnumerator<T> GetEnumerator()
{
if (items is ItemsSourceView<T> isv)
static IEnumerator<T> EnumerateItems(IList list)
{
return isv;
foreach (var o in list)
yield return (T)o;
}
else if (items is null)
{
return Empty;
}
else
var inner = Inner;
return inner switch
{
return new ItemsSourceView<T>(items);
}
IEnumerable<T> e => e.GetEnumerator(),
_ => EnumerateItems(inner),
};
}
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
}
}

25
src/Avalonia.Controls/ListBox.cs

@ -47,12 +47,6 @@ namespace Avalonia.Controls
public static readonly new StyledProperty<SelectionMode> SelectionModeProperty =
SelectingItemsControl.SelectionModeProperty;
/// <summary>
/// Defines the <see cref="VirtualizationMode"/> property.
/// </summary>
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
ItemsPresenter.VirtualizationModeProperty.AddOwner<ListBox>();
private IScrollable? _scroll;
/// <summary>
@ -61,7 +55,6 @@ namespace Avalonia.Controls
static ListBox()
{
ItemsPanelProperty.OverrideDefaultValue<ListBox>(DefaultPanel);
VirtualizationModeProperty.OverrideDefaultValue<ListBox>(ItemVirtualizationMode.Simple);
}
/// <summary>
@ -100,15 +93,6 @@ namespace Avalonia.Controls
set { base.SelectionMode = value; }
}
/// <summary>
/// Gets or sets the virtualization mode for the items.
/// </summary>
public ItemVirtualizationMode VirtualizationMode
{
get { return GetValue(VirtualizationModeProperty); }
set { SetValue(VirtualizationModeProperty, value); }
}
/// <summary>
/// Selects all items in the <see cref="ListBox"/>.
/// </summary>
@ -119,14 +103,7 @@ namespace Avalonia.Controls
/// </summary>
public void UnselectAll() => Selection.Clear();
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<ListBoxItem>(
this,
ListBoxItem.ContentProperty,
ListBoxItem.ContentTemplateProperty);
}
protected internal override Control CreateContainerForItemOverride() => new ListBoxItem();
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)

10
src/Avalonia.Controls/Menu.cs

@ -93,5 +93,15 @@ namespace Avalonia.Controls
inputRoot.AccessKeyHandler.MainMenu = this;
}
}
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
base.PrepareContainerForItemOverride(element, item, index);
// Child menu items should not inherit the menu's ItemContainerTheme as that is specific
// for top-level menu items.
if ((element as MenuItem)?.ItemContainerTheme == ItemContainerTheme)
element.ClearValue(ItemContainerThemeProperty);
}
}
}

17
src/Avalonia.Controls/MenuBase.cs

@ -95,15 +95,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
IEnumerable<IMenuItem> IMenuElement.SubItems
{
get
{
return ItemContainerGenerator.Containers
.Select(x => x.ContainerControl)
.OfType<IMenuItem>();
}
}
IEnumerable<IMenuItem> IMenuElement.SubItems => GetRealizedContainers().OfType<IMenuItem>();
/// <summary>
/// Gets the interaction handler for the menu.
@ -141,11 +133,8 @@ namespace Avalonia.Controls
/// <inheritdoc/>
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<MenuItem>(this, MenuItem.HeaderProperty, null);
}
protected internal override Control CreateContainerForItemOverride() => new MenuItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator;
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)

19
src/Avalonia.Controls/MenuItem.cs

@ -329,15 +329,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
IEnumerable<IMenuItem> IMenuElement.SubItems
{
get
{
return ItemContainerGenerator.Containers
.Select(x => x.ContainerControl)
.OfType<IMenuItem>();
}
}
IEnumerable<IMenuItem> IMenuElement.SubItems => GetRealizedContainers().OfType<IMenuItem>();
/// <summary>
/// Opens the submenu.
@ -358,11 +350,8 @@ namespace Avalonia.Controls
/// <inheritdoc/>
void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent));
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
protected internal override Control CreateContainerForItemOverride() => new MenuItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator;
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
@ -674,7 +663,7 @@ namespace Avalonia.Controls
if (value)
{
foreach (var item in Items!.OfType<MenuItem>())
foreach (var item in ItemsView.OfType<MenuItem>())
{
item.TryUpdateCanExecute();
}

6
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@ -163,6 +163,12 @@ namespace Avalonia.Platform
/// </summary>
WindowTransparencyLevel TransparencyLevel { get; }
/// <summary>
/// Sets the <see cref="PlatformThemeVariant"/> on the frame if it should be dark or light.
/// Also applies for the mobile status bar.
/// </summary>
void SetFrameThemeVariant(PlatformThemeVariant themeVariant);
/// <summary>
/// Gets the <see cref="AcrylicPlatformCompensationLevels"/> for the platform.
/// </summary>

276
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@ -1,276 +0,0 @@
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Reactive;
using System.Threading.Tasks;
using Avalonia.Animation;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Data;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Displays pages inside an <see cref="ItemsControl"/>.
/// </summary>
public class CarouselPresenter : ItemsPresenterBase
{
/// <summary>
/// Defines the <see cref="IsVirtualized"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsVirtualizedProperty =
Carousel.IsVirtualizedProperty.AddOwner<CarouselPresenter>();
/// <summary>
/// Defines the <see cref="SelectedIndex"/> property.
/// </summary>
public static readonly DirectProperty<CarouselPresenter, int> SelectedIndexProperty =
SelectingItemsControl.SelectedIndexProperty.AddOwner<CarouselPresenter>(
o => o.SelectedIndex,
(o, v) => o.SelectedIndex = v);
/// <summary>
/// Defines the <see cref="PageTransition"/> property.
/// </summary>
public static readonly StyledProperty<IPageTransition?> PageTransitionProperty =
Carousel.PageTransitionProperty.AddOwner<CarouselPresenter>();
private int _selectedIndex = -1;
private Task? _currentTransition;
private int _queuedTransitionIndex = -1;
/// <summary>
/// Initializes static members of the <see cref="CarouselPresenter"/> class.
/// </summary>
static CarouselPresenter()
{
IsVirtualizedProperty.Changed.AddClassHandler<CarouselPresenter>((x, e) => x.IsVirtualizedChanged(e));
SelectedIndexProperty.Changed.AddClassHandler<CarouselPresenter>((x, e) => x.SelectedIndexChanged(e));
}
/// <summary>
/// Gets or sets a value indicating whether the items in the carousel are virtualized.
/// </summary>
/// <remarks>
/// When the carousel is virtualized, only the active page is held in memory.
/// </remarks>
public bool IsVirtualized
{
get { return GetValue(IsVirtualizedProperty); }
set { SetValue(IsVirtualizedProperty, value); }
}
/// <summary>
/// Gets or sets the index of the selected page.
/// </summary>
public int SelectedIndex
{
get
{
return _selectedIndex;
}
set
{
var old = SelectedIndex;
var effective = (value >= 0 && value < Items?.Cast<object>().Count()) ? value : -1;
if (old != effective)
{
_selectedIndex = effective;
RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue);
}
}
}
/// <summary>
/// Gets or sets a transition to use when switching pages.
/// </summary>
public IPageTransition? PageTransition
{
get { return GetValue(PageTransitionProperty); }
set { SetValue(PageTransitionProperty, value); }
}
/// <inheritdoc/>
protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
{
if (!IsVirtualized)
{
base.ItemsChanged(e);
if (Items == null || SelectedIndex >= Items.Count())
{
SelectedIndex = Items.Count() - 1;
}
foreach (var c in ItemContainerGenerator.Containers)
{
c.ContainerControl.IsVisible = c.Index == SelectedIndex;
}
}
else if (SelectedIndex != -1 && Panel != null)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
if (e.NewStartingIndex > SelectedIndex)
{
return;
}
break;
case NotifyCollectionChangedAction.Remove:
if (e.OldStartingIndex > SelectedIndex)
{
return;
}
break;
case NotifyCollectionChangedAction.Replace:
if (e.OldStartingIndex > SelectedIndex ||
e.OldStartingIndex + e.OldItems!.Count - 1 < SelectedIndex)
{
return;
}
break;
case NotifyCollectionChangedAction.Move:
if (e.OldStartingIndex > SelectedIndex &&
e.NewStartingIndex > SelectedIndex)
{
return;
}
break;
}
if (Items == null || SelectedIndex >= Items.Count())
{
SelectedIndex = Items.Count() - 1;
}
Panel.Children.Clear();
ItemContainerGenerator.Clear();
if (SelectedIndex != -1)
{
GetOrCreateContainer(SelectedIndex);
}
}
}
protected override void PanelCreated(Panel panel)
{
ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
/// <summary>
/// Moves to the selected page, animating if a <see cref="PageTransition"/> is set.
/// </summary>
/// <param name="fromIndex">The index of the old page.</param>
/// <param name="toIndex">The index of the new page.</param>
/// <returns>A task tracking the animation.</returns>
private async Task MoveToPage(int fromIndex, int toIndex)
{
if (fromIndex != toIndex)
{
var generator = ItemContainerGenerator;
Control? from = null;
Control? to = null;
if (fromIndex != -1)
{
from = generator.ContainerFromIndex(fromIndex);
}
if (toIndex != -1)
{
to = GetOrCreateContainer(toIndex);
}
if (PageTransition != null && (from != null || to != null))
{
await PageTransition.Start((Visual?)from, (Visual?)to, fromIndex < toIndex, default);
}
else if (to != null)
{
to.IsVisible = true;
}
if (from != null)
{
if (IsVirtualized)
{
Panel!.Children.Remove(from);
generator.Dematerialize(fromIndex, 1);
}
else
{
from.IsVisible = false;
}
}
}
}
private Control? GetOrCreateContainer(int index)
{
var container = ItemContainerGenerator.ContainerFromIndex(index);
if (container == null && IsVirtualized)
{
var item = Items!.Cast<object>().ElementAt(index);
var materialized = ItemContainerGenerator.Materialize(index, item);
Panel!.Children.Add(materialized.ContainerControl);
container = materialized.ContainerControl;
}
return container;
}
/// <summary>
/// Called when the <see cref="IsVirtualized"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private void IsVirtualizedChanged(AvaloniaPropertyChangedEventArgs e)
{
if (Panel != null)
{
ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
/// <summary>
/// Called when the <see cref="SelectedIndex"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private async void SelectedIndexChanged(AvaloniaPropertyChangedEventArgs e)
{
if (Panel != null)
{
if (_currentTransition == null)
{
int fromIndex = (int)e.OldValue!;
int toIndex = (int)e.NewValue!;
for (;;)
{
_currentTransition = MoveToPage(fromIndex, toIndex);
await _currentTransition;
if (_queuedTransitionIndex != -1)
{
fromIndex = toIndex;
toIndex = _queuedTransitionIndex;
_queuedTransitionIndex = -1;
}
else
{
_currentTransition = null;
break;
}
}
}
else
{
_queuedTransitionIndex = (int)e.NewValue!;
}
}
}
}
}

18
src/Avalonia.Controls/Presenters/IItemsPresenter.cs

@ -1,18 +0,0 @@
using System.Collections;
using System.Collections.Specialized;
using Avalonia.Metadata;
namespace Avalonia.Controls.Presenters
{
[NotClientImplementable]
public interface IItemsPresenter : IPresenter
{
IEnumerable? Items { get; set; }
Panel? Panel { get; }
void ItemsChanged(NotifyCollectionChangedEventArgs e);
void ScrollIntoView(int index);
}
}

26
src/Avalonia.Controls/Presenters/IItemsPresenterHost.cs

@ -1,26 +0,0 @@
using Avalonia.Metadata;
using Avalonia.Styling;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Represents a control which hosts an items presenter.
/// </summary>
/// <remarks>
/// This interface is implemented by <see cref="ItemsControl"/> which usually contains an
/// <see cref="ItemsPresenter"/> and exposes it through its
/// <see cref="ItemsControl.Presenter"/> property. ItemsPresenters can be within
/// nested templates or in popups and so are not necessarily created immediately when the
/// parent control's template is instantiated so they register themselves using this
/// interface.
/// </remarks>
[NotClientImplementable]
public interface IItemsPresenterHost
{
/// <summary>
/// Registers an <see cref="IItemsPresenter"/> with a host control.
/// </summary>
/// <param name="presenter">The items presenter.</param>
void RegisterItemsPresenter(IItemsPresenter presenter);
}
}

125
src/Avalonia.Controls/Presenters/ItemContainerSync.cs

@ -1,125 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Utils;
namespace Avalonia.Controls.Presenters
{
internal static class ItemContainerSync
{
public static void ItemsChanged(
ItemsPresenterBase owner,
IEnumerable? items,
NotifyCollectionChangedEventArgs e)
{
var generator = owner.ItemContainerGenerator;
var panel = owner.Panel;
if (panel == null)
{
return;
}
void Add()
{
if (e.NewStartingIndex + e.NewItems!.Count < items!.Count())
{
generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count);
}
AddContainers(owner, e.NewStartingIndex, e.NewItems);
}
void Remove()
{
RemoveContainers(panel, generator.RemoveRange(e.OldStartingIndex, e.OldItems!.Count));
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Add();
break;
case NotifyCollectionChangedAction.Remove:
Remove();
break;
case NotifyCollectionChangedAction.Replace:
RemoveContainers(panel, generator.Dematerialize(e.OldStartingIndex, e.OldItems!.Count));
var containers = AddContainers(owner, e.NewStartingIndex, e.NewItems!);
var i = e.NewStartingIndex;
foreach (var container in containers)
{
panel.Children[i++] = container.ContainerControl;
}
break;
case NotifyCollectionChangedAction.Move:
Remove();
Add();
break;
case NotifyCollectionChangedAction.Reset:
RemoveContainers(panel, generator.Clear());
if (items != null)
{
AddContainers(owner, 0, items);
}
break;
}
}
private static IList<ItemContainerInfo> AddContainers(
ItemsPresenterBase owner,
int index,
IEnumerable items)
{
var generator = owner.ItemContainerGenerator;
var result = new List<ItemContainerInfo>();
var panel = owner.Panel;
foreach (var item in items)
{
var i = generator.Materialize(index++, item);
if (i.ContainerControl != null)
{
if (i.Index < panel!.Children.Count)
{
// TODO: This will insert at the wrong place when there are null items.
panel.Children.Insert(i.Index, i.ContainerControl);
}
else
{
panel.Children.Add(i.ContainerControl);
}
}
result.Add(i);
}
return result;
}
private static void RemoveContainers(
Panel panel,
IEnumerable<ItemContainerInfo> items)
{
foreach (var i in items)
{
if (i.ContainerControl != null)
{
panel.Children.Remove(i.ContainerControl);
}
}
}
}
}

303
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@ -1,303 +0,0 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using Avalonia.Reactive;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Base class for classes which handle virtualization for an <see cref="ItemsPresenter"/>.
/// </summary>
internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable
{
private double _crossAxisOffset;
private IDisposable? _subscriptions;
/// <summary>
/// Initializes a new instance of the <see cref="ItemVirtualizer"/> class.
/// </summary>
/// <param name="owner"></param>
public ItemVirtualizer(ItemsPresenter owner)
{
Owner = owner;
Items = owner.Items;
ItemCount = owner.Items.Count();
var panel = VirtualizingPanel;
if (panel != null)
{
_subscriptions = ((AvaloniaObject)panel).GetObservable(Panel.BoundsProperty)
.Skip(1)
.Subscribe(_ => InvalidateScroll());
}
}
/// <summary>
/// Gets the <see cref="ItemsPresenter"/> which owns the virtualizer.
/// </summary>
public ItemsPresenter Owner { get; }
/// <summary>
/// Gets the <see cref="IVirtualizingPanel"/> which will host the items.
/// </summary>
public IVirtualizingPanel? VirtualizingPanel => Owner.Panel as IVirtualizingPanel;
/// <summary>
/// Gets the items to display.
/// </summary>
public IEnumerable? Items { get; private set; }
/// <summary>
/// Gets the number of items in <see cref="Items"/>.
/// </summary>
public int ItemCount { get; private set; }
/// <summary>
/// Gets or sets the index of the first item displayed in the panel.
/// </summary>
public int FirstIndex { get; protected set; }
/// <summary>
/// Gets or sets the index of the first item beyond those displayed in the panel.
/// </summary>
public int NextIndex { get; protected set; }
/// <summary>
/// Gets a value indicating whether the items should be scroll horizontally or vertically.
/// </summary>
public bool Vertical => VirtualizingPanel?.ScrollDirection == Orientation.Vertical;
/// <summary>
/// Gets a value indicating whether logical scrolling is enabled.
/// </summary>
public abstract bool IsLogicalScrollEnabled { get; }
/// <summary>
/// Gets the value of the scroll extent.
/// </summary>
public abstract double ExtentValue { get; }
/// <summary>
/// Gets or sets the value of the current scroll offset.
/// </summary>
public abstract double OffsetValue { get; set; }
/// <summary>
/// Gets the value of the scrollable viewport.
/// </summary>
public abstract double ViewportValue { get; }
/// <summary>
/// Gets the <see cref="ExtentValue"/> as a <see cref="Size"/>.
/// </summary>
public Size Extent
{
get
{
if (IsLogicalScrollEnabled && Owner.Panel is Panel panel)
{
return Vertical ?
new Size(panel.DesiredSize.Width, ExtentValue) :
new Size(ExtentValue, panel.DesiredSize.Height);
}
return default;
}
}
/// <summary>
/// Gets the <see cref="ViewportValue"/> as a <see cref="Size"/>.
/// </summary>
public Size Viewport
{
get
{
if (IsLogicalScrollEnabled && Owner.Panel is Panel panel)
{
return Vertical ?
new Size(panel.Bounds.Width, ViewportValue) :
new Size(ViewportValue, panel.Bounds.Height);
}
return default;
}
}
/// <summary>
/// Gets or sets the <see cref="OffsetValue"/> as a <see cref="Vector"/>.
/// </summary>
public Vector Offset
{
get
{
if (IsLogicalScrollEnabled)
{
return Vertical ? new Vector(_crossAxisOffset, OffsetValue) : new Vector(OffsetValue, _crossAxisOffset);
}
return default;
}
set
{
if (!IsLogicalScrollEnabled)
{
throw new NotSupportedException("Logical scrolling disabled.");
}
var oldCrossAxisOffset = _crossAxisOffset;
if (Vertical)
{
OffsetValue = value.Y;
_crossAxisOffset = value.X;
}
else
{
OffsetValue = value.X;
_crossAxisOffset = value.Y;
}
if (_crossAxisOffset != oldCrossAxisOffset)
{
Owner.InvalidateArrange();
}
}
}
/// <summary>
/// Creates an <see cref="ItemVirtualizer"/> based on an item presenter's
/// <see cref="ItemVirtualizationMode"/>.
/// </summary>
/// <param name="owner">The items presenter.</param>
/// <returns>An <see cref="ItemVirtualizer"/>.</returns>
public static ItemVirtualizer? Create(ItemsPresenter owner)
{
if (owner.Panel == null)
{
return null;
}
var virtualizingPanel = owner.Panel as IVirtualizingPanel;
var scrollContentPresenter = owner.Parent as IScrollable;
ItemVirtualizer? result = null;
if (virtualizingPanel != null && scrollContentPresenter is object)
{
switch (owner.VirtualizationMode)
{
case ItemVirtualizationMode.Simple:
result = new ItemVirtualizerSimple(owner);
break;
}
}
if (result == null)
{
result = new ItemVirtualizerNone(owner);
}
if (virtualizingPanel != null)
{
virtualizingPanel.Controller = result;
}
return result;
}
/// <summary>
/// Carries out a measure for the related <see cref="ItemsPresenter"/>.
/// </summary>
/// <param name="availableSize">The size available to the control.</param>
/// <returns>The desired size for the control.</returns>
public virtual Size MeasureOverride(Size availableSize)
{
Owner.Panel!.Measure(availableSize);
return Owner.Panel.DesiredSize;
}
/// <summary>
/// Carries out an arrange for the related <see cref="ItemsPresenter"/>.
/// </summary>
/// <param name="finalSize">The size available to the control.</param>
/// <returns>The actual size used.</returns>
public virtual Size ArrangeOverride(Size finalSize)
{
if (VirtualizingPanel != null)
{
VirtualizingPanel.CrossAxisOffset = _crossAxisOffset;
Owner.Panel!.Arrange(new Rect(finalSize));
}
else
{
var origin = Vertical ? new Point(-_crossAxisOffset, 0) : new Point(0, _crossAxisOffset);
Owner.Panel!.Arrange(new Rect(origin, finalSize));
}
return finalSize;
}
/// <inheritdoc/>
public virtual void UpdateControls()
{
}
/// <summary>
/// Gets the next control in the specified direction.
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
public virtual Control? GetControlInDirection(NavigationDirection direction, Control? from)
{
return null;
}
/// <summary>
/// Called when the items for the presenter change, either because
/// <see cref="ItemsPresenterBase.Items"/> has been set, the items collection has been
/// modified, or the panel has been created.
/// </summary>
/// <param name="items">The items.</param>
/// <param name="e">A description of the change.</param>
public virtual void ItemsChanged(IEnumerable? items, NotifyCollectionChangedEventArgs e)
{
Items = items;
ItemCount = items?.Count() ?? 0;
}
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="index">The index of the item.</param>
public virtual void ScrollIntoView(int index)
{
}
/// <inheritdoc/>
public virtual void Dispose()
{
_subscriptions?.Dispose();
_subscriptions = null;
if (VirtualizingPanel != null)
{
VirtualizingPanel.Controller = null;
VirtualizingPanel.Children.Clear();
}
Owner.ItemContainerGenerator.Clear();
}
/// <summary>
/// Invalidates the current scroll.
/// </summary>
protected void InvalidateScroll() => ((ILogicalScrollable)Owner).RaiseScrollInvalidated(EventArgs.Empty);
}
}

106
src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs

@ -1,106 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Utils;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Represents an item virtualizer for an <see cref="ItemsPresenter"/> that doesn't actually
/// virtualize items - it just creates a container for every item.
/// </summary>
internal class ItemVirtualizerNone : ItemVirtualizer
{
public ItemVirtualizerNone(ItemsPresenter owner)
: base(owner)
{
if (Items != null && owner.Panel != null)
{
AddContainers(0, Items);
}
}
/// <inheritdoc/>
public override bool IsLogicalScrollEnabled => false;
/// <summary>
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
/// false.
/// </summary>
public override double ExtentValue
{
get { throw new NotSupportedException(); }
}
/// <summary>
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
/// false.
/// </summary>
public override double OffsetValue
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
/// <summary>
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
/// false.
/// </summary>
public override double ViewportValue
{
get { throw new NotSupportedException(); }
}
/// <inheritdoc/>
public override void ItemsChanged(IEnumerable? items, NotifyCollectionChangedEventArgs e)
{
base.ItemsChanged(items, e);
ItemContainerSync.ItemsChanged(Owner, items, e);
Owner.InvalidateMeasure();
}
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="index">The index of the item.</param>
public override void ScrollIntoView(int index)
{
if (index != -1)
{
var container = Owner.ItemContainerGenerator.ContainerFromIndex(index);
container?.BringIntoView();
}
}
private IList<ItemContainerInfo> AddContainers(int index, IEnumerable items)
{
var generator = Owner.ItemContainerGenerator;
var result = new List<ItemContainerInfo>();
var panel = Owner.Panel;
foreach (var item in items)
{
var i = generator.Materialize(index++, item);
if (i.ContainerControl != null)
{
if (i.Index < panel!.Children.Count)
{
// TODO: This will insert at the wrong place when there are null items.
panel.Children.Insert(i.Index, i.ContainerControl);
}
else
{
panel.Children.Add(i.ContainerControl);
}
}
result.Add(i);
}
return result;
}
}
}

606
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -1,606 +0,0 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Handles virtualization in an <see cref="ItemsPresenter"/> for
/// <see cref="ItemVirtualizationMode.Simple"/>.
/// </summary>
internal class ItemVirtualizerSimple : ItemVirtualizer
{
private int _anchor;
/// <summary>
/// Initializes a new instance of the <see cref="ItemVirtualizerSimple"/> class.
/// </summary>
/// <param name="owner"></param>
public ItemVirtualizerSimple(ItemsPresenter owner)
: base(owner)
{
// Don't need to add children here as UpdateControls should be called by the panel
// measure/arrange.
}
/// <inheritdoc/>
public override bool IsLogicalScrollEnabled => true;
/// <inheritdoc/>
public override double ExtentValue => ItemCount;
/// <inheritdoc/>
public override double OffsetValue
{
get
{
var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0;
return FirstIndex + offset;
}
set
{
var panel = VirtualizingPanel;
var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0;
var delta = (int)(value - (FirstIndex + offset));
if (delta != 0)
{
var newLastIndex = (NextIndex - 1) + delta;
if (newLastIndex < ItemCount)
{
if (panel.PixelOffset > 0)
{
panel.PixelOffset = 0;
delta += 1;
}
if (delta != 0)
{
RecycleContainersForMove(delta);
}
}
else
{
// We're moving to a partially obscured item at the end of the list so
// offset the panel by the height of the first item.
var firstIndex = ItemCount - panel.Children.Count;
RecycleContainersForMove(firstIndex - FirstIndex);
double pixelOffset;
var child = panel.Children[0];
if (child.IsArrangeValid)
{
pixelOffset = VirtualizingPanel.ScrollDirection == Orientation.Vertical ?
child.Bounds.Height :
child.Bounds.Width;
}
else
{
pixelOffset = VirtualizingPanel.ScrollDirection == Orientation.Vertical ?
child.DesiredSize.Height :
child.DesiredSize.Width;
}
panel.PixelOffset = pixelOffset;
}
}
}
}
/// <inheritdoc/>
public override double ViewportValue
{
get
{
// If we can't fit the last item in the panel fully, subtract 1 from the viewport.
var overflow = VirtualizingPanel.PixelOverflow > 0 ? 1 : 0;
return VirtualizingPanel.Children.Count - overflow;
}
}
public new IVirtualizingPanel VirtualizingPanel => base.VirtualizingPanel!;
/// <inheritdoc/>
public override Size MeasureOverride(Size availableSize)
{
var scrollable = (ILogicalScrollable)Owner;
var visualRoot = Owner.GetVisualRoot();
var maxAvailableSize = (visualRoot as WindowBase)?.PlatformImpl?.MaxAutoSizeHint
?? (visualRoot as TopLevel)?.ClientSize;
// If infinity is passed as the available size and we're virtualized then we need to
// fill the available space, but to do that we *don't* want to materialize all our
// items! Take a look at the root of the tree for a MaxClientSize and use that as
// the available size.
if (VirtualizingPanel.ScrollDirection == Orientation.Vertical)
{
if (availableSize.Height == double.PositiveInfinity)
{
if (maxAvailableSize.HasValue)
{
availableSize = availableSize.WithHeight(maxAvailableSize.Value.Height);
}
}
if (scrollable.CanHorizontallyScroll)
{
availableSize = availableSize.WithWidth(double.PositiveInfinity);
}
}
else
{
if (availableSize.Width == double.PositiveInfinity)
{
if (maxAvailableSize.HasValue)
{
availableSize = availableSize.WithWidth(maxAvailableSize.Value.Width);
}
}
if (scrollable.CanVerticallyScroll)
{
availableSize = availableSize.WithHeight(double.PositiveInfinity);
}
}
Owner.Panel!.Measure(availableSize);
return Owner.Panel.DesiredSize;
}
/// <inheritdoc/>
public override void UpdateControls()
{
CreateAndRemoveContainers();
InvalidateScroll();
}
/// <inheritdoc/>
public override void ItemsChanged(IEnumerable? items, NotifyCollectionChangedEventArgs e)
{
base.ItemsChanged(items, e);
var panel = VirtualizingPanel;
if (items != null)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
CreateAndRemoveContainers();
if (e.NewStartingIndex < NextIndex)
{
RecycleContainers();
}
panel.ForceInvalidateMeasure();
break;
case NotifyCollectionChangedAction.Remove:
if (e.OldStartingIndex < NextIndex ||
panel.Children.Count > ItemCount)
{
RecycleContainersOnRemove();
}
panel.ForceInvalidateMeasure();
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
RecycleContainers();
break;
case NotifyCollectionChangedAction.Reset:
RecycleContainersOnRemove();
CreateAndRemoveContainers();
panel.ForceInvalidateMeasure();
break;
}
}
else
{
Owner.ItemContainerGenerator.Clear();
VirtualizingPanel.Children.Clear();
FirstIndex = NextIndex = 0;
}
// If we are scrolled to view a partially visible last item but controls were added
// then we need to return to a non-offset scroll position.
if (panel.PixelOffset != 0 && FirstIndex + panel.Children.Count < ItemCount)
{
panel.PixelOffset = 0;
RecycleContainersForMove(1);
}
InvalidateScroll();
}
public override Control? GetControlInDirection(NavigationDirection direction, Control? from)
{
var generator = Owner.ItemContainerGenerator;
var panel = VirtualizingPanel;
var itemIndex = generator.IndexFromContainer(from);
var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical;
var newItemIndex = -1;
switch (direction)
{
case NavigationDirection.First:
newItemIndex = 0;
break;
case NavigationDirection.Last:
newItemIndex = ItemCount - 1;
break;
default:
if (itemIndex == -1)
{
return null;
}
break;
}
switch (direction)
{
case NavigationDirection.Up:
if (vertical)
{
newItemIndex = itemIndex - 1;
}
break;
case NavigationDirection.Down:
if (vertical)
{
newItemIndex = itemIndex + 1;
}
break;
case NavigationDirection.Left:
if (!vertical)
{
newItemIndex = itemIndex - 1;
}
break;
case NavigationDirection.Right:
if (!vertical)
{
newItemIndex = itemIndex + 1;
}
break;
case NavigationDirection.PageUp:
newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue);
break;
case NavigationDirection.PageDown:
newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue);
break;
}
return ScrollIntoViewCore(newItemIndex);
}
/// <inheritdoc/>
public override void ScrollIntoView(int index)
{
if (index != -1)
{
ScrollIntoViewCore(index);
}
}
/// <summary>
/// Creates and removes containers such that we have at most enough containers to fill
/// the panel.
/// </summary>
private void CreateAndRemoveContainers()
{
var generator = Owner.ItemContainerGenerator;
var panel = VirtualizingPanel;
var panelControl = (Control)panel;
if (!panel.IsFull && Items != null && panelControl.IsAttachedToVisualTree)
{
var index = NextIndex;
var step = 1;
while (!panel.IsFull && index >= 0)
{
if (index >= ItemCount)
{
// We can fit more containers in the panel, but we're at the end of the
// items. If we're scrolled to the top (FirstIndex == 0), then there are
// no more items to create. Otherwise, go backwards adding containers to
// the beginning of the panel.
if (FirstIndex == 0)
{
break;
}
else
{
index = FirstIndex - 1;
step = -1;
}
}
var materialized = generator.Materialize(index, Items.ElementAt(index)!);
if (step == 1)
{
panel.Children.Add(materialized.ContainerControl);
}
else
{
panel.Children.Insert(0, materialized.ContainerControl);
}
index += step;
}
if (step == 1)
{
NextIndex = index;
}
else
{
NextIndex = ItemCount;
FirstIndex = index + 1;
}
}
if (panel.OverflowCount > 0)
{
if (_anchor <= FirstIndex)
{
RemoveContainers(panel.OverflowCount);
}
}
}
/// <summary>
/// Updates the containers in the panel to make sure they are displaying the correct item
/// based on <see cref="ItemVirtualizer.FirstIndex"/>.
/// </summary>
/// <remarks>
/// This method requires that <see cref="ItemVirtualizer.FirstIndex"/> + the number of
/// materialized containers is not more than <see cref="ItemVirtualizer.ItemCount"/>.
/// </remarks>
private void RecycleContainers()
{
var panel = VirtualizingPanel;
var generator = Owner.ItemContainerGenerator;
var containers = generator.Containers.ToList();
var itemIndex = FirstIndex;
foreach (var container in containers)
{
var item = Items!.ElementAt(itemIndex)!;
if (!object.Equals(container.Item, item))
{
if (!generator.TryRecycle(itemIndex, itemIndex, item))
{
throw new NotImplementedException();
}
}
++itemIndex;
}
}
/// <summary>
/// Recycles containers when a move occurs.
/// </summary>
/// <param name="delta">The delta of the move.</param>
/// <remarks>
/// If the move is less than a page, then this method moves the containers for the items
/// that are still visible to the correct place, and recycles and moves the others. For
/// example: if there are 20 items and 10 containers visible and the user scrolls 5
/// items down, then the bottom 5 containers will be moved to the top and the top 5 will
/// be moved to the bottom and recycled to display the newly visible item. Updates
/// <see cref="ItemVirtualizer.FirstIndex"/> and <see cref="ItemVirtualizer.NextIndex"/>
/// with their new values.
/// </remarks>
private void RecycleContainersForMove(int delta)
{
var panel = VirtualizingPanel;
var generator = Owner.ItemContainerGenerator;
//validate delta it should never overflow last index or generate index < 0
delta = MathUtilities.Clamp(delta, -FirstIndex, ItemCount - FirstIndex - panel.Children.Count);
var sign = delta < 0 ? -1 : 1;
var count = Math.Min(Math.Abs(delta), panel.Children.Count);
var move = count < panel.Children.Count;
var first = delta < 0 && move ? panel.Children.Count + delta : 0;
for (var i = 0; i < count; ++i)
{
var oldItemIndex = FirstIndex + first + i;
var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign);
var item = Items!.ElementAt(newItemIndex)!;
if (!generator.TryRecycle(oldItemIndex, newItemIndex, item))
{
throw new NotImplementedException();
}
}
if (move)
{
if (delta > 0)
{
panel.Children.MoveRange(first, count, panel.Children.Count);
}
else
{
panel.Children.MoveRange(first, count, 0);
}
}
FirstIndex += delta;
NextIndex += delta;
}
/// <summary>
/// Recycles containers due to items being removed.
/// </summary>
private void RecycleContainersOnRemove()
{
var panel = VirtualizingPanel;
if (NextIndex <= ItemCount)
{
// Items have been removed but FirstIndex..NextIndex is still a valid range in the
// items, so just recycle the containers to adapt to the new state.
RecycleContainers();
}
else
{
// Items have been removed and now the range FirstIndex..NextIndex goes out of
// the item bounds. Remove any excess containers, try to scroll up and then recycle
// the containers to make sure they point to the correct item.
var newFirstIndex = Math.Max(0, FirstIndex - (NextIndex - ItemCount));
var delta = newFirstIndex - FirstIndex;
var newNextIndex = NextIndex + delta;
if (newNextIndex > ItemCount)
{
RemoveContainers(newNextIndex - ItemCount);
}
if (delta != 0)
{
RecycleContainersForMove(delta);
}
RecycleContainers();
}
}
/// <summary>
/// Removes the specified number of containers from the end of the panel and updates
/// <see cref="ItemVirtualizer.NextIndex"/>.
/// </summary>
/// <param name="count">The number of containers to remove.</param>
private void RemoveContainers(int count)
{
var index = VirtualizingPanel.Children.Count - count;
VirtualizingPanel.Children.RemoveRange(index, count);
Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count);
NextIndex -= count;
}
/// <summary>
/// Scrolls the item with the specified index into view.
/// </summary>
/// <param name="index">The item index.</param>
/// <returns>The container that was brought into view.</returns>
private Control? ScrollIntoViewCore(int index)
{
var panel = VirtualizingPanel;
var panelControl = (Control)panel;
var generator = Owner.ItemContainerGenerator;
var newOffset = -1.0;
//better not trigger any container generation/recycle while or layout stuff
//before panel is attached/visible
if (!panelControl.IsAttachedToVisualTree)
{
return null;
}
if (!panelControl.IsMeasureValid && panelControl.PreviousMeasure.HasValue)
{
//before any kind of scrolling we need to make sure panel measure is valid
//or we risk get panel into not valid state
//we make a preemptive quick measure so scrolling is valid
panelControl.Measure(panelControl.PreviousMeasure.Value);
}
if (index >= 0 && index < ItemCount)
{
if (index <= FirstIndex)
{
newOffset = index;
}
else if (index >= NextIndex)
{
newOffset = index - Math.Ceiling(ViewportValue - 1);
}
if (newOffset != -1)
{
OffsetValue = newOffset;
}
var container = generator.ContainerFromIndex(index);
var layoutManager = (Owner.GetVisualRoot() as ILayoutRoot)?.LayoutManager;
// We need to do a layout here because it's possible that the container we moved to
// is only partially visible due to differing item sizes. If the container is only
// partially visible, scroll again. Don't do this if there's no layout manager:
// it means we're running a unit test.
if (container != null && layoutManager != null)
{
_anchor = index;
layoutManager.ExecuteLayoutPass();
_anchor = -1;
if (newOffset != -1 && newOffset != OffsetValue)
{
OffsetValue = newOffset;
}
if (panel.ScrollDirection == Orientation.Vertical)
{
if (container.Bounds.Y < panelControl.Bounds.Y || container.Bounds.Bottom > panelControl.Bounds.Bottom)
{
OffsetValue += 1;
}
}
else
{
if (container.Bounds.X < panelControl.Bounds.X || container.Bounds.Right > panelControl.Bounds.Right)
{
OffsetValue += 1;
}
}
}
return container;
}
return null;
}
/// <summary>
/// Ensures an offset value is within the value range.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>The coerced value.</returns>
private double CoerceOffset(double value)
{
var max = Math.Max(ExtentValue - ViewportValue, 0);
return MathUtilities.Clamp(value, 0, max);
}
}
}

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

@ -1,179 +1,208 @@
using System;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using static Avalonia.Utilities.MathUtilities;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Displays items inside an <see cref="ItemsControl"/>.
/// Presents items inside an <see cref="Avalonia.Controls.ItemsControl"/>.
/// </summary>
public class ItemsPresenter : ItemsPresenterBase, ILogicalScrollable
public class ItemsPresenter : Control, ILogicalScrollable
{
/// <summary>
/// Defines the <see cref="VirtualizationMode"/> property.
/// Defines the <see cref="ItemsPanel"/> property.
/// </summary>
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
AvaloniaProperty.Register<ItemsPresenter, ItemVirtualizationMode>(
nameof(VirtualizationMode),
defaultValue: ItemVirtualizationMode.None);
public static readonly StyledProperty<ITemplate<Panel>> ItemsPanelProperty =
ItemsControl.ItemsPanelProperty.AddOwner<ItemsPresenter>();
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
private PanelContainerGenerator? _generator;
private ILogicalScrollable? _logicalScrollable;
private EventHandler? _scrollInvalidated;
/// <summary>
/// Initializes static members of the <see cref="ItemsPresenter"/> class.
/// </summary>
static ItemsPresenter()
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
typeof(ItemsPresenter),
KeyboardNavigationMode.Once);
}
VirtualizationModeProperty.Changed
.AddClassHandler<ItemsPresenter>((x, e) => x.VirtualizationModeChanged(e));
event EventHandler? ILogicalScrollable.ScrollInvalidated
{
add => _scrollInvalidated += value;
remove => _scrollInvalidated -= value;
}
/// <summary>
/// Gets or sets the virtualization mode for the items.
/// Gets or sets a template which creates the <see cref="Panel"/> used to display the items.
/// </summary>
public ItemVirtualizationMode VirtualizationMode
public ITemplate<Panel> ItemsPanel
{
get { return GetValue(VirtualizationModeProperty); }
set { SetValue(VirtualizationModeProperty, value); }
get => GetValue(ItemsPanelProperty);
set => SetValue(ItemsPanelProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// Gets the panel used to display the items.
/// </summary>
bool ILogicalScrollable.CanHorizontallyScroll
public Panel? Panel { get; private set; }
/// <summary>
/// Gets the owner <see cref="ItemsControl"/>.
/// </summary>
internal ItemsControl? ItemsControl { get; private set; }
bool ILogicalScrollable.CanHorizontallyScroll
{
get { return _canHorizontallyScroll; }
get => _logicalScrollable?.CanHorizontallyScroll ?? false;
set
{
_canHorizontallyScroll = value;
InvalidateMeasure();
if (_logicalScrollable is not null)
_logicalScrollable.CanHorizontallyScroll = value;
}
}
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
bool ILogicalScrollable.CanVerticallyScroll
bool ILogicalScrollable.CanVerticallyScroll
{
get { return _canVerticallyScroll; }
get => _logicalScrollable?.CanVerticallyScroll ?? false;
set
{
_canVerticallyScroll = value;
InvalidateMeasure();
if (_logicalScrollable is not null)
_logicalScrollable.CanVerticallyScroll = value;
}
}
/// <inheritdoc/>
bool ILogicalScrollable.IsLogicalScrollEnabled
{
get { return Virtualizer?.IsLogicalScrollEnabled ?? false; }
}
/// <inheritdoc/>
Size IScrollable.Extent => Virtualizer?.Extent ?? default;
/// <inheritdoc/>
Vector IScrollable.Offset
Vector IScrollable.Offset
{
get { return Virtualizer?.Offset ?? new Vector(); }
get => _logicalScrollable?.Offset ?? default;
set
{
if (Virtualizer != null)
{
Virtualizer.Offset = CoerceOffset(value);
}
if (_logicalScrollable is not null)
_logicalScrollable.Offset = value;
}
}
/// <inheritdoc/>
Size IScrollable.Viewport => Virtualizer?.Viewport ?? Bounds.Size;
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;
/// <inheritdoc/>
event EventHandler? ILogicalScrollable.ScrollInvalidated
public override sealed void ApplyTemplate()
{
add => _scrollInvalidated += value;
remove => _scrollInvalidated -= value;
}
if (Panel is null && ItemsControl is not null)
{
if (_logicalScrollable is not null)
{
_logicalScrollable.ScrollInvalidated -= OnLogicalScrollInvalidated;
}
/// <inheritdoc/>
Size ILogicalScrollable.ScrollSize => new Size(ScrollViewer.DefaultSmallChange, 1);
Panel = ItemsPanel.Build();
Panel.SetValue(TemplatedParentProperty, TemplatedParent);
LogicalChildren.Add(Panel);
VisualChildren.Add(Panel);
/// <inheritdoc/>
Size ILogicalScrollable.PageScrollSize => Virtualizer?.Viewport ?? new Size(16, 16);
if (Panel is VirtualizingPanel v)
v.Attach(ItemsControl);
else
CreateSimplePanelGenerator();
internal ItemVirtualizer? Virtualizer { get; private set; }
_logicalScrollable = Panel as ILogicalScrollable;
/// <inheritdoc/>
bool ILogicalScrollable.BringIntoView(Control target, Rect targetRect)
{
return false;
if (_logicalScrollable is not null)
{
_logicalScrollable.ScrollInvalidated += OnLogicalScrollInvalidated;
}
}
}
/// <inheritdoc/>
Control? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, Control? from)
{
return Virtualizer?.GetControlInDirection(direction, from);
}
bool ILogicalScrollable.BringIntoView(Control target, Rect targetRect) =>
_logicalScrollable?.BringIntoView(target, targetRect) ?? false;
Control? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, Control? from) =>
_logicalScrollable?.GetControlInDirection(direction, from);
void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e) => _scrollInvalidated?.Invoke(this, e);
/// <inheritdoc/>
void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e)
internal void ScrollIntoView(int index)
{
_scrollInvalidated?.Invoke(this, e);
if (Panel is VirtualizingPanel v)
v.ScrollIntoView(index);
else if (index >= 0 && index < Panel?.Children.Count)
Panel.Children[index].BringIntoView();
}
public override void ScrollIntoView(int index)
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
Virtualizer?.ScrollIntoView(index);
base.OnPropertyChanged(change);
if (change.Property == TemplatedParentProperty)
{
ResetState();
ItemsControl = null;
if (change.NewValue is ItemsControl itemsControl)
{
ItemsControl = itemsControl;
ItemsControl.RegisterItemsPresenter(this);
}
}
else if (change.Property == ItemsPanelProperty)
{
ResetState();
InvalidateMeasure();
}
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
internal void Refresh()
{
return Virtualizer?.MeasureOverride(availableSize) ?? default;
if (Panel is VirtualizingPanel v)
v.Refresh();
else
_generator?.Refresh();
}
protected override Size ArrangeOverride(Size finalSize)
private void ResetState()
{
return Virtualizer?.ArrangeOverride(finalSize) ?? default;
_generator?.Dispose();
_generator = null;
LogicalChildren.Clear();
VisualChildren.Clear();
(Panel as VirtualizingPanel)?.Detach();
Panel = null;
}
/// <inheritdoc/>
protected override void PanelCreated(Panel panel)
private void CreateSimplePanelGenerator()
{
Virtualizer?.Dispose();
Virtualizer = ItemVirtualizer.Create(this);
_scrollInvalidated?.Invoke(this, EventArgs.Empty);
Debug.Assert(Panel is not VirtualizingPanel);
KeyboardNavigation.SetTabNavigation(
(InputElement)panel,
KeyboardNavigation.GetTabNavigation(this));
if (ItemsControl is null || Panel is null)
return;
_generator?.Dispose();
_generator = new(this);
}
protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
internal Control? ContainerFromIndex(int index)
{
Virtualizer?.ItemsChanged(Items, e);
if (Panel is VirtualizingPanel v)
return v.ContainerFromIndex(index);
return index >= 0 && index < Panel?.Children.Count ? Panel.Children[index] : null;
}
private Vector CoerceOffset(Vector value)
internal IEnumerable<Control>? GetRealizedContainers()
{
var scrollable = (ILogicalScrollable)this;
var maxX = Math.Max(scrollable.Extent.Width - scrollable.Viewport.Width, 0);
var maxY = Math.Max(scrollable.Extent.Height - scrollable.Viewport.Height, 0);
return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY));
if (Panel is VirtualizingPanel v)
return v.GetRealizedContainers();
return Panel?.Children;
}
private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e)
internal int IndexFromContainer(Control container)
{
Virtualizer?.Dispose();
Virtualizer = ItemVirtualizer.Create(this);
_scrollInvalidated?.Invoke(this, EventArgs.Empty);
if (Panel is VirtualizingPanel v)
return v.IndexFromContainer(container);
return Panel?.Children.IndexOf(container) ?? -1;
}
private void OnLogicalScrollInvalidated(object? sender, EventArgs e) => _scrollInvalidated?.Invoke(this, e);
}
}

308
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@ -1,308 +0,0 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Base class for controls that present items inside an <see cref="ItemsControl"/>.
/// </summary>
public abstract class ItemsPresenterBase : Control, IItemsPresenter, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="Items"/> property.
/// </summary>
public static readonly DirectProperty<ItemsPresenterBase, IEnumerable?> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<ItemsPresenterBase>(o => o.Items, (o, v) => o.Items = v);
/// <summary>
/// Defines the <see cref="ItemsPanel"/> property.
/// </summary>
public static readonly StyledProperty<ITemplate<Panel>> ItemsPanelProperty =
ItemsControl.ItemsPanelProperty.AddOwner<ItemsPresenterBase>();
/// <summary>
/// Defines the <see cref="ItemTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
ItemsControl.ItemTemplateProperty.AddOwner<ItemsPresenterBase>();
/// <summary>
/// Defines the <see cref="DisplayMemberBinding" /> property
/// </summary>
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
ItemsControl.DisplayMemberBindingProperty.AddOwner<ItemsPresenterBase>();
private IEnumerable? _items;
private IDisposable? _itemsSubscription;
private bool _createdPanel;
private IItemContainerGenerator? _generator;
private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
/// <summary>
/// Initializes static members of the <see cref="ItemsPresenter"/> class.
/// </summary>
static ItemsPresenterBase()
{
TemplatedParentProperty.Changed.AddClassHandler<ItemsPresenterBase>((x,e) => x.TemplatedParentChanged(e));
}
/// <summary>
/// Gets or sets the items to be displayed.
/// </summary>
public IEnumerable? Items
{
get
{
return _items;
}
set
{
_itemsSubscription?.Dispose();
_itemsSubscription = null;
if (!IsHosted && _createdPanel && value is INotifyCollectionChanged incc)
{
_itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
}
SetAndRaise(ItemsProperty, ref _items, value);
if (_createdPanel)
{
ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
}
/// <summary>
/// Gets the item container generator.
/// </summary>
public IItemContainerGenerator ItemContainerGenerator
{
get
{
if (_generator == null)
{
_generator = CreateItemContainerGenerator();
}
return _generator;
}
internal set
{
if (_generator != null)
{
throw new InvalidOperationException("ItemContainerGenerator already created.");
}
_generator = value;
}
}
/// <summary>
/// Gets or sets a template which creates the <see cref="Panel"/> used to display the items.
/// </summary>
public ITemplate<Panel> ItemsPanel
{
get { return GetValue(ItemsPanelProperty); }
set { SetValue(ItemsPanelProperty, value); }
}
/// <summary>
/// Gets or sets the data template used to display the items in the control.
/// </summary>
public IDataTemplate? ItemTemplate
{
get { return GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
/// <summary>
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
/// </summary>
public IBinding? DisplayMemberBinding
{
get { return GetValue(DisplayMemberBindingProperty); }
set { SetValue(DisplayMemberBindingProperty, value); }
}
/// <summary>
/// Gets the panel used to display the items.
/// </summary>
public Panel? Panel
{
get;
private set;
}
protected bool IsHosted => TemplatedParent is IItemsPresenterHost;
event EventHandler<ChildIndexChangedEventArgs>? IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
/// <inheritdoc/>
public override sealed void ApplyTemplate()
{
if (!_createdPanel)
{
CreatePanel();
}
}
/// <inheritdoc/>
public virtual void ScrollIntoView(int index)
{
}
/// <inheritdoc/>
void IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e)
{
if (Panel != null)
{
ItemsChanged(e);
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
}
}
/// <summary>
/// Creates the <see cref="ItemContainerGenerator"/> for the control.
/// </summary>
/// <returns>
/// An <see cref="IItemContainerGenerator"/> or null.
/// </returns>
protected virtual IItemContainerGenerator CreateItemContainerGenerator()
{
var i = TemplatedParent as ItemsControl;
var result = i?.ItemContainerGenerator;
if (result == null)
{
result = new ItemContainerGenerator(this);
result.ItemTemplate = ItemTemplate;
result.DisplayMemberBinding = DisplayMemberBinding;
}
result.Materialized += ContainerActionHandler;
result.Dematerialized += ContainerActionHandler;
result.Recycled += ContainerActionHandler;
return result;
}
private void ContainerActionHandler(object? sender, ItemContainerEventArgs e)
{
for (var i = 0; i < e.Containers.Count; i++)
{
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl));
}
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
Panel!.Measure(availableSize);
return Panel.DesiredSize;
}
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
Panel!.Arrange(new Rect(finalSize));
return finalSize;
}
/// <summary>
/// Called when the <see cref="Panel"/> is created.
/// </summary>
/// <param name="panel">The panel.</param>
protected virtual void PanelCreated(Panel panel)
{
}
/// <summary>
/// Called when the items for the presenter change, either because <see cref="Items"/>
/// has been set, the items collection has been modified, or the panel has been created.
/// </summary>
/// <param name="e">A description of the change.</param>
/// <remarks>
/// The panel is guaranteed to be created when this method is called.
/// </remarks>
protected virtual void ItemsChanged(NotifyCollectionChangedEventArgs e)
{
ItemContainerSync.ItemsChanged(this, Items, e);
}
/// <summary>
/// Creates the <see cref="Panel"/> when <see cref="ApplyTemplate"/> is called for the first
/// time.
/// </summary>
private void CreatePanel()
{
Panel = ItemsPanel.Build();
Panel.SetValue(TemplatedParentProperty, TemplatedParent);
LogicalChildren.Clear();
VisualChildren.Clear();
LogicalChildren.Add(Panel);
VisualChildren.Add(Panel);
_createdPanel = true;
if (!IsHosted && _itemsSubscription == null && Items is INotifyCollectionChanged incc)
{
_itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
}
PanelCreated(Panel);
}
/// <summary>
/// Called when the <see cref="Items"/> collection changes.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (_createdPanel)
{
ItemsChanged(e);
}
}
private void TemplatedParentChanged(AvaloniaPropertyChangedEventArgs e)
{
(e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this);
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
if (child is Control control && ItemContainerGenerator is { } generator)
{
var index = ItemContainerGenerator.IndexFromContainer(control);
return index;
}
return -1;
}
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
return Items.TryGetCountFast(out count);
}
}
}

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

@ -0,0 +1,160 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using Avalonia.Controls.Utils;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Generates containers for <see cref="ItemsPresenter"/>s that have non-virtualizing panels.
/// </summary>
internal class PanelContainerGenerator : IDisposable
{
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
AvaloniaProperty.RegisterAttached<PanelContainerGenerator, Control, bool>("ItemIsOwnContainer");
private readonly ItemsPresenter _presenter;
public PanelContainerGenerator(ItemsPresenter presenter)
{
Debug.Assert(presenter.ItemsControl is not null);
Debug.Assert(presenter.Panel is not null or VirtualizingPanel);
_presenter = presenter;
_presenter.ItemsControl.PropertyChanged += OnItemsControlPropertyChanged;
_presenter.ItemsControl.ItemsView.PostCollectionChanged += OnItemsChanged;
OnItemsChanged(null, CollectionUtils.ResetEventArgs);
}
public void Dispose()
{
if (_presenter.ItemsControl is { } itemsControl)
{
itemsControl.PropertyChanged -= OnItemsControlPropertyChanged;
itemsControl.ItemsView.PostCollectionChanged -= OnItemsChanged;
ClearItemsControlLogicalChildren();
}
_presenter.Panel?.Children.Clear();
}
internal void Refresh() => OnItemsChanged(null, CollectionUtils.ResetEventArgs);
private void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ItemsControl.ItemsProperty)
{
if (e.OldValue is INotifyCollectionChanged inccOld)
inccOld.CollectionChanged -= OnItemsChanged;
OnItemsChanged(null, CollectionUtils.ResetEventArgs);
if (e.NewValue is INotifyCollectionChanged inccNew)
inccNew.CollectionChanged += OnItemsChanged;
}
}
private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (_presenter.Panel is null || _presenter.ItemsControl is null)
return;
var itemsControl = _presenter.ItemsControl;
var generator = itemsControl.ItemContainerGenerator;
var children = _presenter.Panel.Children;
void Add(int index, IEnumerable items)
{
var i = index;
foreach (var item in items)
InsertContainer(itemsControl, children, item, i++);
var childCount = children.Count;
var delta = i - index;
for (; i < childCount; ++i)
generator.ItemContainerIndexChanged(children[i], i - delta, i);
}
void Remove(int index, int count)
{
for (var i = 0; i < count; ++i)
{
var c = children[index + i];
if (!c.IsSet(ItemIsOwnContainerProperty))
itemsControl.RemoveLogicalChild(children[i + index]);
else
generator.ClearItemContainer(c);
}
children.RemoveRange(index, count);
var childCount = children.Count;
for (var i = index; i < childCount; ++i)
generator.ItemContainerIndexChanged(children[i], i + count, i);
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Add(e.NewStartingIndex, e.NewItems!);
break;
case NotifyCollectionChangedAction.Remove:
Remove(e.OldStartingIndex, e.OldItems!.Count);
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
Remove(e.OldStartingIndex, e.OldItems!.Count);
Add(e.NewStartingIndex, e.NewItems!);
break;
case NotifyCollectionChangedAction.Reset:
ClearItemsControlLogicalChildren();
children.Clear();
Add(0, _presenter.ItemsControl.ItemsView);
break;
}
}
private static void InsertContainer(
ItemsControl itemsControl,
Controls children,
object? item,
int index)
{
var generator = itemsControl.ItemContainerGenerator;
Control container;
if (item is Control c && generator.IsItemItsOwnContainer(c))
{
container = c;
container.SetValue(ItemIsOwnContainerProperty, true);
}
else
{
container = generator.CreateContainer();
}
generator.PrepareItemContainer(container, item, index);
itemsControl.AddLogicalChild(container);
children.Insert(index, container);
generator.ItemContainerPrepared(container, item, index);
}
private void ClearItemsControlLogicalChildren()
{
if (_presenter.Panel is null || _presenter.ItemsControl is null)
return;
var itemsControl = _presenter.ItemsControl;
var panel = _presenter.Panel;
foreach (var c in panel.Children)
{
if (!c.IsSet(ItemIsOwnContainerProperty))
itemsControl.RemoveLogicalChild(c);
}
}
}
}

16
src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs

@ -1,5 +1,6 @@
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
namespace Avalonia.Controls.Primitives
@ -15,6 +16,12 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<object?> HeaderProperty =
HeaderedContentControl.HeaderProperty.AddOwner<HeaderedItemsControl>();
/// <summary>
/// Defines the <see cref="HeaderTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> HeaderTemplateProperty =
AvaloniaProperty.Register<HeaderedItemsControl, IDataTemplate?>(nameof(HeaderTemplate));
/// <summary>
/// Initializes static members of the <see cref="ContentControl"/> class.
/// </summary>
@ -32,6 +39,15 @@ namespace Avalonia.Controls.Primitives
set { SetValue(HeaderProperty, value); }
}
/// <summary>
/// Gets or sets the data template used to display the header content of the control.
/// </summary>
public IDataTemplate? HeaderTemplate
{
get => GetValue(HeaderTemplateProperty);
set => SetValue(HeaderTemplateProperty, value);
}
/// <summary>
/// Gets the header presenter from the control's template.
/// </summary>

91
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Xml.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Selection;
using Avalonia.Data;
@ -377,7 +378,7 @@ namespace Avalonia.Controls.Primitives
/// Scrolls the specified item into view.
/// </summary>
/// <param name="item">The item.</param>
public void ScrollIntoView(object item) => ScrollIntoView(IndexOf(Items, item));
public void ScrollIntoView(object item) => ScrollIntoView(ItemsView.IndexOf(item));
/// <summary>
/// Tries to get the container that was the source of an event.
@ -389,7 +390,7 @@ namespace Avalonia.Controls.Primitives
for (var current = eventSource as Visual; current != null; current = current.VisualParent)
{
if (current is Control control && control.Parent == this &&
ItemContainerGenerator?.IndexFromContainer(control) != -1)
IndexFromContainer(control) != -1)
{
return control;
}
@ -430,54 +431,40 @@ namespace Avalonia.Controls.Primitives
}
}
/// <inheritdoc/>
protected override void OnContainersMaterialized(ItemContainerEventArgs e)
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
base.OnContainersMaterialized(e);
base.PrepareContainerForItemOverride(element, item, index);
foreach (var container in e.Containers)
if ((element as ISelectable)?.IsSelected == true)
{
if ((container.ContainerControl as ISelectable)?.IsSelected == true)
{
Selection.Select(container.Index);
MarkContainerSelected(container.ContainerControl, true);
}
else
{
var selected = Selection.IsSelected(container.Index);
MarkContainerSelected(container.ContainerControl, selected);
}
Selection.Select(index);
MarkContainerSelected(element, true);
}
else
{
var selected = Selection.IsSelected(index);
MarkContainerSelected(element, selected);
}
}
/// <inheritdoc/>
protected override void OnContainersDematerialized(ItemContainerEventArgs e)
protected override void ContainerIndexChangedOverride(Control container, int oldIndex, int newIndex)
{
base.OnContainersDematerialized(e);
if (Presenter?.Panel is InputElement panel)
{
foreach (var container in e.Containers)
{
if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl)
{
KeyboardNavigation.SetTabOnceActiveElement(panel, null);
break;
}
}
}
base.ContainerIndexChangedOverride(container, oldIndex, newIndex);
MarkContainerSelected(container, Selection.IsSelected(newIndex));
}
protected override void OnContainersRecycled(ItemContainerEventArgs e)
protected internal override void ClearContainerForItemOverride(Control element)
{
foreach (var i in e.Containers)
base.ClearContainerForItemOverride(element);
if (Presenter?.Panel is InputElement panel &&
KeyboardNavigation.GetTabOnceActiveElement(panel) == element)
{
if (i.ContainerControl != null && i.Item != null)
{
bool selected = Selection.IsSelected(i.Index);
MarkContainerSelected(i.ContainerControl, selected);
}
KeyboardNavigation.SetTabOnceActiveElement(panel, null);
}
if (element is ISelectable selectable)
MarkContainerSelected(element, false);
}
/// <inheritdoc/>
@ -533,9 +520,9 @@ namespace Avalonia.Controls.Primitives
_textSearchTerm += e.Text;
bool Match(ItemContainerInfo info)
bool Match(Control container)
{
if (info.ContainerControl is AvaloniaObject ao && ao.IsSet(TextSearch.TextProperty))
if (container is AvaloniaObject ao && ao.IsSet(TextSearch.TextProperty))
{
var searchText = ao.GetValue(TextSearch.TextProperty);
@ -545,15 +532,15 @@ namespace Avalonia.Controls.Primitives
}
}
return info.ContainerControl is IContentControl control &&
return container is IContentControl control &&
control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true;
}
var info = ItemContainerGenerator?.Containers.FirstOrDefault(Match);
if (info != null)
var container = GetRealizedContainers().FirstOrDefault(Match);
if (container != null)
{
SelectedIndex = info.Index;
SelectedIndex = IndexFromContainer(container);
}
StartTextSearchTimer();
@ -632,7 +619,7 @@ namespace Avalonia.Controls.Primitives
/// <returns>True if the selection was moved; otherwise false.</returns>
protected bool MoveSelection(NavigationDirection direction, bool wrap)
{
var from = SelectedIndex != -1 ? ItemContainerGenerator?.ContainerFromIndex(SelectedIndex) : null;
var from = SelectedIndex != -1 ? ContainerFromIndex(SelectedIndex) : null;
return MoveSelection(from, direction, wrap);
}
@ -648,7 +635,7 @@ namespace Avalonia.Controls.Primitives
if (Presenter?.Panel is INavigableContainer container &&
GetNextControl(container, direction, from, wrap) is Control next)
{
var index = ItemContainerGenerator?.IndexFromContainer(next) ?? -1;
var index = IndexFromContainer(next);
if (index != -1)
{
@ -731,7 +718,7 @@ namespace Avalonia.Controls.Primitives
if (Presenter?.Panel != null)
{
var container = ItemContainerGenerator?.ContainerFromIndex(index);
var container = ContainerFromIndex(index);
KeyboardNavigation.SetTabOnceActiveElement(
(InputElement)Presenter.Panel,
container);
@ -755,7 +742,7 @@ namespace Avalonia.Controls.Primitives
bool rightButton = false,
bool fromFocus = false)
{
var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1;
var index = IndexFromContainer(container);
if (index != -1)
{
@ -840,7 +827,7 @@ namespace Avalonia.Controls.Primitives
{
void Mark(int index, bool selected)
{
var container = ItemContainerGenerator?.ContainerFromIndex(index);
var container = ContainerFromIndex(index);
if (container != null)
{
@ -907,7 +894,7 @@ namespace Avalonia.Controls.Primitives
e.Source is Control control &&
e.Source is ISelectable selectable &&
control.Parent == this &&
ItemContainerGenerator?.IndexFromContainer(control) != -1)
IndexFromContainer(control) != -1)
{
UpdateSelection(control, selectable.IsSelected);
}
@ -959,7 +946,7 @@ namespace Avalonia.Controls.Primitives
{
MarkContainerSelected(
container,
Selection.IsSelected(ItemContainerGenerator.IndexFromContainer(container)));
Selection.IsSelected(IndexFromContainer(container)));
}
}
}

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

@ -18,13 +18,8 @@ namespace Avalonia.Controls.Primitives
ItemsPanelProperty.OverrideDefaultValue<TabStrip>(DefaultPanel);
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<TabStripItem>(
this,
ContentControl.ContentProperty,
ContentControl.ContentTemplateProperty);
}
protected internal override Control CreateContainerForItemOverride() => new TabStripItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabStripItem;
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)

6
src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs

@ -151,7 +151,11 @@ namespace Avalonia.Controls
_content.Loaded += (s, e) =>
{
var composition = ElementComposition.GetElementVisual(_content);
var compositor = composition!.Compositor;
if(composition == null)
return;
var compositor = composition.Compositor;
composition.Opacity = 0;
var smoothRotationAnimation

3
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -442,7 +442,7 @@ namespace Avalonia.Controls
var newDataSource = newEnumerable as ItemsSourceView;
if (newEnumerable != null && newDataSource == null)
{
newDataSource = new ItemsSourceView(newEnumerable);
newDataSource = ItemsSourceView.GetOrCreate(newEnumerable);
}
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
@ -628,7 +628,6 @@ namespace Avalonia.Controls
oldValue.CollectionChanged -= OnItemsSourceViewChanged;
}
ItemsSourceView?.Dispose();
ItemsSourceView = newValue;
if (newValue != null)

1
src/Avalonia.Controls/Selection/SelectionModel.cs

@ -439,6 +439,7 @@ namespace Avalonia.Controls.Selection
if ((e.Action == NotifyCollectionChangedAction.Remove && e.OldStartingIndex <= oldSelectedIndex) ||
(e.Action == NotifyCollectionChangedAction.Replace && e.OldStartingIndex == oldSelectedIndex) ||
(e.Action == NotifyCollectionChangedAction.Move && e.OldStartingIndex == oldSelectedIndex) ||
e.Action == NotifyCollectionChangedAction.Reset)
{
RaisePropertyChanged(nameof(SelectedItem));

9
src/Avalonia.Controls/Selection/SelectionNodeBase.cs

@ -21,10 +21,12 @@ namespace Avalonia.Controls.Selection
{
if (_source != value)
{
ItemsView?.RemoveListener(this);
if (ItemsView?.Inner is INotifyCollectionChanged inccOld)
CollectionChangedEventManager.Instance.RemoveListener(inccOld, this);
_source = value;
ItemsView = value is object ? ItemsSourceView<T>.GetOrCreate(value) : null;
ItemsView?.AddListener(this);
ItemsView = value is object ? ItemsSourceView.GetOrCreate<T>(value) : null;
if (ItemsView?.Inner is INotifyCollectionChanged inccNew)
CollectionChangedEventManager.Instance.AddListener(inccNew, this);
}
}
}
@ -254,6 +256,7 @@ namespace Avalonia.Controls.Selection
break;
}
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
{
var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems!);
var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems!);

64
src/Avalonia.Controls/TabControl.cs

@ -57,12 +57,6 @@ namespace Avalonia.Controls
/// </summary>
public static readonly StyledProperty<IDataTemplate?> SelectedContentTemplateProperty =
AvaloniaProperty.Register<TabControl, IDataTemplate?>(nameof(SelectedContentTemplate));
/// <summary>
/// Defines the <see cref="HeaderDisplayMemberBinding" /> property
/// </summary>
public static readonly StyledProperty<IBinding?> HeaderDisplayMemberBindingProperty =
AvaloniaProperty.Register<HeaderedItemsControl, IBinding?>(nameof(HeaderDisplayMemberBinding));
/// <summary>
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
@ -141,16 +135,6 @@ namespace Avalonia.Controls
get { return GetValue(SelectedContentTemplateProperty); }
internal set { SetValue(SelectedContentTemplateProperty, value); }
}
/// <summary>
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each tab-items header.
/// </summary>
[AssignBinding]
public IBinding? HeaderDisplayMemberBinding
{
get { return GetValue(HeaderDisplayMemberBindingProperty); }
set { SetValue(HeaderDisplayMemberBindingProperty, value); }
}
internal ItemsPresenter? ItemsPresenterPart { get; private set; }
@ -165,15 +149,40 @@ namespace Avalonia.Controls
return RegisterContentPresenter(presenter);
}
protected override void OnContainersMaterialized(ItemContainerEventArgs e)
protected internal override Control CreateContainerForItemOverride() => new TabItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabItem;
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
base.OnContainersMaterialized(e);
UpdateSelectedContent();
base.PrepareContainerForItemOverride(element, item, index);
if (element is TabItem tabItem)
{
if (ContentTemplate is { } ct)
tabItem.ContentTemplate = ct;
tabItem.SetValue(TabStripPlacementProperty, TabStripPlacement);
}
if (index == SelectedIndex && element is ContentControl container)
{
SelectedContentTemplate = container.ContentTemplate;
SelectedContent = container.Content;
}
}
protected override void OnContainersRecycled(ItemContainerEventArgs e)
protected override void ContainerIndexChangedOverride(Control container, int oldIndex, int newIndex)
{
base.OnContainersRecycled(e);
base.ContainerIndexChangedOverride(container, oldIndex, newIndex);
var selectedIndex = SelectedIndex;
if (selectedIndex == oldIndex || selectedIndex == newIndex)
UpdateSelectedContent();
}
protected internal override void ClearContainerForItemOverride(Control element)
{
base.ClearContainerForItemOverride(element);
UpdateSelectedContent();
}
@ -207,11 +216,6 @@ namespace Avalonia.Controls
return false;
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new TabItemContainerGenerator(this);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
ItemsPresenterPart = e.NameScope.Get<ItemsPresenter>("PART_ItemsPresenter");
@ -252,5 +256,13 @@ namespace Avalonia.Controls
}
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == TabStripPlacementProperty)
RefreshContainers();
}
}
}

15
src/Avalonia.Controls/TextBlock.cs

@ -826,12 +826,12 @@ namespace Avalonia.Controls
protected readonly record struct SimpleTextSource : ITextSource
{
private readonly CharacterBufferRange _text;
private readonly string _text;
private readonly TextRunProperties _defaultProperties;
public SimpleTextSource(string text, TextRunProperties defaultProperties)
{
_text = new CharacterBufferRange(new CharacterBufferReference(text), text.Length);
_text = text;
_defaultProperties = defaultProperties;
}
@ -842,14 +842,14 @@ namespace Avalonia.Controls
return new TextEndOfParagraph();
}
var runText = _text.Skip(textSourceIndex);
var runText = _text.AsMemory(textSourceIndex);
if (runText.IsEmpty)
{
return new TextEndOfParagraph();
}
return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultProperties);
return new TextCharacters(runText, _defaultProperties);
}
}
@ -884,14 +884,9 @@ namespace Avalonia.Controls
if (textRun is TextCharacters)
{
var characterBufferReference = textRun.CharacterBufferReference;
var skip = Math.Max(0, textSourceIndex - currentPosition);
return new TextCharacters(
new CharacterBufferReference(characterBufferReference.CharacterBuffer, characterBufferReference.OffsetToFirstChar + skip),
textRun.Length - skip,
textRun.Properties!);
return new TextCharacters(textRun.Text.Slice(skip), textRun.Properties!);
}
return textRun;

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

Loading…
Cancel
Save