diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index b70e0bf77f..54645e461e 100644
--- a/azure-pipelines.yml
+++ b/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'
diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs
index bf177d64cd..9bc7ba9e2f 100644
--- a/src/Avalonia.Controls/AutoCompleteBox.cs
+++ b/src/Avalonia.Controls/AutoCompleteBox.cs
@@ -1630,7 +1630,7 @@ namespace Avalonia.Controls
///
/// The source object.
/// The event data.
- 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)
{
diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs
index 07e42c64e4..b4e4ad1452 100644
--- a/src/Avalonia.Controls/Calendar/DatePicker.cs
+++ b/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);
}
diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs
index 4b7d931d80..1daa6a5630 100644
--- a/src/Avalonia.Controls/ComboBox.cs
+++ b/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();
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index f069903e05..66f2153b6c 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -95,7 +95,7 @@ namespace Avalonia.Controls.Primitives
///
/// Raised when the popup closes.
///
- public event EventHandler? Closed;
+ public event EventHandler? Closed;
///
/// Raised when the popup opens.
@@ -270,7 +270,7 @@ namespace Avalonia.Controls.Primitives
if (parentPopupRoot?.Parent is Popup popup)
{
- DeferCleanup(SubscribeToEventHandler(popup, ParentClosed,
+ DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed,
(x, handler) => x.Closed += handler,
(x, handler) => x.Closed -= handler));
}
@@ -306,28 +306,7 @@ namespace Avalonia.Controls.Primitives
///
/// Closes the popup.
///
- 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);
///
/// 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);
}
}
diff --git a/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs b/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs
new file mode 100644
index 0000000000..c51543438c
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs
@@ -0,0 +1,33 @@
+using System;
+using Avalonia.Interactivity;
+
+#nullable enable
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ /// Holds data for the event.
+ ///
+ public class PopupClosedEventArgs : EventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ public PopupClosedEventArgs(EventArgs? closeEvent)
+ {
+ CloseEvent = closeEvent;
+ }
+
+ ///
+ /// Gets the event that closed the popup, if any.
+ ///
+ ///
+ /// If 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.
+ ///
+ public EventArgs? CloseEvent { get; }
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs
index 4c84d32637..788fe03162 100644
--- a/src/Avalonia.Controls/Primitives/PopupRoot.cs
+++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs
@@ -117,20 +117,41 @@ namespace Avalonia.Controls.Primitives
});
}
- ///
- /// Carries out the arrange pass of the window.
- ///
- /// The final window size.
- /// The parameter unchanged.
- protected override Size ArrangeOverride(Size finalSize)
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ var measured = base.MeasureOverride(availableSize);
+ var width = measured.Width;
+ var height = measured.Height;
+ var widthCache = Width;
+ var heightCache = Height;
+
+ if (!double.IsNaN(widthCache))
+ {
+ width = widthCache;
+ }
+
+ width = Math.Min(width, MaxWidth);
+ width = Math.Max(width, MinWidth);
+
+ if (!double.IsNaN(heightCache))
+ {
+ height = heightCache;
+ }
+
+ height = Math.Min(height, MaxHeight);
+ height = Math.Max(height, MinHeight);
+
+ return new Size(width, height);
+ }
+
+ 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));
}
}
}
diff --git a/src/Avalonia.Controls/ScrollChangedEventArgs.cs b/src/Avalonia.Controls/ScrollChangedEventArgs.cs
new file mode 100644
index 0000000000..fed23964f5
--- /dev/null
+++ b/src/Avalonia.Controls/ScrollChangedEventArgs.cs
@@ -0,0 +1,45 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Describes a change in scrolling state.
+ ///
+ public class ScrollChangedEventArgs : RoutedEventArgs
+ {
+ public ScrollChangedEventArgs(
+ Vector extentDelta,
+ Vector offsetDelta,
+ Vector viewportDelta)
+ : this(ScrollViewer.ScrollChangedEvent, extentDelta, offsetDelta, viewportDelta)
+ {
+ }
+
+ public ScrollChangedEventArgs(
+ RoutedEvent routedEvent,
+ Vector extentDelta,
+ Vector offsetDelta,
+ Vector viewportDelta)
+ : base(routedEvent)
+ {
+ ExtentDelta = extentDelta;
+ OffsetDelta = offsetDelta;
+ ViewportDelta = viewportDelta;
+ }
+
+ ///
+ /// Gets the change to the value of .
+ ///
+ public Vector ExtentDelta { get; }
+
+ ///
+ /// Gets the change to the value of .
+ ///
+ public Vector OffsetDelta { get; }
+
+ ///
+ /// Gets the change to the value of .
+ ///
+ public Vector ViewportDelta { get; }
+ }
+}
diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs
index 8bfcc1f9db..c3f0dc0056 100644
--- a/src/Avalonia.Controls/ScrollViewer.cs
+++ b/src/Avalonia.Controls/ScrollViewer.cs
@@ -2,6 +2,7 @@ using System;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
+using Avalonia.Interactivity;
namespace Avalonia.Controls
{
@@ -165,6 +166,14 @@ namespace Avalonia.Controls
nameof(VerticalScrollBarVisibility),
ScrollBarVisibility.Auto);
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent ScrollChangedEvent =
+ RoutedEvent.Register(
+ nameof(ScrollChanged),
+ RoutingStrategies.Bubble);
+
internal const double DefaultSmallChange = 16;
private IDisposable _childSubscription;
@@ -191,6 +200,15 @@ namespace Avalonia.Controls
{
}
+ ///
+ /// Occurs when changes are detected to the scroll position, extent, or viewport size.
+ ///
+ public event EventHandler ScrollChanged
+ {
+ add => AddHandler(ScrollChangedEvent, value);
+ remove => RemoveHandler(ScrollChangedEvent, value);
+ }
+
///
/// Gets the extent of the scrollable content.
///
@@ -203,9 +221,11 @@ namespace Avalonia.Controls
private set
{
+ var old = _extent;
+
if (SetAndRaise(ExtentProperty, ref _extent, value))
{
- CalculatedPropertiesChanged();
+ CalculatedPropertiesChanged(extentDelta: value - old);
}
}
}
@@ -222,11 +242,13 @@ namespace Avalonia.Controls
set
{
+ var old = _offset;
+
value = ValidateOffset(this, value);
if (SetAndRaise(OffsetProperty, ref _offset, value))
{
- CalculatedPropertiesChanged();
+ CalculatedPropertiesChanged(offsetDelta: value - old);
}
}
}
@@ -243,9 +265,11 @@ namespace Avalonia.Controls
private set
{
+ var old = _viewport;
+
if (SetAndRaise(ViewportProperty, ref _viewport, value))
{
- CalculatedPropertiesChanged();
+ CalculatedPropertiesChanged(viewportDelta: value - old);
}
}
}
@@ -525,7 +549,10 @@ namespace Avalonia.Controls
}
}
- private void CalculatedPropertiesChanged()
+ private void CalculatedPropertiesChanged(
+ Size extentDelta = default,
+ Vector offsetDelta = default,
+ Size viewportDelta = default)
{
// Pass old values of 0 here because we don't have the old values at this point,
// and it shouldn't matter as only the template uses these properies.
@@ -546,6 +573,20 @@ namespace Avalonia.Controls
SetAndRaise(SmallChangeProperty, ref _smallChange, new Size(DefaultSmallChange, DefaultSmallChange));
SetAndRaise(LargeChangeProperty, ref _largeChange, Viewport);
}
+
+ if (extentDelta != default || offsetDelta != default || viewportDelta != default)
+ {
+ using var route = BuildEventRoute(ScrollChangedEvent);
+
+ if (route.HasHandlers)
+ {
+ var e = new ScrollChangedEventArgs(
+ new Vector(extentDelta.Width, extentDelta.Height),
+ offsetDelta,
+ new Vector(viewportDelta.Width, viewportDelta.Height));
+ route.RaiseEvent(this, e);
+ }
+ }
}
protected override void OnKeyDown(KeyEventArgs e)
diff --git a/src/Avalonia.Controls/Shapes/Path.cs b/src/Avalonia.Controls/Shapes/Path.cs
index 84c3ededa5..3fd84c0c7b 100644
--- a/src/Avalonia.Controls/Shapes/Path.cs
+++ b/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(DataProperty);
+ DataProperty.Changed.AddClassHandler((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();
+ }
}
}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 680d7e7d2b..06624c555f 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -473,8 +473,10 @@ namespace Avalonia.Controls
{
if (!IsPasswordBox)
{
+ _undoRedoHelper.Snapshot();
Copy();
DeleteSelection();
+ _undoRedoHelper.Snapshot();
}
handled = true;
@@ -600,6 +602,7 @@ namespace Avalonia.Controls
break;
case Key.Back:
+ _undoRedoHelper.Snapshot();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlBackspace();
@@ -623,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();
@@ -649,6 +654,7 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters));
}
+ _undoRedoHelper.Snapshot();
handled = true;
break;
@@ -656,7 +662,9 @@ namespace Avalonia.Controls
case Key.Enter:
if (AcceptsReturn)
{
+ _undoRedoHelper.Snapshot();
HandleTextInput(NewLine);
+ _undoRedoHelper.Snapshot();
handled = true;
}
@@ -665,7 +673,9 @@ namespace Avalonia.Controls
case Key.Tab:
if (AcceptsTab)
{
+ _undoRedoHelper.Snapshot();
HandleTextInput("\t");
+ _undoRedoHelper.Snapshot();
handled = true;
}
else
diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs
index 387bf0adb8..b21f18a59d 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -313,22 +313,7 @@ namespace Avalonia.Controls
/// Should be called from left mouse button press event handler
///
public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e);
-
- ///
- /// Carries out the arrange pass of the window.
- ///
- /// The final window size.
- /// The parameter unchanged.
- protected override Size ArrangeOverride(Size finalSize)
- {
- using (BeginAutoSizing())
- {
- PlatformImpl?.Resize(finalSize);
- }
- return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
- }
-
///
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,38 +567,60 @@ namespace Avalonia.Controls
}
}
- ///
protected override Size MeasureOverride(Size availableSize)
{
var sizeToContent = SizeToContent;
- var clientSize = ClientSize;
var constraint = availableSize;
+ var clientSize = 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 (!double.IsInfinity(availableSize.Width))
+ {
+ result = result.WithWidth(availableSize.Width);
+ }
+ else
+ {
+ result = result.WithWidth(clientSize.Width);
+ }
}
- if ((sizeToContent & SizeToContent.Height) == 0)
+ if (!sizeToContent.HasFlagCustom(SizeToContent.Height))
{
- result = result.WithHeight(clientSize.Height);
+ if (!double.IsInfinity(availableSize.Height))
+ {
+ result = result.WithHeight(availableSize.Height);
+ }
+ else
+ {
+ result = result.WithHeight(clientSize.Height);
+ }
}
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));
@@ -616,6 +636,9 @@ namespace Avalonia.Controls
SizeToContent = SizeToContent.Manual;
}
+ Width = clientSize.Width;
+ Height = clientSize.Height;
+
base.HandleResized(clientSize);
}
diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs
index 63eabb32f4..bb63d1b353 100644
--- a/src/Avalonia.Controls/WindowBase.cs
+++ b/src/Avalonia.Controls/WindowBase.cs
@@ -224,16 +224,54 @@ namespace Avalonia.Controls
/// The new client size.
protected override void HandleResized(Size clientSize)
{
- if (!AutoSizing)
- {
- Width = clientSize.Width;
- Height = clientSize.Height;
- }
ClientSize = clientSize;
LayoutManager.ExecuteLayoutPass();
Renderer?.Resized(clientSize);
}
+ ///
+ /// Overrides the core measure logic for windows.
+ ///
+ /// The available size.
+ /// The measured size.
+ ///
+ /// 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.
+ ///
+ protected override Size MeasureCore(Size availableSize)
+ {
+ ApplyStyling();
+ ApplyTemplate();
+
+ var constraint = LayoutHelper.ApplyLayoutConstraints(this, availableSize);
+
+ return MeasureOverride(constraint);
+ }
+
+ ///
+ /// Overrides the core arrange logic for windows.
+ ///
+ /// The final arrange rect.
+ ///
+ /// 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.
+ ///
+ protected override void ArrangeCore(Rect finalRect)
+ {
+ var constraint = ArrangeSetBounds(finalRect.Size);
+ var arrangeSize = ArrangeOverride(constraint);
+ Bounds = new Rect(arrangeSize);
+ }
+
+ ///
+ /// Called durung the arrange pass to set the size of the window.
+ ///
+ /// The requested size of the window.
+ /// The actual size of the window.
+ protected virtual Size ArrangeSetBounds(Size size) => size;
+
///
/// Handles a window position change notification from
/// .
diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/ManagedFileChooser.xaml
index af0c91e7bd..8a0451f982 100644
--- a/src/Avalonia.Dialogs/ManagedFileChooser.xaml
+++ b/src/Avalonia.Dialogs/ManagedFileChooser.xaml
@@ -58,7 +58,7 @@
diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs
index bbd5515da0..62a1dd5d84 100644
--- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs
+++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs
@@ -63,7 +63,7 @@ namespace Avalonia.Input.Raw
///
/// Gets the type of the event.
///
- public RawPointerEventType Type { get; private set; }
+ public RawPointerEventType Type { get; set; }
///
/// Gets the input modifiers.
diff --git a/src/Avalonia.Layout/AttachedLayout.cs b/src/Avalonia.Layout/AttachedLayout.cs
index 5622731a7c..d22566442a 100644
--- a/src/Avalonia.Layout/AttachedLayout.cs
+++ b/src/Avalonia.Layout/AttachedLayout.cs
@@ -46,7 +46,23 @@ namespace Avalonia.Layout
/// to provide the behavior for
/// this method in a derived class.
///
- 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();
+ }
+ }
///
/// 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.
///
- 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();
+ }
+ }
///
/// 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.
///
///
- 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();
+ }
+ }
///
/// 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 actual size that is used after the element is arranged in layout.
- 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();
+ }
+ }
///
/// Invalidates the measurement state (layout) for all ILayoutable containers that reference
@@ -102,5 +166,37 @@ namespace Avalonia.Layout
/// occurs asynchronously.
///
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();
+ }
+ }
}
}
diff --git a/src/Avalonia.Layout/LayoutContextAdapter.cs b/src/Avalonia.Layout/LayoutContextAdapter.cs
new file mode 100644
index 0000000000..695866df94
--- /dev/null
+++ b/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) { }
+ }
+}
diff --git a/src/Avalonia.Layout/LayoutHelper.cs b/src/Avalonia.Layout/LayoutHelper.cs
index af7d8ee52e..2b61de00a7 100644
--- a/src/Avalonia.Layout/LayoutHelper.cs
+++ b/src/Avalonia.Layout/LayoutHelper.cs
@@ -1,4 +1,5 @@
using System;
+using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Layout
@@ -19,16 +20,11 @@ namespace Avalonia.Layout
/// The control's size.
public static Size ApplyLayoutConstraints(ILayoutable control, Size constraints)
{
- var controlWidth = control.Width;
- var controlHeight = control.Height;
-
- double width = (controlWidth > 0) ? controlWidth : constraints.Width;
- double height = (controlHeight > 0) ? controlHeight : constraints.Height;
- width = Math.Min(width, control.MaxWidth);
- width = Math.Max(width, control.MinWidth);
- height = Math.Min(height, control.MaxHeight);
- height = Math.Max(height, control.MinHeight);
- return new Size(width, height);
+ var minmax = new MinMax(control);
+
+ return new Size(
+ MathUtilities.Clamp(constraints.Width, minmax.MinWidth, minmax.MaxWidth),
+ MathUtilities.Clamp(constraints.Height, minmax.MinHeight, minmax.MaxHeight));
}
public static Size MeasureChild(ILayoutable control, Size availableSize, Thickness padding,
@@ -85,5 +81,39 @@ namespace Avalonia.Layout
InnerInvalidateMeasure(control);
}
+
+ ///
+ /// Calculates the min and max height for a control. Ported from WPF.
+ ///
+ private readonly struct MinMax
+ {
+ public MinMax(ILayoutable e)
+ {
+ MaxHeight = e.MaxHeight;
+ MinHeight = e.MinHeight;
+ double l = e.Height;
+
+ double height = (double.IsNaN(l) ? double.PositiveInfinity : l);
+ MaxHeight = Math.Max(Math.Min(height, MaxHeight), MinHeight);
+
+ height = (double.IsNaN(l) ? 0 : l);
+ MinHeight = Math.Max(Math.Min(MaxHeight, height), MinHeight);
+
+ MaxWidth = e.MaxWidth;
+ MinWidth = e.MinWidth;
+ l = e.Width;
+
+ double width = (double.IsNaN(l) ? double.PositiveInfinity : l);
+ MaxWidth = Math.Max(Math.Min(width, MaxWidth), MinWidth);
+
+ width = (double.IsNaN(l) ? 0 : l);
+ MinWidth = Math.Max(Math.Min(MaxWidth, width), MinWidth);
+ }
+
+ public double MinWidth { get; }
+ public double MaxWidth { get; }
+ public double MinHeight { get; }
+ public double MaxHeight { get; }
+ }
}
}
diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs
index 5d27ba9199..fb6b0dd4c9 100644
--- a/src/Avalonia.Layout/NonVirtualizingLayout.cs
+++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs
@@ -17,30 +17,6 @@ namespace Avalonia.Layout
///
public abstract class NonVirtualizingLayout : AttachedLayout
{
- ///
- public sealed override void InitializeForContext(LayoutContext context)
- {
- InitializeForContextCore((NonVirtualizingLayoutContext)context);
- }
-
- ///
- public sealed override void UninitializeForContext(LayoutContext context)
- {
- UninitializeForContextCore((NonVirtualizingLayoutContext)context);
- }
-
- ///
- public sealed override Size Measure(LayoutContext context, Size availableSize)
- {
- return MeasureOverride((NonVirtualizingLayoutContext)context, availableSize);
- }
-
- ///
- public sealed override Size Arrange(LayoutContext context, Size finalSize)
- {
- return ArrangeOverride((NonVirtualizingLayoutContext)context, finalSize);
- }
-
///
/// 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.
///
- 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.
///
- 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.
///
- protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize);
+ protected internal abstract Size MeasureOverride(
+ NonVirtualizingLayoutContext context,
+ Size availableSize);
///
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@@ -98,6 +76,8 @@ namespace Avalonia.Layout
/// its children.
///
/// The actual size that is used after the element is arranged in layout.
- protected virtual Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize) => finalSize;
+ protected internal virtual Size ArrangeOverride(
+ NonVirtualizingLayoutContext context,
+ Size finalSize) => finalSize;
}
}
diff --git a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs
index d3dec83e9b..cef551f32e 100644
--- a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs
+++ b/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
{
///
@@ -10,5 +12,20 @@ namespace Avalonia.Layout
///
public abstract class NonVirtualizingLayoutContext : LayoutContext
{
+ private VirtualizingLayoutContext _contextAdapter;
+
+ ///
+ /// Gets the collection of child controls from the container that provides the context.
+ ///
+ public IReadOnlyList Children => ChildrenCore;
+
+ ///
+ /// Implements the behavior for getting the return value of in a
+ /// derived or custom .
+ ///
+ protected abstract IReadOnlyList ChildrenCore { get; }
+
+ internal VirtualizingLayoutContext GetVirtualizingContextAdapter() =>
+ _contextAdapter ?? (_contextAdapter = new LayoutContextAdapter(this));
}
}
diff --git a/src/Avalonia.Layout/NonVirtualizingStackLayout.cs b/src/Avalonia.Layout/NonVirtualizingStackLayout.cs
new file mode 100644
index 0000000000..0b730315e1
--- /dev/null
+++ b/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
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty OrientationProperty =
+ StackLayout.OrientationProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty SpacingProperty =
+ StackLayout.SpacingProperty.AddOwner();
+
+ ///
+ /// Gets or sets the axis along which items are laid out.
+ ///
+ ///
+ /// One of the enumeration values that specifies the axis along which items are laid out.
+ /// The default is Vertical.
+ ///
+ public Orientation Orientation
+ {
+ get => GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ ///
+ /// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the
+ /// direction of the StackLayout's Orientation.
+ ///
+ 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);
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs
index e8ad49e9b9..9b8eb4814e 100644
--- a/src/Avalonia.Layout/StackLayout.cs
+++ b/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,
diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs
index 54c3ccbb90..ee9cff4a01 100644
--- a/src/Avalonia.Layout/UniformGridLayout.cs
+++ b/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,
diff --git a/src/Avalonia.Layout/VirtualLayoutContextAdapter.cs b/src/Avalonia.Layout/VirtualLayoutContextAdapter.cs
new file mode 100644
index 0000000000..80ccee2114
--- /dev/null
+++ b/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 ChildrenCore =>
+ _children ?? (_children = new ChildrenCollection(_virtualizingContext));
+
+ private class ChildrenCollection : IReadOnlyList
+ {
+ 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 GetEnumerator()
+ {
+ for (var i = 0; i < Count; ++i)
+ {
+ yield return this[i];
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/VirtualizingLayout.cs b/src/Avalonia.Layout/VirtualizingLayout.cs
index 4c601175f3..15c7749dfe 100644
--- a/src/Avalonia.Layout/VirtualizingLayout.cs
+++ b/src/Avalonia.Layout/VirtualizingLayout.cs
@@ -19,30 +19,6 @@ namespace Avalonia.Layout
///
public abstract class VirtualizingLayout : AttachedLayout
{
- ///
- public sealed override void InitializeForContext(LayoutContext context)
- {
- InitializeForContextCore((VirtualizingLayoutContext)context);
- }
-
- ///
- public sealed override void UninitializeForContext(LayoutContext context)
- {
- UninitializeForContextCore((VirtualizingLayoutContext)context);
- }
-
- ///
- public sealed override Size Measure(LayoutContext context, Size availableSize)
- {
- return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
- }
-
- ///
- public sealed override Size Arrange(LayoutContext context, Size finalSize)
- {
- return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
- }
-
///
/// 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.
///
- 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.
///
- 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.
///
- protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize);
+ protected internal abstract Size MeasureOverride(
+ VirtualizingLayoutContext context,
+ Size availableSize);
///
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@@ -119,7 +97,9 @@ namespace Avalonia.Layout
/// its children.
///
/// The actual size that is used after the element is arranged in layout.
- protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize;
+ protected internal virtual Size ArrangeOverride(
+ VirtualizingLayoutContext context,
+ Size finalSize) => finalSize;
///
/// Notifies the layout when the data collection assigned to the container element (Items)
diff --git a/src/Avalonia.Layout/VirtualizingLayoutContext.cs b/src/Avalonia.Layout/VirtualizingLayoutContext.cs
index 980daec2eb..079b91a90f 100644
--- a/src/Avalonia.Layout/VirtualizingLayoutContext.cs
+++ b/src/Avalonia.Layout/VirtualizingLayoutContext.cs
@@ -43,6 +43,8 @@ namespace Avalonia.Layout
///
public abstract class VirtualizingLayoutContext : LayoutContext
{
+ private NonVirtualizingLayoutContext _contextAdapter;
+
///
/// Gets the number of items in the data.
///
@@ -186,5 +188,8 @@ namespace Avalonia.Layout
///
/// The element to clear.
protected abstract void RecycleElementCore(ILayoutable element);
+
+ internal NonVirtualizingLayoutContext GetNonVirtualizingContextAdapter() =>
+ _contextAdapter ?? (_contextAdapter = new VirtualLayoutContextAdapter(this));
}
}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
index b4bf4c799a..b2e827fa26 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
@@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph
///
public class Scene : IDisposable
{
- private Dictionary _index;
+ private readonly Dictionary _index;
///
/// Initializes a new instance of the class.
@@ -83,7 +83,7 @@ namespace Avalonia.Rendering.SceneGraph
/// The cloned scene.
public Scene CloneScene()
{
- var index = new Dictionary();
+ var index = new Dictionary(_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 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;
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs
index 5960b4f560..25f7383a1a 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs
@@ -11,16 +11,28 @@ namespace Avalonia.Rendering.SceneGraph
public class SceneLayers : IEnumerable
{
private readonly IVisual _root;
- private readonly List _inner = new List();
- private readonly Dictionary _index = new Dictionary();
+ private readonly List _inner;
+ private readonly Dictionary _index;
///
/// Initializes a new instance of the class.
///
/// The scene's root visual.
- public SceneLayers(IVisual root)
+ public SceneLayers(IVisual root) : this(root, 0)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The scene's root visual.
+ /// Initial layer capacity.
+ public SceneLayers(IVisual root, int capacity)
{
_root = root;
+
+ _inner = new List(capacity);
+ _index = new Dictionary(capacity);
}
///
@@ -84,7 +96,7 @@ namespace Avalonia.Rendering.SceneGraph
/// The cloned layers.
public SceneLayers Clone()
{
- var result = new SceneLayers(_root);
+ var result = new SceneLayers(_root, Count);
foreach (var src in _inner)
{
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
index 82444a0c29..8cd1a47795 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
+++ b/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 _children;
- private List> _drawOperations;
+ private PooledList _children;
+ private PooledList> _drawOperations;
private IRef _drawOperationsRefCounter;
private bool _drawOperationsCloned;
private Matrix transformRestore;
@@ -349,6 +350,18 @@ namespace Avalonia.Rendering.SceneGraph
context.Transform = transformRestore;
}
+ ///
+ /// Inserts default constructed children into collection and returns a span for the newly created range.
+ ///
+ /// Count of children that will be added.
+ ///
+ internal Span 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();
+ _children = new PooledList(capacity);
}
}
@@ -377,13 +390,21 @@ namespace Avalonia.Rendering.SceneGraph
{
if (_drawOperations == null)
{
- _drawOperations = new List>();
+ _drawOperations = new PooledList>();
_drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
_drawOperationsCloned = false;
}
else if (_drawOperationsCloned)
{
- _drawOperations = new List>(_drawOperations.Select(op => op.Clone()));
+ var oldDrawOperations = _drawOperations;
+
+ _drawOperations = new PooledList>(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
///
/// Draw operations that need to be disposed.
/// Disposable for given draw operations.
- private static IDisposable CreateDisposeDrawOperations(List> drawOperations)
+ private static IDisposable CreateDisposeDrawOperations(PooledList> 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;
}
}
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index c81a270145..3a919e2bc4 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/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;
diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs
index ac14efe133..0734532d92 100644
--- a/src/Avalonia.X11/XI2Manager.cs
+++ b/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; }
}
diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
index 9612fa3d9b..d0157815a9 100644
--- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
+++ b/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];
}
diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
index 5a47a86e51..28e87dd671 100644
--- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
+++ b/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);
}
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
index 501c0455d0..a61d313c8c 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
@@ -2,11 +2,14 @@ using System;
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
+using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
+using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests.Primitives
@@ -172,9 +175,146 @@ 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()
{
- var result = new PopupRoot(popupParent, popupParent.PlatformImpl.CreatePopup())
+ 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 Child_Should_Be_Measured_With_MaxWidth_MaxHeight_When_Set()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var child = new ChildControl();
+ var window = new Window();
+ var target = CreateTarget(window);
+
+ target.MaxWidth = 500;
+ target.MaxHeight = 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);
+
+ var child = new Canvas
+ {
+ Width = 400,
+ Height = 1344,
+ };
+
+ var target = CreateTarget(window, popupImpl.Object);
+ target.Content = child;
+
+ target.Show();
+
+ Assert.Equal(new Size(400, 1024), 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 MinWidth_MinHeight_Should_Be_Respected()
+ {
+ // Issue #3796
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var window = new Window();
+ var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl);
+
+ var target = CreateTarget(window, popupImpl.Object);
+ target.MinWidth = 400;
+ target.MinHeight = 800;
+ target.Content = new Border
+ {
+ Width = 100,
+ Height = 100,
+ };
+
+ target.Show();
+
+ Assert.Equal(new Rect(0, 0, 400, 800), target.Bounds);
+ Assert.Equal(new Size(400, 800), target.ClientSize);
+ Assert.Equal(new Size(400, 800), target.PlatformImpl.ClientSize);
+ }
+ }
+
+ [Fact]
+ public void Setting_Width_Should_Resize_WindowImpl()
+ {
+ // Issue #3796
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var window = new Window();
+ var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl);
+ var positioner = new Mock();
+ popupImpl.Setup(x => x.PopupPositioner).Returns(positioner.Object);
+
+ var target = CreateTarget(window, popupImpl.Object);
+ target.Width = 400;
+ target.Height = 800;
+
+ target.Show();
+
+ Assert.Equal(400, target.Width);
+ Assert.Equal(800, target.Height);
+
+ target.Width = 410;
+ target.LayoutManager.ExecuteLayoutPass();
+
+ positioner.Verify(x =>
+ x.Update(It.Is(x => x.Size.Width == 410)));
+ Assert.Equal(410, target.Width);
+ }
+ }
+
+ private PopupRoot CreateTarget(TopLevel popupParent, IPopupImpl impl = null)
+ {
+ impl ??= popupParent.PlatformImpl.CreatePopup();
+
+ var result = new PopupRoot(popupParent, impl)
{
Template = new FuncControlTemplate((parent, scope) =>
new ContentPresenter
@@ -217,5 +357,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);
+ }
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
index a4c0fa054b..0b9c94f850 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
+++ b/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
diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
index 5375a244c9..8da1e26f0d 100644
--- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
@@ -4,7 +4,6 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
-using Avalonia.LogicalTree;
using Moq;
using Xunit;
@@ -147,6 +146,78 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Size(45, 67), target.LargeChange);
}
+ [Fact]
+ public void Changing_Extent_Should_Raise_ScrollChanged()
+ {
+ var target = new ScrollViewer();
+ var raised = 0;
+
+ target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100));
+ target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50));
+ target.Offset = new Vector(10, 10);
+
+ target.ScrollChanged += (s, e) =>
+ {
+ Assert.Equal(new Vector(11, 12), e.ExtentDelta);
+ Assert.Equal(default, e.OffsetDelta);
+ Assert.Equal(default, e.ViewportDelta);
+ ++raised;
+ };
+
+ target.SetValue(ScrollViewer.ExtentProperty, new Size(111, 112));
+
+ Assert.Equal(1, raised);
+
+ }
+
+ [Fact]
+ public void Changing_Offset_Should_Raise_ScrollChanged()
+ {
+ var target = new ScrollViewer();
+ var raised = 0;
+
+ target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100));
+ target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50));
+ target.Offset = new Vector(10, 10);
+
+ target.ScrollChanged += (s, e) =>
+ {
+ Assert.Equal(default, e.ExtentDelta);
+ Assert.Equal(new Vector(12, 14), e.OffsetDelta);
+ Assert.Equal(default, e.ViewportDelta);
+ ++raised;
+ };
+
+ target.Offset = new Vector(22, 24);
+
+ Assert.Equal(1, raised);
+
+ }
+
+ [Fact]
+ public void Changing_Viewport_Should_Raise_ScrollChanged()
+ {
+ var target = new ScrollViewer();
+ var raised = 0;
+
+ target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100));
+ target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50));
+ target.Offset = new Vector(10, 10);
+
+ target.ScrollChanged += (s, e) =>
+ {
+ Assert.Equal(default, e.ExtentDelta);
+ Assert.Equal(default, e.OffsetDelta);
+ Assert.Equal(new Vector(6, 8), e.ViewportDelta);
+ ++raised;
+ };
+
+ target.SetValue(ScrollViewer.ViewportProperty, new Size(56, 58));
+
+ Assert.Equal(1, raised);
+
+ }
+
private Control CreateTemplate(ScrollViewer control, INameScope scope)
{
return new Grid
diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs
index 05224c2495..5a9ca410e4 100644
--- a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs
+++ b/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);
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
index 6bb3d55799..d2f62cde04 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
+++ b/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;
@@ -554,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().ToSingleton();
+
+ 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(),
@@ -616,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 GetTextAsync() => Task.FromResult("");
+
+ public Task SetTextAsync(string text) => Task.CompletedTask;
+
+ public Task ClearAsync() => Task.CompletedTask;
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
index fed63fc683..1ac4d7a236 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs
+++ b/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,149 @@ 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())).Callback(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);
+ }
+ }
+
+ [Fact]
+ public void Setting_Width_Should_Resize_WindowImpl()
+ {
+ // Issue #3796
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var target = new Window()
+ {
+ Width = 400,
+ Height = 800,
+ };
+
+ target.Show();
+
+ Assert.Equal(400, target.Width);
+ Assert.Equal(800, target.Height);
+
+ target.Width = 410;
+ target.LayoutManager.ExecuteLayoutPass();
+
+ var windowImpl = Mock.Get(target.PlatformImpl);
+ windowImpl.Verify(x => x.Resize(new Size(410, 800)));
+ Assert.Equal(410, target.Width);
+ }
+ }
+
private IWindowImpl CreateImpl(Mock renderer)
{
return Mock.Of(x =>
diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
index 69eff0b65d..dcc29a9716 100644
--- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
+++ b/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 GetLines() => new FormattedTextLine[0];
-
- public TextHitTestResult HitTestPoint(Point point) => new TextHitTestResult();
-
- public Rect HitTestTextPosition(int index) => new Rect();
-
- public IEnumerable HitTestTextRange(int index, int length) => new Rect[0];
-
- public Size Measure() => Constraint;
- }
-
- private void RegisterServices()
- {
- var globalStyles = new Mock();
- var globalStylesResources = globalStyles.As();
- var outObj = (object)10;
- globalStylesResources.Setup(x => x.TryGetResource("FontSizeNormal", out outObj)).Returns(true);
-
- var renderInterface = new Mock();
- renderInterface.Setup(x =>
- x.CreateFormattedText(
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny>()))
- .Returns(new FormattedTextMock("TEST"));
-
- var streamGeometry = new Mock();
- streamGeometry.Setup(x =>
- x.Open())
- .Returns(new Mock().Object);
-
- renderInterface.Setup(x =>
- x.CreateStreamGeometry())
- .Returns(streamGeometry.Object);
-
- var windowImpl = new Mock();
-
- Size clientSize = default(Size);
-
- windowImpl.SetupGet(x => x.ClientSize).Returns(() => clientSize);
- windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(s => clientSize = s);
- windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1024, 1024));
- windowImpl.SetupGet(x => x.Scaling).Returns(1);
-
- AvaloniaLocator.CurrentMutable
- .Bind().ToConstant(new CursorFactoryMock())
- .Bind().ToConstant(new AssetLoader())
- .Bind().ToConstant(new Mock().Object)
- .Bind().ToConstant(globalStyles.Object)
- .Bind().ToConstant(new AppBuilder().RuntimePlatform)
- .Bind().ToConstant(renderInterface.Object)
- .Bind().ToConstant(new Styler())
- .Bind().ToConstant(new MockFontManagerImpl())
- .Bind().ToConstant(new MockTextShaperImpl())
- .Bind().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);
- }
}
}
diff --git a/tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs
new file mode 100644
index 0000000000..a7b378c322
--- /dev/null
+++ b/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 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;
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
index 782e4a0974..b3e4b4edbc 100644
--- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
+++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
@@ -8,65 +8,122 @@ namespace Avalonia.UnitTests
{
public class MockWindowingPlatform : IWindowingPlatform
{
+ private static readonly Size s_screenSize = new Size(1280, 1024);
private readonly Func _windowImpl;
- private readonly Func _popupImpl;
+ private readonly Func _popupImpl;
- public MockWindowingPlatform(Func windowImpl = null, Func popupImpl = null )
+ public MockWindowingPlatform(
+ Func windowImpl = null,
+ Func popupImpl = null )
{
_windowImpl = windowImpl;
_popupImpl = popupImpl;
}
- public static Mock CreateWindowMock(Func popupImpl = null)
+ public static Mock CreateWindowMock()
{
- var win = Mock.Of(x => x.Scaling == 1);
- var mock = Mock.Get(win);
- mock.Setup(x => x.Show()).Callback(() =>
+ var windowImpl = new Mock();
+ 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())).Callback(x =>
+ {
+ position = x;
+ windowImpl.Object.PositionChanged?.Invoke(x);
+ });
+ windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(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())).Callback(new Action(np => pos = np));
- SetupToplevel(mock);
- return mock;
+
+ return windowImpl;
}
- static void SetupToplevel(Mock mock) where T : class, ITopLevelImpl
+ public static Mock CreatePopupMock(IWindowBaseImpl parent)
{
- mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice());
+ var popupImpl = new Mock();
+ var clientSize = new Size();
+
+ var positionerHelper = new ManagedPopupPositionerPopupImplHelper(parent, (pos, size, scale) =>
+ {
+ clientSize = size.Constrain(s_screenSize);
+ popupImpl.Object.PositionChanged?.Invoke(pos);
+ popupImpl.Object.Resized?.Invoke(clientSize);
+ });
+
+ var positioner = new ManagedPopupPositioner(positionerHelper);
+
+ popupImpl.SetupAllProperties();
+ popupImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
+ popupImpl.Setup(x => x.Scaling).Returns(1);
+ popupImpl.Setup(x => x.PopupPositioner).Returns(positioner);
+
+ SetupToplevel(popupImpl);
+
+ return popupImpl;
}
- public static Mock CreatePopupMock()
+ public static Mock CreateScreenMock()
{
- var positioner = Mock.Of();
- var popup = Mock.Of(x => x.Scaling == 1);
- var mock = Mock.Get(popup);
- mock.SetupGet(x => x.PopupPositioner).Returns(positioner);
- SetupToplevel(mock);
-
- return mock;
+ var screenImpl = new Mock();
+ 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(Mock mock) where T : class, ITopLevelImpl
+ {
+ mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice());
+ }
}
}