Browse Source

Merge branch 'master' of https://github.com/AvaloniaUI/Avalonia into add_common_math_operators_to_vector

pull/2761/head
JaggerJo 7 years ago
parent
commit
cc194726b2
  1. 3
      .editorconfig
  2. 1
      azure-pipelines.yml
  3. 2
      build/SkiaSharp.props
  4. 20
      native/Avalonia.Native/src/OSX/AvnString.mm
  5. 11
      native/Avalonia.Native/src/OSX/clipboard.mm
  6. 31
      samples/ControlCatalog.NetCore/Program.cs
  7. 2
      samples/ControlCatalog/DecoratedWindow.xaml.cs
  8. 25
      samples/ControlCatalog/MainView.xaml
  9. 1
      samples/ControlCatalog/MainWindow.xaml
  10. 2
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  11. 26
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  12. 81
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  13. 15
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  14. 52
      samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
  15. 9
      samples/ControlCatalog/Pages/PointersPage.cs
  16. 5
      samples/ControlCatalog/Pages/ScreenPage.cs
  17. 33
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  18. 93
      samples/ControlCatalog/Pages/TreeViewPage.xaml.cs
  19. 26
      samples/ControlCatalog/SideBar.xaml
  20. 27
      samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
  21. 3
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  22. 1
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  23. 5
      src/Android/Avalonia.Android/AndroidPlatform.cs
  24. 112
      src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs
  25. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  26. 103
      src/Avalonia.Base/AvaloniaObject.cs
  27. 9
      src/Avalonia.Base/IPriorityValueOwner.cs
  28. 34
      src/Avalonia.Base/PriorityValue.cs
  29. 150
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  30. 206
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  31. 152
      src/Avalonia.Base/ValueStore.cs
  32. 1
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  33. 3
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  34. 17
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  35. 2
      src/Avalonia.Controls/ComboBox.cs
  36. 3
      src/Avalonia.Controls/ContextMenu.cs
  37. 1
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  38. 1
      src/Avalonia.Controls/GridSplitter.cs
  39. 9
      src/Avalonia.Controls/IScrollAnchorProvider.cs
  40. 2
      src/Avalonia.Controls/Image.cs
  41. 2
      src/Avalonia.Controls/LayoutTransformControl.cs
  42. 1
      src/Avalonia.Controls/Menu.cs
  43. 2
      src/Avalonia.Controls/MenuItem.cs
  44. 1
      src/Avalonia.Controls/Mixins/ContentControlMixin.cs
  45. 2
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  46. 2
      src/Avalonia.Controls/Panel.cs
  47. 19
      src/Avalonia.Controls/PlacementMode.cs
  48. 2
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  49. 4
      src/Avalonia.Controls/Platform/IPopupImpl.cs
  50. 2
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  51. 24
      src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
  52. 27
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  53. 1
      src/Avalonia.Controls/Platform/IWindowingPlatform.cs
  54. 3
      src/Avalonia.Controls/Platform/InProcessDragSource.cs
  55. 85
      src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
  56. 5
      src/Avalonia.Controls/Platform/PlatformManager.cs
  57. 7
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  58. 1
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  59. 19
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  60. 42
      src/Avalonia.Controls/Primitives/AdornerDecorator.cs
  61. 2
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  62. 26
      src/Avalonia.Controls/Primitives/IPopupHost.cs
  63. 38
      src/Avalonia.Controls/Primitives/OverlayLayer.cs
  64. 149
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  65. 284
      src/Avalonia.Controls/Primitives/Popup.cs
  66. 358
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  67. 175
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  68. 50
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
  69. 119
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  70. 1
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  71. 5
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  72. 1
      src/Avalonia.Controls/Primitives/TabStrip.cs
  73. 1
      src/Avalonia.Controls/Primitives/Track.cs
  74. 93
      src/Avalonia.Controls/Primitives/VisualLayerManager.cs
  75. 7
      src/Avalonia.Controls/ProgressBar.cs
  76. 54
      src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs
  77. 724
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  78. 24
      src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs
  79. 44
      src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs
  80. 35
      src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs
  81. 145
      src/Avalonia.Controls/Repeater/ItemsSourceView.cs
  82. 106
      src/Avalonia.Controls/Repeater/RecyclePool.cs
  83. 65
      src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs
  84. 54
      src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs
  85. 682
      src/Avalonia.Controls/Repeater/ViewManager.cs
  86. 501
      src/Avalonia.Controls/Repeater/ViewportManager.cs
  87. 118
      src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
  88. 15
      src/Avalonia.Controls/ScrollViewer.cs
  89. 1
      src/Avalonia.Controls/Slider.cs
  90. 149
      src/Avalonia.Controls/StackPanel.cs
  91. 29
      src/Avalonia.Controls/TextBlock.cs
  92. 14
      src/Avalonia.Controls/ToolTip.cs
  93. 45
      src/Avalonia.Controls/Window.cs
  94. 44
      src/Avalonia.Controls/WindowBase.cs
  95. 1
      src/Avalonia.Controls/WrapPanel.cs
  96. 5
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  97. 4
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  98. 23
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  99. 36
      src/Avalonia.Diagnostics/DevTools.xaml
  100. 89
      src/Avalonia.Diagnostics/DevTools.xaml.cs

3
.editorconfig

@ -131,13 +131,14 @@ csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
space_within_single_line_array_initializer_braces = true
# Wrapping preferences
csharp_wrap_before_ternary_opsigns = false
# Xaml files
[*.xaml]
indent_size = 4
indent_size = 2
# Xml project files
[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]

1
azure-pipelines.yml

@ -134,3 +134,4 @@ jobs:
pathToPublish: '$(Build.SourcesDirectory)/artifacts/zip'
artifactName: 'Samples'
condition: succeeded()

2
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.68.0" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.2" />
</ItemGroup>
</Project>

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

@ -11,14 +11,26 @@
class AvnStringImpl : public virtual ComSingleObject<IAvnString, &IID_IAvnString>
{
private:
NSString* _string;
int _length;
const char* _cstring;
public:
FORWARD_IUNKNOWN()
AvnStringImpl(NSString* string)
{
auto cstring = [string cStringUsingEncoding:NSUTF8StringEncoding];
_length = (int)[string lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
_cstring = (const char*)malloc(_length + 5);
memset((void*)_cstring, 0, _length + 5);
memcpy((void*)_cstring, (void*)cstring, _length);
}
virtual ~AvnStringImpl()
{
_string = string;
free((void*)_cstring);
}
virtual HRESULT Pointer(void**retOut) override
@ -30,7 +42,7 @@ public:
return E_POINTER;
}
*retOut = (void*)_string.UTF8String;
*retOut = (void*)_cstring;
return S_OK;
}
@ -43,7 +55,7 @@ public:
return E_POINTER;
}
*retOut = (int)_string.length;
*retOut = _length;
return S_OK;
}

11
native/Avalonia.Native/src/OSX/clipboard.mm

@ -8,6 +8,13 @@ class Clipboard : public ComSingleObject<IAvnClipboard, &IID_IAvnClipboard>
{
public:
FORWARD_IUNKNOWN()
Clipboard()
{
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
[pasteBoard stringForType:NSPasteboardTypeString];
}
virtual HRESULT GetText (IAvnString**ppv) override
{
@autoreleasepool
@ -39,7 +46,9 @@ public:
{
@autoreleasepool
{
[[NSPasteboard generalPasteboard] clearContents];
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
[pasteBoard clearContents];
[pasteBoard setString:@"" forType:NSPasteboardTypeString];
}
return S_OK;

31
samples/ControlCatalog.NetCore/Program.cs

@ -1,9 +1,11 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Skia;
using Avalonia.ReactiveUI;
@ -27,10 +29,24 @@ namespace ControlCatalog.NetCore
}
var builder = BuildAvaloniaApp();
double GetScaling()
{
var idx = Array.IndexOf(args, "--scaling");
if (idx != 0 && args.Length > idx + 1 &&
double.TryParse(args[idx + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out var scaling))
return scaling;
return 1;
}
if (args.Contains("--fbdev"))
{
System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer());
return builder.StartLinuxFramebuffer(args);
SilenceConsole();
return builder.StartLinuxFbDev(args, scaling: GetScaling());
}
else if (args.Contains("--drm"))
{
SilenceConsole();
return builder.StartLinuxDrm(args, scaling: GetScaling());
}
else
return builder.StartWithClassicDesktopLifetime(args);
@ -51,11 +67,14 @@ namespace ControlCatalog.NetCore
.UseSkia()
.UseReactiveUI();
static void ConsoleSilencer()
static void SilenceConsole()
{
Console.CursorVisible = false;
while (true)
Console.ReadKey(true);
new Thread(() =>
{
Console.CursorVisible = false;
while (true)
Console.ReadKey(true);
}) {IsBackground = true}.Start();
}
}
}

2
samples/ControlCatalog/DecoratedWindow.xaml.cs

@ -34,7 +34,7 @@ namespace ControlCatalog
SetupSide("Left", StandardCursorType.LeftSide, WindowEdge.West);
SetupSide("Right", StandardCursorType.RightSide, WindowEdge.East);
SetupSide("Top", StandardCursorType.TopSide, WindowEdge.North);
SetupSide("Bottom", StandardCursorType.BottomSize, WindowEdge.South);
SetupSide("Bottom", StandardCursorType.BottomSide, WindowEdge.South);
SetupSide("TopLeft", StandardCursorType.TopLeftCorner, WindowEdge.NorthWest);
SetupSide("TopRight", StandardCursorType.TopRightCorner, WindowEdge.NorthEast);
SetupSide("BottomLeft", StandardCursorType.BottomLeftCorner, WindowEdge.SouthWest);

25
samples/ControlCatalog/MainView.xaml

@ -6,10 +6,13 @@
Foreground="{DynamicResource ThemeForegroundBrush}"
FontSize="{DynamicResource FontSizeNormal}">
<Grid>
<ComboBox x:Name="Themes" SelectedIndex="0" Width="100" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<ComboBoxItem>Light</ComboBoxItem>
<ComboBoxItem>Dark</ComboBoxItem>
</ComboBox>
<Grid.Styles>
<Style Selector="TextBlock.h2">
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="MaxWidth" Value="400"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
</Grid.Styles>
<TabControl Classes="sidebar" Name="Sidebar">
<TabItem Header="AutoCompleteBox"><pages:AutoCompleteBoxPage/></TabItem>
<TabItem Header="Border"><pages:BorderPage/></TabItem>
@ -21,11 +24,17 @@
<TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem>
<TabItem Header="ComboBox"><pages:ComboBoxPage/></TabItem>
<TabItem Header="ContextMenu"><pages:ContextMenuPage/></TabItem>
<TabItem Header="DataGrid"><pages:DataGridPage/></TabItem>
<!-- DataGrid is our special snowflake -->
<TabItem Header="DataGrid"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<pages:DataGridPage/>
</TabItem>
<TabItem Header="DatePicker"><pages:DatePickerPage/></TabItem>
<TabItem Header="Drag+Drop"><pages:DragAndDropPage/></TabItem>
<TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
<TabItem Header="Image"><pages:ImagePage/></TabItem>
<TabItem Header="ItemsRepeater"><pages:ItemsRepeaterPage/></TabItem>
<TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>
<TabItem Header="ListBox"><pages:ListBoxPage/></TabItem>
<TabItem Header="Menu"><pages:MenuPage/></TabItem>
@ -41,6 +50,12 @@
<TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
<TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
<TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>
<TabControl.Tag>
<ComboBox x:Name="Themes" SelectedIndex="0" Width="100" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<ComboBoxItem>Light</ComboBoxItem>
<ComboBoxItem>Dark</ComboBoxItem>
</ComboBox>
</TabControl.Tag>
</TabControl>
</Grid>
</UserControl>

1
samples/ControlCatalog/MainWindow.xaml

@ -1,4 +1,5 @@
<Window xmlns="https://github.com/avaloniaui" MinWidth="500" MinHeight="300"
Width="1024" Height="800"
xmlns:pages="clr-namespace:ControlCatalog.Pages"
Title="Avalonia Control Gallery"
Icon="/Assets/test_icon.ico"

2
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@ -29,7 +29,7 @@ namespace ControlCatalog.Pages
DataObject dragData = new DataObject();
dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times");
var result = await DragDrop.DoDragDrop(dragData, DragDropEffects.Copy);
var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy);
switch(result)
{
case DragDropEffects.Copy:

26
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -0,0 +1,26 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ItemsRepeaterPage">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Spacing="4" Margin="0 0 0 16">
<TextBlock Classes="h1">ItemsRepeater</TextBlock>
<TextBlock Classes="h2">A data-driven collection control that incorporates a flexible layout system, custom views, and virtualization.</TextBlock>
</StackPanel>
<StackPanel DockPanel.Dock="Right" Margin="8 0" Spacing="4">
<ComboBox SelectedIndex="0" SelectionChanged="LayoutChanged">
<ComboBoxItem>Stack - Vertical</ComboBoxItem>
<ComboBoxItem>Stack - Horizontal</ComboBoxItem>
<ComboBoxItem>UniformGrid - Vertical</ComboBoxItem>
<ComboBoxItem>UniformGrid - Horizontal</ComboBoxItem>
</ComboBox>
<Button Command="{Binding AddItem}">Add Item</Button>
</StackPanel>
<Border BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Margin="0 0 0 16">
<ScrollViewer Name="scroller"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}"/>
</ScrollViewer>
</Border>
</DockPanel>
</UserControl>

81
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs

@ -0,0 +1,81 @@
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
public class ItemsRepeaterPage : UserControl
{
private ItemsRepeater _repeater;
private ScrollViewer _scroller;
public ItemsRepeaterPage()
{
this.InitializeComponent();
_repeater = this.FindControl<ItemsRepeater>("repeater");
_scroller = this.FindControl<ScrollViewer>("scroller");
_repeater.PointerPressed += RepeaterClick;
DataContext = new ItemsRepeaterPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void LayoutChanged(object sender, SelectionChangedEventArgs e)
{
if (_repeater == null)
{
return;
}
var comboBox = (ComboBox)sender;
switch (comboBox.SelectedIndex)
{
case 0:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
_repeater.Layout = new StackLayout { Orientation = Orientation.Vertical };
break;
case 1:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
_repeater.Layout = new StackLayout { Orientation = Orientation.Horizontal };
break;
case 2:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
_repeater.Layout = new UniformGridLayout
{
Orientation = Orientation.Vertical,
MinItemWidth = 200,
MinItemHeight = 200,
};
break;
case 3:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
_repeater.Layout = new UniformGridLayout
{
Orientation = Orientation.Horizontal,
MinItemWidth = 200,
MinItemHeight = 200,
};
break;
}
}
private void RepeaterClick(object sender, PointerPressedEventArgs e)
{
var item = (e.Source as TextBlock)?.DataContext as string;
((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item;
}
}
}

15
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -9,7 +9,20 @@
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
<ListBox Items="{Binding}" Width="250" Height="350"></ListBox>
<StackPanel Orientation="Vertical" Spacing="8">
<ListBox Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
<Button Command="{Binding AddItemCommand}">Add</Button>
<Button Command="{Binding RemoveItemCommand}">Remove</Button>
<ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}">
<ComboBoxItem>Single</ComboBoxItem>
<ComboBoxItem>Multiple</ComboBoxItem>
<ComboBoxItem>Toggle</ComboBoxItem>
<ComboBoxItem>AlwaysSelected</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>

52
samples/ControlCatalog/Pages/ListBoxPage.xaml.cs

@ -1,9 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ReactiveUI;
namespace ControlCatalog.Pages
{
@ -11,9 +11,8 @@ namespace ControlCatalog.Pages
{
public ListBoxPage()
{
this.InitializeComponent();
DataContext = Enumerable.Range(1, 10).Select(i => $"Item {i}" )
.ToArray();
InitializeComponent();
DataContext = new PageViewModel();
}
private void InitializeComponent()
@ -21,5 +20,46 @@ namespace ControlCatalog.Pages
AvaloniaXamlLoader.Load(this);
}
private class PageViewModel : ReactiveObject
{
private int _counter;
private SelectionMode _selectionMode;
public PageViewModel()
{
Items = new ObservableCollection<string>(Enumerable.Range(1, 10).Select(i => GenerateItem()));
SelectedItems = new ObservableCollection<string>();
AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
RemoveItemCommand = ReactiveCommand.Create(() =>
{
while (SelectedItems.Count > 0)
{
Items.Remove(SelectedItems[0]);
}
});
}
public ObservableCollection<string> Items { get; }
public ObservableCollection<string> SelectedItems { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public SelectionMode SelectionMode
{
get => _selectionMode;
set
{
SelectedItems.Clear();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
private string GenerateItem() => $"Item {_counter++}";
}
}
}

9
samples/ControlCatalog/Pages/PointersPage.cs

@ -69,16 +69,25 @@ namespace ControlCatalog.Pages
{
UpdatePointer(e);
e.Pointer.Capture(this);
e.Handled = true;
base.OnPointerPressed(e);
}
protected override void OnPointerMoved(PointerEventArgs e)
{
UpdatePointer(e);
e.Handled = true;
base.OnPointerMoved(e);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
_pointers.Remove(e.Pointer);
e.Handled = true;
InvalidateVisual();
}
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
_pointers.Remove(e.Pointer);
InvalidateVisual();

5
samples/ControlCatalog/Pages/ScreenPage.cs

@ -22,7 +22,10 @@ namespace ControlCatalog.Pages
public override void Render(DrawingContext context)
{
base.Render(context);
Window w = (Window)VisualRoot;
if (!(VisualRoot is Window w))
{
return;
}
var screens = w.Screens.All;
var scaling = ((IRenderRoot)w).RenderScaling;

33
samples/ControlCatalog/Pages/TreeViewPage.xaml

@ -6,16 +6,29 @@
<TextBlock Classes="h2">Displays a hierachical tree of data.</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
<TreeView SelectionMode="Multiple" Items="{Binding}" Width="250" Height="350">
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Header}"/>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<TreeView Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Header}"/>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<Button Command="{Binding AddItemCommand}">Add</Button>
<Button Command="{Binding RemoveItemCommand}">Remove</Button>
<ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}">
<ComboBoxItem>Single</ComboBoxItem>
<ComboBoxItem>Multiple</ComboBoxItem>
<ComboBoxItem>Toggle</ComboBoxItem>
<ComboBoxItem>AlwaysSelected</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>

93
samples/ControlCatalog/Pages/TreeViewPage.xaml.cs

@ -1,8 +1,9 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ReactiveUI;
namespace ControlCatalog.Pages
{
@ -10,8 +11,8 @@ namespace ControlCatalog.Pages
{
public TreeViewPage()
{
this.InitializeComponent();
DataContext = new Node().Children;
InitializeComponent();
DataContext = new PageViewModel();
}
private void InitializeComponent()
@ -19,22 +20,96 @@ namespace ControlCatalog.Pages
AvaloniaXamlLoader.Load(this);
}
public class Node
private class PageViewModel : ReactiveObject
{
private IList<Node> _children;
private SelectionMode _selectionMode;
public PageViewModel()
{
Node root = new Node();
Items = root.Children;
SelectedItems = new ObservableCollection<Node>();
AddItemCommand = ReactiveCommand.Create(() =>
{
Node parentItem = SelectedItems.Count > 0 ? SelectedItems[0] : root;
parentItem.AddNewItem();
});
RemoveItemCommand = ReactiveCommand.Create(() =>
{
while (SelectedItems.Count > 0)
{
Node lastItem = SelectedItems[0];
RecursiveRemove(Items, lastItem);
SelectedItems.Remove(lastItem);
}
bool RecursiveRemove(ObservableCollection<Node> items, Node selectedItem)
{
if (items.Remove(selectedItem))
{
return true;
}
foreach (Node item in items)
{
if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem))
{
return true;
}
}
return false;
}
});
}
public ObservableCollection<Node> Items { get; }
public ObservableCollection<Node> SelectedItems { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public SelectionMode SelectionMode
{
get => _selectionMode;
set
{
SelectedItems.Clear();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
}
private class Node
{
private int _counter;
private ObservableCollection<Node> _children;
public string Header { get; private set; }
public IList<Node> Children
public bool AreChildrenInitialized => _children != null;
public ObservableCollection<Node> Children
{
get
{
if (_children == null)
{
_children = Enumerable.Range(1, 10).Select(i => new Node() {Header = $"Item {i}"})
.ToArray();
_children = new ObservableCollection<Node>(Enumerable.Range(1, 10).Select(i => CreateNewNode()));
}
return _children;
}
}
public void AddNewItem() => Children.Add(CreateNewNode());
public override string ToString() => Header;
private Node CreateNewNode() => new Node {Header = $"Item {_counter++}"};
}
}
}

26
samples/ControlCatalog/SideBar.xaml

@ -24,7 +24,8 @@
Name="PART_ScrollViewer"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
Background="{TemplateBinding Background}">
Background="{TemplateBinding Background}"
DockPanel.Dock="Left">
<ItemsPresenter
Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
@ -32,14 +33,19 @@
ItemTemplate="{TemplateBinding ItemTemplate}">
</ItemsPresenter>
</ScrollViewer>
<ContentPresenter
Name="PART_SelectedContentHost"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding SelectedContent}"
ContentTemplate="{TemplateBinding SelectedContentTemplate}">
</ContentPresenter>
<ContentControl Content="{TemplateBinding Tag}" HorizontalContentAlignment="Right" DockPanel.Dock="Bottom"/>
<ScrollViewer
HorizontalScrollBarVisibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SelectedItem.(ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SelectedItem.(ScrollViewer.VerticalScrollBarVisibility)}">
<ContentPresenter
Name="PART_SelectedContentHost"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding SelectedContent}"
ContentTemplate="{TemplateBinding SelectedContentTemplate}">
</ContentPresenter>
</ScrollViewer>
</DockPanel>
</Border>
</ControlTemplate>
@ -58,6 +64,8 @@
<DoubleTransition Property="Opacity" Duration="0:0:0.150"/>
</Transitions>
</Setter>
<Setter Property="(ScrollViewer.HorizontalScrollBarVisibility)" Value="Auto"/>
<Setter Property="(ScrollViewer.VerticalScrollBarVisibility)" Value="Auto"/>
</Style>
<Style Selector="TabControl.sidebar > TabItem:pointerover">
<Setter Property="Opacity" Value="1"/>

27
samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs

@ -0,0 +1,27 @@
using System.Collections.ObjectModel;
using System.Linq;
using ReactiveUI;
namespace ControlCatalog.ViewModels
{
public class ItemsRepeaterPageViewModel : ReactiveObject
{
private int newItemIndex = 1;
public ItemsRepeaterPageViewModel()
{
Items = new ObservableCollection<string>(
Enumerable.Range(1, 100000).Select(i => $"Item {i}"));
}
public ObservableCollection<string> Items { get; }
public string SelectedItem { get; set; }
public void AddItem()
{
var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1;
Items.Insert(index + 1, $"New Item {newItemIndex++}");
}
}
}

3
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -1,11 +1,10 @@
using System.Reactive;
using Avalonia.Controls.Notifications;
using Avalonia.Diagnostics.ViewModels;
using ReactiveUI;
namespace ControlCatalog.ViewModels
{
class MainWindowViewModel : ViewModelBase
class MainWindowViewModel : ReactiveObject
{
private IManagedNotificationManager _notificationManager;

1
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

@ -9,6 +9,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using ReactiveUI.Legacy;
using ReactiveUI;
using Avalonia.Layout;
namespace VirtualizationDemo.ViewModels
{

5
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -71,10 +71,5 @@ namespace Avalonia.Android
{
throw new NotSupportedException();
}
public IPopupImpl CreatePopup()
{
return new PopupImpl();
}
}
}

112
src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs

@ -1,112 +0,0 @@
using System;
using Android.Content;
using Android.Graphics;
using Android.Runtime;
using Android.Views;
using Avalonia.Controls;
using Avalonia.Platform;
namespace Avalonia.Android.Platform.SkiaPlatform
{
class PopupImpl : TopLevelImpl, IPopupImpl
{
private PixelPoint _position;
private bool _isAdded;
Action IWindowBaseImpl.Activated { get; set; }
public Action<PixelPoint> PositionChanged { get; set; }
public Action Deactivated { get; set; }
public PopupImpl() : base(ActivityTracker.Current, true)
{
}
private Size _clientSize = new Size(1, 1);
public void Resize(Size value)
{
if (View == null)
return;
_clientSize = value;
UpdateParams();
}
public void SetMinMaxSize(Size minSize, Size maxSize)
{
}
public IScreenImpl Screen { get; }
public PixelPoint Position
{
get { return _position; }
set
{
_position = value;
PositionChanged?.Invoke(_position);
UpdateParams();
}
}
WindowManagerLayoutParams CreateParams() => new WindowManagerLayoutParams(0,
WindowManagerFlags.NotTouchModal, Format.Translucent)
{
Gravity = GravityFlags.Left | GravityFlags.Top,
WindowAnimations = 0,
X = (int) _position.X,
Y = (int) _position.Y,
Width = Math.Max(1, (int) _clientSize.Width),
Height = Math.Max(1, (int) _clientSize.Height)
};
void UpdateParams()
{
if (_isAdded)
ActivityTracker.Current?.WindowManager?.UpdateViewLayout(View, CreateParams());
}
public override void Show()
{
if (_isAdded)
return;
ActivityTracker.Current.WindowManager.AddView(View, CreateParams());
_isAdded = true;
}
public override void Hide()
{
if (_isAdded)
{
var wm = View.Context.ApplicationContext.GetSystemService(Context.WindowService)
.JavaCast<IWindowManager>();
wm.RemoveView(View);
_isAdded = false;
}
}
public override void Dispose()
{
Hide();
base.Dispose();
}
public void Activate()
{
}
public void BeginMoveDrag()
{
//Not supported
}
public void BeginResizeDrag(WindowEdge edge)
{
//Not supported
}
public void SetTopmost(bool value)
{
//Not supported
}
}
}

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

@ -191,6 +191,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
}
}
public IPopupImpl CreatePopup() => null;
ILockedFramebuffer IFramebufferPlatformSurface.Lock()=>new AndroidFramebuffer(_view.Holder.Surface);
}
}

103
src/Avalonia.Base/AvaloniaObject.cs

@ -163,6 +163,37 @@ namespace Avalonia
SetValue(property, AvaloniaProperty.UnsetValue);
}
/// <summary>
/// Compares two objects using reference equality.
/// </summary>
/// <param name="obj">The object to compare.</param>
/// <remarks>
/// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons:
///
/// - AvaloniaObjects are by their nature mutable
/// - The presence of attached properties means that the semantics of equality are
/// difficult to define
///
/// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted
/// this.
/// </remarks>
public sealed override bool Equals(object obj) => base.Equals(obj);
/// <summary>
/// Gets the hash code for the object.
/// </summary>
/// <remarks>
/// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons:
///
/// - AvaloniaObjects are by their nature mutable
/// - The presence of attached properties means that the semantics of equality are
/// difficult to define
///
/// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted
/// this.
/// </remarks>
public sealed override int GetHashCode() => base.GetHashCode();
/// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value.
/// </summary>
@ -466,7 +497,7 @@ namespace Avalonia
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
protected void RaisePropertyChanged(
protected internal void RaisePropertyChanged(
AvaloniaProperty property,
object oldValue,
object newValue,
@ -508,45 +539,6 @@ namespace Avalonia
}
}
/// <summary>
/// A callback type for encapsulating complex logic for setting direct properties.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="value">The value to which to set the property.</param>
/// <param name="field">The backing field for the property.</param>
/// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
protected delegate void SetAndRaiseCallback<T>(T value, ref T field, Action<Action> notifyWrapper);
/// <summary>
/// Sets the backing field for a direct avalonia property, raising the
/// <see cref="PropertyChanged"/> event if the value has changed.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="field">The backing field.</param>
/// <param name="setterCallback">A callback called to actually set the value to the backing field.</param>
/// <param name="value">The value.</param>
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
protected bool SetAndRaise<T>(
AvaloniaProperty<T> property,
ref T field,
SetAndRaiseCallback<T> setterCallback,
T value)
{
Contract.Requires<ArgumentNullException>(setterCallback != null);
return Values.Setter.SetAndNotify(
property,
ref field,
(object update, ref T backing, Action<Action> notify) =>
{
setterCallback((T)update, ref backing, notify);
return true;
},
value);
}
/// <summary>
/// Sets the backing field for a direct avalonia property, raising the
/// <see cref="PropertyChanged"/> event if the value has changed.
@ -561,32 +553,15 @@ namespace Avalonia
protected bool SetAndRaise<T>(AvaloniaProperty<T> property, ref T field, T value)
{
VerifyAccess();
return SetAndRaise(
property,
ref field,
(T val, ref T backing, Action<Action> notifyWrapper)
=> SetAndRaiseCore(property, ref backing, val, notifyWrapper),
value);
}
/// <summary>
/// Default assignment logic for SetAndRaise.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="field">The backing field.</param>
/// <param name="value">The value.</param>
/// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
private bool SetAndRaiseCore<T>(AvaloniaProperty property, ref T field, T value, Action<Action> notifyWrapper)
{
var old = field;
field = value;
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}
DeferredSetter<T> setter = Values.GetDirectDeferredSetter(property);
notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue));
return true;
return setter.SetAndNotify(this, property, ref field, value);
}
/// <summary>

9
src/Avalonia.Base/IPriorityValueOwner.cs

@ -29,6 +29,13 @@ namespace Avalonia
/// <param name="notification">The notification.</param>
void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification);
/// <summary>
/// Returns deferred setter for given non-direct property.
/// </summary>
/// <param name="property">Property.</param>
/// <returns>Deferred setter for given property.</returns>
DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property);
/// <summary>
/// Logs a binding error.
/// </summary>
@ -40,7 +47,5 @@ namespace Avalonia
/// Ensures that the current thread is the UI thread.
/// </summary>
void VerifyAccess();
DeferredSetter<object> Setter { get; }
}
}

34
src/Avalonia.Base/PriorityValue.cs

@ -30,7 +30,9 @@ namespace Avalonia
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private readonly Func<object, object> _validate;
private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback;
private (object value, int priority) _value;
private DeferredSetter<object> _setter;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityValue"/> class.
@ -50,6 +52,7 @@ namespace Avalonia
_valueType = valueType;
_value = (AvaloniaProperty.UnsetValue, int.MaxValue);
_validate = validate;
_setAndNotifyCallback = SetAndNotify;
}
/// <summary>
@ -242,22 +245,22 @@ namespace Avalonia
/// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority)
{
Owner.Setter.SetAndNotify(Property,
ref _value,
UpdateCore,
(value, priority));
}
var newValue = (value, priority);
if (newValue == _value)
{
return;
}
private bool UpdateCore(
object update,
ref (object value, int priority) backing,
Action<Action> notify)
=> UpdateCore(((object, int))update, ref backing, notify);
if (_setter == null)
{
_setter = Owner.GetNonDirectDeferredSetter(Property);
}
_setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue);
}
private bool UpdateCore(
(object value, int priority) update,
ref (object value, int priority) backing,
Action<Action> notify)
private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update)
{
var val = update.value;
var notification = val as BindingNotification;
@ -286,7 +289,7 @@ namespace Avalonia
if (notification == null || notification.HasValue)
{
notify(() => Owner?.Changed(Property, ValuePriority, old, Value));
Owner?.Changed(Property, ValuePriority, old, Value);
}
if (notification != null)
@ -305,7 +308,6 @@ namespace Avalonia
val,
val?.GetType());
}
return true;
}
}
}

150
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@ -0,0 +1,150 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
namespace Avalonia.Utilities
{
/// <summary>
/// Stores values with <see cref="AvaloniaProperty"/> as key.
/// </summary>
/// <typeparam name="TValue">Stored value type.</typeparam>
internal sealed class AvaloniaPropertyValueStore<TValue>
{
private Entry[] _entries;
public AvaloniaPropertyValueStore()
{
// The last item in the list is always int.MaxValue
_entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } };
}
private (int, bool) TryFindEntry(int propertyId)
{
if (_entries.Length <= 12)
{
// For small lists, we use an optimized linear search. Since the last item in the list
// is always int.MaxValue, we can skip a conditional branch in each iteration.
// By unrolling the loop, we can skip another unconditional branch in each iteration.
if (_entries[0].PropertyId >= propertyId)
return (0, _entries[0].PropertyId == propertyId);
if (_entries[1].PropertyId >= propertyId)
return (1, _entries[1].PropertyId == propertyId);
if (_entries[2].PropertyId >= propertyId)
return (2, _entries[2].PropertyId == propertyId);
if (_entries[3].PropertyId >= propertyId)
return (3, _entries[3].PropertyId == propertyId);
if (_entries[4].PropertyId >= propertyId)
return (4, _entries[4].PropertyId == propertyId);
if (_entries[5].PropertyId >= propertyId)
return (5, _entries[5].PropertyId == propertyId);
if (_entries[6].PropertyId >= propertyId)
return (6, _entries[6].PropertyId == propertyId);
if (_entries[7].PropertyId >= propertyId)
return (7, _entries[7].PropertyId == propertyId);
if (_entries[8].PropertyId >= propertyId)
return (8, _entries[8].PropertyId == propertyId);
if (_entries[9].PropertyId >= propertyId)
return (9, _entries[9].PropertyId == propertyId);
if (_entries[10].PropertyId >= propertyId)
return (10, _entries[10].PropertyId == propertyId);
}
else
{
int low = 0;
int high = _entries.Length;
int id;
while (high - low > 3)
{
int pivot = (high + low) / 2;
id = _entries[pivot].PropertyId;
if (propertyId == id)
return (pivot, true);
if (propertyId <= id)
high = pivot;
else
low = pivot + 1;
}
do
{
id = _entries[low].PropertyId;
if (id == propertyId)
return (low, true);
if (id > propertyId)
break;
++low;
}
while (low < high);
}
return (0, false);
}
public bool TryGetValue(AvaloniaProperty property, out TValue value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
{
value = default;
return false;
}
value = _entries[index].Value;
return true;
}
public void AddValue(AvaloniaProperty property, TValue value)
{
Entry[] entries = new Entry[_entries.Length + 1];
for (int i = 0; i < _entries.Length; ++i)
{
if (_entries[i].PropertyId > property.Id)
{
if (i > 0)
{
Array.Copy(_entries, 0, entries, 0, i);
}
entries[i] = new Entry { PropertyId = property.Id, Value = value };
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
break;
}
}
_entries = entries;
}
public void SetValue(AvaloniaProperty property, TValue value)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
}
public Dictionary<AvaloniaProperty, TValue> ToDictionary()
{
var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1);
for (int i = 0; i < _entries.Length - 1; ++i)
{
dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
}
return dict;
}
private struct Entry
{
internal int PropertyId;
internal TValue Value;
}
}
}

206
src/Avalonia.Base/Utilities/DeferredSetter.cs

@ -1,168 +1,122 @@
using System;
using System.Collections.Generic;
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Utilities
{
/// <summary>
/// Callback invoked when deferred setter wants to set a value.
/// </summary>
/// <typeparam name="TValue">Value type.</typeparam>
/// <param name="property">Property being set.</param>
/// <param name="backing">Backing field reference.</param>
/// <param name="value">New value.</param>
internal delegate void SetAndNotifyCallback<TValue>(AvaloniaProperty property, ref TValue backing, TValue value);
/// <summary>
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
/// Used to fix #855.
/// </summary>
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
class DeferredSetter<TSetRecord>
internal sealed class DeferredSetter<TSetRecord>
{
private struct NotifyDisposable : IDisposable
private readonly SingleOrQueue<TSetRecord> _pendingValues;
private bool _isNotifying;
public DeferredSetter()
{
private readonly SettingStatus status;
_pendingValues = new SingleOrQueue<TSetRecord>();
}
internal NotifyDisposable(SettingStatus status)
{
this.status = status;
status.Notifying = true;
}
private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty<TSetRecord> property, ref TSetRecord backing, TSetRecord value)
{
var old = backing;
public void Dispose()
{
status.Notifying = false;
}
backing = value;
source.RaisePropertyChanged(property, old, value);
}
/// <summary>
/// Information on current setting/notification status of a property.
/// </summary>
private class SettingStatus
public bool SetAndNotify(
AvaloniaObject source,
AvaloniaProperty<TSetRecord> property,
ref TSetRecord backing,
TSetRecord value)
{
public bool Notifying { get; set; }
private SingleOrQueue<TSetRecord> pendingValues;
public SingleOrQueue<TSetRecord> PendingValues
if (!_isNotifying)
{
get
using (new NotifyDisposable(this))
{
return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>());
SetAndRaisePropertyChanged(source, property, ref backing, value);
}
}
}
private Dictionary<AvaloniaProperty, SettingStatus> _setRecords;
private Dictionary<AvaloniaProperty, SettingStatus> SetRecords
=> _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>());
if (!_pendingValues.Empty)
{
using (new NotifyDisposable(this))
{
while (!_pendingValues.Empty)
{
SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue());
}
}
}
private SettingStatus GetOrCreateStatus(AvaloniaProperty property)
{
if (!SetRecords.TryGetValue(property, out var status))
{
status = new SettingStatus();
SetRecords.Add(property, status);
return true;
}
return status;
_pendingValues.Enqueue(value);
return false;
}
/// <summary>
/// Mark the property as currently notifying.
/// </summary>
/// <param name="property">The property to mark as notifying.</param>
/// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
private NotifyDisposable MarkNotifying(AvaloniaProperty property)
public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, SetAndNotifyCallback<TValue> setAndNotifyCallback, ref TValue backing, TValue value)
where TValue : TSetRecord
{
Contract.Requires<InvalidOperationException>(!IsNotifying(property));
SettingStatus status = GetOrCreateStatus(property);
return new NotifyDisposable(status);
}
if (!_isNotifying)
{
using (new NotifyDisposable(this))
{
setAndNotifyCallback(property, ref backing, value);
}
/// <summary>
/// Check if the property is currently notifying listeners.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>If the property is currently notifying listeners.</returns>
private bool IsNotifying(AvaloniaProperty property)
=> SetRecords.TryGetValue(property, out var value) && value.Notifying;
if (!_pendingValues.Empty)
{
using (new NotifyDisposable(this))
{
while (!_pendingValues.Empty)
{
setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue());
}
}
}
/// <summary>
/// Add a pending assignment for the property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value to assign.</param>
private void AddPendingSet(AvaloniaProperty property, TSetRecord value)
{
Contract.Requires<InvalidOperationException>(IsNotifying(property));
return true;
}
GetOrCreateStatus(property).PendingValues.Enqueue(value);
}
_pendingValues.Enqueue(value);
/// <summary>
/// Checks if there are any pending assignments for the property.
/// </summary>
/// <param name="property">The property to check.</param>
/// <returns>If the property has any pending assignments.</returns>
private bool HasPendingSet(AvaloniaProperty property)
{
return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
return false;
}
/// <summary>
/// Gets the first pending assignment for the property.
/// Disposable that marks the property as currently notifying.
/// When disposed, marks the property as done notifying.
/// </summary>
/// <param name="property">The property to check.</param>
/// <returns>The first pending assignment for the property.</returns>
private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
private readonly struct NotifyDisposable : IDisposable
{
return GetOrCreateStatus(property).PendingValues.Dequeue();
}
public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
private readonly DeferredSetter<TSetRecord> _setter;
/// <summary>
/// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824
/// </summary>
/// <param name="property">The property to set.</param>
/// <param name="backing">The backing field for the property</param>
/// <param name="setterCallback">
/// A callback that actually sets the property.
/// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification.
/// </param>
/// <param name="value">The value to try to set.</param>
public bool SetAndNotify<TValue>(
AvaloniaProperty property,
ref TValue backing,
SetterDelegate<TValue> setterCallback,
TSetRecord value)
{
Contract.Requires<ArgumentNullException>(setterCallback != null);
if (!IsNotifying(property))
internal NotifyDisposable(DeferredSetter<TSetRecord> setter)
{
bool updated = false;
if (!object.Equals(value, backing))
{
updated = setterCallback(value, ref backing, notification =>
{
using (MarkNotifying(property))
{
notification();
}
});
}
while (HasPendingSet(property))
{
updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification =>
{
using (MarkNotifying(property))
{
notification();
}
});
}
return updated;
_setter = setter;
_setter._isNotifying = true;
}
else if(!object.Equals(value, backing))
public void Dispose()
{
AddPendingSet(property, value);
_setter._isNotifying = false;
}
return false;
}
}
}

152
src/Avalonia.Base/ValueStore.cs

@ -7,21 +7,15 @@ namespace Avalonia
{
internal class ValueStore : IPriorityValueOwner
{
private struct Entry
{
internal int PropertyId;
internal object Value;
}
private readonly AvaloniaPropertyValueStore<object> _propertyValues;
private readonly AvaloniaPropertyValueStore<object> _deferredSetters;
private readonly AvaloniaObject _owner;
private Entry[] _entries;
public ValueStore(AvaloniaObject owner)
{
_owner = owner;
// The last item in the list is always int.MaxValue
_entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } };
_propertyValues = new AvaloniaPropertyValueStore<object>();
_deferredSetters = new AvaloniaPropertyValueStore<object>();
}
public IDisposable AddBinding(
@ -31,7 +25,7 @@ namespace Avalonia
{
PriorityValue priorityValue;
if (TryGetValue(property, out var v))
if (_propertyValues.TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
@ -39,13 +33,13 @@ namespace Avalonia
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
SetValueInternal(property, priorityValue);
_propertyValues.SetValue(property, priorityValue);
}
}
else
{
priorityValue = CreatePriorityValue(property);
AddValueInternal(property, priorityValue);
_propertyValues.AddValue(property, priorityValue);
}
return priorityValue.Add(source, (int)priority);
@ -55,7 +49,7 @@ namespace Avalonia
{
PriorityValue priorityValue;
if (TryGetValue(property, out var v))
if (_propertyValues.TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
@ -63,7 +57,7 @@ namespace Avalonia
{
if (priority == (int)BindingPriority.LocalValue)
{
SetValueInternal(property, Validate(property, value));
_propertyValues.SetValue(property, Validate(property, value));
Changed(property, priority, v, value);
return;
}
@ -71,7 +65,7 @@ namespace Avalonia
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
SetValueInternal(property, priorityValue);
_propertyValues.SetValue(property, priorityValue);
}
}
}
@ -84,14 +78,14 @@ namespace Avalonia
if (priority == (int)BindingPriority.LocalValue)
{
AddValueInternal(property, Validate(property, value));
_propertyValues.AddValue(property, Validate(property, value));
Changed(property, priority, AvaloniaProperty.UnsetValue, value);
return;
}
else
{
priorityValue = CreatePriorityValue(property);
AddValueInternal(property, priorityValue);
_propertyValues.AddValue(property, priorityValue);
}
}
@ -110,14 +104,9 @@ namespace Avalonia
public IDictionary<AvaloniaProperty, object> GetSetValues()
{
var dict = new Dictionary<AvaloniaProperty, object>(_entries.Length - 1);
for (int i = 0; i < _entries.Length - 1; ++i)
{
dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
}
return dict;
return _propertyValues.ToDictionary();
}
public void LogError(AvaloniaProperty property, Exception e)
{
_owner.LogBindingError(property, e);
@ -127,7 +116,7 @@ namespace Avalonia
{
var result = AvaloniaProperty.UnsetValue;
if (TryGetValue(property, out var value))
if (_propertyValues.TryGetValue(property, out var value))
{
result = (value is PriorityValue priorityValue) ? priorityValue.Value : value;
}
@ -137,12 +126,12 @@ namespace Avalonia
public bool IsAnimating(AvaloniaProperty property)
{
return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
}
public bool IsSet(AvaloniaProperty property)
{
if (TryGetValue(property, out var value))
if (_propertyValues.TryGetValue(property, out var value))
{
return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue;
}
@ -152,7 +141,7 @@ namespace Avalonia
public void Revalidate(AvaloniaProperty property)
{
if (TryGetValue(property, out var value))
if (_propertyValues.TryGetValue(property, out var value))
{
(value as PriorityValue)?.Revalidate();
}
@ -189,113 +178,28 @@ namespace Avalonia
return value;
}
private DeferredSetter<object> _deferredSetter;
public DeferredSetter<object> Setter
private DeferredSetter<T> GetDeferredSetter<T>(AvaloniaProperty property)
{
get
if (_deferredSetters.TryGetValue(property, out var deferredSetter))
{
return _deferredSetter ??
(_deferredSetter = new DeferredSetter<object>());
return (DeferredSetter<T>)deferredSetter;
}
}
private bool TryGetValue(AvaloniaProperty property, out object value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
{
value = null;
return false;
}
value = _entries[index].Value;
return true;
}
private void AddValueInternal(AvaloniaProperty property, object value)
{
Entry[] entries = new Entry[_entries.Length + 1];
for (int i = 0; i < _entries.Length; ++i)
{
if (_entries[i].PropertyId > property.Id)
{
if (i > 0)
{
Array.Copy(_entries, 0, entries, 0, i);
}
var newDeferredSetter = new DeferredSetter<T>();
entries[i] = new Entry { PropertyId = property.Id, Value = value };
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
break;
}
}
_deferredSetters.AddValue(property, newDeferredSetter);
_entries = entries;
return newDeferredSetter;
}
private void SetValueInternal(AvaloniaProperty property, object value)
public DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
return GetDeferredSetter<object>(property);
}
private (int, bool) TryFindEntry(int propertyId)
public DeferredSetter<T> GetDirectDeferredSetter<T>(AvaloniaProperty<T> property)
{
if (_entries.Length <= 12)
{
// For small lists, we use an optimized linear search. Since the last item in the list
// is always int.MaxValue, we can skip a conditional branch in each iteration.
// By unrolling the loop, we can skip another unconditional branch in each iteration.
if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId);
if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId);
if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId);
if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId);
if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId);
if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId);
if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId);
if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId);
if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId);
if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId);
if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId);
}
else
{
int low = 0;
int high = _entries.Length;
int id;
while (high - low > 3)
{
int pivot = (high + low) / 2;
id = _entries[pivot].PropertyId;
if (propertyId == id)
return (pivot, true);
if (propertyId <= id)
high = pivot;
else
low = pivot + 1;
}
do
{
id = _entries[low].PropertyId;
if (id == propertyId)
return (low, true);
if (id > propertyId)
break;
++low;
}
while (low < high);
}
return (0, false);
return GetDeferredSetter<T>(property);
}
}
}

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

@ -24,6 +24,7 @@ using System.Linq;
using Avalonia.Input.Platform;
using System.ComponentModel.DataAnnotations;
using Avalonia.Controls.Utils;
using Avalonia.Layout;
namespace Avalonia.Controls
{

3
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@ -195,7 +195,6 @@
<Setter Property="GridLinesVisibility" Value="Vertical" />
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource ThemeBorderLightBrush}" />
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource ThemeBorderLightBrush}" />
<Setter Property="HeadersVisibility" Value="Column" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderDarkBrush}"/>
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}" />
<Setter Property="DropLocationIndicatorTemplate">
@ -230,4 +229,4 @@
</ControlTemplate>
</Setter>
</Style>
</Styles>
</Styles>

17
src/Avalonia.Controls/Calendar/CalendarItem.cs

@ -4,6 +4,7 @@
// All other rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using Avalonia.Data;
@ -193,6 +194,9 @@ namespace Avalonia.Controls.Primitives
{
if (MonthView != null)
{
var childCount = Calendar.RowsPerMonth + Calendar.RowsPerMonth * Calendar.ColumnsPerMonth;
var children = new List<IControl>(childCount);
for (int i = 0; i < Calendar.RowsPerMonth; i++)
{
if (_dayTitleTemplate != null)
@ -201,7 +205,7 @@ namespace Avalonia.Controls.Primitives
cell.DataContext = string.Empty;
cell.SetValue(Grid.RowProperty, 0);
cell.SetValue(Grid.ColumnProperty, i);
MonthView.Children.Add(cell);
children.Add(cell);
}
}
@ -222,13 +226,18 @@ namespace Avalonia.Controls.Primitives
cell.PointerEnter += Cell_MouseEnter;
cell.PointerLeave += Cell_MouseLeave;
cell.Click += Cell_Click;
MonthView.Children.Add(cell);
children.Add(cell);
}
}
MonthView.Children.AddRange(children);
}
if (YearView != null)
{
var childCount = Calendar.RowsPerYear * Calendar.ColumnsPerYear;
var children = new List<IControl>(childCount);
CalendarButton month;
for (int i = 0; i < Calendar.RowsPerYear; i++)
{
@ -246,9 +255,11 @@ namespace Avalonia.Controls.Primitives
month.CalendarLeftMouseButtonUp += Month_CalendarButtonMouseUp;
month.PointerEnter += Month_MouseEnter;
month.PointerLeave += Month_MouseLeave;
YearView.Children.Add(month);
children.Add(month);
}
}
YearView.Children.AddRange(children);
}
}

2
src/Avalonia.Controls/ComboBox.cs

@ -202,7 +202,7 @@ namespace Avalonia.Controls
{
if (!e.Handled)
{
if (_popup?.PopupRoot != null && ((IVisual)e.Source).GetVisualRoot() == _popup?.PopupRoot)
if (_popup?.IsInsidePopup((IVisual)e.Source) == true)
{
if (UpdateSelectionFromEventSource(e.Source))
{

3
src/Avalonia.Controls/ContextMenu.cs

@ -7,6 +7,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
namespace Avalonia.Controls
@ -90,6 +91,8 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param>
public void Open(Control control)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (IsOpen)
{
return;

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

@ -61,5 +61,6 @@ namespace Avalonia.Controls.Embedding.Offscreen
public Action Closed { get; set; }
public abstract IMouseDevice MouseDevice { get; }
public IPopupImpl CreatePopup() => null;
}
}

1
src/Avalonia.Controls/GridSplitter.cs

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls

9
src/Avalonia.Controls/IScrollAnchorProvider.cs

@ -0,0 +1,9 @@
namespace Avalonia.Controls
{
public interface IScrollAnchorProvider
{
IControl CurrentAnchor { get; }
void RegisterAnchorCandidate(IControl element);
void UnregisterAnchorCandidate(IControl element);
}
}

2
src/Avalonia.Controls/Image.cs

@ -96,7 +96,7 @@ namespace Avalonia.Controls
}
}
return result.Constrain(availableSize);
return result;
}
/// <inheritdoc/>

2
src/Avalonia.Controls/LayoutTransformControl.cs

@ -45,7 +45,7 @@ namespace Avalonia.Controls
}
/// <summary>
/// Utilize the <see cref="RenderTransformProperty"/> for layout transforms.
/// Utilize the <see cref="Visual.RenderTransformProperty"/> for layout transforms.
/// </summary>
public bool UseRenderTransform
{

1
src/Avalonia.Controls/Menu.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.Platform;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace Avalonia.Controls
{

2
src/Avalonia.Controls/MenuItem.cs

@ -224,7 +224,7 @@ namespace Avalonia.Controls
public bool IsTopLevel => Parent is Menu;
/// <inheritdoc/>
bool IMenuItem.IsPointerOverSubMenu => _popup.PopupRoot?.IsPointerOver ?? false;
bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false;
/// <inheritdoc/>
IMenuElement IMenuItem.Parent => Parent as IMenuElement;

1
src/Avalonia.Controls/Mixins/ContentControlMixin.cs

@ -150,6 +150,7 @@ namespace Avalonia.Controls.Mixins
if (oldValue is IControl child)
{
logicalChildren.Remove(child);
((ISetInheritanceParent)child).SetParent(child.Parent);
}
child = newValue as IControl;

2
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@ -150,7 +150,7 @@ namespace Avalonia.Controls.Notifications
private void Install(Window host)
{
var adornerLayer = host.GetVisualDescendants()
.OfType<AdornerDecorator>()
.OfType<VisualLayerManager>()
.FirstOrDefault()
?.AdornerLayer;

2
src/Avalonia.Controls/Panel.cs

@ -112,7 +112,7 @@ namespace Avalonia.Controls
case NotifyCollectionChangedAction.Add:
controls = e.NewItems.OfType<Control>().ToList();
LogicalChildren.InsertRange(e.NewStartingIndex, controls);
VisualChildren.AddRange(e.NewItems.OfType<Visual>());
VisualChildren.InsertRange(e.NewStartingIndex, e.NewItems.OfType<Visual>());
break;
case NotifyCollectionChangedAction.Move:

19
src/Avalonia.Controls/PlacementMode.cs

@ -23,6 +23,21 @@ namespace Avalonia.Controls
/// <summary>
/// The popup is placed at the top right of its target.
/// </summary>
Right
Right,
/// <summary>
/// The popup is placed at the top left of its target.
/// </summary>
Left,
/// <summary>
/// The popup is placed at the top left of its target.
/// </summary>
Top,
/// <summary>
/// The popup is placed according to anchor and gravity rules
/// </summary>
AnchorAndGravity
}
}
}

2
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -396,7 +396,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void WindowDeactivated(object sender, EventArgs e)
{
Menu.Close();
Menu?.Close();
}
protected void Click(IMenuItem item)

4
src/Avalonia.Controls/Platform/IPopupImpl.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls.Primitives.PopupPositioning;
namespace Avalonia.Platform
{
/// <summary>
@ -8,6 +10,6 @@ namespace Avalonia.Platform
/// </summary>
public interface IPopupImpl : IWindowBaseImpl
{
IPopupPositioner PopupPositioner { get; }
}
}

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

@ -107,5 +107,7 @@ namespace Avalonia.Platform
/// </summary>
[CanBeNull]
IMouseDevice MouseDevice { get; }
IPopupImpl CreatePopup();
}
}

24
src/Avalonia.Controls/Platform/IWindowBaseImpl.cs

@ -15,21 +15,10 @@ namespace Avalonia.Platform
/// </summary>
void Hide();
/// <summary>
/// Starts moving a window with left button being held. Should be called from left mouse button press event handler.
/// </summary>
void BeginMoveDrag();
/// <summary>
/// Starts resizing a window. This function is used if an application has window resizing controls.
/// Should be called from left mouse button press event handler
/// </summary>
void BeginResizeDrag(WindowEdge edge);
/// <summary>
/// Gets the position of the window in device pixels.
/// </summary>
PixelPoint Position { get; set; }
PixelPoint Position { get; }
/// <summary>
/// Gets or sets a method called when the window's position changes.
@ -61,17 +50,6 @@ namespace Avalonia.Platform
/// </summary>
Size MaxClientSize { get; }
/// <summary>
/// Sets the client size of the top level.
/// </summary>
void Resize(Size clientSize);
/// <summary>
/// Minimum width of the window.
/// </summary>
///
void SetMinMaxSize(Size minSize, Size maxSize);
/// <summary>
/// Sets whether this window appears on top of all other windows
/// </summary>

27
src/Avalonia.Controls/Platform/IWindowImpl.cs

@ -57,5 +57,32 @@ namespace Avalonia.Platform
/// Return true to prevent the underlying implementation from closing.
/// </summary>
Func<bool> Closing { get; set; }
/// <summary>
/// Starts moving a window with left button being held. Should be called from left mouse button press event handler.
/// </summary>
void BeginMoveDrag();
/// <summary>
/// Starts resizing a window. This function is used if an application has window resizing controls.
/// Should be called from left mouse button press event handler
/// </summary>
void BeginResizeDrag(WindowEdge edge);
/// <summary>
/// Sets the client size of the top level.
/// </summary>
void Resize(Size clientSize);
/// <summary>
/// Sets the client size of the top level.
/// </summary>
void Move(PixelPoint point);
/// <summary>
/// Minimum width of the window.
/// </summary>
///
void SetMinMaxSize(Size minSize, Size maxSize);
}
}

1
src/Avalonia.Controls/Platform/IWindowingPlatform.cs

@ -4,6 +4,5 @@ namespace Avalonia.Platform
{
IWindowImpl CreateWindow();
IEmbeddableWindowImpl CreateEmbeddableWindow();
IPopupImpl CreatePopup();
}
}

3
src/Avalonia.Controls/Platform/InProcessDragSource.cs

@ -33,9 +33,10 @@ namespace Avalonia.Platform
_dragDrop = AvaloniaLocator.Current.GetService<IDragDropDevice>();
}
public async Task<DragDropEffects> DoDragDrop(IDataObject data, DragDropEffects allowedEffects)
public async Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects)
{
Dispatcher.UIThread.VerifyAccess();
triggerEvent.Pointer.Capture(null);
if (_draggedData == null)
{
_draggedData = data;

85
src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs

@ -9,94 +9,69 @@ using Avalonia.Threading;
namespace Avalonia.Controls.Platform
{
public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderTimer
public class InternalPlatformThreadingInterface : IPlatformThreadingInterface
{
public InternalPlatformThreadingInterface()
{
TlsCurrentThreadIsLoopThread = true;
StartTimer(
DispatcherPriority.Render,
new TimeSpan(0, 0, 0, 0, 66),
() => Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount)));
}
private readonly AutoResetEvent _signaled = new AutoResetEvent(false);
private readonly AutoResetEvent _queued = new AutoResetEvent(false);
private readonly Queue<Action> _actions = new Queue<Action>();
public void RunLoop(CancellationToken cancellationToken)
{
var handles = new[] {_signaled, _queued};
while (true)
{
if (0 == WaitHandle.WaitAny(handles))
Signaled?.Invoke(null);
else
{
while (true)
{
Action item;
lock (_actions)
if (_actions.Count == 0)
break;
else
item = _actions.Dequeue();
item();
}
}
Signaled?.Invoke(null);
_signaled.WaitOne();
}
}
public void Send(Action cb)
{
lock (_actions)
{
_actions.Enqueue(cb);
_queued.Set();
}
}
class WatTimer : IDisposable
class TimerImpl : IDisposable
{
private readonly IDisposable _timer;
private readonly DispatcherPriority _priority;
private readonly TimeSpan _interval;
private readonly Action _tick;
private Timer _timer;
private GCHandle _handle;
public WatTimer(IDisposable timer)
public TimerImpl(DispatcherPriority priority, TimeSpan interval, Action tick)
{
_timer = timer;
_priority = priority;
_interval = interval;
_tick = tick;
_timer = new Timer(OnTimer, null, interval, TimeSpan.FromMilliseconds(-1));
_handle = GCHandle.Alloc(_timer);
}
private void OnTimer(object state)
{
if (_timer == null)
return;
Dispatcher.UIThread.Post(() =>
{
if (_timer == null)
return;
_tick();
_timer?.Change(_interval, TimeSpan.FromMilliseconds(-1));
});
}
public void Dispose()
{
_handle.Free();
_timer.Dispose();
_timer = null;
}
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
return new WatTimer(new System.Threading.Timer(delegate
{
var tcs = new TaskCompletionSource<int>();
Send(() =>
{
try
{
tick();
}
finally
{
tcs.SetResult(0);
}
});
tcs.Task.Wait();
}, null, TimeSpan.Zero, interval));
return new TimerImpl(priority, interval, tick);
}
public void Signal(DispatcherPriority prio)

5
src/Avalonia.Controls/Platform/PlatformManager.cs

@ -41,10 +41,5 @@ namespace Avalonia.Controls.Platform
throw new Exception("Could not CreateEmbeddableWindow(): IWindowingPlatform is not registered.");
return platform.CreateEmbeddableWindow();
}
public static IPopupImpl CreatePopup()
{
return AvaloniaLocator.Current.GetService<IWindowingPlatform>().CreatePopup();
}
}
}

7
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -237,7 +237,7 @@ namespace Avalonia.Controls.Presenters
// template.
LogicalChildren.Remove(oldChild);
}
else
else if (TemplatedParent != null)
{
// If we're in a ContentControl's template then invoke ChildChanging to let
// ContentControlMixin handle removing the logical child.
@ -248,6 +248,10 @@ namespace Avalonia.Controls.Presenters
newChild,
BindingPriority.LocalValue));
}
else if (oldChild != null)
{
((ISetInheritanceParent)oldChild).SetParent(oldChild.Parent);
}
}
// Set the DataContext if the data isn't a control.
@ -433,6 +437,7 @@ namespace Avalonia.Controls.Presenters
{
VisualChildren.Remove(Child);
LogicalChildren.Remove(Child);
((ISetInheritanceParent)Child).SetParent(Child.Parent);
Child = null;
_dataTemplate = null;
}

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

@ -8,6 +8,7 @@ using System.Reactive.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls.Presenters
{

19
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -49,6 +49,14 @@ namespace Avalonia.Controls.Presenters
AffectsRender<TextPresenter>(PasswordCharProperty,
SelectionBrushProperty, SelectionForegroundBrushProperty,
SelectionStartProperty, SelectionEndProperty);
Observable.Merge(
SelectionStartProperty.Changed,
SelectionEndProperty.Changed,
PasswordCharProperty.Changed
).AddClassHandler<TextPresenter>((x,_) => x.InvalidateFormattedText());
CaretIndexProperty.Changed.AddClassHandler<TextPresenter>((x, e) => x.CaretIndexChanged((int)e.NewValue));
}
public TextPresenter()
@ -56,17 +64,6 @@ namespace Avalonia.Controls.Presenters
_caretTimer = new DispatcherTimer();
_caretTimer.Interval = TimeSpan.FromMilliseconds(500);
_caretTimer.Tick += CaretTimerTick;
Observable.Merge(
this.GetObservable(SelectionStartProperty),
this.GetObservable(SelectionEndProperty))
.Subscribe(_ => InvalidateFormattedText());
this.GetObservable(CaretIndexProperty)
.Subscribe(CaretIndexChanged);
this.GetObservable(PasswordCharProperty)
.Subscribe(_ => InvalidateFormattedText());
}
public int CaretIndex

42
src/Avalonia.Controls/Primitives/AdornerDecorator.cs

@ -1,42 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.LogicalTree;
namespace Avalonia.Controls.Primitives
{
public class AdornerDecorator : Decorator
{
public AdornerDecorator()
{
AdornerLayer = new AdornerLayer();
((ISetLogicalParent)AdornerLayer).SetParent(this);
AdornerLayer.ZIndex = int.MaxValue;
VisualChildren.Add(AdornerLayer);
}
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
((ILogical)AdornerLayer).NotifyAttachedToLogicalTree(e);
}
public AdornerLayer AdornerLayer
{
get;
}
protected override Size MeasureOverride(Size availableSize)
{
AdornerLayer.Measure(availableSize);
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
AdornerLayer.Arrange(new Rect(finalSize));
return base.ArrangeOverride(finalSize);
}
}
}

2
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -42,7 +42,7 @@ namespace Avalonia.Controls.Primitives
public static AdornerLayer GetAdornerLayer(IVisual visual)
{
return visual.GetVisualAncestors()
.OfType<AdornerDecorator>()
.OfType<VisualLayerManager>()
.FirstOrDefault()
?.AdornerLayer;
}

26
src/Avalonia.Controls/Primitives/IPopupHost.cs

@ -0,0 +1,26 @@
using System;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
public interface IPopupHost : IDisposable
{
void SetChild(IControl control);
IContentPresenter Presenter { get; }
IVisual HostedVisualTreeRoot { get; }
event EventHandler<TemplateAppliedEventArgs> TemplateApplied;
void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
PopupPositioningEdge anchor = PopupPositioningEdge.None,
PopupPositioningEdge gravity = PopupPositioningEdge.None);
void Show();
void Hide();
IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty,
StyledProperty<double> minWidthProperty, StyledProperty<double> maxWidthProperty,
StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty);
}
}

38
src/Avalonia.Controls/Primitives/OverlayLayer.cs

@ -0,0 +1,38 @@
using System.Linq;
using Avalonia.Rendering;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
public class OverlayLayer : Canvas, ICustomSimpleHitTest
{
public Size AvailableSize { get; private set; }
public static OverlayLayer GetOverlayLayer(IVisual visual)
{
foreach(var v in visual.GetVisualAncestors())
if(v is VisualLayerManager vlm)
if (vlm.OverlayLayer != null)
return vlm.OverlayLayer;
if (visual is TopLevel tl)
{
var layers = tl.GetVisualDescendants().OfType<VisualLayerManager>().FirstOrDefault();
return layers?.OverlayLayer;
}
return null;
}
public bool HitTest(Point point)
{
return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true);
}
protected override Size ArrangeOverride(Size finalSize)
{
// We are saving it here since child controls might need to know the entire size of the overlay
// and Bounds won't be updated in time
AvailableSize = finalSize;
return base.ArrangeOverride(finalSize);
}
}
}

149
src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
public class OverlayPopupHost : ContentControl, IPopupHost, IInteractive, IManagedPopupPositionerPopup
{
private readonly OverlayLayer _overlayLayer;
private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters();
private ManagedPopupPositioner _positioner;
private Point _lastRequestedPosition;
private bool _shown;
public OverlayPopupHost(OverlayLayer overlayLayer)
{
_overlayLayer = overlayLayer;
_positioner = new ManagedPopupPositioner(this);
}
public void SetChild(IControl control)
{
Content = control;
}
public IVisual HostedVisualTreeRoot => null;
/// <inheritdoc/>
IInteractive IInteractive.InteractiveParent => Parent;
public void Dispose() => Hide();
public void Show()
{
_overlayLayer.Children.Add(this);
_shown = true;
}
public void Hide()
{
_overlayLayer.Children.Remove(this);
_shown = false;
}
public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty, StyledProperty<double> minWidthProperty,
StyledProperty<double> maxWidthProperty, StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty)
{
// Topmost property is not supported
var bindings = new List<IDisposable>();
void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to]));
Bind(WidthProperty, widthProperty);
Bind(MinWidthProperty, minWidthProperty);
Bind(MaxWidthProperty, maxWidthProperty);
Bind(HeightProperty, heightProperty);
Bind(MinHeightProperty, minHeightProperty);
Bind(MaxHeightProperty, maxHeightProperty);
return Disposable.Create(() =>
{
foreach (var x in bindings)
x.Dispose();
});
}
public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
PopupPositioningEdge anchor = PopupPositioningEdge.None, PopupPositioningEdge gravity = PopupPositioningEdge.None)
{
_positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot(), target, placement, offset, anchor,
gravity);
UpdatePosition();
}
protected override Size ArrangeOverride(Size finalSize)
{
if (_positionerParameters.Size != finalSize)
{
_positionerParameters.Size = finalSize;
UpdatePosition();
}
return base.ArrangeOverride(finalSize);
}
private void UpdatePosition()
{
// Don't bother the positioner with layout system artifacts
if (_positionerParameters.Size.Width == 0 || _positionerParameters.Size.Height == 0)
return;
if (_shown)
{
_positioner.Update(_positionerParameters);
}
}
IReadOnlyList<ManagedPopupPositionerScreenInfo> IManagedPopupPositionerPopup.Screens
{
get
{
var rc = new Rect(default, _overlayLayer.AvailableSize);
return new[] {new ManagedPopupPositionerScreenInfo(rc, rc)};
}
}
Rect IManagedPopupPositionerPopup.ParentClientAreaScreenGeometry =>
new Rect(default, _overlayLayer.Bounds.Size);
void IManagedPopupPositionerPopup.MoveAndResize(Point devicePoint, Size virtualSize)
{
_lastRequestedPosition = devicePoint;
Dispatcher.UIThread.Post(() =>
{
OverlayLayer.SetLeft(this, _lastRequestedPosition.X);
OverlayLayer.SetTop(this, _lastRequestedPosition.Y);
}, DispatcherPriority.Layout);
}
Point IManagedPopupPositionerPopup.TranslatePoint(Point pt) => pt;
Size IManagedPopupPositionerPopup.TranslateSize(Size size) => size;
public static IPopupHost CreatePopupHost(IVisual target, IAvaloniaDependencyResolver dependencyResolver)
{
var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup();
if (platform != null)
return new PopupRoot((TopLevel)target.GetVisualRoot(), platform, dependencyResolver);
var overlayLayer = OverlayLayer.GetOverlayLayer(target);
if (overlayLayer == null)
throw new InvalidOperationException(
"Unable to create IPopupImpl and no overlay layer is found for the target control");
return new OverlayPopupHost(overlayLayer);
}
public override void Render(DrawingContext context)
{
context.FillRectangle(Brushes.White, new Rect(default, Bounds.Size));
}
}
}

284
src/Avalonia.Controls/Primitives/Popup.cs

@ -2,7 +2,12 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Presenters;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
@ -42,7 +47,7 @@ namespace Avalonia.Controls.Primitives
/// Defines the <see cref="ObeyScreenEdges"/> property.
/// </summary>
public static readonly StyledProperty<bool> ObeyScreenEdgesProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges));
AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true);
/// <summary>
/// Defines the <see cref="HorizontalOffset"/> property.
@ -75,10 +80,12 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register<Popup, bool>(nameof(Topmost));
private bool _isOpen;
private PopupRoot _popupRoot;
private IPopupHost _popupHost;
private TopLevel _topLevel;
private IDisposable _nonClientListener;
private IDisposable _presenterSubscription;
bool _ignoreIsOpenChanged = false;
private List<IDisposable> _bindings = new List<IDisposable>();
/// <summary>
/// Initializes static members of the <see cref="Popup"/> class.
@ -88,7 +95,11 @@ namespace Avalonia.Controls.Primitives
IsHitTestVisibleProperty.OverrideDefaultValue<Popup>(false);
ChildProperty.Changed.AddClassHandler<Popup>(x => x.ChildChanged);
IsOpenProperty.Changed.AddClassHandler<Popup>(x => x.IsOpenChanged);
TopmostProperty.Changed.AddClassHandler<Popup>((p, e) => p.PopupRoot.Topmost = (bool)e.NewValue);
}
public Popup()
{
}
/// <summary>
@ -101,10 +112,7 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public event EventHandler Opened;
/// <summary>
/// Raised when the popup root has been created, but before it has been shown.
/// </summary>
public event EventHandler PopupRootCreated;
public IPopupHost Host => _popupHost;
/// <summary>
/// Gets or sets the control to display in the popup.
@ -147,10 +155,7 @@ namespace Avalonia.Controls.Primitives
set { SetValue(PlacementModeProperty, value); }
}
/// <summary>
/// Gets or sets a value indicating whether the popup positions itself within the nearest screen boundary
/// when its opened at a position where it would otherwise overlap the screen edge.
/// </summary>
[Obsolete("This property has no effect")]
public bool ObeyScreenEdges
{
get => GetValue(ObeyScreenEdgesProperty);
@ -184,11 +189,6 @@ namespace Avalonia.Controls.Primitives
set { SetValue(PlacementTargetProperty, value); }
}
/// <summary>
/// Gets the root of the popup window.
/// </summary>
public PopupRoot PopupRoot => _popupRoot;
/// <summary>
/// Gets or sets a value indicating whether the popup should stay open when the popup is
/// pressed or loses focus.
@ -211,63 +211,58 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Gets the root of the popup window.
/// </summary>
IVisual IVisualTreeHost.Root => _popupRoot;
IVisual IVisualTreeHost.Root => _popupHost?.HostedVisualTreeRoot;
/// <summary>
/// Opens the popup.
/// </summary>
public void Open()
{
if (_popupRoot == null)
// Popup is currently open
if (_topLevel != null)
return;
CloseCurrent();
var placementTarget = PlacementTarget ?? this.GetLogicalAncestors().OfType<IVisual>().FirstOrDefault();
if (placementTarget == null)
throw new InvalidOperationException("Popup has no logical parent and PlacementTarget is null");
_topLevel = placementTarget.GetVisualRoot() as TopLevel;
if (_topLevel == null)
{
_popupRoot = new PopupRoot(DependencyResolver)
{
[~ContentControl.ContentProperty] = this[~ChildProperty],
[~WidthProperty] = this[~WidthProperty],
[~HeightProperty] = this[~HeightProperty],
[~MinWidthProperty] = this[~MinWidthProperty],
[~MaxWidthProperty] = this[~MaxWidthProperty],
[~MinHeightProperty] = this[~MinHeightProperty],
[~MaxHeightProperty] = this[~MaxHeightProperty],
};
((ISetLogicalParent)_popupRoot).SetParent(this);
throw new InvalidOperationException(
"Attempted to open a popup not attached to a TopLevel");
}
_popupRoot.Position = GetPosition();
_popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver);
_bindings.Add(_popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty));
if (_topLevel == null && PlacementTarget != null)
_popupHost.SetChild(Child);
((ISetLogicalParent)_popupHost).SetParent(this);
_popupHost.ConfigurePosition(placementTarget,
PlacementMode, new Point(HorizontalOffset, VerticalOffset));
_popupHost.TemplateApplied += RootTemplateApplied;
var window = _topLevel as Window;
if (window != null)
{
_topLevel = PlacementTarget.GetSelfAndLogicalAncestors().First(x => x is TopLevel) as TopLevel;
window.Deactivated += WindowDeactivated;
}
if (_topLevel != null)
else
{
var window = _topLevel as Window;
if (window != null)
var parentPopuproot = _topLevel as PopupRoot;
if (parentPopuproot?.Parent is Popup popup)
{
window.Deactivated += WindowDeactivated;
popup.Closed += ParentClosed;
}
else
{
var parentPopuproot = _topLevel as PopupRoot;
if (parentPopuproot?.Parent is Popup popup)
{
popup.Closed += ParentClosed;
}
}
_topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
_nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick);
}
_topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
_nonClientListener = InputManager.Instance?.Process.Subscribe(ListenForNonClientClick);
PopupRootCreated?.Invoke(this, EventArgs.Empty);
_popupRoot.Show();
if (ObeyScreenEdges)
{
_popupRoot.SnapInsideScreenEdges();
}
_popupHost.Show();
using (BeginIgnoringIsOpen())
{
@ -282,29 +277,14 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public void Close()
{
if (_popupRoot != null)
if (_popupHost != null)
{
if (_topLevel != null)
{
_topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
var window = _topLevel as Window;
if (window != null)
window.Deactivated -= WindowDeactivated;
else
{
var parentPopuproot = _topLevel as PopupRoot;
if (parentPopuproot?.Parent is Popup popup)
{
popup.Closed -= ParentClosed;
}
}
_nonClientListener?.Dispose();
_nonClientListener = null;
}
_popupRoot.Hide();
_popupHost.TemplateApplied -= RootTemplateApplied;
}
_presenterSubscription?.Dispose();
CloseCurrent();
using (BeginIgnoringIsOpen())
{
IsOpen = false;
@ -313,6 +293,41 @@ namespace Avalonia.Controls.Primitives
Closed?.Invoke(this, EventArgs.Empty);
}
void CloseCurrent()
{
if (_topLevel != null)
{
_topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
var window = _topLevel as Window;
if (window != null)
window.Deactivated -= WindowDeactivated;
else
{
var parentPopuproot = _topLevel as PopupRoot;
if (parentPopuproot?.Parent is Popup popup)
{
popup.Closed -= ParentClosed;
}
}
_nonClientListener?.Dispose();
_nonClientListener = null;
_topLevel = null;
}
if (_popupHost != null)
{
foreach(var b in _bindings)
b.Dispose();
_bindings.Clear();
_popupHost.SetChild(null);
_popupHost.Hide();
((ISetLogicalParent)_popupHost).SetParent(null);
_popupHost.Dispose();
_popupHost = null;
}
}
/// <summary>
/// Measures the control.
/// </summary>
@ -323,27 +338,14 @@ namespace Avalonia.Controls.Primitives
return new Size();
}
/// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
_topLevel = e.Root as TopLevel;
}
/// <inheritdoc/>
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
_topLevel = null;
if (_popupRoot != null)
{
((ISetLogicalParent)_popupRoot).SetParent(null);
_popupRoot.Dispose();
_popupRoot = null;
}
Close();
}
/// <summary>
/// Called when the <see cref="IsOpen"/> property changes.
/// </summary>
@ -380,49 +382,6 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Gets the position for the popup based on the placement properties.
/// </summary>
/// <returns>The popup's position in screen coordinates.</returns>
protected virtual PixelPoint GetPosition()
{
var result = GetPosition(PlacementTarget ?? this.GetVisualParent<Control>(), PlacementMode, PopupRoot,
HorizontalOffset, VerticalOffset);
return result;
}
internal static PixelPoint GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset)
{
var root = target?.GetVisualRoot();
var mode = root != null ? placement : PlacementMode.Pointer;
var scaling = root?.RenderScaling ?? 1;
switch (mode)
{
case PlacementMode.Pointer:
if (popupRoot != null)
{
var screenOffset = PixelPoint.FromPoint(new Point(horizontalOffset, verticalOffset), scaling);
var mouseOffset = ((IInputRoot)popupRoot)?.MouseDevice?.Position ?? default;
return new PixelPoint(
screenOffset.X + mouseOffset.X,
screenOffset.Y + mouseOffset.Y);
}
return default;
case PlacementMode.Bottom:
return target?.PointToScreen(new Point(0 + horizontalOffset, target.Bounds.Height + verticalOffset)) ?? default;
case PlacementMode.Right:
return target?.PointToScreen(new Point(target.Bounds.Width + horizontalOffset, 0 + verticalOffset)) ?? default;
default:
throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
}
}
private void ListenForNonClientClick(RawInputEventArgs e)
{
var mouse = e as RawPointerEventArgs;
@ -445,17 +404,62 @@ namespace Avalonia.Controls.Primitives
}
}
private bool IsChildOrThis(IVisual child)
private void RootTemplateApplied(object sender, TemplateAppliedEventArgs e)
{
IVisual root = child.GetVisualRoot();
while (root is PopupRoot)
_popupHost.TemplateApplied -= RootTemplateApplied;
if (_presenterSubscription != null)
{
if (root == PopupRoot) return true;
root = ((PopupRoot)root).Parent.GetVisualRoot();
_presenterSubscription.Dispose();
_presenterSubscription = null;
}
// If the Popup appears in a control template, then the child controls
// that appear in the popup host need to have their TemplatedParent
// properties set.
if (TemplatedParent != null)
{
_popupHost.Presenter?.ApplyTemplate();
_popupHost.Presenter?.GetObservable(ContentPresenter.ChildProperty)
.Subscribe(SetTemplatedParentAndApplyChildTemplates);
}
}
private void SetTemplatedParentAndApplyChildTemplates(IControl control)
{
if (control != null)
{
var templatedParent = TemplatedParent;
if (control.TemplatedParent == null)
{
control.SetValue(TemplatedParentProperty, templatedParent);
}
control.ApplyTemplate();
if (!(control is IPresenter) && control.TemplatedParent == templatedParent)
{
foreach (IControl child in control.GetVisualChildren())
{
SetTemplatedParentAndApplyChildTemplates(child);
}
}
}
return false;
}
private bool IsChildOrThis(IVisual child)
{
return _popupHost != null && ((IVisual)_popupHost).FindCommonVisualAncestor(child) == _popupHost;
}
public bool IsInsidePopup(IVisual visual)
{
return _popupHost != null && ((IVisual)_popupHost)?.IsVisualAncestorOf(visual) == true;
}
public bool IsPointerOverPopup => ((IInputElement)_popupHost).IsPointerOver;
private void WindowDeactivated(object sender, EventArgs e)
{
if (!StaysOpen)

358
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@ -0,0 +1,358 @@
// The documentation and flag names in this file are initially taken from
// xdg_shell wayland protocol this API is designed after
// therefore, I'm including the license from wayland-protocols repo
/*
Copyright © 2008-2013 Kristian Høgsberg
Copyright © 2010-2013 Intel Corporation
Copyright © 2013 Rafael Antognolli
Copyright © 2013 Jasper St. Pierre
Copyright © 2014 Jonas Ådahl
Copyright © 2014 Jason Ekstrand
Copyright © 2014-2015 Collabora, Ltd.
Copyright © 2015 Red Hat Inc.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
---
The above is the version of the MIT "Expat" License used by X.org:
http://cgit.freedesktop.org/xorg/xserver/tree/COPYING
Adjustments for Avalonia needs:
Copyright © 2019 Nikita Tsukanov
*/
using System;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives.PopupPositioning
{
/// <summary>
///
/// The IPopupPositioner provides a collection of rules for the placement of a
/// a popup relative to its parent. Rules can be defined to ensure
/// the popup remains within the visible area's borders, and to
/// specify how the popup changes its position, such as sliding along
/// an axis, or flipping around a rectangle. These positioner-created rules are
/// constrained by the requirement that a popup must intersect with or
/// be at least partially adjacent to its parent surface.
/// </summary>
public struct PopupPositionerParameters
{
private PopupPositioningEdge _gravity;
private PopupPositioningEdge _anchor;
/// <summary>
/// Set the size of the popup that is to be positioned with the positioner
/// object. The size is in scaled coordinates.
/// </summary>
public Size Size { get; set; }
/// <summary>
/// Specify the anchor rectangle within the parent that the popup
/// will be placed relative to. The rectangle is relative to the
/// parent geometry
///
/// The anchor rectangle may not extend outside the window geometry of the
/// popup's parent. The anchor rectangle is in scaled coordinates
/// </summary>
public Rect AnchorRectangle { get; set; }
/// <summary>
/// Defines the anchor point for the anchor rectangle. The specified anchor
/// is used derive an anchor point that the popup will be
/// positioned relative to. If a corner anchor is set (e.g. 'TopLeft' or
/// 'BottomRight'), the anchor point will be at the specified corner;
/// otherwise, the derived anchor point will be centered on the specified
/// edge, or in the center of the anchor rectangle if no edge is specified.
/// </summary>
public PopupPositioningEdge Anchor
{
get => _anchor;
set
{
PopupPositioningEdgeHelper.ValidateEdge(value);
_anchor = value;
}
}
/// <summary>
/// Defines in what direction a popup should be positioned, relative to
/// the anchor point of the parent. If a corner gravity is
/// specified (e.g. 'BottomRight' or 'TopLeft'), then the popup
/// will be placed towards the specified gravity; otherwise, the popup
/// will be centered over the anchor point on any axis that had no
/// gravity specified.
/// </summary>
public PopupPositioningEdge Gravity
{
get => _gravity;
set
{
PopupPositioningEdgeHelper.ValidateEdge(value);
_gravity = value;
}
}
/// <summary>
/// Specify how the popup should be positioned if the originally intended
/// position caused the popup to be constrained, meaning at least
/// partially outside positioning boundaries set by the positioner. The
/// adjustment is set by constructing a bitmask describing the adjustment to
/// be made when the popup is constrained on that axis.
///
/// If no bit for one axis is set, the positioner will assume that the child
/// surface should not change its position on that axis when constrained.
///
/// If more than one bit for one axis is set, the order of how adjustments
/// are applied is specified in the corresponding adjustment descriptions.
///
/// The default adjustment is none.
/// </summary>
public PopupPositionerConstraintAdjustment ConstraintAdjustment { get; set; }
/// <summary>
/// Specify the popup position offset relative to the position of the
/// anchor on the anchor rectangle and the anchor on the popup. For
/// example if the anchor of the anchor rectangle is at (x, y), the popup
/// has the gravity bottom|right, and the offset is (ox, oy), the calculated
/// surface position will be (x + ox, y + oy). The offset position of the
/// surface is the one used for constraint testing. See
/// set_constraint_adjustment.
///
/// An example use case is placing a popup menu on top of a user interface
/// element, while aligning the user interface element of the parent surface
/// with some user interface element placed somewhere in the popup.
/// </summary>
public Point Offset { get; set; }
}
/// <summary>
/// The constraint adjustment value define ways how popup position will
/// be adjusted if the unadjusted position would result in the popup
/// being partly constrained.
///
/// Whether a popup is considered 'constrained' is left to the positioner
/// to determine. For example, the popup may be partly outside the
/// target platform defined 'work area', thus necessitating the popup's
/// position be adjusted until it is entirely inside the work area.
/// </summary>
[Flags]
public enum PopupPositionerConstraintAdjustment
{
/// <summary>
/// Don't alter the surface position even if it is constrained on some
/// axis, for example partially outside the edge of an output.
/// </summary>
None = 0,
/// <summary>
/// Slide the surface along the x axis until it is no longer constrained.
/// First try to slide towards the direction of the gravity on the x axis
/// until either the edge in the opposite direction of the gravity is
/// unconstrained or the edge in the direction of the gravity is
/// constrained.
///
/// Then try to slide towards the opposite direction of the gravity on the
/// x axis until either the edge in the direction of the gravity is
/// unconstrained or the edge in the opposite direction of the gravity is
/// constrained.
/// </summary>
SlideX = 1,
/// <summary>
/// Slide the surface along the y axis until it is no longer constrained.
///
/// First try to slide towards the direction of the gravity on the y axis
/// until either the edge in the opposite direction of the gravity is
/// unconstrained or the edge in the direction of the gravity is
/// constrained.
///
/// Then try to slide towards the opposite direction of the gravity on the
/// y axis until either the edge in the direction of the gravity is
/// unconstrained or the edge in the opposite direction of the gravity is
/// constrained.
/// */
/// </summary>
SlideY = 2,
/// <summary>
/// Invert the anchor and gravity on the x axis if the surface is
/// constrained on the x axis. For example, if the left edge of the
/// surface is constrained, the gravity is 'left' and the anchor is
/// 'left', change the gravity to 'right' and the anchor to 'right'.
///
/// If the adjusted position also ends up being constrained, the resulting
/// position of the flip_x adjustment will be the one before the
/// adjustment.
/// </summary>
FlipX = 4,
/// <summary>
/// Invert the anchor and gravity on the y axis if the surface is
/// constrained on the y axis. For example, if the bottom edge of the
/// surface is constrained, the gravity is 'bottom' and the anchor is
/// 'bottom', change the gravity to 'top' and the anchor to 'top'.
///
/// The adjusted position is calculated given the original anchor
/// rectangle and offset, but with the new flipped anchor and gravity
/// values.
///
/// If the adjusted position also ends up being constrained, the resulting
/// position of the flip_y adjustment will be the one before the
/// adjustment.
/// </summary>
FlipY = 8,
All = SlideX|SlideY|FlipX|FlipY
}
static class PopupPositioningEdgeHelper
{
public static void ValidateEdge(this PopupPositioningEdge edge)
{
if (((edge & PopupPositioningEdge.Left) != 0 && (edge & PopupPositioningEdge.Right) != 0)
||
((edge & PopupPositioningEdge.Top) != 0 && (edge & PopupPositioningEdge.Bottom) != 0))
throw new ArgumentException("Opposite edges specified");
}
public static PopupPositioningEdge Flip(this PopupPositioningEdge edge)
{
var hmask = PopupPositioningEdge.Left | PopupPositioningEdge.Right;
var vmask = PopupPositioningEdge.Top | PopupPositioningEdge.Bottom;
if ((edge & hmask) != 0)
edge ^= hmask;
if ((edge & vmask) != 0)
edge ^= vmask;
return edge;
}
public static PopupPositioningEdge FlipX(this PopupPositioningEdge edge)
{
if ((edge & PopupPositioningEdge.HorizontalMask) != 0)
edge ^= PopupPositioningEdge.HorizontalMask;
return edge;
}
public static PopupPositioningEdge FlipY(this PopupPositioningEdge edge)
{
if ((edge & PopupPositioningEdge.VerticalMask) != 0)
edge ^= PopupPositioningEdge.VerticalMask;
return edge;
}
}
[Flags]
public enum PopupPositioningEdge
{
None,
Top = 1,
Bottom = 2,
Left = 4,
Right = 8,
TopLeft = Top | Left,
TopRight = Top | Right,
BottomLeft = Bottom | Left,
BottomRight = Bottom | Right,
VerticalMask = Top | Bottom,
HorizontalMask = Left | Right,
AllMask = VerticalMask|HorizontalMask
}
public interface IPopupPositioner
{
void Update(PopupPositionerParameters parameters);
}
static class PopupPositionerExtensions
{
public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters,
TopLevel topLevel,
IVisual target, PlacementMode placement, Point offset,
PopupPositioningEdge anchor, PopupPositioningEdge gravity)
{
// We need a better way for tracking the last pointer position
var pointer = topLevel.PointToClient(topLevel.PlatformImpl.MouseDevice.Position);
positionerParameters.Offset = offset;
positionerParameters.ConstraintAdjustment = PopupPositionerConstraintAdjustment.All;
if (placement == PlacementMode.Pointer)
{
positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1));
positionerParameters.Anchor = PopupPositioningEdge.BottomRight;
positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
}
else
{
if (target == null)
throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null");
var matrix = target.TransformToVisual(topLevel);
if (matrix == null)
{
if (target.GetVisualRoot() == null)
throw new InvalidCastException("Target control is not attached to the visual tree");
throw new InvalidCastException("Target control is not in the same tree as the popup parent");
}
positionerParameters.AnchorRectangle = new Rect(default, target.Bounds.Size)
.TransformToAABB(matrix.Value);
if (placement == PlacementMode.Right)
{
positionerParameters.Anchor = PopupPositioningEdge.TopRight;
positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
}
else if (placement == PlacementMode.Bottom)
{
positionerParameters.Anchor = PopupPositioningEdge.BottomLeft;
positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
}
else if (placement == PlacementMode.Left)
{
positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
positionerParameters.Gravity = PopupPositioningEdge.BottomLeft;
}
else if (placement == PlacementMode.Top)
{
positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
positionerParameters.Gravity = PopupPositioningEdge.TopRight;
}
else if (placement == PlacementMode.AnchorAndGravity)
{
positionerParameters.Anchor = anchor;
positionerParameters.Gravity = gravity;
}
else
throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
}
}
}
}

175
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs

@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Controls.Primitives.PopupPositioning
{
public interface IManagedPopupPositionerPopup
{
IReadOnlyList<ManagedPopupPositionerScreenInfo> Screens { get; }
Rect ParentClientAreaScreenGeometry { get; }
void MoveAndResize(Point devicePoint, Size virtualSize);
Point TranslatePoint(Point pt);
Size TranslateSize(Size size);
}
public class ManagedPopupPositionerScreenInfo
{
public Rect Bounds { get; }
public Rect WorkingArea { get; }
public ManagedPopupPositionerScreenInfo(Rect bounds, Rect workingArea)
{
Bounds = bounds;
WorkingArea = workingArea;
}
}
public class ManagedPopupPositioner : IPopupPositioner
{
private readonly IManagedPopupPositionerPopup _popup;
public ManagedPopupPositioner(IManagedPopupPositionerPopup popup)
{
_popup = popup;
}
private static Point GetAnchorPoint(Rect anchorRect, PopupPositioningEdge edge)
{
double x, y;
if ((edge & PopupPositioningEdge.Left) != 0)
x = anchorRect.X;
else if ((edge & PopupPositioningEdge.Right) != 0)
x = anchorRect.Right;
else
x = anchorRect.X + anchorRect.Width / 2;
if ((edge & PopupPositioningEdge.Top) != 0)
y = anchorRect.Y;
else if ((edge & PopupPositioningEdge.Bottom) != 0)
y = anchorRect.Bottom;
else
y = anchorRect.Y + anchorRect.Height / 2;
return new Point(x, y);
}
private static Point Gravitate(Point anchorPoint, Size size, PopupPositioningEdge gravity)
{
double x, y;
if ((gravity & PopupPositioningEdge.Left) != 0)
x = -size.Width;
else if ((gravity & PopupPositioningEdge.Right) != 0)
x = 0;
else
x = -size.Width / 2;
if ((gravity & PopupPositioningEdge.Top) != 0)
y = -size.Height;
else if ((gravity & PopupPositioningEdge.Bottom) != 0)
y = 0;
else
y = -size.Height / 2;
return anchorPoint + new Point(x, y);
}
public void Update(PopupPositionerParameters parameters)
{
Update(_popup.TranslateSize(parameters.Size), parameters.Size,
new Rect(_popup.TranslatePoint(parameters.AnchorRectangle.TopLeft),
_popup.TranslateSize(parameters.AnchorRectangle.Size)),
parameters.Anchor, parameters.Gravity, parameters.ConstraintAdjustment,
_popup.TranslatePoint(parameters.Offset));
}
private void Update(Size translatedSize, Size originalSize,
Rect anchorRect, PopupPositioningEdge anchor, PopupPositioningEdge gravity,
PopupPositionerConstraintAdjustment constraintAdjustment, Point offset)
{
var parentGeometry = _popup.ParentClientAreaScreenGeometry;
anchorRect = anchorRect.Translate(parentGeometry.TopLeft);
Rect GetBounds()
{
var screens = _popup.Screens;
var targetScreen = screens.FirstOrDefault(s => s.Bounds.Contains(anchorRect.TopLeft))
?? screens.FirstOrDefault(s => s.Bounds.Intersects(anchorRect))
?? screens.FirstOrDefault(s => s.Bounds.Contains(parentGeometry.TopLeft))
?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry))
?? screens.FirstOrDefault();
return targetScreen?.WorkingArea
?? new Rect(0, 0, double.MaxValue, double.MaxValue);
}
var bounds = GetBounds();
bool FitsInBounds(Rect rc, PopupPositioningEdge edge = PopupPositioningEdge.AllMask)
{
if ((edge & PopupPositioningEdge.Left) != 0
&& rc.X < bounds.X)
return false;
if ((edge & PopupPositioningEdge.Top) != 0
&& rc.Y < bounds.Y)
return false;
if ((edge & PopupPositioningEdge.Right) != 0
&& rc.Right > bounds.Right)
return false;
if ((edge & PopupPositioningEdge.Bottom) != 0
&& rc.Bottom > bounds.Bottom)
return false;
return true;
}
Rect GetUnconstrained(PopupPositioningEdge a, PopupPositioningEdge g) =>
new Rect(Gravitate(GetAnchorPoint(anchorRect, a), translatedSize, g) + offset, translatedSize);
var geo = GetUnconstrained(anchor, gravity);
// If flipping geometry and anchor is allowed and helps, use the flipped one,
// otherwise leave it as is
if (!FitsInBounds(geo, PopupPositioningEdge.HorizontalMask)
&& (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipX) != 0)
{
var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX());
if (FitsInBounds(flipped, PopupPositioningEdge.HorizontalMask))
geo = geo.WithX(flipped.X);
}
// If sliding is allowed, try moving the rect into the bounds
if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideX) != 0)
{
geo = geo.WithX(Math.Max(geo.X, bounds.X));
if (geo.Right > bounds.Right)
geo = geo.WithX(bounds.Right - geo.Width);
}
// If flipping geometry and anchor is allowed and helps, use the flipped one,
// otherwise leave it as is
if (!FitsInBounds(geo, PopupPositioningEdge.VerticalMask)
&& (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipY) != 0)
{
var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY());
if (FitsInBounds(flipped, PopupPositioningEdge.VerticalMask))
geo = geo.WithY(flipped.Y);
}
// If sliding is allowed, try moving the rect into the bounds
if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideY) != 0)
{
geo = geo.WithY(Math.Max(geo.Y, bounds.Y));
if (geo.Bottom > bounds.Bottom)
geo = geo.WithY(bounds.Bottom - geo.Height);
}
_popup.MoveAndResize(geo.TopLeft, originalSize);
}
}
}

50
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform;
namespace Avalonia.Controls.Primitives.PopupPositioning
{
/// <summary>
/// This class is used to simplify integration of IPopupImpl implementations with popup positioner
/// </summary>
public class ManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup
{
private readonly IWindowBaseImpl _parent;
public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling);
private readonly MoveResizeDelegate _moveResize;
public ManagedPopupPositionerPopupImplHelper(IWindowBaseImpl parent, MoveResizeDelegate moveResize)
{
_parent = parent;
_moveResize = moveResize;
}
public IReadOnlyList<ManagedPopupPositionerScreenInfo> Screens =>
_parent.Screen.AllScreens.Select(s => new ManagedPopupPositionerScreenInfo(
s.Bounds.ToRect(1), s.WorkingArea.ToRect(1))).ToList();
public Rect ParentClientAreaScreenGeometry
{
get
{
// Popup positioner operates with abstract coordinates, but in our case they are pixel ones
var point = _parent.PointToScreen(default);
var size = PixelSize.FromSize(_parent.ClientSize, _parent.Scaling);
return new Rect(point.X, point.Y, size.Width, size.Height);
}
}
public void MoveAndResize(Point devicePoint, Size virtualSize)
{
_moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.Scaling);
}
public Point TranslatePoint(Point pt) => pt * _parent.Scaling;
public Size TranslateSize(Size size) => size * _parent.Scaling;
}
}

119
src/Avalonia.Controls/Primitives/PopupRoot.cs

@ -2,8 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Presenters;
using System.Collections.Generic;
using System.Reactive.Disposables;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Platform;
@ -16,9 +17,10 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// The root window of a <see cref="Popup"/>.
/// </summary>
public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost
public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost
{
private IDisposable _presenterSubscription;
private readonly TopLevel _parent;
private PopupPositionerParameters _positionerParameters;
/// <summary>
/// Initializes static members of the <see cref="PopupRoot"/> class.
@ -31,8 +33,8 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Initializes a new instance of the <see cref="PopupRoot"/> class.
/// </summary>
public PopupRoot()
: this(null)
public PopupRoot(TopLevel parent, IPopupImpl impl)
: this(parent, impl,null)
{
}
@ -42,9 +44,10 @@ namespace Avalonia.Controls.Primitives
/// <param name="dependencyResolver">
/// The dependency resolver to use. If null the default dependency resolver will be used.
/// </param>
public PopupRoot(IAvaloniaDependencyResolver dependencyResolver)
: base(PlatformManager.CreatePopup(), dependencyResolver)
public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver dependencyResolver)
: base(impl, dependencyResolver)
{
_parent = parent;
}
/// <summary>
@ -74,73 +77,61 @@ namespace Avalonia.Controls.Primitives
/// <inheritdoc/>
public void Dispose() => PlatformImpl?.Dispose();
/// <summary>
/// Moves the Popups position so that it doesnt overlap screen edges.
/// This method can be called immediately after Show has been called.
/// </summary>
public void SnapInsideScreenEdges()
private void UpdatePosition()
{
var screen = (VisualRoot as WindowBase)?.Screens?.ScreenFromPoint(Position);
if (screen != null)
{
var scaling = VisualRoot.RenderScaling;
var bounds = PixelRect.FromRect(Bounds, scaling);
var screenX = Position.X + bounds.Width - screen.Bounds.X;
var screenY = Position.Y + bounds.Height - screen.Bounds.Y;
if (screenX > screen.Bounds.Width)
{
Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width));
}
if (screenY > screen.Bounds.Height)
{
Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height));
}
}
PlatformImpl?.PopupPositioner.Update(_positionerParameters);
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
PopupPositioningEdge anchor = PopupPositioningEdge.None,
PopupPositioningEdge gravity = PopupPositioningEdge.None)
{
base.OnTemplateApplied(e);
_positionerParameters.ConfigurePosition(_parent, target,
placement, offset, anchor, gravity);
if (_positionerParameters.Size != default)
UpdatePosition();
}
public void SetChild(IControl control) => Content = control;
if (Parent?.TemplatedParent != null)
IVisual IPopupHost.HostedVisualTreeRoot => this;
public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty, StyledProperty<double> minWidthProperty,
StyledProperty<double> maxWidthProperty, StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty)
{
var bindings = new List<IDisposable>();
void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to]));
Bind(WidthProperty, widthProperty);
Bind(MinWidthProperty, minWidthProperty);
Bind(MaxWidthProperty, maxWidthProperty);
Bind(HeightProperty, heightProperty);
Bind(MinHeightProperty, minHeightProperty);
Bind(MaxHeightProperty, maxHeightProperty);
Bind(TopmostProperty, topmostProperty);
return Disposable.Create(() =>
{
if (_presenterSubscription != null)
{
_presenterSubscription.Dispose();
_presenterSubscription = null;
}
Presenter?.ApplyTemplate();
Presenter?.GetObservable(ContentPresenter.ChildProperty)
.Subscribe(SetTemplatedParentAndApplyChildTemplates);
}
foreach (var x in bindings)
x.Dispose();
});
}
private void SetTemplatedParentAndApplyChildTemplates(IControl control)
/// <summary>
/// Carries out the arrange pass of the window.
/// </summary>
/// <param name="finalSize">The final window size.</param>
/// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
if (control != null)
using (BeginAutoSizing())
{
var templatedParent = Parent.TemplatedParent;
if (control.TemplatedParent == null)
{
control.SetValue(TemplatedParentProperty, templatedParent);
}
control.ApplyTemplate();
if (!(control is IPresenter) && control.TemplatedParent == templatedParent)
{
foreach (IControl child in control.GetVisualChildren())
{
SetTemplatedParentAndApplyChildTemplates(child);
}
}
_positionerParameters.Size = finalSize;
UpdatePosition();
}
return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
}
}
}

1
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -7,6 +7,7 @@ using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls.Primitives
{

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

@ -333,6 +333,11 @@ namespace Avalonia.Controls.Primitives
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Reset:
SelectedIndex = IndexOf(Items, SelectedItem);
if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
{
SelectedIndex = 0;
}
break;
}
}

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

@ -4,6 +4,7 @@
using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls.Primitives
{

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

@ -3,6 +3,7 @@
using System;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Metadata;
namespace Avalonia.Controls.Primitives

93
src/Avalonia.Controls/Primitives/VisualLayerManager.cs

@ -0,0 +1,93 @@
using System.Collections.Generic;
using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Controls.Primitives
{
public class VisualLayerManager : Decorator
{
private const int AdornerZIndex = int.MaxValue - 100;
private const int OverlayZIndex = int.MaxValue - 99;
private IStyleHost _styleRoot;
private readonly List<Control> _layers = new List<Control>();
public bool IsPopup { get; set; }
public AdornerLayer AdornerLayer
{
get
{
var rv = FindLayer<AdornerLayer>();
if (rv == null)
AddLayer(rv = new AdornerLayer(), AdornerZIndex);
return rv;
}
}
public OverlayLayer OverlayLayer
{
get
{
if (IsPopup)
return null;
var rv = FindLayer<OverlayLayer>();
if(rv == null)
AddLayer(rv = new OverlayLayer(), OverlayZIndex);
return rv;
}
}
T FindLayer<T>() where T : class
{
foreach (var layer in _layers)
if (layer is T match)
return match;
return null;
}
void AddLayer(Control layer, int zindex)
{
_layers.Add(layer);
((ISetLogicalParent)layer).SetParent(this);
layer.ZIndex = zindex;
VisualChildren.Add(layer);
if (((ILogical)this).IsAttachedToLogicalTree)
((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_styleRoot));
InvalidateArrange();
}
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
_styleRoot = e.Root;
foreach (var l in _layers)
((ILogical)l).NotifyAttachedToLogicalTree(e);
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_styleRoot = null;
base.OnDetachedFromLogicalTree(e);
foreach (var l in _layers)
((ILogical)l).NotifyDetachedFromLogicalTree(e);
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (var l in _layers)
l.Measure(availableSize);
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (var l in _layers)
l.Arrange(new Rect(finalSize));
return base.ArrangeOverride(finalSize);
}
}
}

7
src/Avalonia.Controls/ProgressBar.cs

@ -3,6 +3,7 @@
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
namespace Avalonia.Controls
{
@ -33,8 +34,8 @@ namespace Avalonia.Controls
static ProgressBar()
{
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
PseudoClass<ProgressBar>(IsIndeterminateProperty, ":indeterminate");
ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.UpdateIndicatorWhenPropChanged);
@ -120,4 +121,4 @@ namespace Avalonia.Controls
UpdateIndicator(Bounds.Size);
}
}
}
}

54
src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs

@ -0,0 +1,54 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using Avalonia.Controls.Templates;
namespace Avalonia.Controls
{
internal class ItemTemplateWrapper
{
private readonly IDataTemplate _dataTemplate;
public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate;
public IControl GetElement(IControl parent, object data)
{
var selectedTemplate = _dataTemplate;
var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate);
IControl element = null;
if (recyclePool != null)
{
// try to get an element from the recycle pool.
element = recyclePool.TryGetElement(string.Empty, parent);
}
if (element == null)
{
// no element was found in recycle pool, create a new element
element = selectedTemplate.Build(data);
// Associate template with element
element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate);
}
return element;
}
public void RecycleElement(IControl parent, IControl element)
{
var selectedTemplate = _dataTemplate;
var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate);
if (recyclePool == null)
{
// No Recycle pool in the template, create one.
recyclePool = new RecyclePool();
RecyclePool.SetPoolInstance(selectedTemplate, recyclePool);
}
recyclePool.PutElement(element, "" /* key */, parent);
}
}
}

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

@ -0,0 +1,724 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections;
using System.Collections.Specialized;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a data-driven collection control that incorporates a flexible layout system,
/// custom views, and virtualization.
/// </summary>
public class ItemsRepeater : Panel
{
/// <summary>
/// Defines the <see cref="HorizontalCacheLength"/> property.
/// </summary>
public static readonly AvaloniaProperty<double> HorizontalCacheLengthProperty =
AvaloniaProperty.Register<ItemsRepeater, double>(nameof(HorizontalCacheLength), 2.0);
/// <summary>
/// Defines the <see cref="ItemTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
ItemsControl.ItemTemplateProperty.AddOwner<ItemsRepeater>();
/// <summary>
/// Defines the <see cref="Items"/> property.
/// </summary>
public static readonly DirectProperty<ItemsRepeater, IEnumerable> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<ItemsRepeater>(o => o.Items, (o, v) => o.Items = v);
/// <summary>
/// Defines the <see cref="Layout"/> property.
/// </summary>
public static readonly AvaloniaProperty<AttachedLayout> LayoutProperty =
AvaloniaProperty.Register<ItemsRepeater, AttachedLayout>(nameof(Layout), new StackLayout());
/// <summary>
/// Defines the <see cref="VerticalCacheLength"/> property.
/// </summary>
public static readonly AvaloniaProperty<double> VerticalCacheLengthProperty =
AvaloniaProperty.Register<ItemsRepeater, double>(nameof(VerticalCacheLength), 2.0);
private static readonly AttachedProperty<VirtualizationInfo> VirtualizationInfoProperty =
AvaloniaProperty.RegisterAttached<ItemsRepeater, IControl, VirtualizationInfo>("VirtualizationInfo");
internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1);
internal static readonly Point ClearedElementsArrangePosition = new Point(-10000.0, -10000.0);
private readonly ViewManager _viewManager;
private readonly ViewportManager _viewportManager;
private IEnumerable _items;
private VirtualizingLayoutContext _layoutContext;
private NotifyCollectionChangedEventArgs _processingItemsSourceChange;
private bool _isLayoutInProgress;
private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs;
private ItemsRepeaterElementClearingEventArgs _elementClearingArgs;
private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs;
/// <summary>
/// Initializes a new instance of the <see cref="ItemsRepeater"/> class.
/// </summary>
public ItemsRepeater()
{
_viewManager = new ViewManager(this);
_viewportManager = new ViewportManager(this);
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once);
OnLayoutChanged(null, Layout);
}
static ItemsRepeater()
{
ClipToBoundsProperty.OverrideDefaultValue<ItemsRepeater>(true);
}
/// <summary>
/// Gets or sets the layout used to size and position elements in the ItemsRepeater.
/// </summary>
/// <value>
/// The layout used to size and position elements. The default is a StackLayout with
/// vertical orientation.
/// </value>
public AttachedLayout Layout
{
get => GetValue(LayoutProperty);
set => SetValue(LayoutProperty, value);
}
/// <summary>
/// Gets or sets an object source used to generate the content of the ItemsRepeater.
/// </summary>
public IEnumerable Items
{
get => _items;
set => SetAndRaise(ItemsProperty, ref _items, value);
}
/// <summary>
/// Gets or sets the template used to display each item.
/// </summary>
public IDataTemplate ItemTemplate
{
get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
/// <summary>
/// Gets or sets a value that indicates the size of the buffer used to realize items when
/// panning or scrolling horizontally.
/// </summary>
public double HorizontalCacheLength
{
get => GetValue(HorizontalCacheLengthProperty);
set => SetValue(HorizontalCacheLengthProperty, value);
}
/// <summary>
/// Gets or sets a value that indicates the size of the buffer used to realize items when
/// panning or scrolling vertically.
/// </summary>
public double VerticalCacheLength
{
get => GetValue(VerticalCacheLengthProperty);
set => SetValue(VerticalCacheLengthProperty, value);
}
/// <summary>
/// Gets a standardized view of the supported interactions between a given Items object and
/// the ItemsRepeater control and its associated components.
/// </summary>
public ItemsSourceView ItemsSourceView { get; private set; }
internal ItemTemplateWrapper ItemTemplateShim { get; set; }
internal Point LayoutOrigin { get; set; }
internal object LayoutState { get; set; }
internal IControl MadeAnchor => _viewportManager.MadeAnchor;
internal Rect RealizationWindow => _viewportManager.GetLayoutRealizationWindow();
internal IControl SuggestedAnchor => _viewportManager.SuggestedAnchor;
private bool IsProcessingCollectionChange => _processingItemsSourceChange != null;
private LayoutContext LayoutContext
{
get
{
if (_layoutContext == null)
{
_layoutContext = new RepeaterLayoutContext(this);
}
return _layoutContext;
}
}
/// <summary>
/// Occurs each time an element is cleared and made available to be re-used.
/// </summary>
/// <remarks>
/// This event is raised immediately each time an element is cleared, such as when it falls
/// outside the range of realized items. Elements are cleared when they become available
/// for re-use.
/// </remarks>
public event EventHandler<ItemsRepeaterElementClearingEventArgs> ElementClearing;
/// <summary>
/// Occurs for each realized <see cref="IControl"/> when the index for the item it
/// represents has changed.
/// </summary>
/// <remarks>
/// When you use ItemsRepeater to build a more complex control that supports specific
/// interactions on the child elements (such as selection or click), it is useful to be
/// able to keep an up-to-date identifier for the backing data item.
///
/// This event is raised for each realized IControl where the index for the item it
/// represents has changed. For example, when another item is added or removed in the data
/// source, the index for items that come after in the ordering will be impacted.
/// </remarks>
public event EventHandler<ItemsRepeaterElementIndexChangedEventArgs> ElementIndexChanged;
/// <summary>
/// Occurs each time an element is prepared for use.
/// </summary>
/// <remarks>
/// The prepared element might be newly created or an existing element that is being re-
/// used.
/// </remarks>
public event EventHandler<ItemsRepeaterElementPreparedEventArgs> ElementPrepared;
/// <summary>
/// Retrieves the index of the item from the data source that corresponds to the specified
/// <see cref="IControl"/>.
/// </summary>
/// <param name="element">
/// The element that corresponds to the item to get the index of.
/// </param>
/// <returns>
/// The index of the item from the data source that corresponds to the specified UIElement,
/// or -1 if the element is not supported.
/// </returns>
public int GetElementIndex(IControl element) => GetElementIndexImpl(element);
/// <summary>
/// Retrieves the realized UIElement that corresponds to the item at the specified index in
/// the data source.
/// </summary>
/// <param name="index">The index of the item.</param>
/// <returns>
/// he UIElement that corresponds to the item at the specified index if the item is
/// realized, or null if the item is not realized.
/// </returns>
public IControl TryGetElement(int index) => GetElementFromIndexImpl(index);
internal void PinElement(IControl element) => _viewManager.UpdatePin(element, true);
internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false);
internal IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index);
internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element)
{
var value = element.GetValue(VirtualizationInfoProperty);
return value;
}
internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(IControl element)
{
if (TryGetVirtualizationInfo(element) != null)
{
throw new InvalidOperationException("VirtualizationInfo already created.");
}
var result = new VirtualizationInfo();
element.SetValue(VirtualizationInfoProperty, result);
return result;
}
internal static VirtualizationInfo GetVirtualizationInfo(IControl element)
{
var result = element.GetValue(VirtualizationInfoProperty);
if (result == null)
{
result = new VirtualizationInfo();
element.SetValue(VirtualizationInfoProperty, result);
}
return result;
}
protected override Size MeasureOverride(Size availableSize)
{
if (_isLayoutInProgress)
{
throw new AvaloniaInternalException("Reentrancy detected during layout.");
}
if (IsProcessingCollectionChange)
{
throw new NotSupportedException("Cannot run layout in the middle of a collection change.");
}
_viewportManager.OnOwnerMeasuring();
_isLayoutInProgress = true;
try
{
_viewManager.PrunePinnedElements();
var extent = new Rect();
var desiredSize = new Size();
var layout = Layout;
if (layout != null)
{
var layoutContext = GetLayoutContext();
desiredSize = layout.Measure(layoutContext, availableSize);
extent = new Rect(LayoutOrigin.X, LayoutOrigin.Y, desiredSize.Width, desiredSize.Height);
// Clear auto recycle candidate elements that have not been kept alive by layout - i.e layout did not
// call GetElementAt(index).
foreach (var element in Children)
{
var virtInfo = GetVirtualizationInfo(element);
if (virtInfo.Owner == ElementOwner.Layout &&
virtInfo.AutoRecycleCandidate &&
!virtInfo.KeepAlive)
{
ClearElementImpl(element);
}
}
}
_viewportManager.SetLayoutExtent(extent);
return desiredSize;
}
finally
{
_isLayoutInProgress = false;
}
}
protected override Size ArrangeOverride(Size finalSize)
{
if (_isLayoutInProgress)
{
throw new AvaloniaInternalException("Reentrancy detected during layout.");
}
if (IsProcessingCollectionChange)
{
throw new NotSupportedException("Cannot run layout in the middle of a collection change.");
}
_isLayoutInProgress = true;
try
{
var arrangeSize = Layout?.Arrange(GetLayoutContext(), finalSize) ?? default;
// The view manager might clear elements during this call.
// That's why we call it before arranging cleared elements
// off screen.
_viewManager.OnOwnerArranged();
foreach (var element in Children)
{
var virtInfo = GetVirtualizationInfo(element);
virtInfo.KeepAlive = false;
if (virtInfo.Owner == ElementOwner.ElementFactory ||
virtInfo.Owner == ElementOwner.PinnedPool)
{
// Toss it away. And arrange it with size 0 so that XYFocus won't use it.
element.Arrange(new Rect(
ClearedElementsArrangePosition.X - element.DesiredSize.Width,
ClearedElementsArrangePosition.Y - element.DesiredSize.Height,
0,
0));
}
else
{
var newBounds = element.Bounds;
virtInfo.ArrangeBounds = newBounds;
}
}
_viewportManager.OnOwnerArranged();
return arrangeSize;
}
finally
{
_isLayoutInProgress = false;
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
InvalidateMeasure();
_viewportManager.ResetScrollers();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_viewportManager.ResetScrollers();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
{
var property = args.Property;
if (property == ItemsProperty)
{
var newValue = (IEnumerable)args.NewValue;
var newDataSource = newValue as ItemsSourceView;
if (newValue != null && newDataSource == null)
{
newDataSource = new ItemsSourceView(newValue);
}
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
}
else if (property == ItemTemplateProperty)
{
OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue);
}
else if (property == LayoutProperty)
{
OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue);
}
else if (property == HorizontalCacheLengthProperty)
{
_viewportManager.HorizontalCacheLength = (double)args.NewValue;
}
else if (property == VerticalCacheLengthProperty)
{
_viewportManager.VerticalCacheLength = (double)args.NewValue;
}
else
{
base.OnPropertyChanged(args);
}
}
internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle)
{
var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle);
return element;
}
internal void ClearElementImpl(IControl element)
{
// Clearing an element due to a collection change
// is more strict in that pinned elements will be forcibly
// unpinned and sent back to the view generator.
var isClearedDueToCollectionChange =
_processingItemsSourceChange != null &&
(_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Remove ||
_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Replace ||
_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset);
_viewManager.ClearElement(element, isClearedDueToCollectionChange);
_viewportManager.OnElementCleared(element);
}
private int GetElementIndexImpl(IControl element)
{
var virtInfo = TryGetVirtualizationInfo(element);
return _viewManager.GetElementIndex(virtInfo);
}
private IControl GetElementFromIndexImpl(int index)
{
IControl result = null;
var children = Children;
for (var i = 0; i < children.Count && result == null; ++i)
{
var element = children[i];
var virtInfo = TryGetVirtualizationInfo(element);
if (virtInfo?.IsRealized == true && virtInfo.Index == index)
{
result = element;
}
}
return result;
}
private IControl GetOrCreateElementImpl(int index)
{
if (index >= 0 && index >= ItemsSourceView.Count)
{
throw new ArgumentException("Argument index is invalid.", "index");
}
if (_isLayoutInProgress)
{
throw new NotSupportedException("GetOrCreateElement invocation is not allowed during layout.");
}
var element = GetElementFromIndexImpl(index);
bool isAnchorOutsideRealizedRange = element == null;
if (isAnchorOutsideRealizedRange)
{
if (Layout == null)
{
throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout.");
}
element = (IControl)GetLayoutContext().GetOrCreateElementAt(index);
element.Measure(Size.Infinity);
}
_viewportManager.OnMakeAnchor(element, isAnchorOutsideRealizedRange);
InvalidateMeasure();
return element;
}
internal void OnElementPrepared(IControl element, int index)
{
_viewportManager.OnElementPrepared(element);
if (ElementPrepared != null)
{
if (_elementPreparedArgs == null)
{
_elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index);
}
else
{
_elementPreparedArgs.Update(element, index);
}
ElementPrepared(this, _elementPreparedArgs);
}
}
internal void OnElementClearing(IControl element)
{
if (ElementClearing != null)
{
if (_elementClearingArgs == null)
{
_elementClearingArgs = new ItemsRepeaterElementClearingEventArgs(element);
}
else
{
_elementClearingArgs.Update(element);
}
ElementClearing(this, _elementClearingArgs);
}
}
internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex)
{
if (ElementIndexChanged != null)
{
if (_elementIndexChangedArgs == null)
{
_elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex);
}
else
{
_elementIndexChangedArgs.Update(element, oldIndex, newIndex);
}
ElementIndexChanged(this, _elementIndexChangedArgs);
}
}
private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue)
{
if (_isLayoutInProgress)
{
throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout.");
}
ItemsSourceView?.Dispose();
ItemsSourceView = newValue;
if (oldValue != null)
{
oldValue.CollectionChanged -= OnItemsSourceViewChanged;
}
if (newValue != null)
{
newValue.CollectionChanged += OnItemsSourceViewChanged;
}
if (Layout != null)
{
if (Layout is VirtualizingLayout virtualLayout)
{
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
}
else if (Layout is NonVirtualizingLayout nonVirtualLayout)
{
// Walk through all the elements and make sure they are cleared for
// non-virtualizing layouts.
foreach (var element in Children)
{
if (GetVirtualizationInfo(element).IsRealized)
{
ClearElementImpl(element);
}
}
}
InvalidateMeasure();
}
}
private void OnItemTemplateChanged(IDataTemplate oldValue, IDataTemplate newValue)
{
if (_isLayoutInProgress && oldValue != null)
{
throw new AvaloniaInternalException("ItemTemplate cannot be changed during layout.");
}
// Since the ItemTemplate has changed, we need to re-evaluate all the items that
// have already been created and are now in the tree. The easiest way to do that
// would be to do a reset.. Note that this has to be done before we change the template
// so that the cleared elements go back into the old template.
if (Layout != null)
{
if (Layout is VirtualizingLayout virtualLayout)
{
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
_processingItemsSourceChange = args;
try
{
virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
}
finally
{
_processingItemsSourceChange = null;
}
}
else if (Layout is NonVirtualizingLayout)
{
// Walk through all the elements and make sure they are cleared for
// non-virtualizing layouts.
foreach (var element in Children)
{
if (GetVirtualizationInfo(element).IsRealized)
{
ClearElementImpl(element);
}
}
}
}
ItemTemplateShim = new ItemTemplateWrapper(newValue);
InvalidateMeasure();
}
private void OnLayoutChanged(AttachedLayout oldValue, AttachedLayout newValue)
{
if (_isLayoutInProgress)
{
throw new InvalidOperationException("Layout cannot be changed during layout.");
}
_viewManager.OnLayoutChanging();
if (oldValue != null)
{
oldValue.UninitializeForContext(LayoutContext);
oldValue.MeasureInvalidated -= InvalidateMeasureForLayout;
oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout;
// Walk through all the elements and make sure they are cleared
foreach (var element in Children)
{
if (GetVirtualizationInfo(element).IsRealized)
{
ClearElementImpl(element);
}
}
LayoutState = null;
}
if (newValue != null)
{
newValue.InitializeForContext(LayoutContext);
newValue.MeasureInvalidated += InvalidateMeasureForLayout;
newValue.ArrangeInvalidated += InvalidateArrangeForLayout;
}
bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout;
_viewportManager.OnLayoutChanged(isVirtualizingLayout);
InvalidateMeasure();
}
private void OnItemsSourceViewChanged(object sender, NotifyCollectionChangedEventArgs args)
{
if (_isLayoutInProgress)
{
// Bad things will follow if the data changes while we are in the middle of a layout pass.
throw new InvalidOperationException("Changes in data source are not allowed during layout.");
}
if (IsProcessingCollectionChange)
{
throw new InvalidOperationException("Changes in the data source are not allowed during another change in the data source.");
}
_processingItemsSourceChange = args;
try
{
_viewManager.OnItemsSourceChanged(sender, args);
if (Layout != null)
{
if (Layout is VirtualizingLayout virtualLayout)
{
virtualLayout.OnItemsChanged(GetLayoutContext(), sender, args);
}
else
{
// NonVirtualizingLayout
InvalidateMeasure();
}
}
}
finally
{
_processingItemsSourceChange = null;
}
}
private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure();
private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange();
private VirtualizingLayoutContext GetLayoutContext()
{
if (_layoutContext == null)
{
_layoutContext = new RepeaterLayoutContext(this);
}
return _layoutContext;
}
}
}

24
src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs

@ -0,0 +1,24 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsRepeater.ElementClearing"/> event.
/// </summary>
public class ItemsRepeaterElementClearingEventArgs : EventArgs
{
internal ItemsRepeaterElementClearingEventArgs(IControl element) => Element = element;
/// <summary>
/// Gets the element that is being cleared for re-use.
/// </summary>
public IControl Element { get; private set; }
internal void Update(IControl element) => Element = element;
}
}

44
src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs

@ -0,0 +1,44 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsRepeater.ElementIndexChanged"/> event.
/// </summary>
public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs
{
internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex)
{
Element = element;
NewIndex = newIndex;
OldIndex = oldIndex;
}
/// <summary>
/// Get the element for which the index changed.
/// </summary>
public IControl Element { get; private set; }
/// <summary>
/// Gets the index of the element after the change.
/// </summary>
public int NewIndex { get; private set; }
/// <summary>
/// Gets the index of the element before the change.
/// </summary>
public int OldIndex { get; private set; }
internal void Update(IControl element, int newIndex, int oldIndex)
{
Element = element;
NewIndex = newIndex;
OldIndex = oldIndex;
}
}
}

35
src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs

@ -0,0 +1,35 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsRepeater.ElementPrepared"/> event.
/// </summary>
public class ItemsRepeaterElementPreparedEventArgs
{
internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index)
{
Element = element;
Index = index;
}
/// <summary>
/// Gets the prepared element.
/// </summary>
public IControl Element { get; private set; }
/// <summary>
/// Gets the index of the item the element was prepared for.
/// </summary>
public int Index { get; private set; }
internal void Update(IControl element, int index)
{
Element = element;
Index = index;
}
}
}

145
src/Avalonia.Controls/Repeater/ItemsSourceView.cs

@ -0,0 +1,145 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a standardized view of the supported interactions between a given ItemsSource
/// object and an <see cref="ItemsRepeater"/> 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
{
private readonly IList _inner;
private INotifyCollectionChanged _notifyCollectionChanged;
private int _cachedSize = -1;
/// <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)
{
Contract.Requires<ArgumentNullException>(source != null);
if (source is IList list)
{
_inner = list;
}
else if (source is IEnumerable<object> objectEnumerable)
{
_inner = new List<object>(objectEnumerable);
}
else
{
_inner = new List<object>(source.Cast<object>());
}
ListenToCollectionChanges();
}
/// <summary>
/// Gets the number of items in the collection.
/// </summary>
public int Count
{
get
{
if (_cachedSize == -1)
{
_cachedSize = _inner.Count;
}
return _cachedSize;
}
}
/// <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>
/// Occurs when the collection has changed to indicate the reason for the change and which items changed.
/// </summary>
public event NotifyCollectionChangedEventHandler CollectionChanged;
/// <inheritdoc/>
public void Dispose()
{
if (_notifyCollectionChanged != null)
{
_notifyCollectionChanged.CollectionChanged -= OnCollectionChanged;
}
}
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>the item.</returns>
public object GetAt(int index) => _inner[index];
/// <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>
public string KeyFromIndex(int index)
{
throw new NotImplementedException();
}
/// <summary>
/// Retrieves the unique identifier (key) for the item at the specified index.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The index.</returns>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
public int IndexFromKey(string key)
{
throw new NotImplementedException();
}
protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args)
{
_cachedSize = _inner.Count;
CollectionChanged?.Invoke(this, args);
}
private void ListenToCollectionChanges()
{
if (_inner is INotifyCollectionChanged incc)
{
incc.CollectionChanged += OnCollectionChanged;
_notifyCollectionChanged = incc;
}
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnItemsSourceChanged(e);
}
}
}

106
src/Avalonia.Controls/Repeater/RecyclePool.cs

@ -0,0 +1,106 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Controls.Templates;
namespace Avalonia.Controls
{
internal class RecyclePool
{
public static readonly AttachedProperty<IDataTemplate> OriginTemplateProperty =
AvaloniaProperty.RegisterAttached<Control, IDataTemplate>("OriginTemplate", typeof(RecyclePool));
private static ConditionalWeakTable<IDataTemplate, RecyclePool> s_pools = new ConditionalWeakTable<IDataTemplate, RecyclePool>();
private readonly Dictionary<string, List<ElementInfo>> _elements = new Dictionary<string, List<ElementInfo>>();
public static RecyclePool GetPoolInstance(IDataTemplate dataTemplate)
{
s_pools.TryGetValue(dataTemplate, out var result);
return result;
}
public static void SetPoolInstance(IDataTemplate dataTemplate, RecyclePool value) => s_pools.Add(dataTemplate, value);
public void PutElement(IControl element, string key, IControl owner)
{
var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner);
var elementInfo = new ElementInfo(element, ownerAsPanel);
if (!_elements.TryGetValue(key, out var pool))
{
pool = new List<ElementInfo>();
_elements.Add(key, pool);
}
pool.Add(elementInfo);
}
public IControl TryGetElement(string key, IControl owner)
{
if (_elements.TryGetValue(key, out var elements))
{
if (elements.Count > 0)
{
// Prefer an element from the same owner or with no owner so that we don't incur
// the enter/leave cost during recycling.
// TODO: prioritize elements with the same owner to those without an owner.
var elementInfo = elements.FirstOrDefault(x => x.Owner == owner) ?? elements.LastOrDefault();
elements.Remove(elementInfo);
var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner);
if (elementInfo.Owner != null && elementInfo.Owner != ownerAsPanel)
{
// Element is still under its parent. remove it from its parent.
var panel = elementInfo.Owner;
if (panel != null)
{
int childIndex = panel.Children.IndexOf(elementInfo.Element);
if (childIndex == -1)
{
throw new KeyNotFoundException("ItemsRepeater's child not found in its Children collection.");
}
panel.Children.RemoveAt(childIndex);
}
}
return elementInfo.Element;
}
}
return null;
}
private IPanel EnsureOwnerIsPanelOrNull(IControl owner)
{
if (owner is IPanel panel)
{
return panel;
}
else if (owner != null)
{
throw new InvalidOperationException("Owner must be IPanel or null.");
}
return null;
}
private class ElementInfo
{
public ElementInfo(IControl element, IPanel owner)
{
Element = element;
Owner = owner;
}
public IControl Element { get; }
public IPanel Owner { get;}
}
}
}

65
src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs

@ -0,0 +1,65 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Layout;
namespace Avalonia.Controls
{
internal class RepeaterLayoutContext : VirtualizingLayoutContext
{
private readonly ItemsRepeater _owner;
public RepeaterLayoutContext(ItemsRepeater owner)
{
_owner = owner;
}
protected override Point LayoutOriginCore
{
get => _owner.LayoutOrigin;
set => _owner.LayoutOrigin = value;
}
protected override object LayoutStateCore
{
get => _owner.LayoutState;
set => _owner.LayoutState = value;
}
protected override int RecommendedAnchorIndexCore
{
get
{
int anchorIndex = -1;
var anchor = _owner.SuggestedAnchor;
if (anchor != null)
{
anchorIndex = _owner.GetElementIndex(anchor);
}
return anchorIndex;
}
}
protected override int ItemCountCore() => _owner.ItemsSourceView?.Count ?? 0;
protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options)
{
return _owner.GetElementImpl(
index,
(options & ElementRealizationOptions.ForceCreate) != 0,
(options & ElementRealizationOptions.SuppressAutoRecycle) != 0);
}
protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index);
protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element);
protected override Rect RealizationRectCore() => _owner.RealizationWindow;
}
}

54
src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs

@ -0,0 +1,54 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Controls
{
internal class UniqueIdElementPool : IEnumerable<KeyValuePair<string, IControl>>
{
private readonly Dictionary<string, IControl> _elementMap = new Dictionary<string, IControl>();
private readonly ItemsRepeater _owner;
public UniqueIdElementPool(ItemsRepeater owner) => _owner = owner;
public void Add(IControl element)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var key = virtInfo.UniqueId;
if (_elementMap.ContainsKey(key))
{
throw new InvalidOperationException($"The unique id provided ({key}) is not unique.");
}
_elementMap.Add(key, element);
}
public IControl Remove(int index)
{
// Check if there is already a element in the mapping and if so, use it.
string key = _owner.ItemsSourceView.KeyFromIndex(index);
if (_elementMap.TryGetValue(key, out var element))
{
_elementMap.Remove(key);
}
return element;
}
public void Clear()
{
_elementMap.Clear();
}
public IEnumerator<KeyValuePair<string, IControl>> GetEnumerator() => _elementMap.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

682
src/Avalonia.Controls/Repeater/ViewManager.cs

@ -0,0 +1,682 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
internal sealed class ViewManager
{
private const int FirstRealizedElementIndexDefault = int.MaxValue;
private const int LastRealizedElementIndexDefault = int.MinValue;
private readonly ItemsRepeater _owner;
private readonly List<PinnedElementInfo> _pinnedPool = new List<PinnedElementInfo>();
private readonly UniqueIdElementPool _resetPool;
private IControl _lastFocusedElement;
private bool _isDataSourceStableResetPending;
private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
private bool _eventsSubscribed;
public ViewManager(ItemsRepeater owner)
{
_owner = owner;
_resetPool = new UniqueIdElementPool(owner);
}
public IControl GetElement(int index, bool forceCreate, bool suppressAutoRecycle)
{
var element = forceCreate ? null : GetElementIfAlreadyHeldByLayout(index);
if (element == null)
{
// check if this is the anchor made through repeater in preparation
// for a bring into view.
var madeAnchor = _owner.MadeAnchor;
if (madeAnchor != null)
{
var anchorVirtInfo = ItemsRepeater.TryGetVirtualizationInfo(madeAnchor);
if (anchorVirtInfo.Index == index)
{
element = madeAnchor;
}
}
}
if (element == null) { element = GetElementFromUniqueIdResetPool(index); };
if (element == null) { element = GetElementFromPinnedElements(index); }
if (element == null) { element = GetElementFromElementFactory(index); }
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
if (suppressAutoRecycle)
{
virtInfo.AutoRecycleCandidate = false;
}
else
{
virtInfo.AutoRecycleCandidate = true;
virtInfo.KeepAlive = true;
}
return element;
}
public void ClearElement(IControl element, bool isClearedDueToCollectionChange)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var index = virtInfo.Index;
bool cleared =
ClearElementToUniqueIdResetPool(element, virtInfo) ||
ClearElementToPinnedPool(element, virtInfo, isClearedDueToCollectionChange);
if (!cleared)
{
ClearElementToElementFactory(element);
}
// Both First and Last indices need to be valid or default.
if (index == _firstRealizedElementIndexHeldByLayout && index == _lastRealizedElementIndexHeldByLayout)
{
// First and last were pointing to the same element and that is going away.
InvalidateRealizedIndicesHeldByLayout();
}
else if (index == _firstRealizedElementIndexHeldByLayout)
{
// The FirstElement is going away, shrink the range by one.
++_firstRealizedElementIndexHeldByLayout;
}
else if (index == _lastRealizedElementIndexHeldByLayout)
{
// Last element is going away, shrink the range by one at the end.
--_lastRealizedElementIndexHeldByLayout;
}
else
{
// Index is either outside the range we are keeping track of or inside the range.
// In both these cases, we just keep the range we have. If this clear was due to
// a collection change, then in the CollectionChanged event, we will invalidate these guys.
}
}
public void ClearElementToElementFactory(IControl element)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var clearedIndex = virtInfo.Index;
_owner.OnElementClearing(element);
_owner.ItemTemplateShim.RecycleElement(_owner, element);
virtInfo.MoveOwnershipToElementFactory();
if (_lastFocusedElement == element)
{
// Focused element is going away. Remove the tracked last focused element
// and pick a reasonable next focus if we can find one within the layout
// realized elements.
MoveFocusFromClearedIndex(clearedIndex);
}
}
private void MoveFocusFromClearedIndex(int clearedIndex)
{
IControl focusedChild = null;
var focusCandidate = FindFocusCandidate(clearedIndex, focusedChild);
if (focusCandidate != null)
{
focusCandidate.Focus();
_lastFocusedElement = focusedChild;
// Add pin to hold the focused element.
UpdatePin(focusedChild, true /* addPin */);
}
else
{
// We could not find a candiate.
_lastFocusedElement = null;
}
}
IControl FindFocusCandidate(int clearedIndex, IControl focusedChild)
{
// Walk through all the children and find elements with index before and after the cleared index.
// Note that during a delete the next element would now have the same index.
int previousIndex = int.MinValue;
int nextIndex = int.MaxValue;
IControl nextElement = null;
IControl previousElement = null;
foreach (var child in _owner.Children)
{
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child);
if (virtInfo?.IsHeldByLayout == true)
{
int currentIndex = virtInfo.Index;
if (currentIndex < clearedIndex)
{
if (currentIndex > previousIndex)
{
previousIndex = currentIndex;
previousElement = child;
}
}
else if (currentIndex >= clearedIndex)
{
// Note that we use >= above because if we deleted the focused element,
// the next element would have the same index now.
if (currentIndex < nextIndex)
{
nextIndex = currentIndex;
nextElement = child;
}
}
}
}
// TODO: Find the next element if one exists, if not use the previous element.
// If the container itself is not focusable, find a descendent that is.
return nextElement;
}
public int GetElementIndex(VirtualizationInfo virtInfo)
{
if (virtInfo == null)
{
throw new ArgumentException("Element is not a child of this ItemsRepeater.");
}
return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1;
}
public void PrunePinnedElements()
{
EnsureEventSubscriptions();
// Go through pinned elements and make sure they still have
// a reason to be pinned.
for (var i = 0; i < _pinnedPool.Count; ++i)
{
var elementInfo = _pinnedPool[i];
var virtInfo = elementInfo.VirtualizationInfo;
if (!virtInfo.IsPinned)
{
_pinnedPool.RemoveAt(i);
--i;
// Pinning was the only thing keeping this element alive.
ClearElementToElementFactory(elementInfo.PinnedElement);
}
}
}
public void UpdatePin(IControl element, bool addPin)
{
var parent = element.VisualParent;
var child = (IVisual)element;
while (parent != null)
{
if (parent is ItemsRepeater repeater)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo((IControl)child);
if (virtInfo.IsRealized)
{
if (addPin)
{
virtInfo.AddPin();
}
else if (virtInfo.IsPinned)
{
if (virtInfo.RemovePin() == 0)
{
// ElementFactory is invoked during the measure pass.
// We will clear the element then.
repeater.InvalidateMeasure();
}
}
}
}
child = parent;
parent = child.VisualParent;
}
}
public void OnItemsSourceChanged(object sender, NotifyCollectionChangedEventArgs args)
{
// Note: For items that have been removed, the index will not be touched. It will hold
// the old index before it was removed. It is not valid anymore.
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
{
var newIndex = args.NewStartingIndex;
var newCount = args.NewItems.Count;
EnsureFirstLastRealizedIndices();
if (newIndex <= _lastRealizedElementIndexHeldByLayout)
{
_lastRealizedElementIndexHeldByLayout += newCount;
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized && dataIndex >= newIndex)
{
UpdateElementIndex(element, virtInfo, dataIndex + newCount);
}
}
}
else
{
// Indices held by layout are not affected
// We could still have items in the pinned elements that need updates. This is usually a very small vector.
for (var i = 0; i < _pinnedPool.Count; ++i)
{
var elementInfo = _pinnedPool[i];
var virtInfo = elementInfo.VirtualizationInfo;
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized && dataIndex >= newIndex)
{
var element = elementInfo.PinnedElement;
UpdateElementIndex(element, virtInfo, dataIndex + newCount);
}
}
}
break;
}
case NotifyCollectionChangedAction.Replace:
{
// Requirement: oldStartIndex == newStartIndex. It is not a replace if this is not true.
// Two cases here
// case 1: oldCount == newCount
// indices are not affected. nothing to do here.
// case 2: oldCount != newCount
// Replaced with less or more items. This is like an insert or remove
// depending on the counts.
var oldStartIndex = args.OldStartingIndex;
var newStartingIndex = args.NewStartingIndex;
var oldCount = args.OldItems.Count;
var newCount = args.NewItems.Count;
if (oldStartIndex != newStartingIndex)
{
throw new NotSupportedException("Replace is only allowed with OldStartingIndex equals to NewStartingIndex.");
}
if (oldCount == 0)
{
throw new NotSupportedException("Replace notification with args.OldItemsCount value of 0 is not allowed. Use Insert action instead.");
}
if (newCount == 0)
{
throw new NotSupportedException("Replace notification with args.NewItemCount value of 0 is not allowed. Use Remove action instead.");
}
int countChange = newCount - oldCount;
if (countChange != 0)
{
// countChange > 0 : countChange items were added
// countChange < 0 : -countChange items were removed
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized)
{
if (dataIndex >= oldStartIndex + oldCount)
{
UpdateElementIndex(element, virtInfo, dataIndex + countChange);
}
}
}
EnsureFirstLastRealizedIndices();
_lastRealizedElementIndexHeldByLayout += countChange;
}
break;
}
case NotifyCollectionChangedAction.Remove:
{
var oldStartIndex = args.OldStartingIndex;
var oldCount = args.OldItems.Count;
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized)
{
if (virtInfo.AutoRecycleCandidate && oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount)
{
// If we are doing the mapping, remove the element who's data was removed.
_owner.ClearElementImpl(element);
}
else if (dataIndex >= (oldStartIndex + oldCount))
{
UpdateElementIndex(element, virtInfo, dataIndex - oldCount);
}
}
}
InvalidateRealizedIndicesHeldByLayout();
break;
}
case NotifyCollectionChangedAction.Reset:
if (_owner.ItemsSourceView.HasKeyIndexMapping)
{
_isDataSourceStableResetPending = true;
}
// Walk through all the elements and make sure they are cleared, they will go into
// the stable id reset pool.
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
{
_owner.ClearElementImpl(element);
}
}
InvalidateRealizedIndicesHeldByLayout();
break;
}
}
private void EnsureFirstLastRealizedIndices()
{
if (_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault)
{
// This will ensure that the indexes are updated.
GetElementIfAlreadyHeldByLayout(0);
}
}
public void OnLayoutChanging()
{
if (_owner.ItemsSourceView?.HasKeyIndexMapping == true)
{
_isDataSourceStableResetPending = true;
}
}
public void OnOwnerArranged()
{
if (_isDataSourceStableResetPending)
{
_isDataSourceStableResetPending = false;
foreach (var entry in _resetPool)
{
// TODO: Task 14204306: ItemsRepeater: Find better focus candidate when focused element is deleted in the ItemsSource.
// Focused element is getting cleared. Need to figure out semantics on where
// focus should go when the focused element is removed from the data collection.
ClearElement(entry.Value, true /* isClearedDueToCollectionChange */);
}
_resetPool.Clear();
}
}
// We optimize for the case where index is not realized to return null as quickly as we can.
// Flow layouts manage containers on their own and will never ask for an index that is already realized.
// If an index that is realized is requested by the layout, we unfortunately have to walk the
// children. Not ideal, but a reasonable default to provide consistent behavior between virtualizing
// and non-virtualizing hosts.
private IControl GetElementIfAlreadyHeldByLayout(int index)
{
IControl element = null;
bool cachedFirstLastIndicesInvalid = _firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault;
bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout);
if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange)
{
foreach (var child in _owner.Children)
{
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child);
if (virtInfo?.IsHeldByLayout == true)
{
// Only give back elements held by layout. If someone else is holding it, they will be served by other methods.
int childIndex = virtInfo.Index;
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, childIndex);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, childIndex);
if (virtInfo.Index == index)
{
element = child;
// If we have valid first/last indices, we don't have to walk the rest, but if we
// do not, then we keep walking through the entire children collection to get accurate
// indices once.
if (!cachedFirstLastIndicesInvalid)
{
break;
}
}
}
}
}
return element;
}
private IControl GetElementFromUniqueIdResetPool(int index)
{
IControl element = null;
// See if you can get it from the reset pool.
if (_isDataSourceStableResetPending)
{
element = _resetPool.Remove(index);
if (element != null)
{
// Make sure that the index is updated to the current one
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool();
UpdateElementIndex(element, virtInfo, index);
}
}
return element;
}
private IControl GetElementFromPinnedElements(int index)
{
IControl element = null;
// See if you can find something among the pinned elements.
for (var i = 0; i < _pinnedPool.Count; ++i)
{
var elementInfo = _pinnedPool[i];
var virtInfo = elementInfo.VirtualizationInfo;
if (virtInfo.Index == index)
{
_pinnedPool.RemoveAt(i);
element = elementInfo.PinnedElement;
elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool();
break;
}
}
return element;
}
private IControl GetElementFromElementFactory(int index)
{
// The view generator is the provider of last resort.
var itemTemplateFactory = _owner.ItemTemplateShim;
if (itemTemplateFactory == null)
{
// If no ItemTemplate was provided, use a default
var factory = FuncDataTemplate.Default;
_owner.ItemTemplate = factory;
itemTemplateFactory = _owner.ItemTemplateShim;
}
var data = _owner.ItemsSourceView.GetAt(index);
var element = itemTemplateFactory.GetElement(_owner, data);
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
if (virtInfo == null)
{
virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element);
}
// Prepare the element
element.DataContext = data;
virtInfo.MoveOwnershipToLayoutFromElementFactory(
index,
/* uniqueId: */
_owner.ItemsSourceView.HasKeyIndexMapping ?
_owner.ItemsSourceView.KeyFromIndex(index) :
string.Empty);
// The view generator is the only provider that prepares the element.
var repeater = _owner;
// Add the element to the children collection here before raising OnElementPrepared so
// that handlers can walk up the tree in case they want to find their IndexPath in the
// nested case.
var children = repeater.Children;
if (element.VisualParent != repeater)
{
children.Add(element);
}
repeater.OnElementPrepared(element, index);
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
return element;
}
private bool ClearElementToUniqueIdResetPool(IControl element, VirtualizationInfo virtInfo)
{
if (_isDataSourceStableResetPending)
{
_resetPool.Add(element);
virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
}
return _isDataSourceStableResetPending;
}
private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange)
{
if (_isDataSourceStableResetPending)
{
_resetPool.Add(element);
virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
}
return _isDataSourceStableResetPending;
}
private void UpdateFocusedElement()
{
IControl focusedElement = null;
var child = FocusManager.Instance.Current;
if (child != null)
{
var parent = child.VisualParent;
var owner = _owner;
// Find out if the focused element belongs to one of our direct
// children.
while (parent != null)
{
if (parent is ItemsRepeater repeater)
{
var element = child as IControl;
if (repeater == owner && ItemsRepeater.GetVirtualizationInfo(element).IsRealized)
{
focusedElement = element;
}
break;
}
child = parent as IInputElement;
parent = child.VisualParent;
}
}
// If the focused element has changed,
// we need to unpin the old one and pin the new one.
if (_lastFocusedElement != focusedElement)
{
if (_lastFocusedElement != null)
{
UpdatePin(_lastFocusedElement, false /* addPin */);
}
if (focusedElement != null)
{
UpdatePin(focusedElement, true /* addPin */);
}
_lastFocusedElement = focusedElement;
}
}
private void OnFocusChanged(object sender, RoutedEventArgs e) => UpdateFocusedElement();
private void EnsureEventSubscriptions()
{
if (!_eventsSubscribed)
{
_owner.GotFocus += OnFocusChanged;
_owner.LostFocus += OnFocusChanged;
}
}
private void UpdateElementIndex(IControl element, VirtualizationInfo virtInfo, int index)
{
var oldIndex = virtInfo.Index;
if (oldIndex != index)
{
virtInfo.UpdateIndex(index);
_owner.OnElementIndexChanged(element, oldIndex, index);
}
}
private void InvalidateRealizedIndicesHeldByLayout()
{
_firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
_lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
}
private struct PinnedElementInfo
{
public PinnedElementInfo(IControl element)
{
PinnedElement = element;
VirtualizationInfo = ItemsRepeater.GetVirtualizationInfo(element);
}
public IControl PinnedElement { get; }
public VirtualizationInfo VirtualizationInfo { get; }
}
}
}

501
src/Avalonia.Controls/Repeater/ViewportManager.cs

@ -0,0 +1,501 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Layout;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
internal class ViewportManager
{
private const double CacheBufferPerSideInflationPixelDelta = 40.0;
private readonly ItemsRepeater _owner;
private bool _ensuredScroller;
private IScrollAnchorProvider _scroller;
private IControl _makeAnchorElement;
private bool _isAnchorOutsideRealizedRange;
private Task _cacheBuildAction;
private Rect _visibleWindow;
private Rect _layoutExtent;
// This is the expected shift by the layout.
private Point _expectedViewportShift;
// This is what is pending and not been accounted for.
// Sometimes the scrolling surface cannot service a shift (for example
// it is already at the top and cannot shift anymore.)
private Point _pendingViewportShift;
// Unshiftable shift amount that this view manager can
// handle on its own to fake it to the layout as if the shift
// actually happened. This can happen in cases where no scrollviewer
// in the parent chain can scroll in the shift direction.
private Point _unshiftableShift;
private double _maximumHorizontalCacheLength = 0.0;
private double _maximumVerticalCacheLength = 0.0;
private double _horizontalCacheBufferPerSide;
private double _verticalCacheBufferPerSide;
private bool _isBringIntoViewInProgress;
// For non-virtualizing layouts, we do not need to keep
// updating viewports and invalidating measure often. So when
// a non virtualizing layout is used, we stop doing all that work.
bool _managingViewportDisabled;
private IDisposable _effectiveViewportChangedRevoker;
private bool _layoutUpdatedSubscribed;
public ViewportManager(ItemsRepeater owner)
{
_owner = owner;
}
public IControl SuggestedAnchor
{
get
{
// The element generated during the ItemsRepeater.MakeAnchor call has precedence over the next tick.
var suggestedAnchor = _makeAnchorElement;
var owner = _owner;
if (suggestedAnchor == null)
{
var anchorElement = _scroller?.CurrentAnchor;
if (anchorElement != null)
{
// We can't simply return anchorElement because, in case of nested Repeaters, it may not
// be a direct child of ours, or even an indirect child. We need to walk up the tree starting
// from anchorElement to figure out what child of ours (if any) to use as the suggested element.
var child = anchorElement;
var parent = child.VisualParent as IControl;
while (parent != null)
{
if (parent == owner)
{
suggestedAnchor = child;
break;
}
child = parent;
parent = parent.VisualParent as IControl;
}
}
}
return suggestedAnchor;
}
}
public bool HasScroller => _scroller != null;
public IControl MadeAnchor => _makeAnchorElement;
public double HorizontalCacheLength
{
get => _maximumHorizontalCacheLength;
set
{
if (_maximumHorizontalCacheLength != value)
{
ValidateCacheLength(value);
_maximumHorizontalCacheLength = value;
}
}
}
public double VerticalCacheLength
{
get => _maximumVerticalCacheLength;
set
{
if (_maximumVerticalCacheLength != value)
{
ValidateCacheLength(value);
_maximumVerticalCacheLength = value;
}
}
}
private Rect GetLayoutVisibleWindowDiscardAnchor()
{
var visibleWindow = _visibleWindow;
if (HasScroller)
{
visibleWindow = new Rect(
visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X,
visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y,
visibleWindow.Width,
visibleWindow.Height);
}
return visibleWindow;
}
public Rect GetLayoutVisibleWindow()
{
var visibleWindow = _visibleWindow;
if (_makeAnchorElement != null)
{
// The anchor is not necessarily laid out yet. Its position should default
// to zero and the layout origin is expected to change once layout is done.
// Until then, we need a window that's going to protect the anchor from
// getting recycled.
visibleWindow = visibleWindow.WithX(0).WithY(0);
}
else if (HasScroller)
{
visibleWindow = new Rect(
visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X,
visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y,
visibleWindow.Width,
visibleWindow.Height);
}
return visibleWindow;
}
public Rect GetLayoutRealizationWindow()
{
var realizationWindow = GetLayoutVisibleWindow();
if (HasScroller)
{
realizationWindow = new Rect(
realizationWindow.X - _horizontalCacheBufferPerSide,
realizationWindow.Y - _verticalCacheBufferPerSide,
realizationWindow.Width + _horizontalCacheBufferPerSide * 2.0,
realizationWindow.Height + _verticalCacheBufferPerSide * 2.0);
}
return realizationWindow;
}
public void SetLayoutExtent(Rect extent)
{
_expectedViewportShift = new Point(
_expectedViewportShift.X + _layoutExtent.X - extent.X,
_expectedViewportShift.Y + _layoutExtent.Y - extent.Y);
// We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much.
if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1)
{
// There are cases where we might be expecting a shift but not get it. We will
// be waiting for the effective viewport event but if the scroll viewer is not able
// to perform the shift (perhaps because it cannot scroll in negative offset),
// then we will end up not realizing elements in the visible
// window. To avoid this, we register to layout updated for this layout pass. If we
// get an effective viewport, we know we have a new viewport and we unregister from
// layout updated. If we get the layout updated handler, then we know that the
// scroller was unable to perform the shift and we invalidate measure and unregister
// from the layout updated event.
if (!_layoutUpdatedSubscribed)
{
_owner.LayoutUpdated += OnLayoutUpdated;
_layoutUpdatedSubscribed = true;
}
}
_layoutExtent = extent;
_pendingViewportShift = _expectedViewportShift;
// We just finished a measure pass and have a new extent.
// Let's make sure the scrollers will run its arrange so that they track the anchor.
((IControl)_scroller)?.InvalidateArrange();
}
public Point GetOrigin() => _layoutExtent.TopLeft;
public void OnLayoutChanged(bool isVirtualizing)
{
_managingViewportDisabled = !isVirtualizing;
_layoutExtent = default;
_expectedViewportShift = default;
_pendingViewportShift = default;
_unshiftableShift = default;
_effectiveViewportChangedRevoker?.Dispose();
if (!_managingViewportDisabled)
{
_effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner);
}
}
public void OnElementPrepared(IControl element)
{
// If we have an anchor element, we do not want the
// scroll anchor provider to start anchoring some other element.
////element.CanBeScrollAnchor(true);
}
public void OnElementCleared(ILayoutable element)
{
////element.CanBeScrollAnchor(false);
}
public void OnOwnerMeasuring()
{
// This is because of a bug that causes effective viewport to not
// fire if you register during arrange.
// Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport
EnsureScroller();
}
public void OnOwnerArranged()
{
_expectedViewportShift = default;
if (!_managingViewportDisabled)
{
// This is because of a bug that causes effective viewport to not
// fire if you register during arrange.
// Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport
// EnsureScroller();
if (HasScroller)
{
double maximumHorizontalCacheBufferPerSide = _maximumHorizontalCacheLength * _visibleWindow.Width / 2.0;
double maximumVerticalCacheBufferPerSide = _maximumVerticalCacheLength * _visibleWindow.Height / 2.0;
bool continueBuildingCache =
_horizontalCacheBufferPerSide < maximumHorizontalCacheBufferPerSide ||
_verticalCacheBufferPerSide < maximumVerticalCacheBufferPerSide;
if (continueBuildingCache)
{
_horizontalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta;
_verticalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta;
_horizontalCacheBufferPerSide = Math.Min(_horizontalCacheBufferPerSide, maximumHorizontalCacheBufferPerSide);
_verticalCacheBufferPerSide = Math.Min(_verticalCacheBufferPerSide, maximumVerticalCacheBufferPerSide);
}
}
}
}
private void OnLayoutUpdated(object sender, EventArgs args)
{
_owner.LayoutUpdated -= OnLayoutUpdated;
if (_managingViewportDisabled)
{
return;
}
// We were expecting a viewport shift but we never got one and we are not going to in this
// layout pass. We likely will never get this shift, so lets assume that we are never going to get it and
// adjust our expected shift to track that. One case where this can happen is when there is no scrollviewer
// that can scroll in the direction where the shift is expected.
if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0)
{
// Assume this is never going to come.
_unshiftableShift = new Point(
_unshiftableShift.X + _pendingViewportShift.X,
_unshiftableShift.Y + _pendingViewportShift.Y);
_pendingViewportShift = default;
_expectedViewportShift = default;
TryInvalidateMeasure();
}
}
public void OnMakeAnchor(IControl anchor, bool isAnchorOutsideRealizedRange)
{
_makeAnchorElement = anchor;
_isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange;
}
public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args)
{
if (!_managingViewportDisabled)
{
// During the time between a bring into view request and the element coming into view we do not
// want the anchor provider to pick some anchor and jump to it. Instead we want to anchor on the
// element that is being brought into view. We can do this by making just that element as a potential
// anchor candidate and ensure no other element of this repeater is an anchor candidate.
// Once the layout pass is done and we render the frame, the element will be in frame and we can
// switch back to letting the anchor provider pick a suitable anchor.
// get the targetChild - i.e the immediate child of this repeater that is being brought into view.
// Note that the element being brought into view could be a descendant.
var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject);
// Make sure that only the target child can be the anchor during the bring into view operation.
foreach (var child in _owner.Children)
{
////if (child.CanBeScrollAnchor && child != targetChild)
////{
//// child.CanBeScrollAnchor = false;
////}
}
// Register to rendering event to go back to how things were before where any child can be the anchor.
_isBringIntoViewInProgress = true;
////if (!m_renderingToken)
////{
//// winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr };
//// m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering });
////}
}
}
private IControl GetImmediateChildOfRepeater(IControl descendant)
{
var targetChild = descendant;
var parent = descendant.Parent;
while (parent != null && parent != _owner)
{
targetChild = parent;
parent = (IControl)parent.VisualParent;
}
if (parent == null)
{
throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call");
}
return targetChild;
}
public void ResetScrollers()
{
_scroller = null;
_effectiveViewportChangedRevoker?.Dispose();
_effectiveViewportChangedRevoker = null;
_ensuredScroller = false;
}
private void OnEffectiveViewportChanged(TransformedBounds? bounds)
{
if (!bounds.HasValue)
{
return;
}
var globalClip = bounds.Value.Clip;
var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value;
var clip = globalClip.TransformToAABB(transform);
var effectiveViewport = clip.Intersect(bounds.Value.Bounds);
UpdateViewport(effectiveViewport);
_pendingViewportShift = default;
_unshiftableShift = default;
if (_visibleWindow.IsEmpty)
{
// We got cleared.
_layoutExtent = default;
}
// We got a new viewport, we dont need to wait for layout updated anymore to
// see if our request for a pending shift was handled.
if (_layoutUpdatedSubscribed)
{
_owner.LayoutUpdated -= OnLayoutUpdated;
}
}
private void EnsureScroller()
{
if (!_ensuredScroller)
{
ResetScrollers();
var parent = _owner.GetVisualParent();
while (parent != null)
{
if (parent is IScrollAnchorProvider scroller)
{
_scroller = scroller;
break;
}
parent = parent.VisualParent;
}
if (_scroller == null)
{
// We usually update the viewport in the post arrange handler. But, since we don't have
// a scroller, let's do it now.
UpdateViewport(Rect.Empty);
}
else if (!_managingViewportDisabled)
{
_effectiveViewportChangedRevoker?.Dispose();
_effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner);
}
_ensuredScroller = true;
}
}
private void UpdateViewport(Rect viewport)
{
var currentVisibleWindow = viewport;
if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X &&
-currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y)
{
// We got cleared.
_visibleWindow = default;
}
else
{
_visibleWindow = currentVisibleWindow;
}
TryInvalidateMeasure();
}
private static void ValidateCacheLength(double cacheLength)
{
if (cacheLength < 0.0 || double.IsInfinity(cacheLength) || double.IsNaN(cacheLength))
{
throw new ArgumentException("The maximum cache length must be equal or superior to zero.");
}
}
private void TryInvalidateMeasure()
{
// Don't invalidate measure if we have an invalid window.
if (!_visibleWindow.IsEmpty)
{
// We invalidate measure instead of just invalidating arrange because
// we don't invalidate measure in UpdateViewport if the view is changing to
// avoid layout cycles.
_owner.InvalidateMeasure();
}
}
private IDisposable SubscribeToEffectiveViewportChanged(IControl control)
{
// HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater -
// we can get this from TransformedBounds, but this property is updated after layout has
// run, resulting in the UI being updated too late when scrolling quickly. We can
// partially remedey this by triggering also on Bounds changes, but this won't work so
// well for nested ItemsRepeaters.
//
// UWP uses the EffectiveBoundsChanged event (which I think was implemented specially
// for this case): we need to implement that in Avalonia.
return control.GetObservable(Visual.TransformedBoundsProperty)
.Merge(control.GetObservable(Visual.BoundsProperty).Select(_ => control.TransformedBounds))
.Skip(1)
.Subscribe(OnEffectiveViewportChanged);
}
private class ScrollerInfo
{
public ScrollerInfo(ScrollViewer scroller)
{
Scroller = scroller;
}
public ScrollViewer Scroller { get; }
}
};
}

118
src/Avalonia.Controls/Repeater/VirtualizationInfo.cs

@ -0,0 +1,118 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
namespace Avalonia.Controls
{
internal enum ElementOwner
{
// All elements are originally owned by the view generator.
ElementFactory,
// Ownership is transferred to the layout when it calls GetElement.
Layout,
// Ownership is transferred to the pinned pool if the element is cleared (outside of
// a 'remove' collection change of course).
PinnedPool,
// Ownership is transfered to the reset pool if the element is cleared by a reset and
// the data source supports unique ids.
UniqueIdResetPool,
// Ownership is transfered to the animator if the element is cleared due to a
// 'remove'-like collection change.
Animator
}
internal class VirtualizationInfo
{
private int _pinCounter;
private object _data;
public Rect ArrangeBounds { get; set; }
public bool AutoRecycleCandidate { get; set; }
public int Index { get; private set; }
public bool IsPinned => _pinCounter > 0;
public bool IsHeldByLayout => Owner == ElementOwner.Layout;
public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool;
public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool;
public bool KeepAlive { get; set; }
public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory;
public string UniqueId { get; private set; }
public void MoveOwnershipToLayoutFromElementFactory(int index, string uniqueId)
{
Owner = ElementOwner.Layout;
Index = index;
UniqueId = uniqueId;
}
public void MoveOwnershipToLayoutFromUniqueIdResetPool()
{
Owner = ElementOwner.Layout;
}
public void MoveOwnershipToLayoutFromPinnedPool()
{
Owner = ElementOwner.Layout;
}
public void MoveOwnershipToElementFactory()
{
Owner = ElementOwner.ElementFactory;
_pinCounter = 0;
Index = -1;
UniqueId = string.Empty;
ArrangeBounds = ItemsRepeater.InvalidRect;
}
public void MoveOwnershipToUniqueIdResetPoolFromLayout()
{
Owner = ElementOwner.UniqueIdResetPool;
// Keep the pinCounter the same. If the container survives the reset
// it can go on being pinned as if nothing happened.
}
public void MoveOwnershipToAnimator()
{
// During a unique id reset, some elements might get removed.
// Their ownership will go from the UniqueIdResetPool to the Animator.
// The common path though is for ownership to go from Layout to Animator.
Owner = ElementOwner.Animator;
Index = -1;
_pinCounter = 0;
}
public void MoveOwnershipToPinnedPool()
{
Owner = ElementOwner.PinnedPool;
}
public int AddPin()
{
if (!IsRealized)
{
throw new InvalidOperationException("You can't pin an unrealized element.");
}
return ++_pinCounter;
}
public int RemovePin()
{
if (!IsRealized)
{
throw new InvalidOperationException("You can't unpin an unrealized element.");
}
if (!IsPinned)
{
throw new InvalidOperationException("UnpinElement was called more often than PinElement.");
}
return --_pinCounter;
}
public void UpdateIndex(int newIndex) => Index = newIndex;
}
}

15
src/Avalonia.Controls/ScrollViewer.cs

@ -11,7 +11,7 @@ namespace Avalonia.Controls
/// <summary>
/// A control scrolls its content if the content is bigger than the space available.
/// </summary>
public class ScrollViewer : ContentControl, IScrollable
public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider
{
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
@ -333,6 +333,9 @@ namespace Avalonia.Controls
get { return _viewport.Height; }
}
/// <inheritdoc/>
IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement
/// <summary>
/// Gets the value of the HorizontalScrollBarVisibility attached property.
/// </summary>
@ -373,6 +376,16 @@ namespace Avalonia.Controls
control.SetValue(VerticalScrollBarVisibilityProperty, value);
}
void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element)
{
// TODO: Implement
}
void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
{
// TODO: Implement
}
internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset)
{
var maxX = Math.Max(extent.Width - viewport.Width, 0);

1
src/Avalonia.Controls/Slider.cs

@ -5,6 +5,7 @@ using System;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace Avalonia.Controls
{

149
src/Avalonia.Controls/StackPanel.cs

@ -1,8 +1,9 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
// This source file is adapted from the Windows Presentation Foundation project.
// (https://github.com/dotnet/wpf/)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Linq;
using Avalonia.Input;
using Avalonia.Layout;
@ -17,13 +18,13 @@ namespace Avalonia.Controls
/// Defines the <see cref="Spacing"/> property.
/// </summary>
public static readonly StyledProperty<double> SpacingProperty =
AvaloniaProperty.Register<StackPanel, double>(nameof(Spacing));
StackLayout.SpacingProperty.AddOwner<StackPanel>();
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<StackPanel, Orientation>(nameof(Orientation), Orientation.Vertical);
StackLayout.OrientationProperty.AddOwner<StackPanel>();
/// <summary>
/// Initializes static members of the <see cref="StackPanel"/> class.
@ -155,106 +156,122 @@ namespace Avalonia.Controls
}
/// <summary>
/// Measures the control.
/// General StackPanel layout behavior is to grow unbounded in the "stacking" direction (Size To Content).
/// Children in this dimension are encouraged to be as large as they like. In the other dimension,
/// StackPanel will assume the maximum size of its children.
/// </summary>
/// <param name="availableSize">The available size.</param>
/// <returns>The desired size of the control.</returns>
/// <param name="availableSize">Constraint</param>
/// <returns>Desired size</returns>
protected override Size MeasureOverride(Size availableSize)
{
double childAvailableWidth = double.PositiveInfinity;
double childAvailableHeight = double.PositiveInfinity;
Size stackDesiredSize = new Size();
var children = Children;
Size layoutSlotSize = availableSize;
bool fHorizontal = (Orientation == Orientation.Horizontal);
double spacing = Spacing;
bool hasVisibleChild = false;
if (Orientation == Orientation.Vertical)
//
// Initialize child sizing and iterator data
// Allow children as much size as they want along the stack.
//
if (fHorizontal)
{
childAvailableWidth = availableSize.Width;
if (!double.IsNaN(Width))
{
childAvailableWidth = Width;
}
childAvailableWidth = Math.Min(childAvailableWidth, MaxWidth);
childAvailableWidth = Math.Max(childAvailableWidth, MinWidth);
layoutSlotSize = layoutSlotSize.WithWidth(Double.PositiveInfinity);
}
else
{
childAvailableHeight = availableSize.Height;
layoutSlotSize = layoutSlotSize.WithHeight(Double.PositiveInfinity);
}
if (!double.IsNaN(Height))
{
childAvailableHeight = Height;
}
//
// Iterate through children.
// While we still supported virtualization, this was hidden in a child iterator (see source history).
//
for (int i = 0, count = children.Count; i < count; ++i)
{
// Get next child.
var child = children[i];
childAvailableHeight = Math.Min(childAvailableHeight, MaxHeight);
childAvailableHeight = Math.Max(childAvailableHeight, MinHeight);
}
if (child == null)
{ continue; }
double measuredWidth = 0;
double measuredHeight = 0;
double spacing = Spacing;
bool hasVisibleChild = Children.Any(c => c.IsVisible);
bool isVisible = child.IsVisible;
foreach (Control child in Children)
{
child.Measure(new Size(childAvailableWidth, childAvailableHeight));
Size size = child.DesiredSize;
if (isVisible && !hasVisibleChild)
{
hasVisibleChild = true;
}
if (Orientation == Orientation.Vertical)
// Measure the child.
child.Measure(layoutSlotSize);
Size childDesiredSize = child.DesiredSize;
// Accumulate child size.
if (fHorizontal)
{
measuredHeight += size.Height + (child.IsVisible ? spacing : 0);
measuredWidth = Math.Max(measuredWidth, size.Width);
stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width + (isVisible ? spacing : 0) + childDesiredSize.Width);
stackDesiredSize = stackDesiredSize.WithHeight(Math.Max(stackDesiredSize.Height, childDesiredSize.Height));
}
else
{
measuredWidth += size.Width + (child.IsVisible ? spacing : 0);
measuredHeight = Math.Max(measuredHeight, size.Height);
stackDesiredSize = stackDesiredSize.WithWidth(Math.Max(stackDesiredSize.Width, childDesiredSize.Width));
stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + (isVisible ? spacing : 0) + childDesiredSize.Height);
}
}
if (Orientation == Orientation.Vertical)
if (fHorizontal)
{
measuredHeight -= (hasVisibleChild ? spacing : 0);
stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width - (hasVisibleChild ? spacing : 0));
}
else
{
measuredWidth -= (hasVisibleChild ? spacing : 0);
{
stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0));
}
return new Size(measuredWidth, measuredHeight).Constrain(availableSize);
return stackDesiredSize;
}
/// <inheritdoc/>
/// <summary>
/// Content arrangement.
/// </summary>
/// <param name="finalSize">Arrange size</param>
protected override Size ArrangeOverride(Size finalSize)
{
var orientation = Orientation;
var children = Children;
bool fHorizontal = (Orientation == Orientation.Horizontal);
Rect rcChild = new Rect(finalSize);
double previousChildSize = 0.0;
var spacing = Spacing;
var finalRect = new Rect(finalSize);
var pos = 0.0;
foreach (Control child in Children)
//
// Arrange and Position Children.
//
for (int i = 0, count = children.Count; i < count; ++i)
{
if (!child.IsVisible)
{
continue;
}
var child = children[i];
double childWidth = child.DesiredSize.Width;
double childHeight = child.DesiredSize.Height;
if (child == null)
{ continue; }
if (orientation == Orientation.Vertical)
if (fHorizontal)
{
var rect = new Rect(0, pos, childWidth, childHeight)
.Align(finalRect, child.HorizontalAlignment, VerticalAlignment.Top);
ArrangeChild(child, rect, finalSize, orientation);
pos += childHeight + spacing;
rcChild = rcChild.WithX(rcChild.X + previousChildSize);
previousChildSize = child.DesiredSize.Width;
rcChild = rcChild.WithWidth(previousChildSize);
rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height));
previousChildSize += spacing;
}
else
{
var rect = new Rect(pos, 0, childWidth, childHeight)
.Align(finalRect, HorizontalAlignment.Left, child.VerticalAlignment);
ArrangeChild(child, rect, finalSize, orientation);
pos += childWidth + spacing;
rcChild = rcChild.WithY(rcChild.Y + previousChildSize);
previousChildSize = child.DesiredSize.Height;
rcChild = rcChild.WithHeight(previousChildSize);
rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width));
previousChildSize += spacing;
}
ArrangeChild(child, rcChild, finalSize, Orientation);
}
return finalSize;

29
src/Avalonia.Controls/TextBlock.cs

@ -1,12 +1,9 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive;
using System.Reactive.Linq;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Metadata;
namespace Avalonia.Controls
@ -106,6 +103,14 @@ namespace Avalonia.Controls
FontWeightProperty,
FontSizeProperty,
FontStyleProperty);
Observable.Merge(
TextProperty.Changed,
TextAlignmentProperty.Changed,
FontSizeProperty.Changed,
FontStyleProperty.Changed,
FontWeightProperty.Changed
).AddClassHandler<TextBlock>((x,_) => x.OnTextPropertiesChanged());
}
/// <summary>
@ -114,18 +119,6 @@ namespace Avalonia.Controls
public TextBlock()
{
_text = string.Empty;
Observable.Merge(
this.GetObservable(TextProperty).Select(_ => Unit.Default),
this.GetObservable(TextAlignmentProperty).Select(_ => Unit.Default),
this.GetObservable(FontSizeProperty).Select(_ => Unit.Default),
this.GetObservable(FontStyleProperty).Select(_ => Unit.Default),
this.GetObservable(FontWeightProperty).Select(_ => Unit.Default))
.Subscribe(_ =>
{
InvalidateFormattedText();
InvalidateMeasure();
});
}
/// <summary>
@ -408,5 +401,11 @@ namespace Avalonia.Controls
InvalidateFormattedText();
InvalidateMeasure();
}
private void OnTextPropertiesChanged()
{
InvalidateFormattedText();
InvalidateMeasure();
}
}
}

14
src/Avalonia.Controls/ToolTip.cs

@ -4,6 +4,7 @@
using System;
using System.Reactive.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -60,7 +61,7 @@ namespace Avalonia.Controls
private static readonly AttachedProperty<ToolTip> ToolTipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip>("ToolTip");
private PopupRoot _popup;
private IPopupHost _popup;
/// <summary>
/// Initializes static members of the <see cref="ToolTip"/> class.
@ -234,19 +235,20 @@ namespace Avalonia.Controls
{
Close();
_popup = new PopupRoot { Content = this, };
_popup = OverlayPopupHost.CreatePopupHost(control, null);
_popup.SetChild(this);
((ISetLogicalParent)_popup).SetParent(control);
_popup.Position = Popup.GetPosition(control, GetPlacement(control), _popup,
GetHorizontalOffset(control), GetVerticalOffset(control));
_popup.ConfigurePosition(control, GetPlacement(control),
new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
_popup.Show();
_popup.SnapInsideScreenEdges();
}
private void Close()
{
if (_popup != null)
{
_popup.Content = null;
_popup.SetChild(null);
_popup.Hide();
_popup = null;
}

45
src/Avalonia.Controls/Window.cs

@ -135,6 +135,12 @@ namespace Avalonia.Controls
WindowStateProperty.Changed.AddClassHandler<Window>(
(w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; });
MinWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight)));
MinHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight)));
MaxWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight)));
MaxHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue)));
}
/// <summary>
@ -155,6 +161,7 @@ namespace Avalonia.Controls
impl.Closing = HandleClosing;
impl.WindowStateChanged = HandleWindowStateChanged;
_maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x));
}
/// <summary>
@ -239,6 +246,44 @@ namespace Avalonia.Controls
set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLocation, value); }
}
/// <summary>
/// Gets or sets the window position in screen coordinates.
/// </summary>
public PixelPoint Position
{
get { return PlatformImpl?.Position ?? PixelPoint.Origin; }
set
{
PlatformImpl?.Move(value);
}
}
/// <summary>
/// Starts moving a window with left button being held. Should be called from left mouse button press event handler
/// </summary>
public void BeginMoveDrag() => PlatformImpl?.BeginMoveDrag();
/// <summary>
/// Starts resizing a window. This function is used if an application has window resizing controls.
/// Should be called from left mouse button press event handler
/// </summary>
public void BeginResizeDrag(WindowEdge edge) => PlatformImpl?.BeginResizeDrag(edge);
/// <summary>
/// Carries out the arrange pass of the window.
/// </summary>
/// <param name="finalSize">The final window size.</param>
/// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(finalSize);
}
return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
}
/// <inheritdoc/>
Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize;

44
src/Avalonia.Controls/WindowBase.cs

@ -49,10 +49,6 @@ namespace Avalonia.Controls
IsVisibleProperty.OverrideDefaultValue<WindowBase>(false);
IsVisibleProperty.Changed.AddClassHandler<WindowBase>(x => x.IsVisibleChanged);
MinWidthProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight)));
MinHeightProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight)));
MaxWidthProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight)));
MaxHeightProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue)));
TopmostProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetTopmost((bool)e.NewValue));
}
@ -67,7 +63,6 @@ namespace Avalonia.Controls
impl.Activated = HandleActivated;
impl.Deactivated = HandleDeactivated;
impl.PositionChanged = HandlePositionChanged;
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x));
}
/// <summary>
@ -96,19 +91,6 @@ namespace Avalonia.Controls
get { return _isActive; }
private set { SetAndRaise(IsActiveProperty, ref _isActive, value); }
}
/// <summary>
/// Gets or sets the window position in screen coordinates.
/// </summary>
public PixelPoint Position
{
get { return PlatformImpl?.Position ?? PixelPoint.Origin; }
set
{
if (PlatformImpl is IWindowBaseImpl impl)
impl.Position = value;
}
}
public Screens Screens { get; private set; }
@ -208,21 +190,6 @@ namespace Avalonia.Controls
return Disposable.Create(() => AutoSizing = false);
}
/// <summary>
/// Carries out the arrange pass of the window.
/// </summary>
/// <param name="finalSize">The final window size.</param>
/// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(finalSize);
}
return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
}
/// <summary>
/// Ensures that the window is initialized.
/// </summary>
@ -318,16 +285,5 @@ namespace Avalonia.Controls
}
}
}
/// <summary>
/// Starts moving a window with left button being held. Should be called from left mouse button press event handler
/// </summary>
public void BeginMoveDrag() => PlatformImpl?.BeginMoveDrag();
/// <summary>
/// Starts resizing a window. This function is used if an application has window resizing controls.
/// Should be called from left mouse button press event handler
/// </summary>
public void BeginResizeDrag(WindowEdge edge) => PlatformImpl?.BeginResizeDrag(edge);
}
}

1
src/Avalonia.Controls/WrapPanel.cs

@ -4,6 +4,7 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Utilities;
using static System.Math;

5
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -72,6 +72,11 @@ namespace Avalonia.DesignerSupport.Remote
RenderIfNeeded();
}
public void Move(PixelPoint point)
{
}
public void SetMinMaxSize(Size minSize, Size maxSize)
{
}

4
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@ -40,8 +40,6 @@ namespace Avalonia.DesignerSupport.Remote
return s_lastWindow;
}
public IPopupImpl CreatePopup() => new WindowStub();
public static void Initialize(IAvaloniaRemoteTransportConnection transport)
{
s_transport = transport;
@ -54,7 +52,7 @@ namespace Avalonia.DesignerSupport.Remote
.Bind<IPlatformSettings>().ToConstant(instance)
.Bind<IPlatformThreadingInterface>().ToConstant(threading)
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(threading)
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsStub>()
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()

23
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -5,6 +5,7 @@ using System.Reactive.Disposables;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
@ -13,7 +14,7 @@ using Avalonia.Rendering;
namespace Avalonia.DesignerSupport.Remote
{
class WindowStub : IPopupImpl, IWindowImpl
class WindowStub : IWindowImpl, IPopupImpl
{
public Action Deactivated { get; set; }
public Action Activated { get; set; }
@ -29,10 +30,23 @@ namespace Avalonia.DesignerSupport.Remote
public Func<bool> Closing { get; set; }
public Action Closed { get; set; }
public IMouseDevice MouseDevice { get; } = new MouseDevice();
public IPopupImpl CreatePopup() => new WindowStub(this);
public PixelPoint Position { get; set; }
public Action<PixelPoint> PositionChanged { get; set; }
public WindowState WindowState { get; set; }
public Action<WindowState> WindowStateChanged { get; set; }
public WindowStub(IWindowImpl parent = null)
{
if (parent != null)
PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent,
(_, size, __) =>
{
Resize(size);
}));
}
public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer(root);
public void Dispose()
{
@ -77,6 +91,11 @@ namespace Avalonia.DesignerSupport.Remote
{
}
public void Move(PixelPoint point)
{
}
public IScreenImpl Screen { get; } = new ScreenStub();
public void SetMinMaxSize(Size minSize, Size maxSize)
@ -110,6 +129,8 @@ namespace Avalonia.DesignerSupport.Remote
public void SetTopmost(bool value)
{
}
public IPopupPositioner PopupPositioner { get; }
}
class ClipboardStub : IClipboard

36
src/Avalonia.Diagnostics/DevTools.xaml

@ -1,24 +1,24 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.Diagnostics.DevTools">
<Grid RowDefinitions="*,Auto" Margin="4">
<Grid RowDefinitions="*,Auto" Margin="4">
<TabControl Grid.Row="0" Items="{Binding Tools}" SelectedItem="{Binding SelectedTool}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
<TabControl Grid.Row="0" Items="{Binding Tools}" SelectedItem="{Binding SelectedTool}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
<StackPanel Grid.Row="1" Spacing="4" Orientation="Horizontal">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8" />
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}" />
<Separator Width="8" />
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}" />
</StackPanel>
</Grid>
<StackPanel Grid.Row="1" Spacing="4" Orientation="Horizontal">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8" />
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}" />
<Separator Width="8" />
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}" />
</StackPanel>
</Grid>
</UserControl>

89
src/Avalonia.Diagnostics/DevTools.xaml.cs

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
@ -18,22 +17,27 @@ using Avalonia.VisualTree;
namespace Avalonia
{
public static class DevToolsExtensions
{
public static void AttachDevTools(this TopLevel control)
{
Avalonia.Diagnostics.DevTools.Attach(control);
}
}
public static class DevToolsExtensions
{
public static void AttachDevTools(this TopLevel control)
{
Diagnostics.DevTools.Attach(control, new KeyGesture(Key.F12));
}
public static void AttachDevTools(this TopLevel control, KeyGesture gesture)
{
Diagnostics.DevTools.Attach(control, gesture);
}
}
}
namespace Avalonia.Diagnostics
{
public class DevTools : UserControl
public class DevTools : UserControl
{
private static Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
private static HashSet<IRenderRoot> s_visualTreeRoots = new HashSet<IRenderRoot>();
private IDisposable _keySubscription;
private static readonly Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
private static readonly HashSet<IRenderRoot> s_visualTreeRoots = new HashSet<IRenderRoot>();
private readonly IDisposable _keySubscription;
public DevTools(IControl root)
{
@ -49,51 +53,49 @@ namespace Avalonia.Diagnostics
// HACK: needed for XAMLIL, will fix that later
public DevTools()
{
}
public IControl Root { get; }
public static IDisposable Attach(TopLevel control)
public static IDisposable Attach(TopLevel control, KeyGesture gesture)
{
void PreviewKeyDown(object sender, KeyEventArgs e)
{
if (gesture.Matches(e))
{
OpenDevTools(control);
}
}
return control.AddHandler(
KeyDownEvent,
WindowPreviewKeyDown,
PreviewKeyDown,
RoutingStrategies.Tunnel);
}
private static void WindowPreviewKeyDown(object sender, KeyEventArgs e)
private static void OpenDevTools(TopLevel control)
{
if (e.Key == Key.F12)
if (s_open.TryGetValue(control, out var devToolsWindow))
{
devToolsWindow.Activate();
}
else
{
var control = (TopLevel)sender;
var devToolsWindow = default(Window);
var devTools = new DevTools(control);
if (s_open.TryGetValue(control, out devToolsWindow))
devToolsWindow = new Window
{
devToolsWindow.Activate();
}
else
{
var devTools = new DevTools(control);
devToolsWindow = new Window
{
Width = 1024,
Height = 512,
Content = devTools,
DataTemplates =
{
new ViewLocator<ViewModelBase>(),
},
Title = "Avalonia DevTools"
};
devToolsWindow.Closed += devTools.DevToolsClosed;
s_open.Add(control, devToolsWindow);
MarkAsDevTool(devToolsWindow);
devToolsWindow.Show();
}
Width = 1024,
Height = 512,
Content = devTools,
DataTemplates = { new ViewLocator<ViewModelBase>() },
Title = "Avalonia DevTools"
};
devToolsWindow.Closed += devTools.DevToolsClosed;
s_open.Add(control, devToolsWindow);
MarkAsDevTool(devToolsWindow);
devToolsWindow.Show();
}
}
@ -118,7 +120,6 @@ namespace Avalonia.Diagnostics
if ((e.Modifiers) == modifiers)
{
var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point);
var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible))
.FirstOrDefault();

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

Loading…
Cancel
Save