Browse Source

Merge branch 'master' into feature/scrollchanged

pull/3729/head
Dariusz Komosiński 7 years ago
committed by GitHub
parent
commit
5ab282136c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      azure-pipelines.yml
  2. 2
      src/Avalonia.Base/Threading/DispatcherPriority.cs
  3. 7
      src/Avalonia.Controls/AutoCompleteBox.cs
  4. 7
      src/Avalonia.Controls/Calendar/DatePicker.cs
  5. 7
      src/Avalonia.Controls/ComboBox.cs
  6. 57
      src/Avalonia.Controls/Primitives/Popup.cs
  7. 33
      src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs
  8. 12
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  9. 24
      src/Avalonia.Controls/Shapes/Path.cs
  10. 22
      src/Avalonia.Controls/TextBox.cs
  11. 12
      src/Avalonia.Controls/TreeViewItem.cs
  12. 48
      src/Avalonia.Controls/Window.cs
  13. 60
      src/Avalonia.Controls/WindowBase.cs
  14. 14
      src/Avalonia.Input/FocusManager.cs
  15. 6
      src/Avalonia.Input/IFocusManager.cs
  16. 2
      src/Avalonia.Input/IKeyboardDevice.cs
  17. 6
      src/Avalonia.Input/IKeyboardNavigationHandler.cs
  18. 4
      src/Avalonia.Input/KeyboardDevice.cs
  19. 8
      src/Avalonia.Input/KeyboardNavigationHandler.cs
  20. 2
      src/Avalonia.Input/Raw/RawPointerEventArgs.cs
  21. 104
      src/Avalonia.Layout/AttachedLayout.cs
  22. 45
      src/Avalonia.Layout/LayoutContextAdapter.cs
  23. 36
      src/Avalonia.Layout/NonVirtualizingLayout.cs
  24. 17
      src/Avalonia.Layout/NonVirtualizingLayoutContext.cs
  25. 160
      src/Avalonia.Layout/NonVirtualizingStackLayout.cs
  26. 8
      src/Avalonia.Layout/StackLayout.cs
  27. 8
      src/Avalonia.Layout/UniformGridLayout.cs
  28. 42
      src/Avalonia.Layout/VirtualLayoutContextAdapter.cs
  29. 36
      src/Avalonia.Layout/VirtualizingLayout.cs
  30. 5
      src/Avalonia.Layout/VirtualizingLayoutContext.cs
  31. 17
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  32. 20
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs
  33. 43
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  34. 22
      src/Avalonia.X11/X11Window.cs
  35. 10
      src/Avalonia.X11/XI2Manager.cs
  36. 40
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
  37. 20
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  38. 2
      src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs
  39. 2
      src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs
  40. 7
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  41. 82
      tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
  42. 81
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  43. 18
      tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs
  44. 75
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  45. 29
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  46. 139
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  47. 4
      tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs
  48. 108
      tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
  49. 335
      tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs
  50. 62
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  51. 111
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs

2
azure-pipelines.yml

@ -68,7 +68,7 @@ jobs:
inputs: inputs:
script: | script: |
brew update brew update
brew install castxml brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Nuke' displayName: 'Install Nuke'

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

@ -17,7 +17,7 @@ namespace Avalonia.Threading
SystemIdle = 1, SystemIdle = 1,
/// <summary> /// <summary>
/// The job will be processed when the application sis idle. /// The job will be processed when the application is idle.
/// </summary> /// </summary>
ApplicationIdle = 2, ApplicationIdle = 2,

7
src/Avalonia.Controls/AutoCompleteBox.cs

@ -1630,7 +1630,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
/// <param name="sender">The source object.</param> /// <param name="sender">The source object.</param>
/// <param name="e">The event data.</param> /// <param name="e">The event data.</param>
private void DropDownPopup_Closed(object sender, EventArgs e) private void DropDownPopup_Closed(object sender, PopupClosedEventArgs e)
{ {
// Force the drop down dependency property to be false. // Force the drop down dependency property to be false.
if (IsDropDownOpen) if (IsDropDownOpen)
@ -1638,6 +1638,11 @@ namespace Avalonia.Controls
IsDropDownOpen = false; IsDropDownOpen = false;
} }
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
// Fire the DropDownClosed event // Fire the DropDownClosed event
if (_popupHasOpened) if (_popupHasOpened)
{ {

7
src/Avalonia.Controls/Calendar/DatePicker.cs

@ -895,12 +895,17 @@ namespace Avalonia.Controls
_ignoreButtonClick = false; _ignoreButtonClick = false;
} }
} }
private void PopUp_Closed(object sender, EventArgs e) private void PopUp_Closed(object sender, PopupClosedEventArgs e)
{ {
IsDropDownOpen = false; IsDropDownOpen = false;
if(!_isPopupClosing) if(!_isPopupClosing)
{ {
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
_isPopupClosing = true; _isPopupClosing = true;
Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false); Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false);
} }

7
src/Avalonia.Controls/ComboBox.cs

@ -242,11 +242,16 @@ namespace Avalonia.Controls
} }
} }
private void PopupClosed(object sender, EventArgs e) private void PopupClosed(object sender, PopupClosedEventArgs e)
{ {
_subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null; _subscriptionsOnOpen = null;
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
if (CanFocus(this)) if (CanFocus(this))
{ {
Focus(); Focus();

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

@ -95,7 +95,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Raised when the popup closes. /// Raised when the popup closes.
/// </summary> /// </summary>
public event EventHandler? Closed; public event EventHandler<PopupClosedEventArgs>? Closed;
/// <summary> /// <summary>
/// Raised when the popup opens. /// Raised when the popup opens.
@ -270,7 +270,7 @@ namespace Avalonia.Controls.Primitives
if (parentPopupRoot?.Parent is Popup popup) if (parentPopupRoot?.Parent is Popup popup)
{ {
DeferCleanup(SubscribeToEventHandler<Popup, EventHandler>(popup, ParentClosed, DeferCleanup(SubscribeToEventHandler<Popup, EventHandler<PopupClosedEventArgs>>(popup, ParentClosed,
(x, handler) => x.Closed += handler, (x, handler) => x.Closed += handler,
(x, handler) => x.Closed -= handler)); (x, handler) => x.Closed -= handler));
} }
@ -306,28 +306,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Closes the popup. /// Closes the popup.
/// </summary> /// </summary>
public void Close() public void Close() => CloseCore(null);
{
if (_openState is null)
{
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
return;
}
_openState.Dispose();
_openState = null;
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
Closed?.Invoke(this, EventArgs.Empty);
}
/// <summary> /// <summary>
/// Measures the control. /// Measures the control.
@ -389,22 +368,44 @@ namespace Avalonia.Controls.Primitives
} }
} }
private void CloseCore(EventArgs? closeEvent)
{
if (_openState is null)
{
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
return;
}
_openState.Dispose();
_openState = null;
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent));
}
private void ListenForNonClientClick(RawInputEventArgs e) private void ListenForNonClientClick(RawInputEventArgs e)
{ {
var mouse = e as RawPointerEventArgs; var mouse = e as RawPointerEventArgs;
if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{ {
Close(); CloseCore(e);
} }
} }
private void PointerPressedOutside(object sender, PointerPressedEventArgs e) private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
{ {
if (!StaysOpen && !IsChildOrThis((IVisual)e.Source)) if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v))
{ {
Close(); CloseCore(e);
e.Handled = true;
} }
} }

33
src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs

@ -0,0 +1,33 @@
using System;
using Avalonia.Interactivity;
#nullable enable
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Holds data for the <see cref="Popup.Closed"/> event.
/// </summary>
public class PopupClosedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="PopupClosedEventArgs"/> class.
/// </summary>
/// <param name="closeEvent"></param>
public PopupClosedEventArgs(EventArgs? closeEvent)
{
CloseEvent = closeEvent;
}
/// <summary>
/// Gets the event that closed the popup, if any.
/// </summary>
/// <remarks>
/// If <see cref="Popup.StaysOpen"/> is false, then this property will hold details of the
/// interaction that caused the popup to close if the close was caused by e.g. a pointer press
/// outside the popup. It can be used to mark the event as handled if the event should not
/// be propagated.
/// </remarks>
public EventArgs? CloseEvent { get; }
}
}

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

@ -117,20 +117,14 @@ namespace Avalonia.Controls.Primitives
}); });
} }
/// <summary> protected override sealed Size ArrangeSetBounds(Size size)
/// 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()) using (BeginAutoSizing())
{ {
_positionerParameters.Size = finalSize; _positionerParameters.Size = size;
UpdatePosition(); UpdatePosition();
return ClientSize;
} }
return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
} }
} }
} }

24
src/Avalonia.Controls/Shapes/Path.cs

@ -1,3 +1,5 @@
using System;
using Avalonia.Data;
using Avalonia.Media; using Avalonia.Media;
namespace Avalonia.Controls.Shapes namespace Avalonia.Controls.Shapes
@ -10,6 +12,7 @@ namespace Avalonia.Controls.Shapes
static Path() static Path()
{ {
AffectsGeometry<Path>(DataProperty); AffectsGeometry<Path>(DataProperty);
DataProperty.Changed.AddClassHandler<Path>((o, e) => o.DataChanged(e));
} }
public Geometry Data public Geometry Data
@ -19,5 +22,26 @@ namespace Avalonia.Controls.Shapes
} }
protected override Geometry CreateDefiningGeometry() => Data; protected override Geometry CreateDefiningGeometry() => Data;
private void DataChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldGeometry = (Geometry)e.OldValue;
var newGeometry = (Geometry)e.NewValue;
if (oldGeometry is object)
{
oldGeometry.Changed -= GeometryChanged;
}
if (newGeometry is object)
{
newGeometry.Changed += GeometryChanged;
}
}
private void GeometryChanged(object sender, EventArgs e)
{
InvalidateGeometry();
}
} }
} }

22
src/Avalonia.Controls/TextBox.cs

@ -277,13 +277,15 @@ namespace Avalonia.Controls
get { return GetSelection(); } get { return GetSelection(); }
set set
{ {
if (value == null) _undoRedoHelper.Snapshot();
if (string.IsNullOrEmpty(value))
{ {
return; DeleteSelection();
} }
else
_undoRedoHelper.Snapshot(); {
HandleTextInput(value); HandleTextInput(value);
}
_undoRedoHelper.Snapshot(); _undoRedoHelper.Snapshot();
} }
} }
@ -471,8 +473,10 @@ namespace Avalonia.Controls
{ {
if (!IsPasswordBox) if (!IsPasswordBox)
{ {
_undoRedoHelper.Snapshot();
Copy(); Copy();
DeleteSelection(); DeleteSelection();
_undoRedoHelper.Snapshot();
} }
handled = true; handled = true;
@ -598,6 +602,7 @@ namespace Avalonia.Controls
break; break;
case Key.Back: case Key.Back:
_undoRedoHelper.Snapshot();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd) if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{ {
SetSelectionForControlBackspace(); SetSelectionForControlBackspace();
@ -621,11 +626,13 @@ namespace Avalonia.Controls
CaretIndex -= removedCharacters; CaretIndex -= removedCharacters;
SelectionStart = SelectionEnd = CaretIndex; SelectionStart = SelectionEnd = CaretIndex;
} }
_undoRedoHelper.Snapshot();
handled = true; handled = true;
break; break;
case Key.Delete: case Key.Delete:
_undoRedoHelper.Snapshot();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd) if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{ {
SetSelectionForControlDelete(); SetSelectionForControlDelete();
@ -647,6 +654,7 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) + SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters)); text.Substring(caretIndex + removedCharacters));
} }
_undoRedoHelper.Snapshot();
handled = true; handled = true;
break; break;
@ -654,7 +662,9 @@ namespace Avalonia.Controls
case Key.Enter: case Key.Enter:
if (AcceptsReturn) if (AcceptsReturn)
{ {
_undoRedoHelper.Snapshot();
HandleTextInput(NewLine); HandleTextInput(NewLine);
_undoRedoHelper.Snapshot();
handled = true; handled = true;
} }
@ -663,7 +673,9 @@ namespace Avalonia.Controls
case Key.Tab: case Key.Tab:
if (AcceptsTab) if (AcceptsTab)
{ {
_undoRedoHelper.Snapshot();
HandleTextInput("\t"); HandleTextInput("\t");
_undoRedoHelper.Snapshot();
handled = true; handled = true;
} }
else else

12
src/Avalonia.Controls/TreeViewItem.cs

@ -51,6 +51,7 @@ namespace Avalonia.Controls
SelectableMixin.Attach<TreeViewItem>(IsSelectedProperty); SelectableMixin.Attach<TreeViewItem>(IsSelectedProperty);
FocusableProperty.OverrideDefaultValue<TreeViewItem>(true); FocusableProperty.OverrideDefaultValue<TreeViewItem>(true);
ItemsPanelProperty.OverrideDefaultValue<TreeViewItem>(DefaultPanel); ItemsPanelProperty.OverrideDefaultValue<TreeViewItem>(DefaultPanel);
ParentProperty.Changed.AddClassHandler<TreeViewItem>((o, e) => o.OnParentChanged(e));
RequestBringIntoViewEvent.AddClassHandler<TreeViewItem>((x, e) => x.OnRequestBringIntoView(e)); RequestBringIntoViewEvent.AddClassHandler<TreeViewItem>((x, e) => x.OnRequestBringIntoView(e));
} }
@ -179,5 +180,16 @@ namespace Avalonia.Controls
return logical != null ? result : @default; return logical != null ? result : @default;
} }
private void OnParentChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!((ILogical)this).IsAttachedToLogicalTree && e.NewValue is null)
{
// If we're not attached to the logical tree, then OnDetachedFromLogicalTree isn't going to be
// called when the item is removed. This results in the item not being removed from the index,
// causing #3551. In this case, update the index when Parent is changed to null.
ItemContainerGenerator.UpdateIndex();
}
}
} }
} }

48
src/Avalonia.Controls/Window.cs

@ -313,22 +313,7 @@ namespace Avalonia.Controls
/// Should be called from left mouse button press event handler /// Should be called from left mouse button press event handler
/// </summary> /// </summary>
public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e); public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e);
/// <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/> /// <inheritdoc/>
Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize; Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize;
@ -450,6 +435,19 @@ namespace Avalonia.Controls
EnsureInitialized(); EnsureInitialized();
IsVisible = true; IsVisible = true;
var initialSize = new Size(
double.IsNaN(Width) ? ClientSize.Width : Width,
double.IsNaN(Height) ? ClientSize.Height : Height);
if (initialSize != ClientSize)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(initialSize);
}
}
LayoutManager.ExecuteInitialLayoutPass(this); LayoutManager.ExecuteInitialLayoutPass(this);
using (BeginAutoSizing()) using (BeginAutoSizing())
@ -569,31 +567,30 @@ namespace Avalonia.Controls
} }
} }
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
var sizeToContent = SizeToContent; var sizeToContent = SizeToContent;
var clientSize = ClientSize; var clientSize = ClientSize;
var constraint = availableSize; var constraint = clientSize;
if ((sizeToContent & SizeToContent.Width) != 0) if (sizeToContent.HasFlagCustom(SizeToContent.Width))
{ {
constraint = constraint.WithWidth(double.PositiveInfinity); constraint = constraint.WithWidth(double.PositiveInfinity);
} }
if ((sizeToContent & SizeToContent.Height) != 0) if (sizeToContent.HasFlagCustom(SizeToContent.Height))
{ {
constraint = constraint.WithHeight(double.PositiveInfinity); constraint = constraint.WithHeight(double.PositiveInfinity);
} }
var result = base.MeasureOverride(constraint); var result = base.MeasureOverride(constraint);
if ((sizeToContent & SizeToContent.Width) == 0) if (!sizeToContent.HasFlagCustom(SizeToContent.Width))
{ {
result = result.WithWidth(clientSize.Width); result = result.WithWidth(clientSize.Width);
} }
if ((sizeToContent & SizeToContent.Height) == 0) if (!sizeToContent.HasFlagCustom(SizeToContent.Height))
{ {
result = result.WithHeight(clientSize.Height); result = result.WithHeight(clientSize.Height);
} }
@ -601,6 +598,15 @@ namespace Avalonia.Controls
return result; return result;
} }
protected sealed override Size ArrangeSetBounds(Size size)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(size);
return ClientSize;
}
}
protected sealed override void HandleClosed() protected sealed override void HandleClosed()
{ {
RaiseEvent(new RoutedEventArgs(WindowClosedEvent)); RaiseEvent(new RoutedEventArgs(WindowClosedEvent));

60
src/Avalonia.Controls/WindowBase.cs

@ -224,16 +224,66 @@ namespace Avalonia.Controls
/// <param name="clientSize">The new client size.</param> /// <param name="clientSize">The new client size.</param>
protected override void HandleResized(Size clientSize) protected override void HandleResized(Size clientSize)
{ {
if (!AutoSizing) Width = clientSize.Width;
{ Height = clientSize.Height;
Width = clientSize.Width;
Height = clientSize.Height;
}
ClientSize = clientSize; ClientSize = clientSize;
LayoutManager.ExecuteLayoutPass(); LayoutManager.ExecuteLayoutPass();
Renderer?.Resized(clientSize); Renderer?.Resized(clientSize);
} }
/// <summary>
/// Overrides the core measure logic for windows.
/// </summary>
/// <param name="availableSize">The available size.</param>
/// <returns>The measured size.</returns>
/// <remarks>
/// The layout logic for top-level windows is different than for other controls because
/// they don't have a parent, meaning that many layout properties handled by the default
/// MeasureCore (such as margins and alignment) make no sense.
/// </remarks>
protected override Size MeasureCore(Size availableSize)
{
ApplyStyling();
ApplyTemplate();
var constraint = availableSize;
if (!double.IsNaN(Width))
{
constraint = constraint.WithWidth(Width);
}
if (!double.IsNaN(Height))
{
constraint = constraint.WithHeight(Height);
}
return MeasureOverride(constraint);
}
/// <summary>
/// Overrides the core arrange logic for windows.
/// </summary>
/// <param name="finalRect">The final arrange rect.</param>
/// <remarks>
/// The layout logic for top-level windows is different than for other controls because
/// they don't have a parent, meaning that many layout properties handled by the default
/// ArrangeCore (such as margins and alignment) make no sense.
/// </remarks>
protected override void ArrangeCore(Rect finalRect)
{
var constraint = ArrangeSetBounds(finalRect.Size);
var arrangeSize = ArrangeOverride(constraint);
Bounds = new Rect(arrangeSize);
}
/// <summary>
/// Called durung the arrange pass to set the size of the window.
/// </summary>
/// <param name="size">The requested size of the window.</param>
/// <returns>The actual size of the window.</returns>
protected virtual Size ArrangeSetBounds(Size size) => size;
/// <summary> /// <summary>
/// Handles a window position change notification from /// Handles a window position change notification from
/// <see cref="IWindowBaseImpl.PositionChanged"/>. /// <see cref="IWindowBaseImpl.PositionChanged"/>.

14
src/Avalonia.Input/FocusManager.cs

@ -53,11 +53,11 @@ namespace Avalonia.Input
/// </summary> /// </summary>
/// <param name="control">The control to focus.</param> /// <param name="control">The control to focus.</param>
/// <param name="method">The method by which focus was changed.</param> /// <param name="method">The method by which focus was changed.</param>
/// <param name="modifiers">Any input modifiers active at the time of focus.</param> /// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
public void Focus( public void Focus(
IInputElement control, IInputElement control,
NavigationMethod method = NavigationMethod.Unspecified, NavigationMethod method = NavigationMethod.Unspecified,
InputModifiers modifiers = InputModifiers.None) KeyModifiers keyModifiers = KeyModifiers.None)
{ {
if (control != null) if (control != null)
{ {
@ -67,7 +67,7 @@ namespace Avalonia.Input
if (scope != null) if (scope != null)
{ {
Scope = scope; Scope = scope;
SetFocusedElement(scope, control, method, modifiers); SetFocusedElement(scope, control, method, keyModifiers);
} }
} }
else if (Current != null) else if (Current != null)
@ -95,7 +95,7 @@ namespace Avalonia.Input
/// <param name="scope">The focus scope.</param> /// <param name="scope">The focus scope.</param>
/// <param name="element">The element to focus. May be null.</param> /// <param name="element">The element to focus. May be null.</param>
/// <param name="method">The method by which focus was changed.</param> /// <param name="method">The method by which focus was changed.</param>
/// <param name="modifiers">Any input modifiers active at the time of focus.</param> /// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
/// <remarks> /// <remarks>
/// If the specified scope is the current <see cref="Scope"/> then the keyboard focus /// If the specified scope is the current <see cref="Scope"/> then the keyboard focus
/// will change. /// will change.
@ -104,7 +104,7 @@ namespace Avalonia.Input
IFocusScope scope, IFocusScope scope,
IInputElement element, IInputElement element,
NavigationMethod method = NavigationMethod.Unspecified, NavigationMethod method = NavigationMethod.Unspecified,
InputModifiers modifiers = InputModifiers.None) KeyModifiers keyModifiers = KeyModifiers.None)
{ {
Contract.Requires<ArgumentNullException>(scope != null); Contract.Requires<ArgumentNullException>(scope != null);
@ -123,7 +123,7 @@ namespace Avalonia.Input
if (Scope == scope) if (Scope == scope)
{ {
KeyboardDevice.Instance?.SetFocusedElement(element, method, modifiers); KeyboardDevice.Instance?.SetFocusedElement(element, method, keyModifiers);
} }
} }
@ -195,7 +195,7 @@ namespace Avalonia.Input
{ {
if (element is IInputElement inputElement && CanFocus(inputElement)) if (element is IInputElement inputElement && CanFocus(inputElement))
{ {
Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.InputModifiers); Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.KeyModifiers);
break; break;
} }

6
src/Avalonia.Input/IFocusManager.cs

@ -20,11 +20,11 @@ namespace Avalonia.Input
/// </summary> /// </summary>
/// <param name="control">The control to focus.</param> /// <param name="control">The control to focus.</param>
/// <param name="method">The method by which focus was changed.</param> /// <param name="method">The method by which focus was changed.</param>
/// <param name="modifiers">Any input modifiers active at the time of focus.</param> /// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
void Focus( void Focus(
IInputElement control, IInputElement control,
NavigationMethod method = NavigationMethod.Unspecified, NavigationMethod method = NavigationMethod.Unspecified,
InputModifiers modifiers = InputModifiers.None); KeyModifiers keyModifiers = KeyModifiers.None);
/// <summary> /// <summary>
/// Notifies the focus manager of a change in focus scope. /// Notifies the focus manager of a change in focus scope.

2
src/Avalonia.Input/IKeyboardDevice.cs

@ -63,6 +63,6 @@ namespace Avalonia.Input
void SetFocusedElement( void SetFocusedElement(
IInputElement element, IInputElement element,
NavigationMethod method, NavigationMethod method,
InputModifiers modifiers); KeyModifiers modifiers);
} }
} }

6
src/Avalonia.Input/IKeyboardNavigationHandler.cs

@ -19,10 +19,10 @@ namespace Avalonia.Input
/// </summary> /// </summary>
/// <param name="element">The current element.</param> /// <param name="element">The current element.</param>
/// <param name="direction">The direction to move.</param> /// <param name="direction">The direction to move.</param>
/// <param name="modifiers">Any input modifiers active at the time of focus.</param> /// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
void Move( void Move(
IInputElement element, IInputElement element,
NavigationDirection direction, NavigationDirection direction,
InputModifiers modifiers = InputModifiers.None); KeyModifiers keyModifiers = KeyModifiers.None);
} }
} }

4
src/Avalonia.Input/KeyboardDevice.cs

@ -35,7 +35,7 @@ namespace Avalonia.Input
public void SetFocusedElement( public void SetFocusedElement(
IInputElement element, IInputElement element,
NavigationMethod method, NavigationMethod method,
InputModifiers modifiers) KeyModifiers keyModifiers)
{ {
if (element != FocusedElement) if (element != FocusedElement)
{ {
@ -53,7 +53,7 @@ namespace Avalonia.Input
{ {
RoutedEvent = InputElement.GotFocusEvent, RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = method, NavigationMethod = method,
InputModifiers = modifiers, KeyModifiers = keyModifiers,
}); });
} }
} }

8
src/Avalonia.Input/KeyboardNavigationHandler.cs

@ -91,11 +91,11 @@ namespace Avalonia.Input
/// </summary> /// </summary>
/// <param name="element">The current element.</param> /// <param name="element">The current element.</param>
/// <param name="direction">The direction to move.</param> /// <param name="direction">The direction to move.</param>
/// <param name="modifiers">Any input modifiers active at the time of focus.</param> /// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
public void Move( public void Move(
IInputElement element, IInputElement element,
NavigationDirection direction, NavigationDirection direction,
InputModifiers modifiers = InputModifiers.None) KeyModifiers keyModifiers = KeyModifiers.None)
{ {
Contract.Requires<ArgumentNullException>(element != null); Contract.Requires<ArgumentNullException>(element != null);
@ -106,7 +106,7 @@ namespace Avalonia.Input
var method = direction == NavigationDirection.Next || var method = direction == NavigationDirection.Next ||
direction == NavigationDirection.Previous ? direction == NavigationDirection.Previous ?
NavigationMethod.Tab : NavigationMethod.Directional; NavigationMethod.Tab : NavigationMethod.Directional;
FocusManager.Instance.Focus(next, method, modifiers); FocusManager.Instance.Focus(next, method, keyModifiers);
} }
} }
@ -123,7 +123,7 @@ namespace Avalonia.Input
{ {
var direction = (e.KeyModifiers & KeyModifiers.Shift) == 0 ? var direction = (e.KeyModifiers & KeyModifiers.Shift) == 0 ?
NavigationDirection.Next : NavigationDirection.Previous; NavigationDirection.Next : NavigationDirection.Previous;
Move(current, direction, e.Modifiers); Move(current, direction, e.KeyModifiers);
e.Handled = true; e.Handled = true;
} }
} }

2
src/Avalonia.Input/Raw/RawPointerEventArgs.cs

@ -63,7 +63,7 @@ namespace Avalonia.Input.Raw
/// <summary> /// <summary>
/// Gets the type of the event. /// Gets the type of the event.
/// </summary> /// </summary>
public RawPointerEventType Type { get; private set; } public RawPointerEventType Type { get; set; }
/// <summary> /// <summary>
/// Gets the input modifiers. /// Gets the input modifiers.

104
src/Avalonia.Layout/AttachedLayout.cs

@ -46,7 +46,23 @@ namespace Avalonia.Layout
/// <see cref="VirtualizingLayout.InitializeForContextCore"/> to provide the behavior for /// <see cref="VirtualizingLayout.InitializeForContextCore"/> to provide the behavior for
/// this method in a derived class. /// this method in a derived class.
/// </remarks> /// </remarks>
public abstract void InitializeForContext(LayoutContext context); public void InitializeForContext(LayoutContext context)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
virtualizingLayout.InitializeForContextCore(virtualizingContext);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
nonVirtualizingLayout.InitializeForContextCore(nonVirtualizingContext);
}
else
{
throw new NotSupportedException();
}
}
/// <summary> /// <summary>
/// Removes any state the layout previously stored on the ILayoutable container. /// Removes any state the layout previously stored on the ILayoutable container.
@ -55,7 +71,23 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host /// The context object that facilitates communication between the layout and its host
/// container. /// container.
/// </param> /// </param>
public abstract void UninitializeForContext(LayoutContext context); public void UninitializeForContext(LayoutContext context)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
virtualizingLayout.UninitializeForContextCore(virtualizingContext);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
nonVirtualizingLayout.UninitializeForContextCore(nonVirtualizingContext);
}
else
{
throw new NotSupportedException();
}
}
/// <summary> /// <summary>
/// Suggests a DesiredSize for a container element. A container element that supports /// Suggests a DesiredSize for a container element. A container element that supports
@ -73,7 +105,23 @@ namespace Avalonia.Layout
/// if scrolling or other resize behavior is possible in that particular container. /// if scrolling or other resize behavior is possible in that particular container.
/// </param> /// </param>
/// <returns></returns> /// <returns></returns>
public abstract Size Measure(LayoutContext context, Size availableSize); public Size Measure(LayoutContext context, Size availableSize)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
return virtualizingLayout.MeasureOverride(virtualizingContext, availableSize);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
return nonVirtualizingLayout.MeasureOverride(nonVirtualizingContext, availableSize);
}
else
{
throw new NotSupportedException();
}
}
/// <summary> /// <summary>
/// Positions child elements and determines a size for a container UIElement. Container /// Positions child elements and determines a size for a container UIElement. Container
@ -88,7 +136,23 @@ namespace Avalonia.Layout
/// The final size that the container computes for the child in layout. /// The final size that the container computes for the child in layout.
/// </param> /// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns> /// <returns>The actual size that is used after the element is arranged in layout.</returns>
public abstract Size Arrange(LayoutContext context, Size finalSize); public Size Arrange(LayoutContext context, Size finalSize)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
return virtualizingLayout.ArrangeOverride(virtualizingContext, finalSize);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
return nonVirtualizingLayout.ArrangeOverride(nonVirtualizingContext, finalSize);
}
else
{
throw new NotSupportedException();
}
}
/// <summary> /// <summary>
/// Invalidates the measurement state (layout) for all ILayoutable containers that reference /// Invalidates the measurement state (layout) for all ILayoutable containers that reference
@ -102,5 +166,37 @@ namespace Avalonia.Layout
/// occurs asynchronously. /// occurs asynchronously.
/// </summary> /// </summary>
protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty); protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty);
private VirtualizingLayoutContext GetVirtualizingLayoutContext(LayoutContext context)
{
if (context is VirtualizingLayoutContext virtualizingContext)
{
return virtualizingContext;
}
else if (context is NonVirtualizingLayoutContext nonVirtualizingContext)
{
return nonVirtualizingContext.GetVirtualizingContextAdapter();
}
else
{
throw new NotSupportedException();
}
}
private NonVirtualizingLayoutContext GetNonVirtualizingLayoutContext(LayoutContext context)
{
if (context is NonVirtualizingLayoutContext nonVirtualizingContext)
{
return nonVirtualizingContext;
}
else if (context is VirtualizingLayoutContext virtualizingContext)
{
return virtualizingContext.GetNonVirtualizingContextAdapter();
}
else
{
throw new NotSupportedException();
}
}
} }
} }

45
src/Avalonia.Layout/LayoutContextAdapter.cs

@ -0,0 +1,45 @@
// 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.Layout
{
internal class LayoutContextAdapter : VirtualizingLayoutContext
{
private readonly NonVirtualizingLayoutContext _nonVirtualizingContext;
public LayoutContextAdapter(NonVirtualizingLayoutContext nonVirtualizingContext)
{
_nonVirtualizingContext = nonVirtualizingContext;
}
protected override object LayoutStateCore
{
get => _nonVirtualizingContext.LayoutState;
set => _nonVirtualizingContext.LayoutState = value;
}
protected override Point LayoutOriginCore
{
get => default;
set
{
if (value != default)
{
throw new InvalidOperationException("LayoutOrigin must be at (0,0) when RealizationRect is infinite sized.");
}
}
}
protected override Rect RealizationRectCore() => new Rect(Size.Infinity);
protected override int ItemCountCore() => _nonVirtualizingContext.Children.Count;
protected override object GetItemAtCore(int index) => _nonVirtualizingContext.Children[index];
protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) =>
_nonVirtualizingContext.Children[index];
protected override void RecycleElementCore(ILayoutable element) { }
}
}

36
src/Avalonia.Layout/NonVirtualizingLayout.cs

@ -17,30 +17,6 @@ namespace Avalonia.Layout
/// </remarks> /// </remarks>
public abstract class NonVirtualizingLayout : AttachedLayout public abstract class NonVirtualizingLayout : AttachedLayout
{ {
/// <inheritdoc/>
public sealed override void InitializeForContext(LayoutContext context)
{
InitializeForContextCore((NonVirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override void UninitializeForContext(LayoutContext context)
{
UninitializeForContextCore((NonVirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override Size Measure(LayoutContext context, Size availableSize)
{
return MeasureOverride((NonVirtualizingLayoutContext)context, availableSize);
}
/// <inheritdoc/>
public sealed override Size Arrange(LayoutContext context, Size finalSize)
{
return ArrangeOverride((NonVirtualizingLayoutContext)context, finalSize);
}
/// <summary> /// <summary>
/// When overridden in a derived class, initializes any per-container state the layout /// When overridden in a derived class, initializes any per-container state the layout
/// requires when it is attached to an ILayoutable container. /// requires when it is attached to an ILayoutable container.
@ -49,7 +25,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host /// The context object that facilitates communication between the layout and its host
/// container. /// container.
/// </param> /// </param>
protected virtual void InitializeForContextCore(LayoutContext context) protected internal virtual void InitializeForContextCore(LayoutContext context)
{ {
} }
@ -61,7 +37,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host /// The context object that facilitates communication between the layout and its host
/// container. /// container.
/// </param> /// </param>
protected virtual void UninitializeForContextCore(LayoutContext context) protected internal virtual void UninitializeForContextCore(LayoutContext context)
{ {
} }
@ -83,7 +59,9 @@ namespace Avalonia.Layout
/// of the allocated sizes for child objects or based on other considerations such as a /// of the allocated sizes for child objects or based on other considerations such as a
/// fixed container size. /// fixed container size.
/// </returns> /// </returns>
protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize); protected internal abstract Size MeasureOverride(
NonVirtualizingLayoutContext context,
Size availableSize);
/// <summary> /// <summary>
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of /// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@ -98,6 +76,8 @@ namespace Avalonia.Layout
/// its children. /// its children.
/// </param> /// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns> /// <returns>The actual size that is used after the element is arranged in layout.</returns>
protected virtual Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize) => finalSize; protected internal virtual Size ArrangeOverride(
NonVirtualizingLayoutContext context,
Size finalSize) => finalSize;
} }
} }

17
src/Avalonia.Layout/NonVirtualizingLayoutContext.cs

@ -3,6 +3,8 @@
// //
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System.Collections.Generic;
namespace Avalonia.Layout namespace Avalonia.Layout
{ {
/// <summary> /// <summary>
@ -10,5 +12,20 @@ namespace Avalonia.Layout
/// </summary> /// </summary>
public abstract class NonVirtualizingLayoutContext : LayoutContext public abstract class NonVirtualizingLayoutContext : LayoutContext
{ {
private VirtualizingLayoutContext _contextAdapter;
/// <summary>
/// Gets the collection of child controls from the container that provides the context.
/// </summary>
public IReadOnlyList<ILayoutable> Children => ChildrenCore;
/// <summary>
/// Implements the behavior for getting the return value of <see cref="Children"/> in a
/// derived or custom <see cref="NonVirtualizingLayoutContext"/>.
/// </summary>
protected abstract IReadOnlyList<ILayoutable> ChildrenCore { get; }
internal VirtualizingLayoutContext GetVirtualizingContextAdapter() =>
_contextAdapter ?? (_contextAdapter = new LayoutContextAdapter(this));
} }
} }

160
src/Avalonia.Layout/NonVirtualizingStackLayout.cs

@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data;
namespace Avalonia.Layout
{
public class NonVirtualizingStackLayout : NonVirtualizingLayout
{
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
StackLayout.OrientationProperty.AddOwner<NonVirtualizingStackLayout>();
/// <summary>
/// Defines the <see cref="Spacing"/> property.
/// </summary>
public static readonly StyledProperty<double> SpacingProperty =
StackLayout.SpacingProperty.AddOwner<NonVirtualizingStackLayout>();
/// <summary>
/// Gets or sets the axis along which items are laid out.
/// </summary>
/// <value>
/// One of the enumeration values that specifies the axis along which items are laid out.
/// The default is Vertical.
/// </value>
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
/// <summary>
/// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the
/// direction of the StackLayout's Orientation.
/// </summary>
public double Spacing
{
get => GetValue(SpacingProperty);
set => SetValue(SpacingProperty, value);
}
protected internal override Size MeasureOverride(
NonVirtualizingLayoutContext context,
Size availableSize)
{
var extentU = 0.0;
var extentV = 0.0;
var childCount = context.Children.Count;
var isVertical = Orientation == Orientation.Vertical;
var spacing = Spacing;
var constraint = isVertical ?
availableSize.WithHeight(double.PositiveInfinity) :
availableSize.WithWidth(double.PositiveInfinity);
for (var i = 0; i < childCount; ++i)
{
var element = context.Children[i];
if (!element.IsVisible)
{
continue;
}
element.Measure(constraint);
if (isVertical)
{
extentU += element.DesiredSize.Height;
extentV = Math.Max(extentV, element.DesiredSize.Width);
}
else
{
extentU += element.DesiredSize.Width;
extentV = Math.Max(extentV, element.DesiredSize.Height);
}
if (i < childCount - 1)
{
extentU += spacing;
}
}
return isVertical ? new Size(extentV, extentU) : new Size(extentU, extentV);
}
protected internal override Size ArrangeOverride(
NonVirtualizingLayoutContext context,
Size finalSize)
{
var u = 0.0;
var childCount = context.Children.Count;
var isVertical = Orientation == Orientation.Vertical;
var spacing = Spacing;
var bounds = new Rect();
for (var i = 0; i < childCount; ++i)
{
var element = context.Children[i];
if (!element.IsVisible)
{
continue;
}
bounds = isVertical ?
LayoutVertical(element, u, finalSize) :
LayoutHorizontal(element, u, finalSize);
element.Arrange(bounds);
u = (isVertical ? bounds.Bottom : bounds.Right) + spacing;
}
return new Size(bounds.Right, bounds.Bottom);
}
private static Rect LayoutVertical(ILayoutable element, double y, Size constraint)
{
var x = 0.0;
var width = element.DesiredSize.Width;
switch (element.HorizontalAlignment)
{
case HorizontalAlignment.Center:
x += (constraint.Width - element.DesiredSize.Width) / 2;
break;
case HorizontalAlignment.Right:
x += constraint.Width - element.DesiredSize.Width;
break;
case HorizontalAlignment.Stretch:
width = constraint.Width;
break;
}
return new Rect(x, y, width, element.DesiredSize.Height);
}
private static Rect LayoutHorizontal(ILayoutable element, double x, Size constraint)
{
var y = 0.0;
var height = element.DesiredSize.Height;
switch (element.VerticalAlignment)
{
case VerticalAlignment.Center:
y += (constraint.Height - element.DesiredSize.Height) / 2;
break;
case VerticalAlignment.Bottom:
y += constraint.Height - element.DesiredSize.Height;
break;
case VerticalAlignment.Stretch:
height = constraint.Height;
break;
}
return new Rect(x, y, element.DesiredSize.Width, height);
}
}
}

8
src/Avalonia.Layout/StackLayout.cs

@ -234,7 +234,7 @@ namespace Avalonia.Layout
return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, }; return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, };
} }
protected override void InitializeForContextCore(VirtualizingLayoutContext context) protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
{ {
var state = context.LayoutState; var state = context.LayoutState;
var stackState = state as StackLayoutState; var stackState = state as StackLayoutState;
@ -254,13 +254,13 @@ namespace Avalonia.Layout
stackState.InitializeForContext(context, this); stackState.InitializeForContext(context, this);
} }
protected override void UninitializeForContextCore(VirtualizingLayoutContext context) protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{ {
var stackState = (StackLayoutState)context.LayoutState; var stackState = (StackLayoutState)context.LayoutState;
stackState.UninitializeForContext(context); stackState.UninitializeForContext(context);
} }
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{ {
var desiredSize = GetFlowAlgorithm(context).Measure( var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize, availableSize,
@ -275,7 +275,7 @@ namespace Avalonia.Layout
return new Size(desiredSize.Width, desiredSize.Height); return new Size(desiredSize.Width, desiredSize.Height);
} }
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{ {
var value = GetFlowAlgorithm(context).Arrange( var value = GetFlowAlgorithm(context).Arrange(
finalSize, finalSize,

8
src/Avalonia.Layout/UniformGridLayout.cs

@ -392,7 +392,7 @@ namespace Avalonia.Layout
{ {
} }
protected override void InitializeForContextCore(VirtualizingLayoutContext context) protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
{ {
var state = context.LayoutState; var state = context.LayoutState;
var gridState = state as UniformGridLayoutState; var gridState = state as UniformGridLayoutState;
@ -412,13 +412,13 @@ namespace Avalonia.Layout
gridState.InitializeForContext(context, this); gridState.InitializeForContext(context, this);
} }
protected override void UninitializeForContextCore(VirtualizingLayoutContext context) protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{ {
var gridState = (UniformGridLayoutState)context.LayoutState; var gridState = (UniformGridLayoutState)context.LayoutState;
gridState.UninitializeForContext(context); gridState.UninitializeForContext(context);
} }
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{ {
// Set the width and height on the grid state. If the user already set them then use the preset. // Set the width and height on the grid state. If the user already set them then use the preset.
// If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items. // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items.
@ -442,7 +442,7 @@ namespace Avalonia.Layout
return new Size(desiredSize.Width, desiredSize.Height); return new Size(desiredSize.Width, desiredSize.Height);
} }
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{ {
var value = GetFlowAlgorithm(context).Arrange( var value = GetFlowAlgorithm(context).Arrange(
finalSize, finalSize,

42
src/Avalonia.Layout/VirtualLayoutContextAdapter.cs

@ -0,0 +1,42 @@
using System.Collections;
using System.Collections.Generic;
namespace Avalonia.Layout
{
public class VirtualLayoutContextAdapter : NonVirtualizingLayoutContext
{
private readonly VirtualizingLayoutContext _virtualizingContext;
private ChildrenCollection _children;
public VirtualLayoutContextAdapter(VirtualizingLayoutContext virtualizingContext)
{
_virtualizingContext = virtualizingContext;
}
protected override object LayoutStateCore
{
get => _virtualizingContext.LayoutState;
set => _virtualizingContext.LayoutState = value;
}
protected override IReadOnlyList<ILayoutable> ChildrenCore =>
_children ?? (_children = new ChildrenCollection(_virtualizingContext));
private class ChildrenCollection : IReadOnlyList<ILayoutable>
{
private readonly VirtualizingLayoutContext _context;
public ChildrenCollection(VirtualizingLayoutContext context) => _context = context;
public ILayoutable this[int index] => _context.GetOrCreateElementAt(index);
public int Count => _context.ItemCount;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<ILayoutable> GetEnumerator()
{
for (var i = 0; i < Count; ++i)
{
yield return this[i];
}
}
}
}
}

36
src/Avalonia.Layout/VirtualizingLayout.cs

@ -19,30 +19,6 @@ namespace Avalonia.Layout
/// </remarks> /// </remarks>
public abstract class VirtualizingLayout : AttachedLayout public abstract class VirtualizingLayout : AttachedLayout
{ {
/// <inheritdoc/>
public sealed override void InitializeForContext(LayoutContext context)
{
InitializeForContextCore((VirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override void UninitializeForContext(LayoutContext context)
{
UninitializeForContextCore((VirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override Size Measure(LayoutContext context, Size availableSize)
{
return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
}
/// <inheritdoc/>
public sealed override Size Arrange(LayoutContext context, Size finalSize)
{
return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
}
/// <summary> /// <summary>
/// Notifies the layout when the data collection assigned to the container element (Items) /// Notifies the layout when the data collection assigned to the container element (Items)
/// has changed. /// has changed.
@ -70,7 +46,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host /// The context object that facilitates communication between the layout and its host
/// container. /// container.
/// </param> /// </param>
protected virtual void InitializeForContextCore(VirtualizingLayoutContext context) protected internal virtual void InitializeForContextCore(VirtualizingLayoutContext context)
{ {
} }
@ -82,7 +58,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host /// The context object that facilitates communication between the layout and its host
/// container. /// container.
/// </param> /// </param>
protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context) protected internal virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
{ {
} }
@ -104,7 +80,9 @@ namespace Avalonia.Layout
/// of the allocated sizes for child objects or based on other considerations such as a /// of the allocated sizes for child objects or based on other considerations such as a
/// fixed container size. /// fixed container size.
/// </returns> /// </returns>
protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize); protected internal abstract Size MeasureOverride(
VirtualizingLayoutContext context,
Size availableSize);
/// <summary> /// <summary>
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of /// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@ -119,7 +97,9 @@ namespace Avalonia.Layout
/// its children. /// its children.
/// </param> /// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns> /// <returns>The actual size that is used after the element is arranged in layout.</returns>
protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize; protected internal virtual Size ArrangeOverride(
VirtualizingLayoutContext context,
Size finalSize) => finalSize;
/// <summary> /// <summary>
/// Notifies the layout when the data collection assigned to the container element (Items) /// Notifies the layout when the data collection assigned to the container element (Items)

5
src/Avalonia.Layout/VirtualizingLayoutContext.cs

@ -43,6 +43,8 @@ namespace Avalonia.Layout
/// </summary> /// </summary>
public abstract class VirtualizingLayoutContext : LayoutContext public abstract class VirtualizingLayoutContext : LayoutContext
{ {
private NonVirtualizingLayoutContext _contextAdapter;
/// <summary> /// <summary>
/// Gets the number of items in the data. /// Gets the number of items in the data.
/// </summary> /// </summary>
@ -186,5 +188,8 @@ namespace Avalonia.Layout
/// </summary> /// </summary>
/// <param name="element">The element to clear.</param> /// <param name="element">The element to clear.</param>
protected abstract void RecycleElementCore(ILayoutable element); protected abstract void RecycleElementCore(ILayoutable element);
internal NonVirtualizingLayoutContext GetNonVirtualizingContextAdapter() =>
_contextAdapter ?? (_contextAdapter = new VirtualLayoutContextAdapter(this));
} }
} }

17
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary> /// </summary>
public class Scene : IDisposable public class Scene : IDisposable
{ {
private Dictionary<IVisual, IVisualNode> _index; private readonly Dictionary<IVisual, IVisualNode> _index;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Scene"/> class. /// Initializes a new instance of the <see cref="Scene"/> class.
@ -83,7 +83,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <returns>The cloned scene.</returns> /// <returns>The cloned scene.</returns>
public Scene CloneScene() public Scene CloneScene()
{ {
var index = new Dictionary<IVisual, IVisualNode>(); var index = new Dictionary<IVisual, IVisualNode>(_index.Count);
var root = Clone((VisualNode)Root, null, index); var root = Clone((VisualNode)Root, null, index);
var result = new Scene(root, index, Layers.Clone(), Generation + 1) var result = new Scene(root, index, Layers.Clone(), Generation + 1)
@ -162,9 +162,18 @@ namespace Avalonia.Rendering.SceneGraph
index.Add(result.Visual, result); index.Add(result.Visual, result);
foreach (var child in source.Children) int childCount = source.Children.Count;
if (childCount > 0)
{ {
result.AddChild(Clone((VisualNode)child, result, index)); Span<IVisualNode> children = result.AddChildrenSpan(childCount);
for (var i = 0; i < childCount; i++)
{
var child = source.Children[i];
children[i] = Clone((VisualNode)child, result, index);
}
} }
return result; return result;

20
src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs

@ -11,16 +11,28 @@ namespace Avalonia.Rendering.SceneGraph
public class SceneLayers : IEnumerable<SceneLayer> public class SceneLayers : IEnumerable<SceneLayer>
{ {
private readonly IVisual _root; private readonly IVisual _root;
private readonly List<SceneLayer> _inner = new List<SceneLayer>(); private readonly List<SceneLayer> _inner;
private readonly Dictionary<IVisual, SceneLayer> _index = new Dictionary<IVisual, SceneLayer>(); private readonly Dictionary<IVisual, SceneLayer> _index;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SceneLayers"/> class. /// Initializes a new instance of the <see cref="SceneLayers"/> class.
/// </summary> /// </summary>
/// <param name="root">The scene's root visual.</param> /// <param name="root">The scene's root visual.</param>
public SceneLayers(IVisual root) public SceneLayers(IVisual root) : this(root, 0)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SceneLayers"/> class.
/// </summary>
/// <param name="root">The scene's root visual.</param>
/// <param name="capacity">Initial layer capacity.</param>
public SceneLayers(IVisual root, int capacity)
{ {
_root = root; _root = root;
_inner = new List<SceneLayer>(capacity);
_index = new Dictionary<IVisual, SceneLayer>(capacity);
} }
/// <summary> /// <summary>
@ -84,7 +96,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <returns>The cloned layers.</returns> /// <returns>The cloned layers.</returns>
public SceneLayers Clone() public SceneLayers Clone()
{ {
var result = new SceneLayers(_root); var result = new SceneLayers(_root, Count);
foreach (var src in _inner) foreach (var src in _inner)
{ {

43
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.Collections.Pooled;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -19,8 +20,8 @@ namespace Avalonia.Rendering.SceneGraph
private Rect? _bounds; private Rect? _bounds;
private double _opacity; private double _opacity;
private List<IVisualNode> _children; private PooledList<IVisualNode> _children;
private List<IRef<IDrawOperation>> _drawOperations; private PooledList<IRef<IDrawOperation>> _drawOperations;
private IRef<IDisposable> _drawOperationsRefCounter; private IRef<IDisposable> _drawOperationsRefCounter;
private bool _drawOperationsCloned; private bool _drawOperationsCloned;
private Matrix transformRestore; private Matrix transformRestore;
@ -349,6 +350,18 @@ namespace Avalonia.Rendering.SceneGraph
context.Transform = transformRestore; context.Transform = transformRestore;
} }
/// <summary>
/// Inserts default constructed children into collection and returns a span for the newly created range.
/// </summary>
/// <param name="count">Count of children that will be added.</param>
/// <returns></returns>
internal Span<IVisualNode> AddChildrenSpan(int count)
{
EnsureChildrenCreated(count);
return _children.AddSpan(count);
}
private Rect CalculateBounds() private Rect CalculateBounds()
{ {
var result = new Rect(); var result = new Rect();
@ -362,11 +375,11 @@ namespace Avalonia.Rendering.SceneGraph
return result; return result;
} }
private void EnsureChildrenCreated() private void EnsureChildrenCreated(int capacity = 0)
{ {
if (_children == null) if (_children == null)
{ {
_children = new List<IVisualNode>(); _children = new PooledList<IVisualNode>(capacity);
} }
} }
@ -377,13 +390,21 @@ namespace Avalonia.Rendering.SceneGraph
{ {
if (_drawOperations == null) if (_drawOperations == null)
{ {
_drawOperations = new List<IRef<IDrawOperation>>(); _drawOperations = new PooledList<IRef<IDrawOperation>>();
_drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
_drawOperationsCloned = false; _drawOperationsCloned = false;
} }
else if (_drawOperationsCloned) else if (_drawOperationsCloned)
{ {
_drawOperations = new List<IRef<IDrawOperation>>(_drawOperations.Select(op => op.Clone())); var oldDrawOperations = _drawOperations;
_drawOperations = new PooledList<IRef<IDrawOperation>>(oldDrawOperations.Count);
foreach (var drawOperation in oldDrawOperations)
{
_drawOperations.Add(drawOperation.Clone());
}
_drawOperationsRefCounter.Dispose(); _drawOperationsRefCounter.Dispose();
_drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
_drawOperationsCloned = false; _drawOperationsCloned = false;
@ -397,14 +418,16 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary> /// </summary>
/// <param name="drawOperations">Draw operations that need to be disposed.</param> /// <param name="drawOperations">Draw operations that need to be disposed.</param>
/// <returns>Disposable for given draw operations.</returns> /// <returns>Disposable for given draw operations.</returns>
private static IDisposable CreateDisposeDrawOperations(List<IRef<IDrawOperation>> drawOperations) private static IDisposable CreateDisposeDrawOperations(PooledList<IRef<IDrawOperation>> drawOperations)
{ {
return Disposable.Create(() => return Disposable.Create(drawOperations, operations =>
{ {
foreach (var operation in drawOperations) foreach (var operation in operations)
{ {
operation.Dispose(); operation.Dispose();
} }
operations.Dispose();
}); });
} }
@ -414,6 +437,8 @@ namespace Avalonia.Rendering.SceneGraph
{ {
_drawOperationsRefCounter?.Dispose(); _drawOperationsRefCounter?.Dispose();
_children?.Dispose();
Disposed = true; Disposed = true;
} }
} }

22
src/Avalonia.X11/X11Window.cs

@ -649,7 +649,27 @@ namespace Avalonia.X11
ScheduleInput(args); ScheduleInput(args);
} }
public void ScheduleInput(RawInputEventArgs args) public void ScheduleXI2Input(RawInputEventArgs args)
{
if (args is RawPointerEventArgs pargs)
{
if ((pargs.Type == RawPointerEventType.TouchBegin
|| pargs.Type == RawPointerEventType.TouchUpdate
|| pargs.Type == RawPointerEventType.LeftButtonDown
|| pargs.Type == RawPointerEventType.RightButtonDown
|| pargs.Type == RawPointerEventType.MiddleButtonDown
|| pargs.Type == RawPointerEventType.NonClientLeftButtonDown)
&& ActivateTransientChildIfNeeded())
return;
if (pargs.Type == RawPointerEventType.TouchEnd
&& ActivateTransientChildIfNeeded())
pargs.Type = RawPointerEventType.TouchCancel;
}
ScheduleInput(args);
}
private void ScheduleInput(RawInputEventArgs args)
{ {
if (args is RawPointerEventArgs mouse) if (args is RawPointerEventArgs mouse)
mouse.Position = mouse.Position / Scaling; mouse.Position = mouse.Position / Scaling;

10
src/Avalonia.X11/XI2Manager.cs

@ -196,7 +196,7 @@ namespace Avalonia.X11
(ev.Type == XiEventType.XI_TouchUpdate ? (ev.Type == XiEventType.XI_TouchUpdate ?
RawPointerEventType.TouchUpdate : RawPointerEventType.TouchUpdate :
RawPointerEventType.TouchEnd); RawPointerEventType.TouchEnd);
client.ScheduleInput(new RawTouchEventArgs(client.TouchDevice, client.ScheduleXI2Input(new RawTouchEventArgs(client.TouchDevice,
ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail)); ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail));
return; return;
} }
@ -230,10 +230,10 @@ namespace Avalonia.X11
} }
if (scrollDelta != default) if (scrollDelta != default)
client.ScheduleInput(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp, client.ScheduleXI2Input(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
client.InputRoot, ev.Position, scrollDelta, ev.Modifiers)); client.InputRoot, ev.Position, scrollDelta, ev.Modifiers));
if (_pointerDevice.HasMotion(ev)) if (_pointerDevice.HasMotion(ev))
client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
RawPointerEventType.Move, ev.Position, ev.Modifiers)); RawPointerEventType.Move, ev.Position, ev.Modifiers));
} }
@ -250,7 +250,7 @@ namespace Avalonia.X11
_ => (RawPointerEventType?)null _ => (RawPointerEventType?)null
}; };
if (type.HasValue) if (type.HasValue)
client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
type.Value, ev.Position, ev.Modifiers)); type.Value, ev.Position, ev.Modifiers));
} }
@ -313,7 +313,7 @@ namespace Avalonia.X11
interface IXI2Client interface IXI2Client
{ {
IInputRoot InputRoot { get; } IInputRoot InputRoot { get; }
void ScheduleInput(RawInputEventArgs args); void ScheduleXI2Input(RawInputEventArgs args);
IMouseDevice MouseDevice { get; } IMouseDevice MouseDevice { get; }
TouchDevice TouchDevice { get; } TouchDevice TouchDevice { get; }
} }

40
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs

@ -104,6 +104,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
} }
} }
if (results != null && result != null)
{
results.Add(result);
}
return results ?? result; return results ?? result;
} }
@ -158,9 +163,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
protected void EmitCall(XamlIlEmitContext context, IXamlIlEmitter codeGen, Func<IXamlIlMethod, bool> method) protected void EmitCall(XamlIlEmitContext context, IXamlIlEmitter codeGen, Func<IXamlIlMethod, bool> method)
{ {
var selectors = context.Configuration.TypeSystem.GetType("Avalonia.Styling.Selectors"); var selectors = context.Configuration.TypeSystem.GetType("Avalonia.Styling.Selectors");
var found = selectors.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && var found = selectors.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && method(m));
m.Parameters[0].FullName == "Avalonia.Styling.Selector"
&& method(m));
codeGen.EmitCall(found); codeGen.EmitCall(found);
} }
} }
@ -308,8 +311,35 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
_selectors.Add(node); _selectors.Add(node);
} }
//TODO: actually find the type public override IXamlIlType TargetType
public override IXamlIlType TargetType => _selectors.FirstOrDefault()?.TargetType; {
get
{
IXamlIlType result = null;
foreach (var selector in _selectors)
{
if (selector.TargetType == null)
{
return null;
}
else if (result == null)
{
result = selector.TargetType;
}
else
{
while (!result.IsAssignableFrom(selector.TargetType))
{
result = result.BaseType;
}
}
}
return result;
}
}
protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen)
{ {
if (_selectors.Count == 0) if (_selectors.Count == 0)

20
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -140,25 +140,17 @@ namespace Avalonia.Skia
public Rect HitTestTextPosition(int index) public Rect HitTestTextPosition(int index)
{ {
if (string.IsNullOrEmpty(Text))
{
var alignmentOffset = TransformX(0, 0, _paint.TextAlign);
return new Rect(alignmentOffset, 0, 0, _lineHeight);
}
var rects = GetRects(); var rects = GetRects();
if (index >= Text.Length || index < 0)
if (index < 0 || index >= rects.Count)
{ {
var r = rects.LastOrDefault(); var r = rects.LastOrDefault();
return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
} }
if (rects.Count == 0)
{
return new Rect(0, 0, 1, _lineHeight);
}
if (index == rects.Count)
{
var lr = rects[rects.Count - 1];
return new Rect(new Point(lr.X + lr.Width, lr.Y), rects[index - 1].Size);
}
return rects[index]; return rects[index];
} }

2
src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs

@ -45,7 +45,7 @@ namespace Avalonia.Win32.Embedding
focused = focused.VisualParent; focused = focused.VisualParent;
if (focused == _root) if (focused == _root)
KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, InputModifiers.None); KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
} }
private void PlatformImpl_LostFocus() private void PlatformImpl_LostFocus()

2
src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs

@ -44,7 +44,7 @@ namespace Avalonia.Win32.Input
public void WindowActivated(Window window) public void WindowActivated(Window window)
{ {
SetFocusedElement(window, NavigationMethod.Unspecified, InputModifiers.None); SetFocusedElement(window, NavigationMethod.Unspecified, KeyModifiers.None);
} }
public string StringFromVirtualKey(uint virtualKey) public string StringFromVirtualKey(uint virtualKey)

7
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -209,16 +209,17 @@ namespace Avalonia.Controls.UnitTests
screenImpl.Setup(x => x.ScreenCount).Returns(1); screenImpl.Setup(x => x.ScreenCount).Returns(1);
screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(1, screen, screen, true) }); screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(1, screen, screen, true) });
popupImpl = MockWindowingPlatform.CreatePopupMock(); var windowImpl = MockWindowingPlatform.CreateWindowMock();
popupImpl = MockWindowingPlatform.CreatePopupMock(windowImpl.Object);
popupImpl.SetupGet(x => x.Scaling).Returns(1); popupImpl.SetupGet(x => x.Scaling).Returns(1);
windowImpl.Setup(x => x.CreatePopup()).Returns(popupImpl.Object);
var windowImpl = MockWindowingPlatform.CreateWindowMock(() => popupImpl.Object);
windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object); windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object);
var services = TestServices.StyledWindow.With( var services = TestServices.StyledWindow.With(
inputManager: new InputManager(), inputManager: new InputManager(),
windowImpl: windowImpl.Object, windowImpl: windowImpl.Object,
windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, () => popupImpl.Object)); windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, x => popupImpl.Object));
return UnitTestApplication.Start(services); return UnitTestApplication.Start(services);
} }

82
tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs

@ -4,6 +4,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Platform;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -172,9 +173,75 @@ namespace Avalonia.Controls.UnitTests.Primitives
} }
} }
private PopupRoot CreateTarget(TopLevel popupParent) [Fact]
public void Child_Should_Be_Measured_With_Infinity()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new ChildControl();
var window = new Window();
var target = CreateTarget(window);
target.Content = child;
target.Show();
Assert.Equal(Size.Infinity, child.MeasureSize);
}
}
[Fact]
public void Child_Should_Be_Measured_With_Width_Height_When_Set()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new ChildControl();
var window = new Window();
var target = CreateTarget(window);
target.Width = 500;
target.Height = 600;
target.Content = child;
target.Show();
Assert.Equal(new Size(500, 600), child.MeasureSize);
}
}
[Fact]
public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size()
{
// Issue #3784.
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window();
var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl);
popupImpl.Setup(x => x.ClientSize).Returns(new Size(400, 480));
var child = new Canvas
{
Width = 400,
Height = 800,
};
var target = CreateTarget(window, popupImpl.Object);
target.Content = child;
target.Show();
Assert.Equal(new Size(400, 480), target.Bounds.Size);
// Issue #3784 causes this to be (0, 160) which makes no sense as Window has no
// parent control to be offset against.
Assert.Equal(new Point(0, 0), target.Bounds.Position);
}
}
private PopupRoot CreateTarget(TopLevel popupParent, IPopupImpl impl = null)
{ {
var result = new PopupRoot(popupParent, popupParent.PlatformImpl.CreatePopup()) impl ??= popupParent.PlatformImpl.CreatePopup();
var result = new PopupRoot(popupParent, impl)
{ {
Template = new FuncControlTemplate<PopupRoot>((parent, scope) => Template = new FuncControlTemplate<PopupRoot>((parent, scope) =>
new ContentPresenter new ContentPresenter
@ -217,5 +284,16 @@ namespace Avalonia.Controls.UnitTests.Primitives
Popup = (Popup)this.GetVisualChildren().Single(); Popup = (Popup)this.GetVisualChildren().Single();
} }
} }
private class ChildControl : Control
{
public Size MeasureSize { get; private set; }
protected override Size MeasureOverride(Size availableSize)
{
MeasureSize = availableSize;
return base.MeasureOverride(availableSize);
}
}
} }
} }

81
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@ -298,13 +298,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
} }
} }
Window PreparedWindow(object content = null)
{
var w = new Window {Content = content};
w.ApplyTemplate();
return w;
}
[Fact] [Fact]
public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit() public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit()
{ {
@ -351,18 +344,88 @@ namespace Avalonia.Controls.UnitTests.Primitives
} }
} }
[Fact]
public void StaysOpen_False_Should_Not_Handle_Closing_Click()
{
using (CreateServices())
{
var window = PreparedWindow();
var target = new Popup()
{
PlacementTarget = window ,
StaysOpen = false,
};
target.Open();
var e = CreatePointerPressedEventArgs(window);
window.RaiseEvent(e);
Assert.False(e.Handled);
}
}
[Fact]
public void Should_Pass_Closing_Click_To_Closed_Event()
{
using (CreateServices())
{
var window = PreparedWindow();
var target = new Popup()
{
PlacementTarget = window,
StaysOpen = false,
};
target.Open();
var press = CreatePointerPressedEventArgs(window);
var raised = 0;
target.Closed += (s, e) =>
{
Assert.Same(press, e.CloseEvent);
++raised;
};
window.RaiseEvent(press);
Assert.Equal(1, raised);
}
}
private IDisposable CreateServices() private IDisposable CreateServices()
{ {
return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform:
new MockWindowingPlatform(null, new MockWindowingPlatform(null,
() => x =>
{ {
if(UsePopupHost) if(UsePopupHost)
return null; return null;
return MockWindowingPlatform.CreatePopupMock().Object; return MockWindowingPlatform.CreatePopupMock(x).Object;
}))); })));
} }
private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source)
{
var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
return new PointerPressedEventArgs(
source,
pointer,
source,
default,
0,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
KeyModifiers.None);
}
private Window PreparedWindow(object content = null)
{
var w = new Window { Content = content };
w.ApplyTemplate();
return w;
}
private static IControl PopupContentControlTemplate(PopupContentControl control, INameScope scope) private static IControl PopupContentControlTemplate(PopupContentControl control, INameScope scope)
{ {
return new Popup return new Popup

18
tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs

@ -1,4 +1,6 @@
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit; using Xunit;
namespace Avalonia.Controls.UnitTests.Shapes namespace Avalonia.Controls.UnitTests.Shapes
@ -12,5 +14,21 @@ namespace Avalonia.Controls.UnitTests.Shapes
target.Measure(Size.Infinity); target.Measure(Size.Infinity);
} }
[Fact]
public void Subscribes_To_Geometry_Changes()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) };
var target = new Path { Data = geometry };
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
geometry.Rect = new Rect(0, 0, 20, 20);
Assert.False(target.IsMeasureValid);
}
} }
} }

75
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -1,10 +1,12 @@
using System; using System;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.UnitTests; using Avalonia.UnitTests;
@ -425,6 +427,42 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Fact]
public void SelectedText_CanClearText()
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
Text = "0123"
};
target.SelectionStart = 1;
target.SelectionEnd = 3;
target.SelectedText = "";
Assert.True(target.Text == "03");
}
}
[Fact]
public void SelectedText_NullClearsText()
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
Text = "0123"
};
target.SelectionStart = 1;
target.SelectionEnd = 3;
target.SelectedText = null;
Assert.True(target.Text == "03");
}
}
[Fact] [Fact]
public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending() public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending()
{ {
@ -518,6 +556,34 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Theory]
[InlineData(Key.X, KeyModifiers.Control)]
[InlineData(Key.Back, KeyModifiers.None)]
[InlineData(Key.Delete, KeyModifiers.None)]
[InlineData(Key.Tab, KeyModifiers.None)]
[InlineData(Key.Enter, KeyModifiers.None)]
public void Keys_Allow_Undo(Key key, KeyModifiers modifiers)
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
Text = "0123",
AcceptsReturn = true,
AcceptsTab = true
};
target.SelectionStart = 1;
target.SelectionEnd = 3;
AvaloniaLocator.CurrentMutable
.Bind<Input.Platform.IClipboard>().ToSingleton<ClipboardStub>();
RaiseKeyEvent(target, key, modifiers);
RaiseKeyEvent(target, Key.Z, KeyModifiers.Control); // undo
Assert.True(target.Text == "0123");
}
}
private static TestServices FocusServices => TestServices.MockThreadingInterface.With( private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(), focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(), keyboardDevice: () => new KeyboardDevice(),
@ -580,5 +646,14 @@ namespace Avalonia.Controls.UnitTests
set { _bar = value; RaisePropertyChanged(); } set { _bar = value; RaisePropertyChanged(); }
} }
} }
private class ClipboardStub : IClipboard // in order to get tests working that use the clipboard
{
public Task<string> GetTextAsync() => Task.FromResult("");
public Task SetTextAsync(string text) => Task.CompletedTask;
public Task ClearAsync() => Task.CompletedTask;
}
} }
} }

29
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -1002,6 +1002,35 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, child2Node.Presenter.Panel.Children.Count); Assert.Equal(1, child2Node.Presenter.Panel.Children.Count);
} }
[Fact]
public void Clearing_TreeView_Items_Clears_Index()
{
// Issue #3551
var tree = CreateTestTreeData();
var target = new TreeView
{
Template = CreateTreeViewTemplate(),
Items = tree,
};
var root = new TestRoot();
root.Child = target;
CreateNodeDataTemplate(target);
ApplyTemplates(target);
var rootNode = tree[0];
var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode);
Assert.NotNull(container);
root.Child = null;
tree.Clear();
Assert.Empty(target.ItemContainerGenerator.Index.Containers);
}
private void ApplyTemplates(TreeView tree) private void ApplyTemplates(TreeView tree)
{ {
tree.ApplyTemplate(); tree.ApplyTemplate();

139
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Layout;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.UnitTests; using Avalonia.UnitTests;
@ -355,6 +356,27 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Fact]
public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var windowImpl = MockWindowingPlatform.CreateWindowMock();
windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450));
var child = new ChildControl();
var target = new Window(windowImpl.Object)
{
SizeToContent = SizeToContent.Manual,
Content = child
};
target.Show();
Assert.Equal(new Size(550, 450), child.MeasureSize);
}
}
[Fact] [Fact]
public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight() public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight()
{ {
@ -375,6 +397,123 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Fact]
public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size()
{
// Issue #3784.
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var windowImpl = MockWindowingPlatform.CreateWindowMock();
var clientSize = new Size(200, 200);
var maxClientSize = new Size(480, 480);
windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(size =>
{
clientSize = size.Constrain(maxClientSize);
windowImpl.Object.Resized?.Invoke(clientSize);
});
windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
var child = new Canvas
{
Width = 400,
Height = 800,
};
var target = new Window(windowImpl.Object)
{
SizeToContent = SizeToContent.WidthAndHeight,
Content = child
};
target.Show();
Assert.Equal(new Size(400, 480), target.Bounds.Size);
// Issue #3784 causes this to be (0, 160) which makes no sense as Window has no
// parent control to be offset against.
Assert.Equal(new Point(0, 0), target.Bounds.Position);
}
}
[Fact]
public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new Canvas
{
Width = 400,
Height = 800,
};
var target = new Window()
{
SizeToContent = SizeToContent.WidthAndHeight,
Content = child
};
target.Show();
Assert.Equal(400, target.Width);
Assert.Equal(800, target.Height);
}
}
[Fact]
public void SizeToContent_Should_Not_Be_Lost_On_Show()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new Canvas
{
Width = 400,
Height = 800,
};
var target = new Window()
{
SizeToContent = SizeToContent.WidthAndHeight,
Content = child
};
target.Show();
Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
}
}
[Fact]
public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new Canvas
{
Width = 400,
Height = 800,
};
var target = new Window()
{
SizeToContent = SizeToContent.WidthAndHeight,
Content = child
};
target.Show();
Assert.Equal(400, target.Width);
Assert.Equal(800, target.Height);
child.Width = 410;
target.LayoutManager.ExecuteLayoutPass();
Assert.Equal(410, target.Width);
Assert.Equal(800, target.Height);
Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
}
}
private IWindowImpl CreateImpl(Mock<IRenderer> renderer) private IWindowImpl CreateImpl(Mock<IRenderer> renderer)
{ {
return Mock.Of<IWindowImpl>(x => return Mock.Of<IWindowImpl>(x =>

4
tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs

@ -35,7 +35,7 @@ namespace Avalonia.Input.UnitTests
target.SetFocusedElement( target.SetFocusedElement(
focused.Object, focused.Object,
NavigationMethod.Unspecified, NavigationMethod.Unspecified,
InputModifiers.None); KeyModifiers.None);
target.ProcessRawEvent( target.ProcessRawEvent(
new RawKeyEventArgs( new RawKeyEventArgs(
@ -75,7 +75,7 @@ namespace Avalonia.Input.UnitTests
target.SetFocusedElement( target.SetFocusedElement(
focused.Object, focused.Object,
NavigationMethod.Unspecified, NavigationMethod.Unspecified,
InputModifiers.None); KeyModifiers.None);
target.ProcessRawEvent( target.ProcessRawEvent(
new RawTextInputEventArgs( new RawTextInputEventArgs(

108
tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs

@ -1,25 +1,12 @@
using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using Moq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Diagnostics;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Shared.PlatformSupport;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Themes.Default; using Avalonia.UnitTests;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Xunit; using Xunit;
using Avalonia.Media;
using System;
using System.Collections.Generic;
using Avalonia.Controls.UnitTests;
using Avalonia.UnitTests;
namespace Avalonia.Layout.UnitTests namespace Avalonia.Layout.UnitTests
{ {
@ -28,10 +15,8 @@ namespace Avalonia.Layout.UnitTests
[Fact] [Fact]
public void Grandchild_Size_Changed() public void Grandchild_Size_Changed()
{ {
using (var context = AvaloniaLocator.EnterScope()) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
RegisterServices();
Border border; Border border;
TextBlock textBlock; TextBlock textBlock;
@ -55,7 +40,6 @@ namespace Avalonia.Layout.UnitTests
}; };
window.Show(); window.Show();
window.LayoutManager.ExecuteInitialLayoutPass(window);
Assert.Equal(new Size(400, 400), border.Bounds.Size); Assert.Equal(new Size(400, 400), border.Bounds.Size);
textBlock.Width = 200; textBlock.Width = 200;
@ -68,10 +52,8 @@ namespace Avalonia.Layout.UnitTests
[Fact] [Fact]
public void Test_ScrollViewer_With_TextBlock() public void Test_ScrollViewer_With_TextBlock()
{ {
using (var context = AvaloniaLocator.EnterScope()) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
RegisterServices();
ScrollViewer scrollViewer; ScrollViewer scrollViewer;
TextBlock textBlock; TextBlock textBlock;
@ -79,7 +61,6 @@ namespace Avalonia.Layout.UnitTests
{ {
Width = 800, Width = 800,
Height = 600, Height = 600,
SizeToContent = SizeToContent.WidthAndHeight,
Content = scrollViewer = new ScrollViewer Content = scrollViewer = new ScrollViewer
{ {
Width = 200, Width = 200,
@ -99,7 +80,6 @@ namespace Avalonia.Layout.UnitTests
window.Resources["ScrollBarThickness"] = 10.0; window.Resources["ScrollBarThickness"] = 10.0;
window.Show(); window.Show();
window.LayoutManager.ExecuteInitialLayoutPass(window);
Assert.Equal(new Size(800, 600), window.Bounds.Size); Assert.Equal(new Size(800, 600), window.Bounds.Size);
Assert.Equal(new Size(200, 200), scrollViewer.Bounds.Size); Assert.Equal(new Size(200, 200), scrollViewer.Bounds.Size);
@ -131,87 +111,5 @@ namespace Avalonia.Layout.UnitTests
{ {
return v.Bounds.Position; return v.Bounds.Position;
} }
class FormattedTextMock : IFormattedTextImpl
{
public FormattedTextMock(string text)
{
Text = text;
}
public Size Constraint { get; set; }
public string Text { get; }
public Rect Bounds => Rect.Empty;
public void Dispose()
{
}
public IEnumerable<FormattedTextLine> GetLines() => new FormattedTextLine[0];
public TextHitTestResult HitTestPoint(Point point) => new TextHitTestResult();
public Rect HitTestTextPosition(int index) => new Rect();
public IEnumerable<Rect> HitTestTextRange(int index, int length) => new Rect[0];
public Size Measure() => Constraint;
}
private void RegisterServices()
{
var globalStyles = new Mock<IGlobalStyles>();
var globalStylesResources = globalStyles.As<IResourceNode>();
var outObj = (object)10;
globalStylesResources.Setup(x => x.TryGetResource("FontSizeNormal", out outObj)).Returns(true);
var renderInterface = new Mock<IPlatformRenderInterface>();
renderInterface.Setup(x =>
x.CreateFormattedText(
It.IsAny<string>(),
It.IsAny<Typeface>(),
It.IsAny<double>(),
It.IsAny<TextAlignment>(),
It.IsAny<TextWrapping>(),
It.IsAny<Size>(),
It.IsAny<IReadOnlyList<FormattedTextStyleSpan>>()))
.Returns(new FormattedTextMock("TEST"));
var streamGeometry = new Mock<IStreamGeometryImpl>();
streamGeometry.Setup(x =>
x.Open())
.Returns(new Mock<IStreamGeometryContextImpl>().Object);
renderInterface.Setup(x =>
x.CreateStreamGeometry())
.Returns(streamGeometry.Object);
var windowImpl = new Mock<IWindowImpl>();
Size clientSize = default(Size);
windowImpl.SetupGet(x => x.ClientSize).Returns(() => clientSize);
windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(s => clientSize = s);
windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1024, 1024));
windowImpl.SetupGet(x => x.Scaling).Returns(1);
AvaloniaLocator.CurrentMutable
.Bind<IStandardCursorFactory>().ToConstant(new CursorFactoryMock())
.Bind<IAssetLoader>().ToConstant(new AssetLoader())
.Bind<IInputManager>().ToConstant(new Mock<IInputManager>().Object)
.Bind<IGlobalStyles>().ToConstant(globalStyles.Object)
.Bind<IRuntimePlatform>().ToConstant(new AppBuilder().RuntimePlatform)
.Bind<IPlatformRenderInterface>().ToConstant(renderInterface.Object)
.Bind<IStyler>().ToConstant(new Styler())
.Bind<IFontManagerImpl>().ToConstant(new MockFontManagerImpl())
.Bind<ITextShaperImpl>().ToConstant(new MockTextShaperImpl())
.Bind<IWindowingPlatform>().ToConstant(new Avalonia.Controls.UnitTests.WindowingPlatformMock(() => windowImpl.Object));
var theme = new DefaultTheme();
globalStyles.Setup(x => x.IsStylesInitialized).Returns(true);
globalStyles.Setup(x => x.Styles).Returns(theme);
}
} }
} }

335
tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs

@ -0,0 +1,335 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Xunit;
namespace Avalonia.Layout.UnitTests
{
public class NonVirtualizingStackLayoutTests
{
[Fact]
public void Lays_Out_Children_Vertically()
{
var target = new NonVirtualizingStackLayout { Orientation = Orientation.Vertical };
var context = CreateContext(new[]
{
new Border { Height = 20, Width = 120 },
new Border { Height = 30 },
new Border { Height = 50 },
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(120, 100), desiredSize);
Assert.Equal(new Size(120, 100), arrangeSize);
Assert.Equal(new Rect(0, 0, 120, 20), context.Children[0].Bounds);
Assert.Equal(new Rect(0, 20, 120, 30), context.Children[1].Bounds);
Assert.Equal(new Rect(0, 50, 120, 50), context.Children[2].Bounds);
}
[Fact]
public void Lays_Out_Children_Horizontally()
{
var target = new NonVirtualizingStackLayout { Orientation = Orientation.Horizontal };
var context = CreateContext(new[]
{
new Border { Width = 20, Height = 120 },
new Border { Width = 30 },
new Border { Width = 50 },
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(100, 120), desiredSize);
Assert.Equal(new Size(100, 120), arrangeSize);
Assert.Equal(new Rect(0, 0, 20, 120), context.Children[0].Bounds);
Assert.Equal(new Rect(20, 0, 30, 120), context.Children[1].Bounds);
Assert.Equal(new Rect(50, 0, 50, 120), context.Children[2].Bounds);
}
[Fact]
public void Lays_Out_Children_Vertically_With_Spacing()
{
var target = new NonVirtualizingStackLayout
{
Orientation = Orientation.Vertical,
Spacing = 10,
};
var context = CreateContext(new[]
{
new Border { Height = 20, Width = 120 },
new Border { Height = 30 },
new Border { Height = 50 },
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(120, 120), desiredSize);
Assert.Equal(new Size(120, 120), arrangeSize);
Assert.Equal(new Rect(0, 0, 120, 20), context.Children[0].Bounds);
Assert.Equal(new Rect(0, 30, 120, 30), context.Children[1].Bounds);
Assert.Equal(new Rect(0, 70, 120, 50), context.Children[2].Bounds);
}
[Fact]
public void Lays_Out_Children_Horizontally_With_Spacing()
{
var target = new NonVirtualizingStackLayout
{
Orientation = Orientation.Horizontal,
Spacing = 10,
};
var context = CreateContext(new[]
{
new Border { Width = 20, Height = 120 },
new Border { Width = 30 },
new Border { Width = 50 },
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(120, 120), desiredSize);
Assert.Equal(new Size(120, 120), arrangeSize);
Assert.Equal(new Rect(0, 0, 20, 120), context.Children[0].Bounds);
Assert.Equal(new Rect(30, 0, 30, 120), context.Children[1].Bounds);
Assert.Equal(new Rect(70, 0, 50, 120), context.Children[2].Bounds);
}
[Fact]
public void Arranges_Vertical_Children_With_Correct_Bounds()
{
var target = new NonVirtualizingStackLayout
{
Orientation = Orientation.Vertical
};
var context = CreateContext(new[]
{
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Left,
MeasureSize = new Size(50, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Left,
MeasureSize = new Size(150, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Center,
MeasureSize = new Size(50, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Center,
MeasureSize = new Size(150, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Right,
MeasureSize = new Size(50, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Right,
MeasureSize = new Size(150, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Stretch,
MeasureSize = new Size(50, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Stretch,
MeasureSize = new Size(150, 10),
},
});
var desiredSize = target.Measure(context, new Size(100, 150));
Assert.Equal(new Size(100, 80), desiredSize);
target.Arrange(context, desiredSize);
var bounds = context.Children.Select(x => x.Bounds).ToArray();
Assert.Equal(
new[]
{
new Rect(0, 0, 50, 10),
new Rect(0, 10, 100, 10),
new Rect(25, 20, 50, 10),
new Rect(0, 30, 100, 10),
new Rect(50, 40, 50, 10),
new Rect(0, 50, 100, 10),
new Rect(0, 60, 100, 10),
new Rect(0, 70, 100, 10),
}, bounds);
}
[Fact]
public void Arranges_Horizontal_Children_With_Correct_Bounds()
{
var target = new NonVirtualizingStackLayout
{
Orientation = Orientation.Horizontal
};
var context = CreateContext(new[]
{
new TestControl
{
VerticalAlignment = VerticalAlignment.Top,
MeasureSize = new Size(10, 50),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Top,
MeasureSize = new Size(10, 150),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Center,
MeasureSize = new Size(10, 50),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Center,
MeasureSize = new Size(10, 150),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Bottom,
MeasureSize = new Size(10, 50),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Bottom,
MeasureSize = new Size(10, 150),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Stretch,
MeasureSize = new Size(10, 50),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Stretch,
MeasureSize = new Size(10, 150),
},
});
var desiredSize = target.Measure(context, new Size(150, 100));
Assert.Equal(new Size(80, 100), desiredSize);
target.Arrange(context, desiredSize);
var bounds = context.Children.Select(x => x.Bounds).ToArray();
Assert.Equal(
new[]
{
new Rect(0, 0, 10, 50),
new Rect(10, 0, 10, 100),
new Rect(20, 25, 10, 50),
new Rect(30, 0, 10, 100),
new Rect(40, 50, 10, 50),
new Rect(50, 0, 10, 100),
new Rect(60, 0, 10, 100),
new Rect(70, 0, 10, 100),
}, bounds);
}
[Theory]
[InlineData(Orientation.Horizontal)]
[InlineData(Orientation.Vertical)]
public void Spacing_Not_Added_For_Invisible_Children(Orientation orientation)
{
var targetThreeChildrenOneInvisble = new NonVirtualizingStackLayout
{
Orientation = orientation,
Spacing = 40,
};
var contextThreeChildrenOneInvisble = CreateContext(new[]
{
new StackPanel { Width = 10, Height= 10, IsVisible = false },
new StackPanel { Width = 10, Height= 10 },
new StackPanel { Width = 10, Height= 10 },
});
var targetTwoChildrenNoneInvisible = new NonVirtualizingStackLayout
{
Spacing = 40,
Orientation = orientation,
};
var contextTwoChildrenNoneInvisible = CreateContext(new[]
{
new StackPanel { Width = 10, Height = 10 },
new StackPanel { Width = 10, Height = 10 }
});
var desiredSize1 = targetThreeChildrenOneInvisble.Measure(contextThreeChildrenOneInvisble, Size.Infinity);
var desiredSize2 = targetTwoChildrenNoneInvisible.Measure(contextTwoChildrenNoneInvisible, Size.Infinity);
Assert.Equal(desiredSize2, desiredSize1);
}
[Theory]
[InlineData(Orientation.Horizontal)]
[InlineData(Orientation.Vertical)]
public void Only_Arrange_Visible_Children(Orientation orientation)
{
var hiddenPanel = new Panel { Width = 10, Height = 10, IsVisible = false };
var panel = new Panel { Width = 10, Height = 10 };
var target = new NonVirtualizingStackLayout
{
Spacing = 40,
Orientation = orientation,
};
var context = CreateContext(new[]
{
hiddenPanel,
panel
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(10, 10), arrangeSize);
}
private NonVirtualizingLayoutContext CreateContext(Control[] children)
{
return new TestLayoutContext(children);
}
private class TestLayoutContext : NonVirtualizingLayoutContext
{
public TestLayoutContext(Control[] children) => ChildrenCore = children;
protected override IReadOnlyList<ILayoutable> ChildrenCore { get; }
}
private class TestControl : Control
{
public Size MeasureConstraint { get; private set; }
public Size MeasureSize { get; set; }
protected override Size MeasureOverride(Size availableSize)
{
MeasureConstraint = availableSize;
return MeasureSize;
}
}
}
}

62
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -275,5 +275,67 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(Colors.Red, ((ISolidColorBrush)notFoo.Background).Color); Assert.Equal(Colors.Red, ((ISolidColorBrush)notFoo.Background).Color);
} }
} }
[Fact]
public void Style_Can_Use_Or_Selector_1()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Border.foo, Border.bar'>
<Setter Property='Background' Value='Red'/>
</Style>
</Window.Styles>
<StackPanel>
<Border Name='foo' Classes='foo'/>
<Border Name='bar' Classes='bar'/>
<Border Name='baz' Classes='baz'/>
</StackPanel>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var foo = window.FindControl<Border>("foo");
var bar = window.FindControl<Border>("bar");
var baz = window.FindControl<Border>("baz");
Assert.Equal(Brushes.Red, foo.Background);
Assert.Equal(Brushes.Red, bar.Background);
Assert.Null(baz.Background);
}
}
[Fact]
public void Style_Can_Use_Or_Selector_2()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Button,Carousel,ListBox'>
<Setter Property='Background' Value='Red'/>
</Style>
</Window.Styles>
<StackPanel>
<Button Name='button'/>
<Carousel Name='carousel'/>
<ListBox Name='listBox'/>
</StackPanel>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
var carousel = window.FindControl<Carousel>("carousel");
var listBox = window.FindControl<ListBox>("listBox");
Assert.Equal(Brushes.Red, button.Background);
Assert.Equal(Brushes.Red, carousel.Background);
Assert.Equal(Brushes.Red, listBox.Background);
}
}
} }
} }

111
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@ -8,65 +8,118 @@ namespace Avalonia.UnitTests
{ {
public class MockWindowingPlatform : IWindowingPlatform public class MockWindowingPlatform : IWindowingPlatform
{ {
private static readonly Size s_screenSize = new Size(1280, 1024);
private readonly Func<IWindowImpl> _windowImpl; private readonly Func<IWindowImpl> _windowImpl;
private readonly Func<IPopupImpl> _popupImpl; private readonly Func<IWindowBaseImpl, IPopupImpl> _popupImpl;
public MockWindowingPlatform(Func<IWindowImpl> windowImpl = null, Func<IPopupImpl> popupImpl = null ) public MockWindowingPlatform(
Func<IWindowImpl> windowImpl = null,
Func<IWindowBaseImpl, IPopupImpl> popupImpl = null )
{ {
_windowImpl = windowImpl; _windowImpl = windowImpl;
_popupImpl = popupImpl; _popupImpl = popupImpl;
} }
public static Mock<IWindowImpl> CreateWindowMock(Func<IPopupImpl> popupImpl = null) public static Mock<IWindowImpl> CreateWindowMock()
{ {
var win = Mock.Of<IWindowImpl>(x => x.Scaling == 1); var windowImpl = new Mock<IWindowImpl>();
var mock = Mock.Get(win); var position = new PixelPoint();
mock.Setup(x => x.Show()).Callback(() => var clientSize = new Size(800, 600);
windowImpl.SetupAllProperties();
windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
windowImpl.Setup(x => x.Scaling).Returns(1);
windowImpl.Setup(x => x.Screen).Returns(CreateScreenMock().Object);
windowImpl.Setup(x => x.Position).Returns(() => position);
SetupToplevel(windowImpl);
windowImpl.Setup(x => x.CreatePopup()).Returns(() =>
{ {
mock.Object.Activated?.Invoke(); return CreatePopupMock(windowImpl.Object).Object;
}); });
mock.Setup(x => x.CreatePopup()).Returns(() =>
windowImpl.Setup(x => x.Dispose()).Callback(() =>
{ {
if (popupImpl != null) windowImpl.Object.Closed?.Invoke();
return popupImpl(); });
return CreatePopupMock().Object;
windowImpl.Setup(x => x.Move(It.IsAny<PixelPoint>())).Callback<PixelPoint>(x =>
{
position = x;
windowImpl.Object.PositionChanged?.Invoke(x);
});
windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(x =>
{
clientSize = x.Constrain(s_screenSize);
windowImpl.Object.Resized?.Invoke(clientSize);
}); });
mock.Setup(x => x.Dispose()).Callback(() =>
windowImpl.Setup(x => x.Show()).Callback(() =>
{ {
mock.Object.Closed?.Invoke(); windowImpl.Object.Activated?.Invoke();
}); });
PixelPoint pos = default;
mock.SetupGet(x => x.Position).Returns(() => pos); return windowImpl;
mock.Setup(x => x.Move(It.IsAny<PixelPoint>())).Callback(new Action<PixelPoint>(np => pos = np));
SetupToplevel(mock);
return mock;
} }
static void SetupToplevel<T>(Mock<T> mock) where T : class, ITopLevelImpl public static Mock<IPopupImpl> CreatePopupMock(IWindowBaseImpl parent)
{ {
mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice()); var popupImpl = new Mock<IPopupImpl>();
var positionerHelper = new ManagedPopupPositionerPopupImplHelper(parent, (pos, size, scale) =>
{
popupImpl.Object.PositionChanged?.Invoke(pos);
popupImpl.Object.Resized?.Invoke(size);
});
var positioner = new ManagedPopupPositioner(positionerHelper);
popupImpl.Setup(x => x.Scaling).Returns(1);
popupImpl.Setup(x => x.PopupPositioner).Returns(positioner);
SetupToplevel(popupImpl);
return popupImpl;
} }
public static Mock<IPopupImpl> CreatePopupMock() public static Mock<IScreenImpl> CreateScreenMock()
{ {
var positioner = Mock.Of<IPopupPositioner>(); var screenImpl = new Mock<IScreenImpl>();
var popup = Mock.Of<IPopupImpl>(x => x.Scaling == 1); var bounds = new PixelRect(0, 0, (int)s_screenSize.Width, (int)s_screenSize.Height);
var mock = Mock.Get(popup); var screen = new Screen(96, bounds, bounds, true);
mock.SetupGet(x => x.PopupPositioner).Returns(positioner); screenImpl.Setup(x => x.AllScreens).Returns(new[] { screen });
SetupToplevel(mock); screenImpl.Setup(x => x.ScreenCount).Returns(1);
return screenImpl;
return mock;
} }
public IWindowImpl CreateWindow() public IWindowImpl CreateWindow()
{ {
return _windowImpl?.Invoke() ?? CreateWindowMock(_popupImpl).Object; if (_windowImpl is object)
{
return _windowImpl();
}
else
{
var mock = CreateWindowMock();
if (_popupImpl is object)
{
mock.Setup(x => x.CreatePopup()).Returns(() => _popupImpl(mock.Object));
}
return mock.Object;
}
} }
public IEmbeddableWindowImpl CreateEmbeddableWindow() public IEmbeddableWindowImpl CreateEmbeddableWindow()
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
private static void SetupToplevel<T>(Mock<T> mock) where T : class, ITopLevelImpl
{
mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice());
}
} }
} }

Loading…
Cancel
Save