Browse Source

Merge branch 'master' into feature/scrollchanged

pull/3729/head
Dariusz Komosiński 6 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:
script: |
brew update
brew install castxml
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb
- task: CmdLine@2
displayName: 'Install Nuke'

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

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

7
src/Avalonia.Controls/AutoCompleteBox.cs

@ -1630,7 +1630,7 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="sender">The source object.</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.
if (IsDropDownOpen)
@ -1638,6 +1638,11 @@ namespace Avalonia.Controls
IsDropDownOpen = false;
}
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
// Fire the DropDownClosed event
if (_popupHasOpened)
{

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

@ -895,12 +895,17 @@ namespace Avalonia.Controls
_ignoreButtonClick = false;
}
}
private void PopUp_Closed(object sender, EventArgs e)
private void PopUp_Closed(object sender, PopupClosedEventArgs e)
{
IsDropDownOpen = false;
if(!_isPopupClosing)
{
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
_isPopupClosing = true;
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 = null;
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
if (CanFocus(this))
{
Focus();

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

@ -95,7 +95,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Raised when the popup closes.
/// </summary>
public event EventHandler? Closed;
public event EventHandler<PopupClosedEventArgs>? Closed;
/// <summary>
/// Raised when the popup opens.
@ -270,7 +270,7 @@ namespace Avalonia.Controls.Primitives
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));
}
@ -306,28 +306,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Closes the popup.
/// </summary>
public void Close()
{
if (_openState is null)
{
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
return;
}
_openState.Dispose();
_openState = null;
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
Closed?.Invoke(this, EventArgs.Empty);
}
public void Close() => CloseCore(null);
/// <summary>
/// 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)
{
var mouse = e as RawPointerEventArgs;
if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
Close();
CloseCore(e);
}
}
private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
{
if (!StaysOpen && !IsChildOrThis((IVisual)e.Source))
if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v))
{
Close();
e.Handled = true;
CloseCore(e);
}
}

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>
/// 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)
protected override sealed Size ArrangeSetBounds(Size size)
{
using (BeginAutoSizing())
{
_positionerParameters.Size = finalSize;
_positionerParameters.Size = size;
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;
namespace Avalonia.Controls.Shapes
@ -10,6 +12,7 @@ namespace Avalonia.Controls.Shapes
static Path()
{
AffectsGeometry<Path>(DataProperty);
DataProperty.Changed.AddClassHandler<Path>((o, e) => o.DataChanged(e));
}
public Geometry Data
@ -19,5 +22,26 @@ namespace Avalonia.Controls.Shapes
}
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(); }
set
{
if (value == null)
_undoRedoHelper.Snapshot();
if (string.IsNullOrEmpty(value))
{
return;
DeleteSelection();
}
_undoRedoHelper.Snapshot();
HandleTextInput(value);
else
{
HandleTextInput(value);
}
_undoRedoHelper.Snapshot();
}
}
@ -471,8 +473,10 @@ namespace Avalonia.Controls
{
if (!IsPasswordBox)
{
_undoRedoHelper.Snapshot();
Copy();
DeleteSelection();
_undoRedoHelper.Snapshot();
}
handled = true;
@ -598,6 +602,7 @@ namespace Avalonia.Controls
break;
case Key.Back:
_undoRedoHelper.Snapshot();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlBackspace();
@ -621,11 +626,13 @@ namespace Avalonia.Controls
CaretIndex -= removedCharacters;
SelectionStart = SelectionEnd = CaretIndex;
}
_undoRedoHelper.Snapshot();
handled = true;
break;
case Key.Delete:
_undoRedoHelper.Snapshot();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlDelete();
@ -647,6 +654,7 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters));
}
_undoRedoHelper.Snapshot();
handled = true;
break;
@ -654,7 +662,9 @@ namespace Avalonia.Controls
case Key.Enter:
if (AcceptsReturn)
{
_undoRedoHelper.Snapshot();
HandleTextInput(NewLine);
_undoRedoHelper.Snapshot();
handled = true;
}
@ -663,7 +673,9 @@ namespace Avalonia.Controls
case Key.Tab:
if (AcceptsTab)
{
_undoRedoHelper.Snapshot();
HandleTextInput("\t");
_undoRedoHelper.Snapshot();
handled = true;
}
else

12
src/Avalonia.Controls/TreeViewItem.cs

@ -51,6 +51,7 @@ namespace Avalonia.Controls
SelectableMixin.Attach<TreeViewItem>(IsSelectedProperty);
FocusableProperty.OverrideDefaultValue<TreeViewItem>(true);
ItemsPanelProperty.OverrideDefaultValue<TreeViewItem>(DefaultPanel);
ParentProperty.Changed.AddClassHandler<TreeViewItem>((o, e) => o.OnParentChanged(e));
RequestBringIntoViewEvent.AddClassHandler<TreeViewItem>((x, e) => x.OnRequestBringIntoView(e));
}
@ -179,5 +180,16 @@ namespace Avalonia.Controls
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
/// </summary>
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/>
Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize;
@ -450,6 +435,19 @@ namespace Avalonia.Controls
EnsureInitialized();
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);
using (BeginAutoSizing())
@ -569,31 +567,30 @@ namespace Avalonia.Controls
}
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
var sizeToContent = SizeToContent;
var clientSize = ClientSize;
var constraint = availableSize;
var constraint = clientSize;
if ((sizeToContent & SizeToContent.Width) != 0)
if (sizeToContent.HasFlagCustom(SizeToContent.Width))
{
constraint = constraint.WithWidth(double.PositiveInfinity);
}
if ((sizeToContent & SizeToContent.Height) != 0)
if (sizeToContent.HasFlagCustom(SizeToContent.Height))
{
constraint = constraint.WithHeight(double.PositiveInfinity);
}
var result = base.MeasureOverride(constraint);
if ((sizeToContent & SizeToContent.Width) == 0)
if (!sizeToContent.HasFlagCustom(SizeToContent.Width))
{
result = result.WithWidth(clientSize.Width);
}
if ((sizeToContent & SizeToContent.Height) == 0)
if (!sizeToContent.HasFlagCustom(SizeToContent.Height))
{
result = result.WithHeight(clientSize.Height);
}
@ -601,6 +598,15 @@ namespace Avalonia.Controls
return result;
}
protected sealed override Size ArrangeSetBounds(Size size)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(size);
return ClientSize;
}
}
protected sealed override void HandleClosed()
{
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>
protected override void HandleResized(Size clientSize)
{
if (!AutoSizing)
{
Width = clientSize.Width;
Height = clientSize.Height;
}
Width = clientSize.Width;
Height = clientSize.Height;
ClientSize = clientSize;
LayoutManager.ExecuteLayoutPass();
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>
/// Handles a window position change notification from
/// <see cref="IWindowBaseImpl.PositionChanged"/>.

14
src/Avalonia.Input/FocusManager.cs

@ -53,11 +53,11 @@ namespace Avalonia.Input
/// </summary>
/// <param name="control">The control to focus.</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(
IInputElement control,
NavigationMethod method = NavigationMethod.Unspecified,
InputModifiers modifiers = InputModifiers.None)
KeyModifiers keyModifiers = KeyModifiers.None)
{
if (control != null)
{
@ -67,7 +67,7 @@ namespace Avalonia.Input
if (scope != null)
{
Scope = scope;
SetFocusedElement(scope, control, method, modifiers);
SetFocusedElement(scope, control, method, keyModifiers);
}
}
else if (Current != null)
@ -95,7 +95,7 @@ namespace Avalonia.Input
/// <param name="scope">The focus scope.</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="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>
/// If the specified scope is the current <see cref="Scope"/> then the keyboard focus
/// will change.
@ -104,7 +104,7 @@ namespace Avalonia.Input
IFocusScope scope,
IInputElement element,
NavigationMethod method = NavigationMethod.Unspecified,
InputModifiers modifiers = InputModifiers.None)
KeyModifiers keyModifiers = KeyModifiers.None)
{
Contract.Requires<ArgumentNullException>(scope != null);
@ -123,7 +123,7 @@ namespace Avalonia.Input
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))
{
Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.InputModifiers);
Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.KeyModifiers);
break;
}

6
src/Avalonia.Input/IFocusManager.cs

@ -20,11 +20,11 @@ namespace Avalonia.Input
/// </summary>
/// <param name="control">The control to focus.</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(
IInputElement control,
IInputElement control,
NavigationMethod method = NavigationMethod.Unspecified,
InputModifiers modifiers = InputModifiers.None);
KeyModifiers keyModifiers = KeyModifiers.None);
/// <summary>
/// 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(
IInputElement element,
NavigationMethod method,
InputModifiers modifiers);
KeyModifiers modifiers);
}
}

6
src/Avalonia.Input/IKeyboardNavigationHandler.cs

@ -19,10 +19,10 @@ namespace Avalonia.Input
/// </summary>
/// <param name="element">The current element.</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(
IInputElement element,
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(
IInputElement element,
NavigationMethod method,
InputModifiers modifiers)
KeyModifiers keyModifiers)
{
if (element != FocusedElement)
{
@ -53,7 +53,7 @@ namespace Avalonia.Input
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = method,
InputModifiers = modifiers,
KeyModifiers = keyModifiers,
});
}
}

8
src/Avalonia.Input/KeyboardNavigationHandler.cs

@ -91,11 +91,11 @@ namespace Avalonia.Input
/// </summary>
/// <param name="element">The current element.</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(
IInputElement element,
NavigationDirection direction,
InputModifiers modifiers = InputModifiers.None)
KeyModifiers keyModifiers = KeyModifiers.None)
{
Contract.Requires<ArgumentNullException>(element != null);
@ -106,7 +106,7 @@ namespace Avalonia.Input
var method = direction == NavigationDirection.Next ||
direction == NavigationDirection.Previous ?
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 ?
NavigationDirection.Next : NavigationDirection.Previous;
Move(current, direction, e.Modifiers);
Move(current, direction, e.KeyModifiers);
e.Handled = true;
}
}

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

@ -63,7 +63,7 @@ namespace Avalonia.Input.Raw
/// <summary>
/// Gets the type of the event.
/// </summary>
public RawPointerEventType Type { get; private set; }
public RawPointerEventType Type { get; set; }
/// <summary>
/// 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
/// this method in a derived class.
/// </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>
/// 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
/// container.
/// </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>
/// 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.
/// </param>
/// <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>
/// 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.
/// </param>
/// <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>
/// Invalidates the measurement state (layout) for all ILayoutable containers that reference
@ -102,5 +166,37 @@ namespace Avalonia.Layout
/// occurs asynchronously.
/// </summary>
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>
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>
/// When overridden in a derived class, initializes any per-container state the layout
/// 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
/// container.
/// </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
/// container.
/// </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
/// fixed container size.
/// </returns>
protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize);
protected internal abstract Size MeasureOverride(
NonVirtualizingLayoutContext context,
Size availableSize);
/// <summary>
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@ -98,6 +76,8 @@ namespace Avalonia.Layout
/// its children.
/// </param>
/// <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.
using System.Collections.Generic;
namespace Avalonia.Layout
{
/// <summary>
@ -10,5 +12,20 @@ namespace Avalonia.Layout
/// </summary>
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, };
}
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
{
var state = context.LayoutState;
var stackState = state as StackLayoutState;
@ -254,13 +254,13 @@ namespace Avalonia.Layout
stackState.InitializeForContext(context, this);
}
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var stackState = (StackLayoutState)context.LayoutState;
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(
availableSize,
@ -275,7 +275,7 @@ namespace Avalonia.Layout
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(
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 gridState = state as UniformGridLayoutState;
@ -412,13 +412,13 @@ namespace Avalonia.Layout
gridState.InitializeForContext(context, this);
}
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var gridState = (UniformGridLayoutState)context.LayoutState;
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.
// 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);
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
var value = GetFlowAlgorithm(context).Arrange(
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>
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>
/// Notifies the layout when the data collection assigned to the container element (Items)
/// has changed.
@ -70,7 +46,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
/// </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
/// container.
/// </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
/// fixed container size.
/// </returns>
protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize);
protected internal abstract Size MeasureOverride(
VirtualizingLayoutContext context,
Size availableSize);
/// <summary>
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@ -119,7 +97,9 @@ namespace Avalonia.Layout
/// its children.
/// </param>
/// <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>
/// 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>
public abstract class VirtualizingLayoutContext : LayoutContext
{
private NonVirtualizingLayoutContext _contextAdapter;
/// <summary>
/// Gets the number of items in the data.
/// </summary>
@ -186,5 +188,8 @@ namespace Avalonia.Layout
/// </summary>
/// <param name="element">The element to clear.</param>
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>
public class Scene : IDisposable
{
private Dictionary<IVisual, IVisualNode> _index;
private readonly Dictionary<IVisual, IVisualNode> _index;
/// <summary>
/// Initializes a new instance of the <see cref="Scene"/> class.
@ -83,7 +83,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <returns>The cloned scene.</returns>
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 result = new Scene(root, index, Layers.Clone(), Generation + 1)
@ -162,9 +162,18 @@ namespace Avalonia.Rendering.SceneGraph
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;

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

@ -11,16 +11,28 @@ namespace Avalonia.Rendering.SceneGraph
public class SceneLayers : IEnumerable<SceneLayer>
{
private readonly IVisual _root;
private readonly List<SceneLayer> _inner = new List<SceneLayer>();
private readonly Dictionary<IVisual, SceneLayer> _index = new Dictionary<IVisual, SceneLayer>();
private readonly List<SceneLayer> _inner;
private readonly Dictionary<IVisual, SceneLayer> _index;
/// <summary>
/// Initializes a new instance of the <see cref="SceneLayers"/> class.
/// </summary>
/// <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;
_inner = new List<SceneLayer>(capacity);
_index = new Dictionary<IVisual, SceneLayer>(capacity);
}
/// <summary>
@ -84,7 +96,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <returns>The cloned layers.</returns>
public SceneLayers Clone()
{
var result = new SceneLayers(_root);
var result = new SceneLayers(_root, Count);
foreach (var src in _inner)
{

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

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Collections.Pooled;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Utilities;
@ -19,8 +20,8 @@ namespace Avalonia.Rendering.SceneGraph
private Rect? _bounds;
private double _opacity;
private List<IVisualNode> _children;
private List<IRef<IDrawOperation>> _drawOperations;
private PooledList<IVisualNode> _children;
private PooledList<IRef<IDrawOperation>> _drawOperations;
private IRef<IDisposable> _drawOperationsRefCounter;
private bool _drawOperationsCloned;
private Matrix transformRestore;
@ -349,6 +350,18 @@ namespace Avalonia.Rendering.SceneGraph
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()
{
var result = new Rect();
@ -362,11 +375,11 @@ namespace Avalonia.Rendering.SceneGraph
return result;
}
private void EnsureChildrenCreated()
private void EnsureChildrenCreated(int capacity = 0)
{
if (_children == null)
{
_children = new List<IVisualNode>();
_children = new PooledList<IVisualNode>(capacity);
}
}
@ -377,13 +390,21 @@ namespace Avalonia.Rendering.SceneGraph
{
if (_drawOperations == null)
{
_drawOperations = new List<IRef<IDrawOperation>>();
_drawOperations = new PooledList<IRef<IDrawOperation>>();
_drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
_drawOperationsCloned = false;
}
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 = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
_drawOperationsCloned = false;
@ -397,14 +418,16 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
/// <param name="drawOperations">Draw operations that need to be disposed.</param>
/// <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();
}
operations.Dispose();
});
}
@ -414,6 +437,8 @@ namespace Avalonia.Rendering.SceneGraph
{
_drawOperationsRefCounter?.Dispose();
_children?.Dispose();
Disposed = true;
}
}

22
src/Avalonia.X11/X11Window.cs

@ -649,7 +649,27 @@ namespace Avalonia.X11
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)
mouse.Position = mouse.Position / Scaling;

10
src/Avalonia.X11/XI2Manager.cs

@ -196,7 +196,7 @@ namespace Avalonia.X11
(ev.Type == XiEventType.XI_TouchUpdate ?
RawPointerEventType.TouchUpdate :
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));
return;
}
@ -230,10 +230,10 @@ namespace Avalonia.X11
}
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));
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));
}
@ -250,7 +250,7 @@ namespace Avalonia.X11
_ => (RawPointerEventType?)null
};
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));
}
@ -313,7 +313,7 @@ namespace Avalonia.X11
interface IXI2Client
{
IInputRoot InputRoot { get; }
void ScheduleInput(RawInputEventArgs args);
void ScheduleXI2Input(RawInputEventArgs args);
IMouseDevice MouseDevice { 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;
}
@ -158,9 +163,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
protected void EmitCall(XamlIlEmitContext context, IXamlIlEmitter codeGen, Func<IXamlIlMethod, bool> method)
{
var selectors = context.Configuration.TypeSystem.GetType("Avalonia.Styling.Selectors");
var found = selectors.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 &&
m.Parameters[0].FullName == "Avalonia.Styling.Selector"
&& method(m));
var found = selectors.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && method(m));
codeGen.EmitCall(found);
}
}
@ -308,8 +311,35 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
_selectors.Add(node);
}
//TODO: actually find the type
public override IXamlIlType TargetType => _selectors.FirstOrDefault()?.TargetType;
public override IXamlIlType 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)
{
if (_selectors.Count == 0)

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

@ -140,25 +140,17 @@ namespace Avalonia.Skia
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();
if (index < 0 || index >= rects.Count)
if (index >= Text.Length || index < 0)
{
var r = rects.LastOrDefault();
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];
}

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

@ -45,7 +45,7 @@ namespace Avalonia.Win32.Embedding
focused = focused.VisualParent;
if (focused == _root)
KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, InputModifiers.None);
KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
}
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)
{
SetFocusedElement(window, NavigationMethod.Unspecified, InputModifiers.None);
SetFocusedElement(window, NavigationMethod.Unspecified, KeyModifiers.None);
}
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.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);
windowImpl.Setup(x => x.CreatePopup()).Returns(popupImpl.Object);
var windowImpl = MockWindowingPlatform.CreateWindowMock(() => popupImpl.Object);
windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object);
var services = TestServices.StyledWindow.With(
inputManager: new InputManager(),
windowImpl: windowImpl.Object,
windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, () => popupImpl.Object));
windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, x => popupImpl.Object));
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.Templates;
using Avalonia.LogicalTree;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.UnitTests;
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) =>
new ContentPresenter
@ -217,5 +284,16 @@ namespace Avalonia.Controls.UnitTests.Primitives
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]
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()
{
return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform:
new MockWindowingPlatform(null,
() =>
x =>
{
if(UsePopupHost)
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)
{
return new Popup

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

@ -1,4 +1,6 @@
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests.Shapes
@ -12,5 +14,21 @@ namespace Avalonia.Controls.UnitTests.Shapes
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.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Media;
using Avalonia.Platform;
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]
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(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
@ -580,5 +646,14 @@ namespace Avalonia.Controls.UnitTests
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);
}
[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)
{
tree.ApplyTemplate();

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

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Layout;
using Avalonia.Platform;
using Avalonia.Rendering;
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]
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)
{
return Mock.Of<IWindowImpl>(x =>

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

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

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

@ -1,25 +1,12 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using Moq;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
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.Themes.Default;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
using Avalonia.Media;
using System;
using System.Collections.Generic;
using Avalonia.Controls.UnitTests;
using Avalonia.UnitTests;
namespace Avalonia.Layout.UnitTests
{
@ -28,10 +15,8 @@ namespace Avalonia.Layout.UnitTests
[Fact]
public void Grandchild_Size_Changed()
{
using (var context = AvaloniaLocator.EnterScope())
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
RegisterServices();
Border border;
TextBlock textBlock;
@ -55,7 +40,6 @@ namespace Avalonia.Layout.UnitTests
};
window.Show();
window.LayoutManager.ExecuteInitialLayoutPass(window);
Assert.Equal(new Size(400, 400), border.Bounds.Size);
textBlock.Width = 200;
@ -68,10 +52,8 @@ namespace Avalonia.Layout.UnitTests
[Fact]
public void Test_ScrollViewer_With_TextBlock()
{
using (var context = AvaloniaLocator.EnterScope())
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
RegisterServices();
ScrollViewer scrollViewer;
TextBlock textBlock;
@ -79,7 +61,6 @@ namespace Avalonia.Layout.UnitTests
{
Width = 800,
Height = 600,
SizeToContent = SizeToContent.WidthAndHeight,
Content = scrollViewer = new ScrollViewer
{
Width = 200,
@ -99,7 +80,6 @@ namespace Avalonia.Layout.UnitTests
window.Resources["ScrollBarThickness"] = 10.0;
window.Show();
window.LayoutManager.ExecuteInitialLayoutPass(window);
Assert.Equal(new Size(800, 600), window.Bounds.Size);
Assert.Equal(new Size(200, 200), scrollViewer.Bounds.Size);
@ -131,87 +111,5 @@ namespace Avalonia.Layout.UnitTests
{
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);
}
}
[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
{
private static readonly Size s_screenSize = new Size(1280, 1024);
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;
_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 mock = Mock.Get(win);
mock.Setup(x => x.Show()).Callback(() =>
var windowImpl = new Mock<IWindowImpl>();
var position = new PixelPoint();
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)
return popupImpl();
return CreatePopupMock().Object;
windowImpl.Object.Closed?.Invoke();
});
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);
mock.Setup(x => x.Move(It.IsAny<PixelPoint>())).Callback(new Action<PixelPoint>(np => pos = np));
SetupToplevel(mock);
return mock;
return windowImpl;
}
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 popup = Mock.Of<IPopupImpl>(x => x.Scaling == 1);
var mock = Mock.Get(popup);
mock.SetupGet(x => x.PopupPositioner).Returns(positioner);
SetupToplevel(mock);
return mock;
var screenImpl = new Mock<IScreenImpl>();
var bounds = new PixelRect(0, 0, (int)s_screenSize.Width, (int)s_screenSize.Height);
var screen = new Screen(96, bounds, bounds, true);
screenImpl.Setup(x => x.AllScreens).Returns(new[] { screen });
screenImpl.Setup(x => x.ScreenCount).Returns(1);
return screenImpl;
}
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()
{
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