committed by
GitHub
92 changed files with 8093 additions and 2683 deletions
@ -0,0 +1,49 @@ |
|||
using System.Diagnostics; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.LogicalTree; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.Threading; |
|||
using Avalonia.Visuals.Media.Imaging; |
|||
|
|||
namespace RenderDemo.Pages |
|||
{ |
|||
public class RenderTargetBitmapPage : Control |
|||
{ |
|||
private RenderTargetBitmap _bitmap; |
|||
|
|||
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) |
|||
{ |
|||
_bitmap = new RenderTargetBitmap(new PixelSize(200, 200), new Vector(96, 96)); |
|||
base.OnAttachedToLogicalTree(e); |
|||
} |
|||
|
|||
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) |
|||
{ |
|||
_bitmap.Dispose(); |
|||
_bitmap = null; |
|||
base.OnDetachedFromLogicalTree(e); |
|||
} |
|||
|
|||
readonly Stopwatch _st = Stopwatch.StartNew(); |
|||
public override void Render(DrawingContext context) |
|||
{ |
|||
using (var ctxi = _bitmap.CreateDrawingContext(null)) |
|||
using(var ctx = new DrawingContext(ctxi, false)) |
|||
using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100) |
|||
* Matrix.CreateRotation(_st.Elapsed.TotalSeconds) |
|||
* Matrix.CreateTranslation(100, 100))) |
|||
{ |
|||
ctxi.Clear(default); |
|||
ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100)); |
|||
} |
|||
|
|||
context.DrawImage(_bitmap, 1, |
|||
new Rect(0, 0, 200, 200), |
|||
new Rect(0, 0, 200, 200)); |
|||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); |
|||
base.Render(context); |
|||
} |
|||
} |
|||
} |
|||
@ -1,26 +1,947 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
// This source file is adapted from the Windows Presentation Foundation project.
|
|||
// (https://github.com/dotnet/wpf/)
|
|||
//
|
|||
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
|||
|
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Base class for <see cref="ColumnDefinition"/> and <see cref="RowDefinition"/>.
|
|||
/// DefinitionBase provides core functionality used internally by Grid
|
|||
/// and ColumnDefinitionCollection / RowDefinitionCollection
|
|||
/// </summary>
|
|||
public class DefinitionBase : AvaloniaObject |
|||
public abstract class DefinitionBase : AvaloniaObject |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the <see cref="SharedSizeGroup"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<string> SharedSizeGroupProperty = |
|||
AvaloniaProperty.Register<DefinitionBase, string>(nameof(SharedSizeGroup), inherits: true); |
|||
//------------------------------------------------------
|
|||
//
|
|||
// Constructors
|
|||
//
|
|||
//------------------------------------------------------
|
|||
|
|||
#region Constructors
|
|||
|
|||
/* internal DefinitionBase(bool isColumnDefinition) |
|||
{ |
|||
_isColumnDefinition = isColumnDefinition; |
|||
_parentIndex = -1; |
|||
}*/ |
|||
|
|||
#endregion Constructors
|
|||
|
|||
//------------------------------------------------------
|
|||
//
|
|||
// Public Properties
|
|||
//
|
|||
//------------------------------------------------------
|
|||
|
|||
#region Public Properties
|
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the name of the shared size group of the column or row.
|
|||
/// SharedSizeGroup property.
|
|||
/// </summary>
|
|||
public string SharedSizeGroup |
|||
{ |
|||
get { return GetValue(SharedSizeGroupProperty); } |
|||
get { return (string) GetValue(SharedSizeGroupProperty); } |
|||
set { SetValue(SharedSizeGroupProperty, value); } |
|||
} |
|||
|
|||
#endregion Public Properties
|
|||
|
|||
//------------------------------------------------------
|
|||
//
|
|||
// Internal Methods
|
|||
//
|
|||
//------------------------------------------------------
|
|||
|
|||
#region Internal Methods
|
|||
|
|||
/// <summary>
|
|||
/// Callback to notify about entering model tree.
|
|||
/// </summary>
|
|||
internal void OnEnterParentTree() |
|||
{ |
|||
this.InheritanceParent = Parent; |
|||
if (_sharedState == null) |
|||
{ |
|||
// start with getting SharedSizeGroup value.
|
|||
// this property is NOT inhereted which should result in better overall perf.
|
|||
string sharedSizeGroupId = SharedSizeGroup; |
|||
if (sharedSizeGroupId != null) |
|||
{ |
|||
SharedSizeScope privateSharedSizeScope = PrivateSharedSizeScope; |
|||
if (privateSharedSizeScope != null) |
|||
{ |
|||
_sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); |
|||
_sharedState.AddMember(this); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (e.Property.PropertyType == typeof(GridLength) |
|||
|| e.Property.PropertyType == typeof(double)) |
|||
OnUserSizePropertyChanged(e); |
|||
|
|||
base.OnPropertyChanged(e); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Callback to notify about exitting model tree.
|
|||
/// </summary>
|
|||
internal void OnExitParentTree() |
|||
{ |
|||
_offset = 0; |
|||
if (_sharedState != null) |
|||
{ |
|||
_sharedState.RemoveMember(this); |
|||
_sharedState = null; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Performs action preparing definition to enter layout calculation mode.
|
|||
/// </summary>
|
|||
internal void OnBeforeLayout(Grid grid) |
|||
{ |
|||
// reset layout state.
|
|||
_minSize = 0; |
|||
LayoutWasUpdated = true; |
|||
|
|||
// defer verification for shared definitions
|
|||
if (_sharedState != null) { _sharedState.EnsureDeferredValidation(grid); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Updates min size.
|
|||
/// </summary>
|
|||
/// <param name="minSize">New size.</param>
|
|||
internal void UpdateMinSize(double minSize) |
|||
{ |
|||
_minSize = Math.Max(_minSize, minSize); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets min size.
|
|||
/// </summary>
|
|||
/// <param name="minSize">New size.</param>
|
|||
internal void SetMinSize(double minSize) |
|||
{ |
|||
_minSize = minSize; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <see cref="PropertyMetadata.PropertyChangedCallback"/>
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method needs to be internal to be accessable from derived classes.
|
|||
/// </remarks>
|
|||
internal void OnUserSizePropertyChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (InParentLogicalTree) |
|||
{ |
|||
if (_sharedState != null) |
|||
{ |
|||
_sharedState.Invalidate(); |
|||
} |
|||
else |
|||
{ |
|||
if (((GridLength)e.OldValue).GridUnitType != ((GridLength)e.NewValue).GridUnitType) |
|||
{ |
|||
Parent.Invalidate(); |
|||
} |
|||
else |
|||
{ |
|||
Parent.InvalidateMeasure(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty.ValidateValueCallback"/>
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method needs to be internal to be accessable from derived classes.
|
|||
/// </remarks>
|
|||
internal static bool IsUserSizePropertyValueValid(object value) |
|||
{ |
|||
return (((GridLength)value).Value >= 0); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <see cref="PropertyMetadata.PropertyChangedCallback"/>
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method needs to be internal to be accessable from derived classes.
|
|||
/// </remarks>
|
|||
internal static void OnUserMinSizePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
DefinitionBase definition = (DefinitionBase) d; |
|||
|
|||
if (definition.InParentLogicalTree) |
|||
{ |
|||
Grid parentGrid = (Grid) definition.Parent; |
|||
parentGrid.InvalidateMeasure(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty.ValidateValueCallback"/>
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method needs to be internal to be accessable from derived classes.
|
|||
/// </remarks>
|
|||
internal static bool IsUserMinSizePropertyValueValid(object value) |
|||
{ |
|||
double v = (double)value; |
|||
return (!double.IsNaN(v) && v >= 0.0d && !Double.IsPositiveInfinity(v)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <see cref="PropertyMetadata.PropertyChangedCallback"/>
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method needs to be internal to be accessable from derived classes.
|
|||
/// </remarks>
|
|||
internal static void OnUserMaxSizePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
DefinitionBase definition = (DefinitionBase) d; |
|||
|
|||
if (definition.InParentLogicalTree) |
|||
{ |
|||
Grid parentGrid = (Grid) definition.Parent; |
|||
parentGrid.InvalidateMeasure(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty.ValidateValueCallback"/>
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method needs to be internal to be accessable from derived classes.
|
|||
/// </remarks>
|
|||
internal static bool IsUserMaxSizePropertyValueValid(object value) |
|||
{ |
|||
double v = (double)value; |
|||
return (!double.IsNaN(v) && v >= 0.0d); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <see cref="PropertyMetadata.PropertyChangedCallback"/>
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method reflects Grid.SharedScopeProperty state by setting / clearing
|
|||
/// dynamic property PrivateSharedSizeScopeProperty. Value of PrivateSharedSizeScopeProperty
|
|||
/// is a collection of SharedSizeState objects for the scope.
|
|||
/// Also PrivateSharedSizeScopeProperty is FrameworkPropertyMetadataOptions.Inherits property. So that all children
|
|||
/// elements belonging to a certain scope can easily access SharedSizeState collection. As well
|
|||
/// as been norified about enter / exit a scope.
|
|||
/// </remarks>
|
|||
internal static void OnIsSharedSizeScopePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
// is it possible to optimize here something like this:
|
|||
// if ((bool)d.GetValue(Grid.IsSharedSizeScopeProperty) == (d.GetLocalValue(PrivateSharedSizeScopeProperty) != null)
|
|||
// { /* do nothing */ }
|
|||
if ((bool) e.NewValue) |
|||
{ |
|||
SharedSizeScope sharedStatesCollection = new SharedSizeScope(); |
|||
d.SetValue(PrivateSharedSizeScopeProperty, sharedStatesCollection); |
|||
} |
|||
else |
|||
{ |
|||
d.ClearValue(PrivateSharedSizeScopeProperty); |
|||
} |
|||
} |
|||
|
|||
#endregion Internal Methods
|
|||
|
|||
//------------------------------------------------------
|
|||
//
|
|||
// Internal Properties
|
|||
//
|
|||
//------------------------------------------------------
|
|||
|
|||
#region Internal Properties
|
|||
|
|||
/// <summary>
|
|||
/// Returns <c>true</c> if this definition is a part of shared group.
|
|||
/// </summary>
|
|||
internal bool IsShared |
|||
{ |
|||
get { return (_sharedState != null); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Internal accessor to user size field.
|
|||
/// </summary>
|
|||
internal GridLength UserSize |
|||
{ |
|||
get { return (_sharedState != null ? _sharedState.UserSize : UserSizeValueCache); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Internal accessor to user min size field.
|
|||
/// </summary>
|
|||
internal double UserMinSize |
|||
{ |
|||
get { return (UserMinSizeValueCache); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Internal accessor to user max size field.
|
|||
/// </summary>
|
|||
internal double UserMaxSize |
|||
{ |
|||
get { return (UserMaxSizeValueCache); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// DefinitionBase's index in the parents collection.
|
|||
/// </summary>
|
|||
internal int Index |
|||
{ |
|||
get |
|||
{ |
|||
return (_parentIndex); |
|||
} |
|||
set |
|||
{ |
|||
Debug.Assert(value >= -1); |
|||
_parentIndex = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Layout-time user size type.
|
|||
/// </summary>
|
|||
internal Grid.LayoutTimeSizeType SizeType |
|||
{ |
|||
get { return (_sizeType); } |
|||
set { _sizeType = value; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns or sets measure size for the definition.
|
|||
/// </summary>
|
|||
internal double MeasureSize |
|||
{ |
|||
get { return (_measureSize); } |
|||
set { _measureSize = value; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns definition's layout time type sensitive preferred size.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Returned value is guaranteed to be true preferred size.
|
|||
/// </remarks>
|
|||
internal double PreferredSize |
|||
{ |
|||
get |
|||
{ |
|||
double preferredSize = MinSize; |
|||
if ( _sizeType != Grid.LayoutTimeSizeType.Auto |
|||
&& preferredSize < _measureSize ) |
|||
{ |
|||
preferredSize = _measureSize; |
|||
} |
|||
return (preferredSize); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns or sets size cache for the definition.
|
|||
/// </summary>
|
|||
internal double SizeCache |
|||
{ |
|||
get { return (_sizeCache); } |
|||
set { _sizeCache = value; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns min size.
|
|||
/// </summary>
|
|||
internal double MinSize |
|||
{ |
|||
get |
|||
{ |
|||
double minSize = _minSize; |
|||
if ( UseSharedMinimum |
|||
&& _sharedState != null |
|||
&& minSize < _sharedState.MinSize ) |
|||
{ |
|||
minSize = _sharedState.MinSize; |
|||
} |
|||
return (minSize); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns min size, always taking into account shared state.
|
|||
/// </summary>
|
|||
internal double MinSizeForArrange |
|||
{ |
|||
get |
|||
{ |
|||
double minSize = _minSize; |
|||
if ( _sharedState != null |
|||
&& (UseSharedMinimum || !LayoutWasUpdated) |
|||
&& minSize < _sharedState.MinSize ) |
|||
{ |
|||
minSize = _sharedState.MinSize; |
|||
} |
|||
return (minSize); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Offset.
|
|||
/// </summary>
|
|||
internal double FinalOffset |
|||
{ |
|||
get { return _offset; } |
|||
set { _offset = value; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Internal helper to access up-to-date UserSize property value.
|
|||
/// </summary>
|
|||
internal abstract GridLength UserSizeValueCache { get; } |
|||
|
|||
/// <summary>
|
|||
/// Internal helper to access up-to-date UserMinSize property value.
|
|||
/// </summary>
|
|||
internal abstract double UserMinSizeValueCache { get; } |
|||
|
|||
/// <summary>
|
|||
/// Internal helper to access up-to-date UserMaxSize property value.
|
|||
/// </summary>
|
|||
internal abstract double UserMaxSizeValueCache { get; } |
|||
|
|||
/// <summary>
|
|||
/// Protected. Returns <c>true</c> if this DefinitionBase instance is in parent's logical tree.
|
|||
/// </summary>
|
|||
internal bool InParentLogicalTree |
|||
{ |
|||
get { return (_parentIndex != -1); } |
|||
} |
|||
|
|||
internal Grid Parent { get; set; } |
|||
|
|||
#endregion Internal Properties
|
|||
|
|||
//------------------------------------------------------
|
|||
//
|
|||
// Private Methods
|
|||
//
|
|||
//------------------------------------------------------
|
|||
|
|||
#region Private Methods
|
|||
|
|||
/// <summary>
|
|||
/// SetFlags is used to set or unset one or multiple
|
|||
/// flags on the object.
|
|||
/// </summary>
|
|||
private void SetFlags(bool value, Flags flags) |
|||
{ |
|||
_flags = value ? (_flags | flags) : (_flags & (~flags)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// CheckFlagsAnd returns <c>true</c> if all the flags in the
|
|||
/// given bitmask are set on the object.
|
|||
/// </summary>
|
|||
private bool CheckFlagsAnd(Flags flags) |
|||
{ |
|||
return ((_flags & flags) == flags); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <see cref="PropertyMetadata.PropertyChangedCallback"/>
|
|||
/// </summary>
|
|||
private static void OnSharedSizeGroupPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
DefinitionBase definition = (DefinitionBase) d; |
|||
|
|||
if (definition.InParentLogicalTree) |
|||
{ |
|||
string sharedSizeGroupId = (string) e.NewValue; |
|||
|
|||
if (definition._sharedState != null) |
|||
{ |
|||
// if definition is already registered AND shared size group id is changing,
|
|||
// then un-register the definition from the current shared size state object.
|
|||
definition._sharedState.RemoveMember(definition); |
|||
definition._sharedState = null; |
|||
} |
|||
|
|||
if ((definition._sharedState == null) && (sharedSizeGroupId != null)) |
|||
{ |
|||
SharedSizeScope privateSharedSizeScope = definition.PrivateSharedSizeScope; |
|||
if (privateSharedSizeScope != null) |
|||
{ |
|||
// if definition is not registered and both: shared size group id AND private shared scope
|
|||
// are available, then register definition.
|
|||
definition._sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); |
|||
definition._sharedState.AddMember(definition); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty.ValidateValueCallback"/>
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Verifies that Shared Size Group Property string
|
|||
/// a) not empty.
|
|||
/// b) contains only letters, digits and underscore ('_').
|
|||
/// c) does not start with a digit.
|
|||
/// </remarks>
|
|||
private static string SharedSizeGroupPropertyValueValid(Control _, string value) |
|||
{ |
|||
Contract.Requires<ArgumentNullException>(value != null); |
|||
|
|||
string id = (string)value; |
|||
|
|||
if (id != string.Empty) |
|||
{ |
|||
int i = -1; |
|||
while (++i < id.Length) |
|||
{ |
|||
bool isDigit = Char.IsDigit(id[i]); |
|||
|
|||
if ( (i == 0 && isDigit) |
|||
|| !( isDigit |
|||
|| Char.IsLetter(id[i]) |
|||
|| '_' == id[i] ) ) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (i == id.Length) |
|||
{ |
|||
return value; |
|||
} |
|||
} |
|||
|
|||
throw new ArgumentException("Invalid SharedSizeGroup string."); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <see cref="PropertyMetadata.PropertyChangedCallback"/>
|
|||
/// </summary>
|
|||
/// <remark>
|
|||
/// OnPrivateSharedSizeScopePropertyChanged is called when new scope enters or
|
|||
/// existing scope just left. In both cases if the DefinitionBase object is already registered
|
|||
/// in SharedSizeState, it should un-register and register itself in a new one.
|
|||
/// </remark>
|
|||
private static void OnPrivateSharedSizeScopePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
DefinitionBase definition = (DefinitionBase)d; |
|||
|
|||
if (definition.InParentLogicalTree) |
|||
{ |
|||
SharedSizeScope privateSharedSizeScope = (SharedSizeScope) e.NewValue; |
|||
|
|||
if (definition._sharedState != null) |
|||
{ |
|||
// if definition is already registered And shared size scope is changing,
|
|||
// then un-register the definition from the current shared size state object.
|
|||
definition._sharedState.RemoveMember(definition); |
|||
definition._sharedState = null; |
|||
} |
|||
|
|||
if ((definition._sharedState == null) && (privateSharedSizeScope != null)) |
|||
{ |
|||
string sharedSizeGroup = definition.SharedSizeGroup; |
|||
if (sharedSizeGroup != null) |
|||
{ |
|||
// if definition is not registered and both: shared size group id AND private shared scope
|
|||
// are available, then register definition.
|
|||
definition._sharedState = privateSharedSizeScope.EnsureSharedState(definition.SharedSizeGroup); |
|||
definition._sharedState.AddMember(definition); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
#endregion Private Methods
|
|||
|
|||
//------------------------------------------------------
|
|||
//
|
|||
// Private Properties
|
|||
//
|
|||
//------------------------------------------------------
|
|||
|
|||
#region Private Properties
|
|||
|
|||
/// <summary>
|
|||
/// Private getter of shared state collection dynamic property.
|
|||
/// </summary>
|
|||
private SharedSizeScope PrivateSharedSizeScope |
|||
{ |
|||
get { return (SharedSizeScope) GetValue(PrivateSharedSizeScopeProperty); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Convenience accessor to UseSharedMinimum flag
|
|||
/// </summary>
|
|||
private bool UseSharedMinimum |
|||
{ |
|||
get { return (CheckFlagsAnd(Flags.UseSharedMinimum)); } |
|||
set { SetFlags(value, Flags.UseSharedMinimum); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Convenience accessor to LayoutWasUpdated flag
|
|||
/// </summary>
|
|||
private bool LayoutWasUpdated |
|||
{ |
|||
get { return (CheckFlagsAnd(Flags.LayoutWasUpdated)); } |
|||
set { SetFlags(value, Flags.LayoutWasUpdated); } |
|||
} |
|||
|
|||
#endregion Private Properties
|
|||
|
|||
//------------------------------------------------------
|
|||
//
|
|||
// Private Fields
|
|||
//
|
|||
//------------------------------------------------------
|
|||
|
|||
#region Private Fields
|
|||
private readonly bool _isColumnDefinition; // when "true", this is a ColumnDefinition; when "false" this is a RowDefinition (faster than a type check)
|
|||
private Flags _flags; // flags reflecting various aspects of internal state
|
|||
internal int _parentIndex = -1; // this instance's index in parent's children collection
|
|||
|
|||
private Grid.LayoutTimeSizeType _sizeType; // layout-time user size type. it may differ from _userSizeValueCache.UnitType when calculating "to-content"
|
|||
|
|||
private double _minSize; // used during measure to accumulate size for "Auto" and "Star" DefinitionBase's
|
|||
private double _measureSize; // size, calculated to be the input contstraint size for Child.Measure
|
|||
private double _sizeCache; // cache used for various purposes (sorting, caching, etc) during calculations
|
|||
private double _offset; // offset of the DefinitionBase from left / top corner (assuming LTR case)
|
|||
|
|||
private SharedSizeState _sharedState; // reference to shared state object this instance is registered with
|
|||
|
|||
internal const bool ThisIsColumnDefinition = true; |
|||
internal const bool ThisIsRowDefinition = false; |
|||
|
|||
#endregion Private Fields
|
|||
|
|||
//------------------------------------------------------
|
|||
//
|
|||
// Private Structures / Classes
|
|||
//
|
|||
//------------------------------------------------------
|
|||
|
|||
#region Private Structures Classes
|
|||
|
|||
[System.Flags] |
|||
private enum Flags : byte |
|||
{ |
|||
//
|
|||
// bool flags
|
|||
//
|
|||
UseSharedMinimum = 0x00000020, // when "1", definition will take into account shared state's minimum
|
|||
LayoutWasUpdated = 0x00000040, // set to "1" every time the parent grid is measured
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Collection of shared states objects for a single scope
|
|||
/// </summary>
|
|||
internal class SharedSizeScope |
|||
{ |
|||
/// <summary>
|
|||
/// Returns SharedSizeState object for a given group.
|
|||
/// Creates a new StatedState object if necessary.
|
|||
/// </summary>
|
|||
internal SharedSizeState EnsureSharedState(string sharedSizeGroup) |
|||
{ |
|||
// check that sharedSizeGroup is not default
|
|||
Debug.Assert(sharedSizeGroup != null); |
|||
|
|||
SharedSizeState sharedState = _registry[sharedSizeGroup] as SharedSizeState; |
|||
if (sharedState == null) |
|||
{ |
|||
sharedState = new SharedSizeState(this, sharedSizeGroup); |
|||
_registry[sharedSizeGroup] = sharedState; |
|||
} |
|||
return (sharedState); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes an entry in the registry by the given key.
|
|||
/// </summary>
|
|||
internal void Remove(object key) |
|||
{ |
|||
Debug.Assert(_registry.Contains(key)); |
|||
_registry.Remove(key); |
|||
} |
|||
|
|||
private Hashtable _registry = new Hashtable(); // storage for shared state objects
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Implementation of per shared group state object
|
|||
/// </summary>
|
|||
internal class SharedSizeState |
|||
{ |
|||
/// <summary>
|
|||
/// Default ctor.
|
|||
/// </summary>
|
|||
internal SharedSizeState(SharedSizeScope sharedSizeScope, string sharedSizeGroupId) |
|||
{ |
|||
Debug.Assert(sharedSizeScope != null && sharedSizeGroupId != null); |
|||
_sharedSizeScope = sharedSizeScope; |
|||
_sharedSizeGroupId = sharedSizeGroupId; |
|||
_registry = new List<DefinitionBase>(); |
|||
_layoutUpdated = new EventHandler(OnLayoutUpdated); |
|||
_broadcastInvalidation = true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Adds / registers a definition instance.
|
|||
/// </summary>
|
|||
internal void AddMember(DefinitionBase member) |
|||
{ |
|||
Debug.Assert(!_registry.Contains(member)); |
|||
_registry.Add(member); |
|||
Invalidate(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes / un-registers a definition instance.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// If the collection of registered definitions becomes empty
|
|||
/// instantiates self removal from owner's collection.
|
|||
/// </remarks>
|
|||
internal void RemoveMember(DefinitionBase member) |
|||
{ |
|||
Invalidate(); |
|||
_registry.Remove(member); |
|||
|
|||
if (_registry.Count == 0) |
|||
{ |
|||
_sharedSizeScope.Remove(_sharedSizeGroupId); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Propogates invalidations for all registered definitions.
|
|||
/// Resets its own state.
|
|||
/// </summary>
|
|||
internal void Invalidate() |
|||
{ |
|||
_userSizeValid = false; |
|||
|
|||
if (_broadcastInvalidation) |
|||
{ |
|||
for (int i = 0, count = _registry.Count; i < count; ++i) |
|||
{ |
|||
Grid parentGrid = (Grid)(_registry[i].Parent); |
|||
parentGrid.Invalidate(); |
|||
} |
|||
_broadcastInvalidation = false; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Makes sure that one and only one layout updated handler is registered for this shared state.
|
|||
/// </summary>
|
|||
internal void EnsureDeferredValidation(Control layoutUpdatedHost) |
|||
{ |
|||
if (_layoutUpdatedHost == null) |
|||
{ |
|||
_layoutUpdatedHost = layoutUpdatedHost; |
|||
_layoutUpdatedHost.LayoutUpdated += _layoutUpdated; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// DefinitionBase's specific code.
|
|||
/// </summary>
|
|||
internal double MinSize |
|||
{ |
|||
get |
|||
{ |
|||
if (!_userSizeValid) { EnsureUserSizeValid(); } |
|||
return (_minSize); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// DefinitionBase's specific code.
|
|||
/// </summary>
|
|||
internal GridLength UserSize |
|||
{ |
|||
get |
|||
{ |
|||
if (!_userSizeValid) { EnsureUserSizeValid(); } |
|||
return (_userSize); |
|||
} |
|||
} |
|||
|
|||
private void EnsureUserSizeValid() |
|||
{ |
|||
_userSize = new GridLength(1, GridUnitType.Auto); |
|||
|
|||
for (int i = 0, count = _registry.Count; i < count; ++i) |
|||
{ |
|||
Debug.Assert( _userSize.GridUnitType == GridUnitType.Auto |
|||
|| _userSize.GridUnitType == GridUnitType.Pixel ); |
|||
|
|||
GridLength currentGridLength = _registry[i].UserSizeValueCache; |
|||
if (currentGridLength.GridUnitType == GridUnitType.Pixel) |
|||
{ |
|||
if (_userSize.GridUnitType == GridUnitType.Auto) |
|||
{ |
|||
_userSize = currentGridLength; |
|||
} |
|||
else if (_userSize.Value < currentGridLength.Value) |
|||
{ |
|||
_userSize = currentGridLength; |
|||
} |
|||
} |
|||
} |
|||
// taking maximum with user size effectively prevents squishy-ness.
|
|||
// this is a "solution" to avoid shared definitions from been sized to
|
|||
// different final size at arrange time, if / when different grids receive
|
|||
// different final sizes.
|
|||
_minSize = _userSize.IsAbsolute ? _userSize.Value : 0.0; |
|||
|
|||
_userSizeValid = true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// OnLayoutUpdated handler. Validates that all participating definitions
|
|||
/// have updated min size value. Forces another layout update cycle if needed.
|
|||
/// </summary>
|
|||
private void OnLayoutUpdated(object sender, EventArgs e) |
|||
{ |
|||
double sharedMinSize = 0; |
|||
|
|||
// accumulate min size of all participating definitions
|
|||
for (int i = 0, count = _registry.Count; i < count; ++i) |
|||
{ |
|||
sharedMinSize = Math.Max(sharedMinSize, _registry[i].MinSize); |
|||
} |
|||
|
|||
bool sharedMinSizeChanged = !MathUtilities.AreClose(_minSize, sharedMinSize); |
|||
|
|||
// compare accumulated min size with min sizes of the individual definitions
|
|||
for (int i = 0, count = _registry.Count; i < count; ++i) |
|||
{ |
|||
DefinitionBase definitionBase = _registry[i]; |
|||
|
|||
if (sharedMinSizeChanged || definitionBase.LayoutWasUpdated) |
|||
{ |
|||
// if definition's min size is different, then need to re-measure
|
|||
if (!MathUtilities.AreClose(sharedMinSize, definitionBase.MinSize)) |
|||
{ |
|||
Grid parentGrid = (Grid)definitionBase.Parent; |
|||
parentGrid.InvalidateMeasure(); |
|||
definitionBase.UseSharedMinimum = true; |
|||
} |
|||
else |
|||
{ |
|||
definitionBase.UseSharedMinimum = false; |
|||
|
|||
// if measure is valid then also need to check arrange.
|
|||
// Note: definitionBase.SizeCache is volatile but at this point
|
|||
// it contains up-to-date final size
|
|||
if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache)) |
|||
{ |
|||
Grid parentGrid = (Grid)definitionBase.Parent; |
|||
parentGrid.InvalidateArrange(); |
|||
} |
|||
} |
|||
|
|||
definitionBase.LayoutWasUpdated = false; |
|||
} |
|||
} |
|||
|
|||
_minSize = sharedMinSize; |
|||
|
|||
_layoutUpdatedHost.LayoutUpdated -= _layoutUpdated; |
|||
_layoutUpdatedHost = null; |
|||
|
|||
_broadcastInvalidation = true; |
|||
} |
|||
|
|||
private readonly SharedSizeScope _sharedSizeScope; // the scope this state belongs to
|
|||
private readonly string _sharedSizeGroupId; // Id of the shared size group this object is servicing
|
|||
private readonly List<DefinitionBase> _registry; // registry of participating definitions
|
|||
private readonly EventHandler _layoutUpdated; // instance event handler for layout updated event
|
|||
private Control _layoutUpdatedHost; // Control for which layout updated event handler is registered
|
|||
private bool _broadcastInvalidation; // "true" when broadcasting of invalidation is needed
|
|||
private bool _userSizeValid; // "true" when _userSize is up to date
|
|||
private GridLength _userSize; // shared state
|
|||
private double _minSize; // shared state
|
|||
} |
|||
|
|||
#endregion Private Structures Classes
|
|||
|
|||
//------------------------------------------------------
|
|||
//
|
|||
// Properties
|
|||
//
|
|||
//------------------------------------------------------
|
|||
|
|||
#region Properties
|
|||
|
|||
/// <summary>
|
|||
/// Private shared size scope property holds a collection of shared state objects for the a given shared size scope.
|
|||
/// <see cref="OnIsSharedSizeScopePropertyChanged"/>
|
|||
/// </summary>
|
|||
internal static readonly AttachedProperty<SharedSizeScope> PrivateSharedSizeScopeProperty = |
|||
AvaloniaProperty.RegisterAttached<DefinitionBase, Control, SharedSizeScope>( |
|||
"PrivateSharedSizeScope", |
|||
defaultValue: null, |
|||
inherits: true); |
|||
|
|||
/// <summary>
|
|||
/// Shared size group property marks column / row definition as belonging to a group "Foo" or "Bar".
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Value of the Shared Size Group Property must satisfy the following rules:
|
|||
/// <list type="bullet">
|
|||
/// <item><description>
|
|||
/// String must not be empty.
|
|||
/// </description></item>
|
|||
/// <item><description>
|
|||
/// String must consist of letters, digits and underscore ('_') only.
|
|||
/// </description></item>
|
|||
/// <item><description>
|
|||
/// String must not start with a digit.
|
|||
/// </description></item>
|
|||
/// </list>
|
|||
/// </remarks>
|
|||
public static readonly AttachedProperty<string> SharedSizeGroupProperty = |
|||
AvaloniaProperty.RegisterAttached<DefinitionBase, Control, string>( |
|||
"SharedSizeGroup", |
|||
validate:SharedSizeGroupPropertyValueValid); |
|||
|
|||
/// <summary>
|
|||
/// Static ctor. Used for static registration of properties.
|
|||
/// </summary>
|
|||
static DefinitionBase() |
|||
{ |
|||
SharedSizeGroupProperty.Changed.AddClassHandler<DefinitionBase>(OnSharedSizeGroupPropertyChanged); |
|||
PrivateSharedSizeScopeProperty.Changed.AddClassHandler<DefinitionBase>(OnPrivateSharedSizeScopePropertyChanged); |
|||
} |
|||
|
|||
#endregion Properties
|
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,55 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
using Avalonia.Collections; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public abstract class DefinitionList<T> : AvaloniaList<T> where T : DefinitionBase |
|||
{ |
|||
internal bool IsDirty = true; |
|||
private Grid _parent; |
|||
|
|||
internal Grid Parent |
|||
{ |
|||
get => _parent; |
|||
set => SetParent(value); |
|||
} |
|||
|
|||
|
|||
private void SetParent(Grid value) |
|||
{ |
|||
_parent = value; |
|||
|
|||
foreach (var pair in this.Select((definitions, index) => (definitions, index))) |
|||
{ |
|||
pair.definitions.Parent = value; |
|||
pair.definitions.Index = pair.index; |
|||
} |
|||
} |
|||
|
|||
internal void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
foreach (var nI in this.Select((d, i) => (d, i))) |
|||
nI.d._parentIndex = nI.i; |
|||
|
|||
foreach (var nD in e.NewItems?.Cast<DefinitionBase>() |
|||
?? Enumerable.Empty<DefinitionBase>()) |
|||
{ |
|||
nD.Parent = this.Parent; |
|||
nD.OnEnterParentTree(); |
|||
} |
|||
|
|||
foreach (var oD in e.OldItems?.Cast<DefinitionBase>() |
|||
?? Enumerable.Empty<DefinitionBase>()) |
|||
{ |
|||
oD.OnExitParentTree(); |
|||
} |
|||
|
|||
IsDirty = true; |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -1,705 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Layout; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Avalonia.Controls.Utils |
|||
{ |
|||
/// <summary>
|
|||
/// Contains algorithms that can help to measure and arrange a Grid.
|
|||
/// </summary>
|
|||
internal class GridLayout |
|||
{ |
|||
/// <summary>
|
|||
/// Initialize a new <see cref="GridLayout"/> instance from the column definitions.
|
|||
/// The instance doesn't care about whether the definitions are rows or columns.
|
|||
/// It will not calculate the column or row differently.
|
|||
/// </summary>
|
|||
internal GridLayout([NotNull] ColumnDefinitions columns) |
|||
{ |
|||
if (columns == null) throw new ArgumentNullException(nameof(columns)); |
|||
_conventions = columns.Count == 0 |
|||
? new List<LengthConvention> { new LengthConvention() } |
|||
: columns.Select(x => new LengthConvention(x.Width, x.MinWidth, x.MaxWidth)).ToList(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initialize a new <see cref="GridLayout"/> instance from the row definitions.
|
|||
/// The instance doesn't care about whether the definitions are rows or columns.
|
|||
/// It will not calculate the column or row differently.
|
|||
/// </summary>
|
|||
internal GridLayout([NotNull] RowDefinitions rows) |
|||
{ |
|||
if (rows == null) throw new ArgumentNullException(nameof(rows)); |
|||
_conventions = rows.Count == 0 |
|||
? new List<LengthConvention> { new LengthConvention() } |
|||
: rows.Select(x => new LengthConvention(x.Height, x.MinHeight, x.MaxHeight)).ToList(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the layout tolerance. If any length offset is less than this value, we will treat them the same.
|
|||
/// </summary>
|
|||
private const double LayoutTolerance = 1.0 / 256.0; |
|||
|
|||
/// <summary>
|
|||
/// Gets all the length conventions that come from column/row definitions.
|
|||
/// These conventions provide cell limitations, such as the expected pixel length, the min/max pixel length and the * count.
|
|||
/// </summary>
|
|||
[NotNull] |
|||
private readonly List<LengthConvention> _conventions; |
|||
|
|||
/// <summary>
|
|||
/// Gets all the length conventions that come from the grid children.
|
|||
/// </summary>
|
|||
[NotNull] |
|||
private readonly List<AdditionalLengthConvention> _additionalConventions = |
|||
new List<AdditionalLengthConvention>(); |
|||
|
|||
/// <summary>
|
|||
/// Appending these elements into the convention list helps lay them out according to their desired sizes.
|
|||
/// <para/>
|
|||
/// Some elements are not only in a single grid cell, they have one or more column/row spans,
|
|||
/// and these elements may affect the grid layout especially the measuring procedure.<para/>
|
|||
/// Append these elements into the convention list can help to layout them correctly through
|
|||
/// their desired size. Only a small subset of children need to be measured before layout starts
|
|||
/// and they will be called via the<paramref name="getDesiredLength"/> callback.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The grid children type.</typeparam>
|
|||
/// <param name="source">
|
|||
/// Contains the safe column/row index and its span.
|
|||
/// Notice that we will not verify whether the range is in the column/row count,
|
|||
/// so you should get the safe column/row info first.
|
|||
/// </param>
|
|||
/// <param name="getDesiredLength">
|
|||
/// This callback will be called if the <see cref="GridLayout"/> thinks that a child should be
|
|||
/// measured first. Usually, these are the children that have the * or Auto length.
|
|||
/// </param>
|
|||
internal void AppendMeasureConventions<T>([NotNull] IDictionary<T, (int index, int span)> source, |
|||
[NotNull] Func<T, double> getDesiredLength) |
|||
{ |
|||
if (source == null) throw new ArgumentNullException(nameof(source)); |
|||
if (getDesiredLength == null) throw new ArgumentNullException(nameof(getDesiredLength)); |
|||
|
|||
// M1/7. Find all the Auto and * length columns/rows. (M1/7 means the 1st procedure of measurement.)
|
|||
// Only these columns/rows' layout can be affected by the child desired size.
|
|||
//
|
|||
// Find all columns/rows that have Auto or * length. We'll measure the children in advance.
|
|||
// Only these kind of columns/rows will affect the Grid layout.
|
|||
// Please note:
|
|||
// - If the column / row has Auto length, the Grid.DesiredSize and the column width
|
|||
// will be affected by the child's desired size.
|
|||
// - If the column / row has* length, the Grid.DesiredSize will be affected by the
|
|||
// child's desired size but the column width not.
|
|||
|
|||
// +-----------------------------------------------------------+
|
|||
// | * | A | * | P | A | * | P | * | * |
|
|||
// +-----------------------------------------------------------+
|
|||
// _conventions: | min | max | | | min | | min max | max |
|
|||
// _additionalC: |<- desired ->| |< desired >|
|
|||
// _additionalC: |< desired >| |<- desired ->|
|
|||
|
|||
// 寻找所有行列范围中包含 Auto 和 * 的元素,使用全部可用尺寸提前测量。
|
|||
// 因为只有这部分元素的布局才会被 Grid 的子元素尺寸影响。
|
|||
// 请注意:
|
|||
// - Auto 长度的行列必定会受到子元素布局影响,会影响到行列的布局长度和 Grid 本身的 DesiredSize;
|
|||
// - 而对于 * 长度,只有 Grid.DesiredSize 会受到子元素布局影响,而行列长度不会受影响。
|
|||
|
|||
// Find all the Auto and * length columns/rows.
|
|||
var found = new Dictionary<T, (int index, int span)>(); |
|||
for (var i = 0; i < _conventions.Count; i++) |
|||
{ |
|||
var index = i; |
|||
var convention = _conventions[index]; |
|||
if (convention.Length.IsAuto || convention.Length.IsStar) |
|||
{ |
|||
foreach (var pair in source.Where(x => |
|||
x.Value.index <= index && index < x.Value.index + x.Value.span)) |
|||
{ |
|||
found[pair.Key] = pair.Value; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Append these layout into the additional convention list.
|
|||
foreach (var pair in found) |
|||
{ |
|||
var t = pair.Key; |
|||
var (index, span) = pair.Value; |
|||
var desiredLength = getDesiredLength(t); |
|||
if (Math.Abs(desiredLength) > LayoutTolerance) |
|||
{ |
|||
_additionalConventions.Add(new AdditionalLengthConvention(index, span, desiredLength)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Run measure procedure according to the <paramref name="containerLength"/> and gets the <see cref="MeasureResult"/>.
|
|||
/// </summary>
|
|||
/// <param name="containerLength">
|
|||
/// The container length. Usually, it is the constraint of the <see cref="Layoutable.MeasureOverride"/> method.
|
|||
/// </param>
|
|||
/// <param name="conventions">
|
|||
/// Overriding conventions that allows the algorithm to handle external inputa
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// The measured result that containing the desired size and all the column/row lengths.
|
|||
/// </returns>
|
|||
[NotNull, Pure] |
|||
internal MeasureResult Measure(double containerLength, IReadOnlyList<LengthConvention> conventions = null) |
|||
{ |
|||
// Prepare all the variables that this method needs to use.
|
|||
conventions = conventions ?? _conventions.Select(x => x.Clone()).ToList(); |
|||
var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); |
|||
var aggregatedLength = 0.0; |
|||
double starUnitLength; |
|||
|
|||
// M2/7. Aggregate all the pixel lengths. Then we can get the remaining length by `containerLength - aggregatedLength`.
|
|||
// We mark the aggregated length as "fix" because we can completely determine their values. Same as below.
|
|||
//
|
|||
// +-----------------------------------------------------------+
|
|||
// | * | A | * | P | A | * | P | * | * |
|
|||
// +-----------------------------------------------------------+
|
|||
// |#fix#| |#fix#|
|
|||
//
|
|||
// 将全部的固定像素长度的行列长度累加。这样,containerLength - aggregatedLength 便能得到剩余长度。
|
|||
// 我们会将所有能够确定下长度的行列标记为 fix。下同。
|
|||
// 请注意:
|
|||
// - 我们并没有直接从 containerLength 一直减下去,而是使用 aggregatedLength 进行累加,是因为无穷大相减得到的是 NaN,不利于后续计算。
|
|||
|
|||
aggregatedLength += conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); |
|||
|
|||
// M3/7. Fix all the * lengths that have reached the minimum.
|
|||
//
|
|||
// +-----------------------------------------------------------+
|
|||
// | * | A | * | P | A | * | P | * | * |
|
|||
// +-----------------------------------------------------------+
|
|||
// | min | max | | | min | | min max | max |
|
|||
// | fix | |#fix#| fix |
|
|||
|
|||
var shouldTestStarMin = true; |
|||
while (shouldTestStarMin) |
|||
{ |
|||
// Calculate the unit * length to estimate the length of each column/row that has * length.
|
|||
// Under this estimated length, check if there is a minimum value that has a length less than its constraint.
|
|||
// If there is such a *, then fix the size of this cell, and then loop it again until there is no * that can be constrained by the minimum value.
|
|||
//
|
|||
// 计算单位 * 的长度,以便预估出每一个 * 行列的长度。
|
|||
// 在此预估的长度下,从前往后寻找是否存在某个 * 长度已经小于其约束的最小值。
|
|||
// 如果发现存在这样的 *,那么将此单元格的尺寸固定下来(Fix),然后循环重来,直至再也没有能被最小值约束的 *。
|
|||
var @fixed = false; |
|||
starUnitLength = (containerLength - aggregatedLength) / starCount; |
|||
foreach (var convention in conventions.Where(x => x.Length.IsStar)) |
|||
{ |
|||
var (star, min) = (convention.Length.Value, convention.MinLength); |
|||
var starLength = star * starUnitLength; |
|||
if (starLength < min) |
|||
{ |
|||
convention.Fix(min); |
|||
starLength = min; |
|||
aggregatedLength += starLength; |
|||
starCount -= star; |
|||
@fixed = true; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
shouldTestStarMin = @fixed; |
|||
} |
|||
|
|||
// M4/7. Determine the absolute pixel size of all columns/rows that have an Auto length.
|
|||
//
|
|||
// +-----------------------------------------------------------+
|
|||
// | * | A | * | P | A | * | P | * | * |
|
|||
// +-----------------------------------------------------------+
|
|||
// | min | max | | | min | | min max | max |
|
|||
// |#fix#| | fix |#fix#| fix | fix |
|
|||
|
|||
var shouldTestAuto = true; |
|||
while (shouldTestAuto) |
|||
{ |
|||
var @fixed = false; |
|||
starUnitLength = (containerLength - aggregatedLength) / starCount; |
|||
for (var i = 0; i < conventions.Count; i++) |
|||
{ |
|||
var convention = conventions[i]; |
|||
if (!convention.Length.IsAuto) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var more = ApplyAdditionalConventionsForAuto(conventions, i, starUnitLength); |
|||
convention.Fix(more); |
|||
aggregatedLength += more; |
|||
@fixed = true; |
|||
break; |
|||
} |
|||
|
|||
shouldTestAuto = @fixed; |
|||
} |
|||
|
|||
// M5/7. Expand the stars according to the additional conventions (usually the child desired length).
|
|||
// We can't fix this kind of length, so we just mark them as desired (des).
|
|||
//
|
|||
// +-----------------------------------------------------------+
|
|||
// | * | A | * | P | A | * | P | * | * |
|
|||
// +-----------------------------------------------------------+
|
|||
// | min | max | | | min | | min max | max |
|
|||
// |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#|
|
|||
|
|||
var (minLengths, desiredStarMin) = AggregateAdditionalConventionsForStars(conventions); |
|||
aggregatedLength += desiredStarMin; |
|||
|
|||
// M6/7. Determine the desired length of the grid for current container length. Its value is stored in desiredLength.
|
|||
// Assume if the container has infinite length, the grid desired length is stored in greedyDesiredLength.
|
|||
//
|
|||
// +-----------------------------------------------------------+
|
|||
// | * | A | * | P | A | * | P | * | * |
|
|||
// +-----------------------------------------------------------+
|
|||
// | min | max | | | min | | min max | max |
|
|||
// |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#|
|
|||
// Note: This table will be stored as the intermediate result into the MeasureResult and it will be reused by Arrange procedure.
|
|||
//
|
|||
// desiredLength = Math.Max(0.0, des + fix + des + fix + fix + fix + fix + des + des)
|
|||
// greedyDesiredLength = des + fix + des + fix + fix + fix + fix + des + des
|
|||
|
|||
var desiredLength = containerLength - aggregatedLength >= 0.0 ? aggregatedLength : containerLength; |
|||
var greedyDesiredLength = aggregatedLength; |
|||
|
|||
// M7/7. Expand all the rest stars. These stars have no conventions or only have
|
|||
// max value they can be expanded from zero to constraint.
|
|||
//
|
|||
// +-----------------------------------------------------------+
|
|||
// | * | A | * | P | A | * | P | * | * |
|
|||
// +-----------------------------------------------------------+
|
|||
// | min | max | | | min | | min max | max |
|
|||
// |#fix#| fix |#fix#| fix | fix | fix | fix | #fix# |#fix#|
|
|||
// Note: This table will be stored as the final result into the MeasureResult.
|
|||
|
|||
var dynamicConvention = ExpandStars(conventions, containerLength); |
|||
Clip(dynamicConvention, containerLength); |
|||
|
|||
// Returns the measuring result.
|
|||
return new MeasureResult(containerLength, desiredLength, greedyDesiredLength, |
|||
conventions, dynamicConvention, minLengths); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Run arrange procedure according to the <paramref name="measure"/> and gets the <see cref="ArrangeResult"/>.
|
|||
/// </summary>
|
|||
/// <param name="finalLength">
|
|||
/// The container length. Usually, it is the finalSize of the <see cref="Layoutable.ArrangeOverride"/> method.
|
|||
/// </param>
|
|||
/// <param name="measure">
|
|||
/// The result that the measuring procedure returns. If it is null, a new measure procedure will run.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// The measured result that containing the desired size and all the column/row length.
|
|||
/// </returns>
|
|||
[NotNull, Pure] |
|||
public ArrangeResult Arrange(double finalLength, [CanBeNull] MeasureResult measure) |
|||
{ |
|||
measure = measure ?? Measure(finalLength); |
|||
|
|||
// If the arrange final length does not equal to the measure length, we should measure again.
|
|||
if (finalLength - measure.ContainerLength > LayoutTolerance) |
|||
{ |
|||
// If the final length is larger, we will rerun the whole measure.
|
|||
measure = Measure(finalLength, measure.LeanLengthList); |
|||
} |
|||
else if (finalLength - measure.ContainerLength < -LayoutTolerance) |
|||
{ |
|||
// If the final length is smaller, we measure the M6/6 procedure only.
|
|||
var dynamicConvention = ExpandStars(measure.LeanLengthList, finalLength); |
|||
measure = new MeasureResult(finalLength, measure.DesiredLength, measure.GreedyDesiredLength, |
|||
measure.LeanLengthList, dynamicConvention, measure.MinLengths); |
|||
} |
|||
|
|||
return new ArrangeResult(measure.LengthList); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Use the <see cref="_additionalConventions"/> to calculate the fixed length of the Auto column/row.
|
|||
/// </summary>
|
|||
/// <param name="conventions">The convention list that all the * with minimum length are fixed.</param>
|
|||
/// <param name="index">The column/row index that should be fixed.</param>
|
|||
/// <param name="starUnitLength">The unit * length for the current rest length.</param>
|
|||
/// <returns>The final length of the Auto length column/row.</returns>
|
|||
[Pure] |
|||
private double ApplyAdditionalConventionsForAuto(IReadOnlyList<LengthConvention> conventions, |
|||
int index, double starUnitLength) |
|||
{ |
|||
// 1. Calculate all the * length with starUnitLength.
|
|||
// 2. Exclude all the fixed length and all the * length.
|
|||
// 3. Compare the rest of the desired length and the convention.
|
|||
// +-----------------+
|
|||
// | * | A | * |
|
|||
// +-----------------+
|
|||
// | exl | | exl |
|
|||
// |< desired >|
|
|||
// |< desired >|
|
|||
|
|||
var more = 0.0; |
|||
foreach (var additional in _additionalConventions) |
|||
{ |
|||
// If the additional convention's last column/row contains the Auto column/row, try to determine the Auto column/row length.
|
|||
if (index == additional.Index + additional.Span - 1) |
|||
{ |
|||
var min = Enumerable.Range(additional.Index, additional.Span) |
|||
.Select(x => |
|||
{ |
|||
var c = conventions[x]; |
|||
if (c.Length.IsAbsolute) return c.Length.Value; |
|||
if (c.Length.IsStar) return c.Length.Value * starUnitLength; |
|||
return 0.0; |
|||
}).Sum(); |
|||
more = Math.Max(additional.Min - min, more); |
|||
} |
|||
} |
|||
|
|||
return Math.Min(conventions[index].MaxLength, more); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Calculate the total desired length of all the * length.
|
|||
/// Bug Warning:
|
|||
/// - The behavior of this method is undefined! Different UI Frameworks have different behaviors.
|
|||
/// - We ignore all the span columns/rows and just take single cells into consideration.
|
|||
/// </summary>
|
|||
/// <param name="conventions">All the conventions that have almost been fixed except the rest *.</param>
|
|||
/// <returns>The total desired length of all the * length.</returns>
|
|||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private (List<double>, double) AggregateAdditionalConventionsForStars( |
|||
IReadOnlyList<LengthConvention> conventions) |
|||
{ |
|||
// 1. Determine all one-span column's desired widths or row's desired heights.
|
|||
// 2. Order the multi-span conventions by its last index
|
|||
// (Notice that the sorted data is much smaller than the source.)
|
|||
// 3. Determine each multi-span last index by calculating the maximum desired size.
|
|||
|
|||
// Before we determine the behavior of this method, we just aggregate the one-span * columns.
|
|||
|
|||
var fixedLength = conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); |
|||
|
|||
// Prepare a lengthList variable indicating the fixed length of each column/row.
|
|||
var lengthList = conventions.Select(x => x.Length.IsAbsolute ? x.Length.Value : 0.0).ToList(); |
|||
foreach (var group in _additionalConventions |
|||
.Where(x => x.Span == 1 && conventions[x.Index].Length.IsStar) |
|||
.ToLookup(x => x.Index)) |
|||
{ |
|||
lengthList[group.Key] = Math.Max(lengthList[group.Key], group.Max(x => x.Min)); |
|||
} |
|||
|
|||
// Now the lengthList is fixed by every one-span columns/rows.
|
|||
// Then we should determine the multi-span column's/row's length.
|
|||
foreach (var group in _additionalConventions |
|||
.Where(x => x.Span > 1) |
|||
.ToLookup(x => x.Index + x.Span - 1) |
|||
// Order the multi-span columns/rows by last index.
|
|||
.OrderBy(x => x.Key)) |
|||
{ |
|||
var length = group.Max(x => x.Min - Enumerable.Range(x.Index, x.Span - 1).Sum(r => lengthList[r])); |
|||
lengthList[group.Key] = Math.Max(lengthList[group.Key], length > 0 ? length : 0); |
|||
} |
|||
|
|||
return (lengthList, lengthList.Sum() - fixedLength); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// This method implements the last procedure (M7/7) of measure.
|
|||
/// It expands all the * length to the fixed length according to the <paramref name="constraint"/>.
|
|||
/// </summary>
|
|||
/// <param name="conventions">All the conventions that have almost been fixed except the remaining *.</param>
|
|||
/// <param name="constraint">The container length.</param>
|
|||
/// <returns>The final pixel length list.</returns>
|
|||
[Pure] |
|||
private static List<double> ExpandStars(IEnumerable<LengthConvention> conventions, double constraint) |
|||
{ |
|||
// Initial.
|
|||
var dynamicConvention = conventions.Select(x => x.Clone()).ToList(); |
|||
constraint -= dynamicConvention.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); |
|||
var starUnitLength = 0.0; |
|||
|
|||
// M6/6.
|
|||
if (constraint >= 0) |
|||
{ |
|||
var starCount = dynamicConvention.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); |
|||
|
|||
var shouldTestStarMax = true; |
|||
while (shouldTestStarMax) |
|||
{ |
|||
var @fixed = false; |
|||
starUnitLength = constraint / starCount; |
|||
foreach (var convention in dynamicConvention.Where(x => |
|||
x.Length.IsStar && !double.IsPositiveInfinity(x.MaxLength))) |
|||
{ |
|||
var (star, max) = (convention.Length.Value, convention.MaxLength); |
|||
var starLength = star * starUnitLength; |
|||
if (starLength > max) |
|||
{ |
|||
convention.Fix(max); |
|||
starLength = max; |
|||
constraint -= starLength; |
|||
starCount -= star; |
|||
@fixed = true; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
shouldTestStarMax = @fixed; |
|||
} |
|||
} |
|||
|
|||
Debug.Assert(dynamicConvention.All(x => !x.Length.IsAuto)); |
|||
|
|||
var starUnit = starUnitLength; |
|||
var result = dynamicConvention.Select(x => |
|||
{ |
|||
if (x.Length.IsStar) |
|||
{ |
|||
return double.IsInfinity(starUnit) ? double.PositiveInfinity : starUnit * x.Length.Value; |
|||
} |
|||
|
|||
return x.Length.Value; |
|||
}).ToList(); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// If the container length is not infinity. It may be not enough to contain all the columns/rows.
|
|||
/// We should clip the columns/rows that have been out of the container bounds.
|
|||
/// Note: This method may change the items value of <paramref name="lengthList"/>.
|
|||
/// </summary>
|
|||
/// <param name="lengthList">A list of all the column widths and row heights with a fixed pixel length</param>
|
|||
/// <param name="constraint">the container length. It can be positive infinity.</param>
|
|||
private static void Clip([NotNull] IList<double> lengthList, double constraint) |
|||
{ |
|||
if (double.IsInfinity(constraint)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var measureLength = 0.0; |
|||
for (var i = 0; i < lengthList.Count; i++) |
|||
{ |
|||
var length = lengthList[i]; |
|||
if (constraint - measureLength > length) |
|||
{ |
|||
measureLength += length; |
|||
} |
|||
else |
|||
{ |
|||
lengthList[i] = constraint - measureLength; |
|||
measureLength = constraint; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Contains the convention of each column/row.
|
|||
/// This is mostly the same as <see cref="RowDefinition"/> or <see cref="ColumnDefinition"/>.
|
|||
/// We use this because we can treat the column and the row the same.
|
|||
/// </summary>
|
|||
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] |
|||
internal class LengthConvention : ICloneable |
|||
{ |
|||
/// <summary>
|
|||
/// Initialize a new instance of <see cref="LengthConvention"/>.
|
|||
/// </summary>
|
|||
public LengthConvention() |
|||
{ |
|||
Length = new GridLength(1.0, GridUnitType.Star); |
|||
MinLength = 0.0; |
|||
MaxLength = double.PositiveInfinity; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initialize a new instance of <see cref="LengthConvention"/>.
|
|||
/// </summary>
|
|||
public LengthConvention(GridLength length, double minLength, double maxLength) |
|||
{ |
|||
Length = length; |
|||
MinLength = minLength; |
|||
MaxLength = maxLength; |
|||
if (length.IsAbsolute) |
|||
{ |
|||
_isFixed = true; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="GridLength"/> of a column or a row.
|
|||
/// </summary>
|
|||
internal GridLength Length { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the minimum convention for a column or a row.
|
|||
/// </summary>
|
|||
internal double MinLength { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the maximum convention for a column or a row.
|
|||
/// </summary>
|
|||
internal double MaxLength { get; } |
|||
|
|||
/// <summary>
|
|||
/// Fix the <see cref="LengthConvention"/>.
|
|||
/// If all columns/rows are fixed, we can get the size of all columns/rows in pixels.
|
|||
/// </summary>
|
|||
/// <param name="pixel">
|
|||
/// The pixel length that should be used to fix the convention.
|
|||
/// </param>
|
|||
/// <exception cref="InvalidOperationException">
|
|||
/// If the convention is pixel length, this exception will throw.
|
|||
/// </exception>
|
|||
public void Fix(double pixel) |
|||
{ |
|||
if (_isFixed) |
|||
{ |
|||
throw new InvalidOperationException("Cannot fix the length convention if it is fixed."); |
|||
} |
|||
|
|||
Length = new GridLength(pixel); |
|||
_isFixed = true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a value that indicates whether this convention is fixed.
|
|||
/// </summary>
|
|||
private bool _isFixed; |
|||
|
|||
/// <summary>
|
|||
/// Helps the debugger to display the intermediate column/row calculation result.
|
|||
/// </summary>
|
|||
private string DebuggerDisplay => |
|||
$"{(_isFixed ? Length.Value.ToString(CultureInfo.InvariantCulture) : (Length.GridUnitType == GridUnitType.Auto ? "Auto" : $"{Length.Value}*"))}, ∈[{MinLength}, {MaxLength}]"; |
|||
|
|||
/// <inheritdoc />
|
|||
object ICloneable.Clone() => Clone(); |
|||
|
|||
/// <summary>
|
|||
/// Get a deep copy of this convention list.
|
|||
/// We need this because we want to store some intermediate states.
|
|||
/// </summary>
|
|||
internal LengthConvention Clone() => new LengthConvention(Length, MinLength, MaxLength); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Contains the convention that comes from the grid children.
|
|||
/// Some children span multiple columns or rows, so even a simple column/row can have multiple conventions.
|
|||
/// </summary>
|
|||
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] |
|||
internal struct AdditionalLengthConvention |
|||
{ |
|||
/// <summary>
|
|||
/// Initialize a new instance of <see cref="AdditionalLengthConvention"/>.
|
|||
/// </summary>
|
|||
public AdditionalLengthConvention(int index, int span, double min) |
|||
{ |
|||
Index = index; |
|||
Span = span; |
|||
Min = min; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the start index of this additional convention.
|
|||
/// </summary>
|
|||
public int Index { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the span of this additional convention.
|
|||
/// </summary>
|
|||
public int Span { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the minimum length of this additional convention.
|
|||
/// This value is usually provided by the child's desired length.
|
|||
/// </summary>
|
|||
public double Min { get; } |
|||
|
|||
/// <summary>
|
|||
/// Helps the debugger to display the intermediate column/row calculation result.
|
|||
/// </summary>
|
|||
private string DebuggerDisplay => |
|||
$"{{{string.Join(",", Enumerable.Range(Index, Span))}}}, ∈[{Min},∞)"; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Stores the result of the measuring procedure.
|
|||
/// This result can be used to measure children and assign the desired size.
|
|||
/// Passing this result to <see cref="Arrange"/> can reduce calculation.
|
|||
/// </summary>
|
|||
[DebuggerDisplay("{" + nameof(LengthList) + ",nq}")] |
|||
internal class MeasureResult |
|||
{ |
|||
/// <summary>
|
|||
/// Initialize a new instance of <see cref="MeasureResult"/>.
|
|||
/// </summary>
|
|||
internal MeasureResult(double containerLength, double desiredLength, double greedyDesiredLength, |
|||
IReadOnlyList<LengthConvention> leanConventions, IReadOnlyList<double> expandedConventions, IReadOnlyList<double> minLengths) |
|||
{ |
|||
ContainerLength = containerLength; |
|||
DesiredLength = desiredLength; |
|||
GreedyDesiredLength = greedyDesiredLength; |
|||
LeanLengthList = leanConventions; |
|||
LengthList = expandedConventions; |
|||
MinLengths = minLengths; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the container length for this result.
|
|||
/// This property will be used by <see cref="Arrange"/> to determine whether to measure again or not.
|
|||
/// </summary>
|
|||
public double ContainerLength { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the desired length of this result.
|
|||
/// Just return this value as the desired size in <see cref="Layoutable.MeasureOverride"/>.
|
|||
/// </summary>
|
|||
public double DesiredLength { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the desired length if the container has infinite length.
|
|||
/// </summary>
|
|||
public double GreedyDesiredLength { get; } |
|||
|
|||
/// <summary>
|
|||
/// Contains the column/row calculation intermediate result.
|
|||
/// This value is used by <see cref="Arrange"/> for reducing repeat calculation.
|
|||
/// </summary>
|
|||
public IReadOnlyList<LengthConvention> LeanLengthList { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the length list for each column/row.
|
|||
/// </summary>
|
|||
public IReadOnlyList<double> LengthList { get; } |
|||
public IReadOnlyList<double> MinLengths { get; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Stores the result of the measuring procedure.
|
|||
/// This result can be used to arrange children and assign the render size.
|
|||
/// </summary>
|
|||
[DebuggerDisplay("{" + nameof(LengthList) + ",nq}")] |
|||
internal class ArrangeResult |
|||
{ |
|||
/// <summary>
|
|||
/// Initialize a new instance of <see cref="ArrangeResult"/>.
|
|||
/// </summary>
|
|||
internal ArrangeResult(IReadOnlyList<double> lengthList) |
|||
{ |
|||
LengthList = lengthList; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the length list for each column/row.
|
|||
/// </summary>
|
|||
public IReadOnlyList<double> LengthList { get; } |
|||
} |
|||
} |
|||
} |
|||
@ -1,651 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.ComponentModel; |
|||
using System.Diagnostics; |
|||
using System.Linq; |
|||
using System.Reactive.Disposables; |
|||
using System.Reactive.Subjects; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Controls.Utils; |
|||
using Avalonia.Layout; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Shared size scope implementation.
|
|||
/// Shares the size information between participating grids.
|
|||
/// An instance of this class is attached to every <see cref="Control"/> that has its
|
|||
/// IsSharedSizeScope property set to true.
|
|||
/// </summary>
|
|||
internal sealed class SharedSizeScopeHost : IDisposable |
|||
{ |
|||
private enum MeasurementState |
|||
{ |
|||
Invalidated, |
|||
Measuring, |
|||
Cached |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Class containing the measured rows/columns for a single grid.
|
|||
/// Monitors changes to the row/column collections as well as the SharedSizeGroup changes
|
|||
/// for the individual items in those collections.
|
|||
/// Notifies the <see cref="SharedSizeScopeHost"/> of SharedSizeGroup changes.
|
|||
/// </summary>
|
|||
private sealed class MeasurementCache : IDisposable |
|||
{ |
|||
readonly CompositeDisposable _subscriptions; |
|||
readonly Subject<(string, string, MeasurementResult)> _groupChanged = new Subject<(string, string, MeasurementResult)>(); |
|||
|
|||
public ISubject<(string oldName, string newName, MeasurementResult result)> GroupChanged => _groupChanged; |
|||
|
|||
public MeasurementCache(Grid grid) |
|||
{ |
|||
Grid = grid; |
|||
Results = grid.RowDefinitions.Cast<DefinitionBase>() |
|||
.Concat(grid.ColumnDefinitions) |
|||
.Select(d => new MeasurementResult(grid, d)) |
|||
.ToList(); |
|||
|
|||
grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged; |
|||
grid.ColumnDefinitions.CollectionChanged += DefinitionsCollectionChanged; |
|||
|
|||
|
|||
_subscriptions = new CompositeDisposable( |
|||
Disposable.Create(() => grid.RowDefinitions.CollectionChanged -= DefinitionsCollectionChanged), |
|||
Disposable.Create(() => grid.ColumnDefinitions.CollectionChanged -= DefinitionsCollectionChanged), |
|||
grid.RowDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged), |
|||
grid.ColumnDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged)); |
|||
|
|||
} |
|||
|
|||
// method to be hooked up once RowDefinitions/ColumnDefinitions collections can be replaced on a grid
|
|||
private void DefinitionsChanged(object sender, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
// route to collection changed as a Reset.
|
|||
DefinitionsCollectionChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); |
|||
} |
|||
|
|||
private void DefinitionPropertyChanged(Tuple<object, PropertyChangedEventArgs> propertyChanged) |
|||
{ |
|||
if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup)) |
|||
{ |
|||
var result = Results.Single(mr => ReferenceEquals(mr.Definition, propertyChanged.Item1)); |
|||
var oldName = result.SizeGroup?.Name; |
|||
var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup; |
|||
_groupChanged.OnNext((oldName, newName, result)); |
|||
} |
|||
} |
|||
|
|||
private void DefinitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
int offset = 0; |
|||
if (sender is ColumnDefinitions) |
|||
offset = Grid.RowDefinitions.Count; |
|||
|
|||
var newItems = e.NewItems?.OfType<DefinitionBase>().Select(db => new MeasurementResult(Grid, db)).ToList() ?? new List<MeasurementResult>(); |
|||
var oldItems = e.OldStartingIndex >= 0 |
|||
? Results.GetRange(e.OldStartingIndex + offset, e.OldItems.Count) |
|||
: new List<MeasurementResult>(); |
|||
|
|||
void NotifyNewItems() |
|||
{ |
|||
foreach (var item in newItems) |
|||
{ |
|||
if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) |
|||
continue; |
|||
|
|||
_groupChanged.OnNext((null, item.Definition.SharedSizeGroup, item)); |
|||
} |
|||
} |
|||
|
|||
void NotifyOldItems() |
|||
{ |
|||
foreach (var item in oldItems) |
|||
{ |
|||
if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) |
|||
continue; |
|||
|
|||
_groupChanged.OnNext((item.Definition.SharedSizeGroup, null, item)); |
|||
} |
|||
} |
|||
|
|||
switch (e.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
Results.InsertRange(e.NewStartingIndex + offset, newItems); |
|||
NotifyNewItems(); |
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Remove: |
|||
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); |
|||
NotifyOldItems(); |
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Move: |
|||
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); |
|||
Results.InsertRange(e.NewStartingIndex + offset, oldItems); |
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Replace: |
|||
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); |
|||
Results.InsertRange(e.NewStartingIndex + offset, newItems); |
|||
|
|||
NotifyOldItems(); |
|||
NotifyNewItems(); |
|||
|
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Reset: |
|||
oldItems = Results; |
|||
newItems = Results = Grid.RowDefinitions.Cast<DefinitionBase>() |
|||
.Concat(Grid.ColumnDefinitions) |
|||
.Select(d => new MeasurementResult(Grid, d)) |
|||
.ToList(); |
|||
NotifyOldItems(); |
|||
NotifyNewItems(); |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Updates the Results collection with Grid Measure results.
|
|||
/// </summary>
|
|||
/// <param name="rowResult">Result of the GridLayout.Measure method for the RowDefinitions in the grid.</param>
|
|||
/// <param name="columnResult">Result of the GridLayout.Measure method for the ColumnDefinitions in the grid.</param>
|
|||
public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) |
|||
{ |
|||
MeasurementState = MeasurementState.Cached; |
|||
for (int i = 0; i < Grid.RowDefinitions.Count; i++) |
|||
{ |
|||
Results[i].MeasuredResult = rowResult.LengthList[i]; |
|||
Results[i].MinLength = rowResult.MinLengths[i]; |
|||
} |
|||
|
|||
for (int i = 0; i < Grid.ColumnDefinitions.Count; i++) |
|||
{ |
|||
Results[i + Grid.RowDefinitions.Count].MeasuredResult = columnResult.LengthList[i]; |
|||
Results[i + Grid.RowDefinitions.Count].MinLength = columnResult.MinLengths[i]; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Clears the measurement cache, in preparation for the Measure pass.
|
|||
/// </summary>
|
|||
public void InvalidateMeasure() |
|||
{ |
|||
var newItems = new List<MeasurementResult>(); |
|||
var oldItems = new List<MeasurementResult>(); |
|||
|
|||
MeasurementState = MeasurementState.Invalidated; |
|||
|
|||
Results.ForEach(r => |
|||
{ |
|||
r.MeasuredResult = double.NaN; |
|||
r.SizeGroup?.Reset(); |
|||
}); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Clears the <see cref="IObservable{T}"/> subscriptions.
|
|||
/// </summary>
|
|||
public void Dispose() |
|||
{ |
|||
_subscriptions.Dispose(); |
|||
_groupChanged.OnCompleted(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="Grid"/> for which this cache has been created.
|
|||
/// </summary>
|
|||
public Grid Grid { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="MeasurementState"/> of this cache.
|
|||
/// </summary>
|
|||
public MeasurementState MeasurementState { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the list of <see cref="MeasurementResult"/> instances.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The list is a 1-1 map of the concatenation of RowDefinitions and ColumnDefinitions
|
|||
/// </remarks>
|
|||
public List<MeasurementResult> Results { get; private set; } |
|||
} |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Class containing the Measure result for a single Row/Column in a grid.
|
|||
/// </summary>
|
|||
private class MeasurementResult |
|||
{ |
|||
public MeasurementResult(Grid owningGrid, DefinitionBase definition) |
|||
{ |
|||
OwningGrid = owningGrid; |
|||
Definition = definition; |
|||
MeasuredResult = double.NaN; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="RowDefinition"/>/<see cref="ColumnDefinition"/> related to this <see cref="MeasurementResult"/>
|
|||
/// </summary>
|
|||
public DefinitionBase Definition { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the actual result of the Measure operation for this column.
|
|||
/// </summary>
|
|||
public double MeasuredResult { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the Minimum constraint for a Row/Column - relevant for star Rows/Columns in unconstrained grids.
|
|||
/// </summary>
|
|||
public double MinLength { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the <see cref="Group"/> that this result belongs to.
|
|||
/// </summary>
|
|||
public Group SizeGroup { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the Grid that is the parent of the Row/Column
|
|||
/// </summary>
|
|||
public Grid OwningGrid { get; } |
|||
|
|||
/// <summary>
|
|||
/// Calculates the effective length that this Row/Column wishes to enforce in the SharedSizeGroup.
|
|||
/// </summary>
|
|||
/// <returns>A tuple of length and the priority in the shared size group.</returns>
|
|||
public (double length, int priority) GetPriorityLength() |
|||
{ |
|||
var length = (Definition as ColumnDefinition)?.Width ?? ((RowDefinition)Definition).Height; |
|||
|
|||
if (length.IsAbsolute) |
|||
return (MeasuredResult, 1); |
|||
if (length.IsAuto) |
|||
return (MeasuredResult, 2); |
|||
if (MinLength > 0) |
|||
return (MinLength, 3); |
|||
return (MeasuredResult, 4); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Visitor class used to gather the final length for a given SharedSizeGroup.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The values are applied according to priorities defined in <see cref="MeasurementResult.GetPriorityLength"/>.
|
|||
/// </remarks>
|
|||
private class LentgthGatherer |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the final Length to be applied to every Row/Column in a SharedSizeGroup
|
|||
/// </summary>
|
|||
public double Length { get; private set; } |
|||
private int gatheredPriority = 6; |
|||
|
|||
/// <summary>
|
|||
/// Visits the <paramref name="result"/> applying the result of <see cref="MeasurementResult.GetPriorityLength"/> to its internal cache.
|
|||
/// </summary>
|
|||
/// <param name="result">The <see cref="MeasurementResult"/> instance to visit.</param>
|
|||
public void Visit(MeasurementResult result) |
|||
{ |
|||
var (length, priority) = result.GetPriorityLength(); |
|||
|
|||
if (gatheredPriority < priority) |
|||
return; |
|||
|
|||
gatheredPriority = priority; |
|||
if (gatheredPriority == priority) |
|||
{ |
|||
Length = Math.Max(length,Length); |
|||
} |
|||
else |
|||
{ |
|||
Length = length; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Representation of a SharedSizeGroup, containing Rows/Columns with the same SharedSizeGroup property value.
|
|||
/// </summary>
|
|||
private class Group |
|||
{ |
|||
private double? cachedResult; |
|||
private List<MeasurementResult> _results = new List<MeasurementResult>(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the name of the SharedSizeGroup.
|
|||
/// </summary>
|
|||
public string Name { get; } |
|||
|
|||
public Group(string name) |
|||
{ |
|||
Name = name; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the collection of the <see cref="MeasurementResult"/> instances.
|
|||
/// </summary>
|
|||
public IReadOnlyList<MeasurementResult> Results => _results; |
|||
|
|||
/// <summary>
|
|||
/// Gets the final, calculated length for all Rows/Columns in the SharedSizeGroup.
|
|||
/// </summary>
|
|||
public double CalculatedLength => (cachedResult ?? (cachedResult = Gather())).Value; |
|||
|
|||
/// <summary>
|
|||
/// Clears the previously cached result in preparation for measurement.
|
|||
/// </summary>
|
|||
public void Reset() |
|||
{ |
|||
cachedResult = null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Ads a measurement result to this group and sets it's <see cref="MeasurementResult.SizeGroup"/> property
|
|||
/// to this instance.
|
|||
/// </summary>
|
|||
/// <param name="result">The <see cref="MeasurementResult"/> to include in this group.</param>
|
|||
public void Add(MeasurementResult result) |
|||
{ |
|||
if (_results.Contains(result)) |
|||
throw new AvaloniaInternalException( |
|||
$"SharedSizeScopeHost: Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result"); |
|||
|
|||
result.SizeGroup = this; |
|||
_results.Add(result); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes the measurement result from this group and clears its <see cref="MeasurementResult.SizeGroup"/> value.
|
|||
/// </summary>
|
|||
/// <param name="result">The <see cref="MeasurementResult"/> to clear.</param>
|
|||
public void Remove(MeasurementResult result) |
|||
{ |
|||
if (!_results.Contains(result)) |
|||
throw new AvaloniaInternalException( |
|||
$"SharedSizeScopeHost: Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result"); |
|||
result.SizeGroup = null; |
|||
_results.Remove(result); |
|||
} |
|||
|
|||
|
|||
private double Gather() |
|||
{ |
|||
var visitor = new LentgthGatherer(); |
|||
|
|||
_results.ForEach(visitor.Visit); |
|||
|
|||
return visitor.Length; |
|||
} |
|||
} |
|||
|
|||
private readonly AvaloniaList<MeasurementCache> _measurementCaches = new AvaloniaList<MeasurementCache>(); |
|||
private readonly Dictionary<string, Group> _groups = new Dictionary<string, Group>(); |
|||
private bool _invalidating; |
|||
|
|||
/// <summary>
|
|||
/// Removes the SharedSizeScope and notifies all affected grids of the change.
|
|||
/// </summary>
|
|||
public void Dispose() |
|||
{ |
|||
while (_measurementCaches.Any()) |
|||
_measurementCaches[0].Grid.SharedScopeChanged(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Registers the grid in this SharedSizeScope, to be called when the grid is added to the visual tree.
|
|||
/// </summary>
|
|||
/// <param name="toAdd">The <see cref="Grid"/> to add to this scope.</param>
|
|||
internal void RegisterGrid(Grid toAdd) |
|||
{ |
|||
if (_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd))) |
|||
throw new AvaloniaInternalException("SharedSizeScopeHost: tried to register a grid twice!"); |
|||
|
|||
var cache = new MeasurementCache(toAdd); |
|||
_measurementCaches.Add(cache); |
|||
AddGridToScopes(cache); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes the registration for a grid in this SharedSizeScope.
|
|||
/// </summary>
|
|||
/// <param name="toRemove">The <see cref="Grid"/> to remove.</param>
|
|||
internal void UnegisterGrid(Grid toRemove) |
|||
{ |
|||
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove)); |
|||
if (cache == null) |
|||
throw new AvaloniaInternalException("SharedSizeScopeHost: tried to unregister a grid that wasn't registered before!"); |
|||
|
|||
_measurementCaches.Remove(cache); |
|||
RemoveGridFromScopes(cache); |
|||
cache.Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Helper method to check if a grid needs to forward its Mesure results to, and requrest Arrange results from this scope.
|
|||
/// </summary>
|
|||
/// <param name="toCheck">The <see cref="Grid"/> that should be checked.</param>
|
|||
/// <returns>True if the grid should forward its calls.</returns>
|
|||
internal bool ParticipatesInScope(Grid toCheck) |
|||
{ |
|||
return _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toCheck)) |
|||
?.Results.Any(r => r.SizeGroup != null) ?? false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Notifies the SharedSizeScope that a grid had requested its measurement to be invalidated.
|
|||
/// Forwards the same call to all affected grids in this scope.
|
|||
/// </summary>
|
|||
/// <param name="grid">The <see cref="Grid"/> that had it's Measure invalidated.</param>
|
|||
internal void InvalidateMeasure(Grid grid) |
|||
{ |
|||
// prevent stack overflow
|
|||
if (_invalidating) |
|||
return; |
|||
_invalidating = true; |
|||
|
|||
InvalidateMeasureImpl(grid); |
|||
|
|||
_invalidating = false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Updates the measurement cache with the results of the <paramref name="grid"/> measurement pass.
|
|||
/// </summary>
|
|||
/// <param name="grid">The <see cref="Grid"/> that has been measured.</param>
|
|||
/// <param name="rowResult">Measurement result for the grid's <see cref="RowDefinitions"/></param>
|
|||
/// <param name="columnResult">Measurement result for the grid's <see cref="ColumnDefinitions"/></param>
|
|||
internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) |
|||
{ |
|||
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); |
|||
if (cache == null) |
|||
throw new AvaloniaInternalException("SharedSizeScopeHost: Attempted to update measurement status for a grid that wasn't registered!"); |
|||
|
|||
cache.UpdateMeasureResult(rowResult, columnResult); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Calculates the measurement result including the impact of any SharedSizeGroups that might affect this grid.
|
|||
/// </summary>
|
|||
/// <param name="grid">The <see cref="Grid"/> that is being Arranged</param>
|
|||
/// <param name="rowResult">The <paramref name="grid"/>'s cached measurement result.</param>
|
|||
/// <param name="columnResult">The <paramref name="grid"/>'s cached measurement result.</param>
|
|||
/// <returns>Row and column measurement result updated with the SharedSizeScope constraints.</returns>
|
|||
internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) |
|||
{ |
|||
return ( |
|||
Arrange(grid.RowDefinitions, rowResult), |
|||
Arrange(grid.ColumnDefinitions, columnResult) |
|||
); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Invalidates the measure of all grids affected by the SharedSizeGroups contained within.
|
|||
/// </summary>
|
|||
/// <param name="grid">The <see cref="Grid"/> that is being invalidated.</param>
|
|||
private void InvalidateMeasureImpl(Grid grid) |
|||
{ |
|||
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); |
|||
|
|||
if (cache == null) |
|||
throw new AvaloniaInternalException( |
|||
$"SharedSizeScopeHost: InvalidateMeasureImpl - called with a grid not present in the internal cache"); |
|||
|
|||
// already invalidated the cache, early out.
|
|||
if (cache.MeasurementState == MeasurementState.Invalidated) |
|||
return; |
|||
|
|||
// we won't calculate, so we should not invalidate.
|
|||
if (!ParticipatesInScope(grid)) |
|||
return; |
|||
|
|||
cache.InvalidateMeasure(); |
|||
|
|||
// maybe there is a condition to only call arrange on some of the calls?
|
|||
grid.InvalidateMeasure(); |
|||
|
|||
// find all the scopes within the invalidated grid
|
|||
var scopeNames = cache.Results |
|||
.Where(mr => mr.SizeGroup != null) |
|||
.Select(mr => mr.SizeGroup.Name) |
|||
.Distinct(); |
|||
// find all grids related to those scopes
|
|||
var otherGrids = scopeNames.SelectMany(sn => _groups[sn].Results) |
|||
.Select(r => r.OwningGrid) |
|||
.Where(g => g.IsMeasureValid) |
|||
.Distinct(); |
|||
|
|||
// invalidate them as well
|
|||
foreach (var otherGrid in otherGrids) |
|||
{ |
|||
InvalidateMeasureImpl(otherGrid); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <see cref="IObserver{T}"/> callback notifying the scope that a <see cref="MeasurementResult"/> has changed its
|
|||
/// SharedSizeGroup
|
|||
/// </summary>
|
|||
/// <param name="change">Old and New name (either can be null) of the SharedSizeGroup, as well as the result.</param>
|
|||
private void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change) |
|||
{ |
|||
RemoveFromGroup(change.oldName, change.result); |
|||
AddToGroup(change.newName, change.result); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Handles the impact of SharedSizeGroups on the Arrange of <see cref="RowDefinitions"/>/<see cref="ColumnDefinitions"/>
|
|||
/// </summary>
|
|||
/// <param name="definitions">Rows/Columns that were measured</param>
|
|||
/// <param name="measureResult">The initial measurement result.</param>
|
|||
/// <returns>Modified measure result</returns>
|
|||
private GridLayout.MeasureResult Arrange(IReadOnlyList<DefinitionBase> definitions, GridLayout.MeasureResult measureResult) |
|||
{ |
|||
var conventions = measureResult.LeanLengthList.ToList(); |
|||
var lengths = measureResult.LengthList.ToList(); |
|||
var desiredLength = 0.0; |
|||
for (int i = 0; i < definitions.Count; i++) |
|||
{ |
|||
var definition = definitions[i]; |
|||
|
|||
// for empty SharedSizeGroups pass on unmodified result.
|
|||
if (string.IsNullOrEmpty(definition.SharedSizeGroup)) |
|||
{ |
|||
desiredLength += measureResult.LengthList[i]; |
|||
continue; |
|||
} |
|||
|
|||
var group = _groups[definition.SharedSizeGroup]; |
|||
// Length calculated over all Definitions participating in a SharedSizeGroup.
|
|||
var length = group.CalculatedLength; |
|||
|
|||
conventions[i] = new GridLayout.LengthConvention( |
|||
new GridLength(length), |
|||
measureResult.LeanLengthList[i].MinLength, |
|||
measureResult.LeanLengthList[i].MaxLength |
|||
); |
|||
lengths[i] = length; |
|||
desiredLength += length; |
|||
} |
|||
|
|||
return new GridLayout.MeasureResult( |
|||
measureResult.ContainerLength, |
|||
desiredLength, |
|||
measureResult.GreedyDesiredLength,//??
|
|||
conventions, |
|||
lengths, |
|||
measureResult.MinLengths); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Adds all measurement results for a grid to their repsective scopes.
|
|||
/// </summary>
|
|||
/// <param name="cache">The <see cref="MeasurementCache"/> for a grid to be added.</param>
|
|||
private void AddGridToScopes(MeasurementCache cache) |
|||
{ |
|||
cache.GroupChanged.Subscribe(SharedGroupChanged); |
|||
|
|||
foreach (var result in cache.Results) |
|||
{ |
|||
var scopeName = result.Definition.SharedSizeGroup; |
|||
AddToGroup(scopeName, result); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Handles adding the <see cref="MeasurementResult"/> to a SharedSizeGroup.
|
|||
/// Does nothing for empty SharedSizeGroups.
|
|||
/// </summary>
|
|||
/// <param name="scopeName">The name (can be null or empty) of the group to add the <paramref name="result"/> to.</param>
|
|||
/// <param name="result">The <see cref="MeasurementResult"/> to add to a scope.</param>
|
|||
private void AddToGroup(string scopeName, MeasurementResult result) |
|||
{ |
|||
if (string.IsNullOrEmpty(scopeName)) |
|||
return; |
|||
|
|||
if (!_groups.TryGetValue(scopeName, out var group)) |
|||
_groups.Add(scopeName, group = new Group(scopeName)); |
|||
|
|||
group.Add(result); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes all measurement results for a grid from their respective scopes.
|
|||
/// </summary>
|
|||
/// <param name="cache">The <see cref="MeasurementCache"/> for a grid to be removed.</param>
|
|||
private void RemoveGridFromScopes(MeasurementCache cache) |
|||
{ |
|||
foreach (var result in cache.Results) |
|||
{ |
|||
var scopeName = result.Definition.SharedSizeGroup; |
|||
RemoveFromGroup(scopeName, result); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Handles removing the <see cref="MeasurementResult"/> from a SharedSizeGroup.
|
|||
/// Does nothing for empty SharedSizeGroups.
|
|||
/// </summary>
|
|||
/// <param name="scopeName">The name (can be null or empty) of the group to remove the <paramref name="result"/> from.</param>
|
|||
/// <param name="result">The <see cref="MeasurementResult"/> to remove from a scope.</param>
|
|||
private void RemoveFromGroup(string scopeName, MeasurementResult result) |
|||
{ |
|||
if (string.IsNullOrEmpty(scopeName)) |
|||
return; |
|||
|
|||
if (!_groups.TryGetValue(scopeName, out var group)) |
|||
throw new AvaloniaInternalException($"SharedSizeScopeHost: The scope {scopeName} wasn't found in the shared size scope"); |
|||
|
|||
group.Remove(result); |
|||
if (!group.Results.Any()) |
|||
_groups.Remove(scopeName); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,127 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Data; |
|||
using Avalonia.LogicalTree; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Avalonia.Input.GestureRecognizers |
|||
{ |
|||
public class GestureRecognizerCollection : IReadOnlyCollection<IGestureRecognizer>, IGestureRecognizerActionsDispatcher |
|||
{ |
|||
private readonly IInputElement _inputElement; |
|||
private List<IGestureRecognizer> _recognizers; |
|||
private Dictionary<IPointer, IGestureRecognizer> _pointerGrabs; |
|||
|
|||
|
|||
public GestureRecognizerCollection(IInputElement inputElement) |
|||
{ |
|||
_inputElement = inputElement; |
|||
} |
|||
|
|||
public void Add(IGestureRecognizer recognizer) |
|||
{ |
|||
if (_recognizers == null) |
|||
{ |
|||
// We initialize the collection when the first recognizer is added
|
|||
_recognizers = new List<IGestureRecognizer>(); |
|||
_pointerGrabs = new Dictionary<IPointer, IGestureRecognizer>(); |
|||
} |
|||
|
|||
_recognizers.Add(recognizer); |
|||
recognizer.Initialize(_inputElement, this); |
|||
|
|||
// Hacks to make bindings work
|
|||
|
|||
if (_inputElement is ILogical logicalParent && recognizer is ISetLogicalParent logical) |
|||
{ |
|||
logical.SetParent(logicalParent); |
|||
if (recognizer is IStyleable styleableRecognizer |
|||
&& _inputElement is IStyleable styleableParent) |
|||
styleableRecognizer.Bind(StyledElement.TemplatedParentProperty, |
|||
styleableParent.GetObservable(StyledElement.TemplatedParentProperty)); |
|||
} |
|||
} |
|||
|
|||
static readonly List<IGestureRecognizer> s_Empty = new List<IGestureRecognizer>(); |
|||
|
|||
public IEnumerator<IGestureRecognizer> GetEnumerator() |
|||
=> _recognizers?.GetEnumerator() ?? s_Empty.GetEnumerator(); |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
|
|||
public int Count => _recognizers?.Count ?? 0; |
|||
|
|||
|
|||
internal bool HandlePointerPressed(PointerPressedEventArgs e) |
|||
{ |
|||
if (_recognizers == null) |
|||
return false; |
|||
foreach (var r in _recognizers) |
|||
{ |
|||
if(e.Handled) |
|||
break; |
|||
r.PointerPressed(e); |
|||
} |
|||
|
|||
return e.Handled; |
|||
} |
|||
|
|||
internal bool HandlePointerReleased(PointerReleasedEventArgs e) |
|||
{ |
|||
if (_recognizers == null) |
|||
return false; |
|||
if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) |
|||
{ |
|||
capture.PointerReleased(e); |
|||
} |
|||
else |
|||
foreach (var r in _recognizers) |
|||
{ |
|||
if (e.Handled) |
|||
break; |
|||
r.PointerReleased(e); |
|||
} |
|||
return e.Handled; |
|||
} |
|||
|
|||
internal bool HandlePointerMoved(PointerEventArgs e) |
|||
{ |
|||
if (_recognizers == null) |
|||
return false; |
|||
if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) |
|||
{ |
|||
capture.PointerMoved(e); |
|||
} |
|||
else |
|||
foreach (var r in _recognizers) |
|||
{ |
|||
if (e.Handled) |
|||
break; |
|||
r.PointerMoved(e); |
|||
} |
|||
return e.Handled; |
|||
} |
|||
|
|||
internal void HandlePointerCaptureLost(PointerCaptureLostEventArgs e) |
|||
{ |
|||
if (_recognizers == null) |
|||
return; |
|||
_pointerGrabs.Remove(e.Pointer); |
|||
foreach (var r in _recognizers) |
|||
{ |
|||
if(e.Handled) |
|||
break; |
|||
r.PointerCaptureLost(e); |
|||
} |
|||
} |
|||
|
|||
void IGestureRecognizerActionsDispatcher.Capture(IPointer pointer, IGestureRecognizer recognizer) |
|||
{ |
|||
pointer.Capture(_inputElement); |
|||
_pointerGrabs[pointer] = recognizer; |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
namespace Avalonia.Input.GestureRecognizers |
|||
{ |
|||
public interface IGestureRecognizer |
|||
{ |
|||
void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions); |
|||
void PointerPressed(PointerPressedEventArgs e); |
|||
void PointerReleased(PointerReleasedEventArgs e); |
|||
void PointerMoved(PointerEventArgs e); |
|||
void PointerCaptureLost(PointerCaptureLostEventArgs e); |
|||
} |
|||
|
|||
public interface IGestureRecognizerActionsDispatcher |
|||
{ |
|||
void Capture(IPointer pointer, IGestureRecognizer recognizer); |
|||
} |
|||
|
|||
public enum GestureRecognizerResult |
|||
{ |
|||
None, |
|||
Capture, |
|||
ReleaseCapture |
|||
} |
|||
} |
|||
@ -0,0 +1,183 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Input.GestureRecognizers |
|||
{ |
|||
public class ScrollGestureRecognizer |
|||
: StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise
|
|||
IGestureRecognizer |
|||
{ |
|||
private bool _scrolling; |
|||
private Point _trackedRootPoint; |
|||
private IPointer _tracking; |
|||
private IInputElement _target; |
|||
private IGestureRecognizerActionsDispatcher _actions; |
|||
private bool _canHorizontallyScroll; |
|||
private bool _canVerticallyScroll; |
|||
private int _gestureId; |
|||
|
|||
// Movement per second
|
|||
private Vector _inertia; |
|||
private ulong? _lastMoveTimestamp; |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="CanHorizontallyScroll"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanHorizontallyScrollProperty = |
|||
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>( |
|||
nameof(CanHorizontallyScroll), |
|||
o => o.CanHorizontallyScroll, |
|||
(o, v) => o.CanHorizontallyScroll = v); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="CanVerticallyScroll"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanVerticallyScrollProperty = |
|||
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>( |
|||
nameof(CanVerticallyScroll), |
|||
o => o.CanVerticallyScroll, |
|||
(o, v) => o.CanVerticallyScroll = v); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
|
|||
/// </summary>
|
|||
public bool CanHorizontallyScroll |
|||
{ |
|||
get => _canHorizontallyScroll; |
|||
set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
|
|||
/// </summary>
|
|||
public bool CanVerticallyScroll |
|||
{ |
|||
get => _canVerticallyScroll; |
|||
set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); |
|||
} |
|||
|
|||
|
|||
public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) |
|||
{ |
|||
_target = target; |
|||
_actions = actions; |
|||
} |
|||
|
|||
public void PointerPressed(PointerPressedEventArgs e) |
|||
{ |
|||
if (e.Pointer.IsPrimary && e.Pointer.Type == PointerType.Touch) |
|||
{ |
|||
EndGesture(); |
|||
_tracking = e.Pointer; |
|||
_gestureId = ScrollGestureEventArgs.GetNextFreeId();; |
|||
_trackedRootPoint = e.GetPosition(null); |
|||
} |
|||
} |
|||
|
|||
// Arbitrary chosen value, probably need to move that to platform settings or something
|
|||
private const double ScrollStartDistance = 30; |
|||
|
|||
// Pixels per second speed that is considered to be the stop of inertiall scroll
|
|||
private const double InertialScrollSpeedEnd = 5; |
|||
|
|||
public void PointerMoved(PointerEventArgs e) |
|||
{ |
|||
if (e.Pointer == _tracking) |
|||
{ |
|||
var rootPoint = e.GetPosition(null); |
|||
if (!_scrolling) |
|||
{ |
|||
if (CanHorizontallyScroll && Math.Abs(_trackedRootPoint.X - rootPoint.X) > ScrollStartDistance) |
|||
_scrolling = true; |
|||
if (CanVerticallyScroll && Math.Abs(_trackedRootPoint.Y - rootPoint.Y) > ScrollStartDistance) |
|||
_scrolling = true; |
|||
if (_scrolling) |
|||
{ |
|||
_actions.Capture(e.Pointer, this); |
|||
} |
|||
} |
|||
|
|||
if (_scrolling) |
|||
{ |
|||
var vector = _trackedRootPoint - rootPoint; |
|||
var elapsed = _lastMoveTimestamp.HasValue ? |
|||
TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) : |
|||
TimeSpan.Zero; |
|||
|
|||
_lastMoveTimestamp = e.Timestamp; |
|||
_trackedRootPoint = rootPoint; |
|||
if (elapsed.TotalSeconds > 0) |
|||
_inertia = vector / elapsed.TotalSeconds; |
|||
_target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); |
|||
e.Handled = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void PointerCaptureLost(PointerCaptureLostEventArgs e) |
|||
{ |
|||
if (e.Pointer == _tracking) EndGesture(); |
|||
} |
|||
|
|||
void EndGesture() |
|||
{ |
|||
_tracking = null; |
|||
if (_scrolling) |
|||
{ |
|||
_inertia = default; |
|||
_scrolling = false; |
|||
_target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); |
|||
_gestureId = 0; |
|||
_lastMoveTimestamp = null; |
|||
} |
|||
|
|||
} |
|||
|
|||
|
|||
public void PointerReleased(PointerReleasedEventArgs e) |
|||
{ |
|||
if (e.Pointer == _tracking && _scrolling) |
|||
{ |
|||
e.Handled = true; |
|||
if (_inertia == default |
|||
|| e.Timestamp == 0 |
|||
|| _lastMoveTimestamp == 0 |
|||
|| e.Timestamp - _lastMoveTimestamp > 200) |
|||
EndGesture(); |
|||
else |
|||
{ |
|||
var savedGestureId = _gestureId; |
|||
var st = Stopwatch.StartNew(); |
|||
var lastTime = TimeSpan.Zero; |
|||
DispatcherTimer.Run(() => |
|||
{ |
|||
// Another gesture has started, finish the current one
|
|||
if (_gestureId != savedGestureId) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var elapsedSinceLastTick = st.Elapsed - lastTime; |
|||
lastTime = st.Elapsed; |
|||
|
|||
var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds); |
|||
var distance = speed * elapsedSinceLastTick.TotalSeconds; |
|||
_target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); |
|||
|
|||
|
|||
|
|||
if (Math.Abs(speed.X) < InertialScrollSpeedEnd || Math.Abs(speed.Y) <= InertialScrollSpeedEnd) |
|||
{ |
|||
EndGesture(); |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
}, TimeSpan.FromMilliseconds(16), DispatcherPriority.Background); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
public class ScrollGestureEventArgs : RoutedEventArgs |
|||
{ |
|||
public int Id { get; } |
|||
public Vector Delta { get; } |
|||
private static int _nextId = 1; |
|||
|
|||
public static int GetNextFreeId() => _nextId++; |
|||
|
|||
public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent) |
|||
{ |
|||
Id = id; |
|||
Delta = delta; |
|||
} |
|||
} |
|||
|
|||
public class ScrollGestureEndedEventArgs : RoutedEventArgs |
|||
{ |
|||
public int Id { get; } |
|||
|
|||
public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent) |
|||
{ |
|||
Id = id; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.OpenGL |
|||
{ |
|||
public class AngleOptions |
|||
{ |
|||
public enum PlatformApi |
|||
{ |
|||
DirectX9, |
|||
DirectX11 |
|||
} |
|||
|
|||
public List<PlatformApi> AllowedPlatformApis = new List<PlatformApi> |
|||
{ |
|||
PlatformApi.DirectX9 |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.Markup.Xaml.Templates; |
|||
using ReactiveUI; |
|||
|
|||
namespace Avalonia.ReactiveUI |
|||
{ |
|||
/// <summary>
|
|||
/// AutoDataTemplateBindingHook is a binding hook that checks ItemsControls
|
|||
/// that don't have DataTemplates, and assigns a default DataTemplate that
|
|||
/// loads the View associated with each ViewModel.
|
|||
/// </summary>
|
|||
public class AutoDataTemplateBindingHook : IPropertyBindingHook |
|||
{ |
|||
private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate<object>(x => |
|||
{ |
|||
var control = new ViewModelViewHost(); |
|||
var context = control.GetObservable(Control.DataContextProperty); |
|||
control.Bind(ViewModelViewHost.ViewModelProperty, context); |
|||
control.HorizontalContentAlignment = HorizontalAlignment.Stretch; |
|||
control.VerticalContentAlignment = VerticalAlignment.Stretch; |
|||
return control; |
|||
}, |
|||
true); |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool ExecuteHook( |
|||
object source, object target, |
|||
Func<IObservedChange<object, object>[]> getCurrentViewModelProperties, |
|||
Func<IObservedChange<object, object>[]> getCurrentViewProperties, |
|||
BindingDirection direction) |
|||
{ |
|||
var viewProperties = getCurrentViewProperties(); |
|||
var lastViewProperty = viewProperties.LastOrDefault(); |
|||
var itemsControl = lastViewProperty?.Sender as ItemsControl; |
|||
if (itemsControl == null) |
|||
return true; |
|||
|
|||
var propertyName = viewProperties.Last().GetPropertyName(); |
|||
if (propertyName != "Items" && |
|||
propertyName != "ItemsSource") |
|||
return true; |
|||
|
|||
if (itemsControl.ItemTemplate != null) |
|||
return true; |
|||
|
|||
itemsControl.ItemTemplate = DefaultItemTemplate; |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using Avalonia.Animation; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Avalonia.ReactiveUI |
|||
{ |
|||
/// <summary>
|
|||
/// A ContentControl that animates the transition when its content is changed.
|
|||
/// </summary>
|
|||
public class TransitioningContentControl : ContentControl, IStyleable |
|||
{ |
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty"/> for the <see cref="PageTransition"/> property.
|
|||
/// </summary>
|
|||
public static readonly AvaloniaProperty<IPageTransition> PageTransitionProperty = |
|||
AvaloniaProperty.Register<TransitioningContentControl, IPageTransition>(nameof(PageTransition), |
|||
new CrossFade(TimeSpan.FromSeconds(0.5))); |
|||
|
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty"/> for the <see cref="DefaultContent"/> property.
|
|||
/// </summary>
|
|||
public static readonly AvaloniaProperty<object> DefaultContentProperty = |
|||
AvaloniaProperty.Register<TransitioningContentControl, object>(nameof(DefaultContent)); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the animation played when content appears and disappears.
|
|||
/// </summary>
|
|||
public IPageTransition PageTransition |
|||
{ |
|||
get => GetValue(PageTransitionProperty); |
|||
set => SetValue(PageTransitionProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the content displayed whenever there is no page currently routed.
|
|||
/// </summary>
|
|||
public object DefaultContent |
|||
{ |
|||
get => GetValue(DefaultContentProperty); |
|||
set => SetValue(DefaultContentProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the content with animation.
|
|||
/// </summary>
|
|||
public new object Content |
|||
{ |
|||
get => base.Content; |
|||
set => UpdateContentWithTransition(value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// TransitioningContentControl uses the default ContentControl
|
|||
/// template from Avalonia default theme.
|
|||
/// </summary>
|
|||
Type IStyleable.StyleKey => typeof(ContentControl); |
|||
|
|||
/// <summary>
|
|||
/// Updates the content with transitions.
|
|||
/// </summary>
|
|||
/// <param name="content">New content to set.</param>
|
|||
private async void UpdateContentWithTransition(object content) |
|||
{ |
|||
if (PageTransition != null) |
|||
await PageTransition.Start(this, null, true); |
|||
base.Content = content; |
|||
if (PageTransition != null) |
|||
await PageTransition.Start(null, this, true); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.Reactive.Disposables; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
|
|||
namespace Avalonia.ReactiveUI |
|||
{ |
|||
/// <summary>
|
|||
/// This content control will automatically load the View associated with
|
|||
/// the ViewModel property and display it. This control is very useful
|
|||
/// inside a DataTemplate to display the View associated with a ViewModel.
|
|||
/// </summary>
|
|||
public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger |
|||
{ |
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty"/> for the <see cref="ViewModel"/> property.
|
|||
/// </summary>
|
|||
public static readonly AvaloniaProperty<object> ViewModelProperty = |
|||
AvaloniaProperty.Register<ViewModelViewHost, object>(nameof(ViewModel)); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ViewModelViewHost"/> class.
|
|||
/// </summary>
|
|||
public ViewModelViewHost() |
|||
{ |
|||
this.WhenActivated(disposables => |
|||
{ |
|||
this.WhenAnyValue(x => x.ViewModel) |
|||
.Subscribe(NavigateToViewModel) |
|||
.DisposeWith(disposables); |
|||
}); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the ViewModel to display.
|
|||
/// </summary>
|
|||
public object ViewModel |
|||
{ |
|||
get => GetValue(ViewModelProperty); |
|||
set => SetValue(ViewModelProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the view locator.
|
|||
/// </summary>
|
|||
public IViewLocator ViewLocator { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Invoked when ReactiveUI router navigates to a view model.
|
|||
/// </summary>
|
|||
/// <param name="viewModel">ViewModel to which the user navigates.</param>
|
|||
private void NavigateToViewModel(object viewModel) |
|||
{ |
|||
if (viewModel == null) |
|||
{ |
|||
this.Log().Info("ViewModel is null. Falling back to default content."); |
|||
Content = DefaultContent; |
|||
return; |
|||
} |
|||
|
|||
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; |
|||
var viewInstance = viewLocator.ResolveView(viewModel); |
|||
if (viewInstance == null) |
|||
{ |
|||
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); |
|||
Content = DefaultContent; |
|||
return; |
|||
} |
|||
|
|||
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); |
|||
viewInstance.ViewModel = viewModel; |
|||
if (viewInstance is IStyledElement styled) |
|||
styled.DataContext = viewModel; |
|||
Content = viewInstance; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
/// <summary>
|
|||
/// Render timer that ticks on UI thread. Useful for debugging or bootstrapping on new platforms
|
|||
/// </summary>
|
|||
|
|||
public class UiThreadRenderTimer : DefaultRenderTimer |
|||
{ |
|||
public UiThreadRenderTimer(int framesPerSecond) : base(framesPerSecond) |
|||
{ |
|||
} |
|||
|
|||
protected override IDisposable StartCore(Action<TimeSpan> tick) |
|||
{ |
|||
bool cancelled = false; |
|||
var st = Stopwatch.StartNew(); |
|||
DispatcherTimer.Run(() => |
|||
{ |
|||
if (cancelled) |
|||
return false; |
|||
tick(st.Elapsed); |
|||
return !cancelled; |
|||
}, TimeSpan.FromSeconds(1.0 / FramesPerSecond), DispatcherPriority.Render); |
|||
return Disposable.Create(() => cancelled = true); |
|||
} |
|||
} |
|||
} |
|||
@ -1 +1 @@ |
|||
Subproject commit 1e3ffc315401f0b2eb96a0e79b25c2fc19a80d78 |
|||
Subproject commit 610cda30c69e32e83c8235060606480904c937bc |
|||
@ -1,184 +0,0 @@ |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Linq; |
|||
using Avalonia.Controls.Utils; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Controls.UnitTests |
|||
{ |
|||
public class GridLayoutTests |
|||
{ |
|||
private const double Inf = double.PositiveInfinity; |
|||
|
|||
[Theory] |
|||
[InlineData("100, 200, 300", 0d, 0d, new[] { 0d, 0d, 0d })] |
|||
[InlineData("100, 200, 300", 800d, 600d, new[] { 100d, 200d, 300d })] |
|||
[InlineData("100, 200, 300", 600d, 600d, new[] { 100d, 200d, 300d })] |
|||
[InlineData("100, 200, 300", 400d, 400d, new[] { 100d, 200d, 100d })] |
|||
public void MeasureArrange_AllPixelLength_Correct(string length, double containerLength, |
|||
double expectedDesiredLength, IList expectedLengthList) |
|||
{ |
|||
TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("*,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] |
|||
[InlineData("*,2*,3*", 600d, 0d, new[] { 100d, 200d, 300d })] |
|||
public void MeasureArrange_AllStarLength_Correct(string length, double containerLength, |
|||
double expectedDesiredLength, IList expectedLengthList) |
|||
{ |
|||
TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("100,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] |
|||
[InlineData("100,2*,3*", 600d, 100d, new[] { 100d, 200d, 300d })] |
|||
[InlineData("100,2*,3*", 100d, 100d, new[] { 100d, 0d, 0d })] |
|||
[InlineData("100,2*,3*", 50d, 50d, new[] { 50d, 0d, 0d })] |
|||
public void MeasureArrange_MixStarPixelLength_Correct(string length, double containerLength, |
|||
double expectedDesiredLength, IList expectedLengthList) |
|||
{ |
|||
TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("100,200,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] |
|||
[InlineData("100,200,Auto", 600d, 300d, new[] { 100d, 200d, 0d })] |
|||
[InlineData("100,200,Auto", 300d, 300d, new[] { 100d, 200d, 0d })] |
|||
[InlineData("100,200,Auto", 200d, 200d, new[] { 100d, 100d, 0d })] |
|||
[InlineData("100,200,Auto", 100d, 100d, new[] { 100d, 0d, 0d })] |
|||
[InlineData("100,200,Auto", 50d, 50d, new[] { 50d, 0d, 0d })] |
|||
public void MeasureArrange_MixAutoPixelLength_Correct(string length, double containerLength, |
|||
double expectedDesiredLength, IList expectedLengthList) |
|||
{ |
|||
TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("*,2*,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] |
|||
[InlineData("*,2*,Auto", 600d, 0d, new[] { 200d, 400d, 0d })] |
|||
public void MeasureArrange_MixAutoStarLength_Correct(string length, double containerLength, |
|||
double expectedDesiredLength, IList expectedLengthList) |
|||
{ |
|||
TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("*,200,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] |
|||
[InlineData("*,200,Auto", 600d, 200d, new[] { 400d, 200d, 0d })] |
|||
[InlineData("*,200,Auto", 200d, 200d, new[] { 0d, 200d, 0d })] |
|||
[InlineData("*,200,Auto", 100d, 100d, new[] { 0d, 100d, 0d })] |
|||
public void MeasureArrange_MixAutoStarPixelLength_Correct(string length, double containerLength, |
|||
double expectedDesiredLength, IList expectedLengthList) |
|||
{ |
|||
TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); |
|||
} |
|||
|
|||
|
|||
/// <summary>
|
|||
/// This is needed because Mono somehow converts double array to object array in attribute metadata
|
|||
/// </summary>
|
|||
static void AssertEqual(IList expected, IReadOnlyList<double> actual) |
|||
{ |
|||
var conv = expected.Cast<double>().ToArray(); |
|||
Assert.Equal(conv, actual); |
|||
} |
|||
|
|||
[SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local")] |
|||
private static void TestRowDefinitionsOnly(string length, double containerLength, |
|||
double expectedDesiredLength, IList expectedLengthList) |
|||
{ |
|||
// Arrange
|
|||
var layout = new GridLayout(new RowDefinitions(length)); |
|||
|
|||
// Measure - Action & Assert
|
|||
var measure = layout.Measure(containerLength); |
|||
Assert.Equal(expectedDesiredLength, measure.DesiredLength); |
|||
AssertEqual(expectedLengthList, measure.LengthList); |
|||
|
|||
// Arrange - Action & Assert
|
|||
var arrange = layout.Arrange(containerLength, measure); |
|||
AssertEqual(expectedLengthList, arrange.LengthList); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("100, 200, 300", 600d, new[] { 100d, 200d, 300d }, new[] { 100d, 200d, 300d })] |
|||
[InlineData("*,2*,3*", 0d, new[] { Inf, Inf, Inf }, new[] { 0d, 0d, 0d })] |
|||
[InlineData("100,2*,3*", 100d, new[] { 100d, Inf, Inf }, new[] { 100d, 0d, 0d })] |
|||
[InlineData("100,200,Auto", 300d, new[] { 100d, 200d, 0d }, new[] { 100d, 200d, 0d })] |
|||
[InlineData("*,2*,Auto", 0d, new[] { Inf, Inf, 0d }, new[] { 0d, 0d, 0d })] |
|||
[InlineData("*,200,Auto", 200d, new[] { Inf, 200d, 0d }, new[] { 0d, 200d, 0d })] |
|||
public void MeasureArrange_InfiniteMeasure_Correct(string length, double expectedDesiredLength, |
|||
IList expectedMeasureList, IList expectedArrangeList) |
|||
{ |
|||
// Arrange
|
|||
var layout = new GridLayout(new RowDefinitions(length)); |
|||
|
|||
// Measure - Action & Assert
|
|||
var measure = layout.Measure(Inf); |
|||
Assert.Equal(expectedDesiredLength, measure.DesiredLength); |
|||
AssertEqual(expectedMeasureList, measure.LengthList); |
|||
|
|||
// Arrange - Action & Assert
|
|||
var arrange = layout.Arrange(measure.DesiredLength, measure); |
|||
AssertEqual(expectedArrangeList, arrange.LengthList); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("Auto,*,*", new[] { 100d, 100d, 100d }, 600d, 300d, new[] { 100d, 250d, 250d })] |
|||
public void MeasureArrange_ChildHasSize_Correct(string length, |
|||
IList childLengthList, double containerLength, |
|||
double expectedDesiredLength, IList expectedLengthList) |
|||
{ |
|||
// Arrange
|
|||
var lengthList = new ColumnDefinitions(length); |
|||
var layout = new GridLayout(lengthList); |
|||
layout.AppendMeasureConventions( |
|||
Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, 1)), |
|||
x => (double)childLengthList[x]); |
|||
|
|||
// Measure - Action & Assert
|
|||
var measure = layout.Measure(containerLength); |
|||
Assert.Equal(expectedDesiredLength, measure.DesiredLength); |
|||
AssertEqual(expectedLengthList, measure.LengthList); |
|||
|
|||
// Arrange - Action & Assert
|
|||
var arrange = layout.Arrange(containerLength, measure); |
|||
AssertEqual(expectedLengthList, arrange.LengthList); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(Inf, 250d, new[] { 100d, Inf, Inf }, new[] { 100d, 50d, 100d })] |
|||
[InlineData(400d, 250d, new[] { 100d, 100d, 200d }, new[] { 100d, 100d, 200d })] |
|||
[InlineData(325d, 250d, new[] { 100d, 75d, 150d }, new[] { 100d, 75d, 150d })] |
|||
[InlineData(250d, 250d, new[] { 100d, 50d, 100d }, new[] { 100d, 50d, 100d })] |
|||
[InlineData(160d, 160d, new[] { 100d, 20d, 40d }, new[] { 100d, 20d, 40d })] |
|||
public void MeasureArrange_ChildHasSizeAndHasMultiSpan_Correct( |
|||
double containerLength, double expectedDesiredLength, |
|||
IList expectedMeasureLengthList, IList expectedArrangeLengthList) |
|||
{ |
|||
var length = "100,*,2*"; |
|||
var childLengthList = new[] { 150d, 150d, 150d }; |
|||
var spans = new[] { 1, 2, 1 }; |
|||
|
|||
// Arrange
|
|||
var lengthList = new ColumnDefinitions(length); |
|||
var layout = new GridLayout(lengthList); |
|||
layout.AppendMeasureConventions( |
|||
Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, spans[x])), |
|||
x => childLengthList[x]); |
|||
|
|||
// Measure - Action & Assert
|
|||
var measure = layout.Measure(containerLength); |
|||
Assert.Equal(expectedDesiredLength, measure.DesiredLength); |
|||
AssertEqual(expectedMeasureLengthList, measure.LengthList); |
|||
|
|||
// Arrange - Action & Assert
|
|||
var arrange = layout.Arrange( |
|||
double.IsInfinity(containerLength) ? measure.DesiredLength : containerLength, |
|||
measure); |
|||
AssertEqual(expectedArrangeLengthList, arrange.LengthList); |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -1,284 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Input; |
|||
using Avalonia.Platform; |
|||
using Avalonia.UnitTests; |
|||
|
|||
using Moq; |
|||
|
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Controls.UnitTests |
|||
{ |
|||
public class SharedSizeScopeTests |
|||
{ |
|||
public SharedSizeScopeTests() |
|||
{ |
|||
} |
|||
|
|||
[Fact] |
|||
public void All_Descendant_Grids_Are_Registered_When_Added_After_Setting_Scope() |
|||
{ |
|||
var grids = new[] { new Grid(), new Grid(), new Grid() }; |
|||
var scope = new Panel(); |
|||
scope.Children.AddRange(grids); |
|||
|
|||
var root = new TestRoot(); |
|||
root.SetValue(Grid.IsSharedSizeScopeProperty, true); |
|||
root.Child = scope; |
|||
|
|||
Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); |
|||
} |
|||
|
|||
[Fact] |
|||
public void All_Descendant_Grids_Are_Registered_When_Setting_Scope() |
|||
{ |
|||
var grids = new[] { new Grid(), new Grid(), new Grid() }; |
|||
var scope = new Panel(); |
|||
scope.Children.AddRange(grids); |
|||
|
|||
var root = new TestRoot(); |
|||
root.Child = scope; |
|||
root.SetValue(Grid.IsSharedSizeScopeProperty, true); |
|||
|
|||
Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); |
|||
} |
|||
|
|||
[Fact] |
|||
public void All_Descendant_Grids_Are_Unregistered_When_Resetting_Scope() |
|||
{ |
|||
var grids = new[] { new Grid(), new Grid(), new Grid() }; |
|||
var scope = new Panel(); |
|||
scope.Children.AddRange(grids); |
|||
|
|||
var root = new TestRoot(); |
|||
root.SetValue(Grid.IsSharedSizeScopeProperty, true); |
|||
root.Child = scope; |
|||
|
|||
Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); |
|||
root.SetValue(Grid.IsSharedSizeScopeProperty, false); |
|||
Assert.All(grids, g => Assert.False(g.HasSharedSizeScope())); |
|||
Assert.Equal(null, root.GetValue(Grid.s_sharedSizeScopeHostProperty)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Size_Is_Propagated_Between_Grids() |
|||
{ |
|||
var grids = new[] { CreateGrid("A", null),CreateGrid(("A",new GridLength(30)), (null, new GridLength()))}; |
|||
var scope = new Panel(); |
|||
scope.Children.AddRange(grids); |
|||
|
|||
var root = new TestRoot(); |
|||
root.SetValue(Grid.IsSharedSizeScopeProperty, true); |
|||
root.Child = scope; |
|||
|
|||
root.Measure(new Size(50, 50)); |
|||
root.Arrange(new Rect(new Point(), new Point(50, 50))); |
|||
Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Size_Propagation_Is_Constrained_To_Innermost_Scope() |
|||
{ |
|||
var grids = new[] { CreateGrid("A", null), CreateGrid(("A", new GridLength(30)), (null, new GridLength())) }; |
|||
var innerScope = new Panel(); |
|||
innerScope.Children.AddRange(grids); |
|||
innerScope.SetValue(Grid.IsSharedSizeScopeProperty, true); |
|||
|
|||
var outerGrid = CreateGrid(("A", new GridLength(0))); |
|||
var outerScope = new Panel(); |
|||
outerScope.Children.AddRange(new[] { outerGrid, innerScope }); |
|||
|
|||
var root = new TestRoot(); |
|||
root.SetValue(Grid.IsSharedSizeScopeProperty, true); |
|||
root.Child = outerScope; |
|||
|
|||
root.Measure(new Size(50, 50)); |
|||
root.Arrange(new Rect(new Point(), new Point(50, 50))); |
|||
Assert.Equal(0, outerGrid.ColumnDefinitions[0].ActualWidth); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Size_Is_Propagated_Between_Rows_And_Columns() |
|||
{ |
|||
var grid = new Grid |
|||
{ |
|||
ColumnDefinitions = new ColumnDefinitions("*,30"), |
|||
RowDefinitions = new RowDefinitions("*,10") |
|||
}; |
|||
|
|||
grid.ColumnDefinitions[1].SharedSizeGroup = "A"; |
|||
grid.RowDefinitions[1].SharedSizeGroup = "A"; |
|||
|
|||
var root = new TestRoot(); |
|||
root.SetValue(Grid.IsSharedSizeScopeProperty, true); |
|||
root.Child = grid; |
|||
|
|||
root.Measure(new Size(50, 50)); |
|||
root.Arrange(new Rect(new Point(), new Point(50, 50))); |
|||
Assert.Equal(30, grid.RowDefinitions[1].ActualHeight); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Size_Group_Changes_Are_Tracked() |
|||
{ |
|||
var grids = new[] { |
|||
CreateGrid((null, new GridLength(0, GridUnitType.Auto)), (null, new GridLength())), |
|||
CreateGrid(("A", new GridLength(30)), (null, new GridLength())) }; |
|||
var scope = new Panel(); |
|||
scope.Children.AddRange(grids); |
|||
|
|||
var root = new TestRoot(); |
|||
root.SetValue(Grid.IsSharedSizeScopeProperty, true); |
|||
root.Child = scope; |
|||
|
|||
root.Measure(new Size(50, 50)); |
|||
root.Arrange(new Rect(new Point(), new Point(50, 50))); |
|||
Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); |
|||
|
|||
grids[0].ColumnDefinitions[0].SharedSizeGroup = "A"; |
|||
|
|||
root.Measure(new Size(51, 51)); |
|||
root.Arrange(new Rect(new Point(), new Point(51, 51))); |
|||
Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth); |
|||
|
|||
grids[0].ColumnDefinitions[0].SharedSizeGroup = null; |
|||
|
|||
root.Measure(new Size(52, 52)); |
|||
root.Arrange(new Rect(new Point(), new Point(52, 52))); |
|||
Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Collection_Changes_Are_Tracked() |
|||
{ |
|||
var grid = CreateGrid( |
|||
("A", new GridLength(20)), |
|||
("A", new GridLength(30)), |
|||
("A", new GridLength(40)), |
|||
(null, new GridLength())); |
|||
|
|||
var scope = new Panel(); |
|||
scope.Children.Add(grid); |
|||
|
|||
var root = new TestRoot(); |
|||
root.SetValue(Grid.IsSharedSizeScopeProperty, true); |
|||
root.Child = scope; |
|||
|
|||
grid.Measure(new Size(200, 200)); |
|||
grid.Arrange(new Rect(new Point(), new Point(200, 200))); |
|||
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(40, cd.ActualWidth)); |
|||
|
|||
grid.ColumnDefinitions.RemoveAt(2); |
|||
|
|||
grid.Measure(new Size(200, 200)); |
|||
grid.Arrange(new Rect(new Point(), new Point(200, 200))); |
|||
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth)); |
|||
|
|||
grid.ColumnDefinitions.Insert(1, new ColumnDefinition { Width = new GridLength(35), SharedSizeGroup = "A" }); |
|||
|
|||
grid.Measure(new Size(200, 200)); |
|||
grid.Arrange(new Rect(new Point(), new Point(200, 200))); |
|||
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(35, cd.ActualWidth)); |
|||
|
|||
grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(10), SharedSizeGroup = "A" }; |
|||
|
|||
grid.Measure(new Size(200, 200)); |
|||
grid.Arrange(new Rect(new Point(), new Point(200, 200))); |
|||
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth)); |
|||
|
|||
grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(50), SharedSizeGroup = "A" }; |
|||
|
|||
grid.Measure(new Size(200, 200)); |
|||
grid.Arrange(new Rect(new Point(), new Point(200, 200))); |
|||
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Size_Priorities_Are_Maintained() |
|||
{ |
|||
var sizers = new List<Control>(); |
|||
var grid = CreateGrid( |
|||
("A", new GridLength(20)), |
|||
("A", new GridLength(20, GridUnitType.Auto)), |
|||
("A", new GridLength(1, GridUnitType.Star)), |
|||
("A", new GridLength(1, GridUnitType.Star)), |
|||
(null, new GridLength())); |
|||
for (int i = 0; i < 3; i++) |
|||
sizers.Add(AddSizer(grid, i, 6 + i * 6)); |
|||
var scope = new Panel(); |
|||
scope.Children.Add(grid); |
|||
|
|||
var root = new TestRoot(); |
|||
root.SetValue(Grid.IsSharedSizeScopeProperty, true); |
|||
root.Child = scope; |
|||
|
|||
grid.Measure(new Size(100, 100)); |
|||
grid.Arrange(new Rect(new Point(), new Point(100, 100))); |
|||
// all in group are equal to the first fixed column
|
|||
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(20, cd.ActualWidth)); |
|||
|
|||
grid.ColumnDefinitions[0].SharedSizeGroup = null; |
|||
|
|||
grid.Measure(new Size(100, 100)); |
|||
grid.Arrange(new Rect(new Point(), new Point(100, 100))); |
|||
// all in group are equal to width (MinWidth) of the sizer in the second column
|
|||
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 1 * 6, cd.ActualWidth)); |
|||
|
|||
grid.ColumnDefinitions[1].SharedSizeGroup = null; |
|||
|
|||
grid.Measure(new Size(double.PositiveInfinity, 100)); |
|||
grid.Arrange(new Rect(new Point(), new Point(100, 100))); |
|||
// with no constraint star columns default to the MinWidth of the sizer in the column
|
|||
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 2 * 6, cd.ActualWidth)); |
|||
} |
|||
|
|||
// grid creators
|
|||
private Grid CreateGrid(params string[] columnGroups) |
|||
{ |
|||
return CreateGrid(columnGroups.Select(s => (s, ColumnDefinition.WidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); |
|||
} |
|||
|
|||
private Grid CreateGrid(params (string name, GridLength width)[] columns) |
|||
{ |
|||
return CreateGrid(columns.Select(c => |
|||
(c.name, c.width, ColumnDefinition.MinWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); |
|||
} |
|||
|
|||
private Grid CreateGrid(params (string name, GridLength width, double minWidth)[] columns) |
|||
{ |
|||
return CreateGrid(columns.Select(c => |
|||
(c.name, c.width, c.minWidth, ColumnDefinition.MaxWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); |
|||
} |
|||
|
|||
private Grid CreateGrid(params (string name, GridLength width, double minWidth, double maxWidth)[] columns) |
|||
{ |
|||
var columnDefinitions = new ColumnDefinitions(); |
|||
|
|||
columnDefinitions.AddRange( |
|||
columns.Select(c => new ColumnDefinition |
|||
{ |
|||
SharedSizeGroup = c.name, |
|||
Width = c.width, |
|||
MinWidth = c.minWidth, |
|||
MaxWidth = c.maxWidth |
|||
}) |
|||
); |
|||
var grid = new Grid |
|||
{ |
|||
ColumnDefinitions = columnDefinitions |
|||
}; |
|||
|
|||
return grid; |
|||
} |
|||
|
|||
private Control AddSizer(Grid grid, int column, double size = 30) |
|||
{ |
|||
var ctrl = new Control { MinWidth = size, MinHeight = size }; |
|||
ctrl.SetValue(Grid.ColumnProperty,column); |
|||
grid.Children.Add(ctrl); |
|||
return ctrl; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Xunit; |
|||
|
|||
// Required to avoid InvalidOperationException sometimes thrown
|
|||
// from Splat.MemoizingMRUCache.cs which is not thread-safe.
|
|||
// Thrown when trying to access WhenActivated concurrently.
|
|||
[assembly: CollectionBehavior(DisableTestParallelization = true)] |
|||
@ -0,0 +1,116 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Xunit; |
|||
using ReactiveUI; |
|||
using Avalonia.ReactiveUI; |
|||
using Avalonia.UnitTests; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Templates; |
|||
using System.Collections.Generic; |
|||
using System.Collections.ObjectModel; |
|||
using System.Linq; |
|||
using Avalonia.VisualTree; |
|||
using Avalonia.Controls.Presenters; |
|||
using Splat; |
|||
using System.Threading.Tasks; |
|||
using System; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class AutoDataTemplateBindingHookTest |
|||
{ |
|||
public class NestedViewModel : ReactiveObject { } |
|||
|
|||
public class NestedView : ReactiveUserControl<NestedViewModel> { } |
|||
|
|||
public class ExampleViewModel : ReactiveObject |
|||
{ |
|||
public ObservableCollection<NestedViewModel> Items { get; } = new ObservableCollection<NestedViewModel>(); |
|||
} |
|||
|
|||
public class ExampleView : ReactiveUserControl<ExampleViewModel> |
|||
{ |
|||
public ItemsControl List { get; } = new ItemsControl(); |
|||
|
|||
public ExampleView() |
|||
{ |
|||
Content = List; |
|||
ViewModel = new ExampleViewModel(); |
|||
this.OneWayBind(ViewModel, x => x.Items, x => x.List.Items); |
|||
} |
|||
} |
|||
|
|||
public AutoDataTemplateBindingHookTest() |
|||
{ |
|||
Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); |
|||
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); |
|||
Locator.CurrentMutable.Register(() => new NestedView(), typeof(IViewFor<NestedViewModel>)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Apply_Data_Template_Binding_When_No_Template_Is_Set() |
|||
{ |
|||
var view = new ExampleView(); |
|||
Assert.NotNull(view.List.ItemTemplate); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Use_View_Model_View_Host_As_Data_Template() |
|||
{ |
|||
var view = new ExampleView(); |
|||
view.ViewModel.Items.Add(new NestedViewModel()); |
|||
|
|||
view.List.Template = GetTemplate(); |
|||
view.List.ApplyTemplate(); |
|||
view.List.Presenter.ApplyTemplate(); |
|||
|
|||
var child = view.List.Presenter.Panel.Children[0]; |
|||
var container = (ContentPresenter) child; |
|||
container.UpdateChild(); |
|||
|
|||
Assert.IsType<ViewModelViewHost>(container.Child); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Resolve_And_Embedd_Appropriate_View_Model() |
|||
{ |
|||
var view = new ExampleView(); |
|||
var root = new TestRoot { Child = view }; |
|||
view.ViewModel.Items.Add(new NestedViewModel()); |
|||
|
|||
view.List.Template = GetTemplate(); |
|||
view.List.ApplyTemplate(); |
|||
view.List.Presenter.ApplyTemplate(); |
|||
|
|||
var child = view.List.Presenter.Panel.Children[0]; |
|||
var container = (ContentPresenter) child; |
|||
container.UpdateChild(); |
|||
|
|||
var host = (ViewModelViewHost) container.Child; |
|||
Assert.IsType<NestedViewModel>(host.ViewModel); |
|||
Assert.IsType<NestedViewModel>(host.DataContext); |
|||
|
|||
host.DataContext = "changed context"; |
|||
Assert.IsType<string>(host.ViewModel); |
|||
Assert.IsType<string>(host.DataContext); |
|||
} |
|||
|
|||
private FuncControlTemplate GetTemplate() |
|||
{ |
|||
return new FuncControlTemplate<ItemsControl>(parent => |
|||
{ |
|||
return new Border |
|||
{ |
|||
Background = new Media.SolidColorBrush(0xffffffff), |
|||
Child = new ItemsPresenter |
|||
{ |
|||
Name = "PART_ItemsPresenter", |
|||
MemberSelector = parent.MemberSelector, |
|||
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], |
|||
} |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia.Controls; |
|||
using Avalonia.UnitTests; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class ReactiveUserControlTest |
|||
{ |
|||
public class ExampleViewModel : ReactiveObject { } |
|||
|
|||
public class ExampleView : ReactiveUserControl<ExampleViewModel> { } |
|||
|
|||
[Fact] |
|||
public void Data_Context_Should_Stay_In_Sync_With_Reactive_User_Control_View_Model() |
|||
{ |
|||
var view = new ExampleView(); |
|||
var viewModel = new ExampleViewModel(); |
|||
Assert.Null(view.ViewModel); |
|||
|
|||
view.DataContext = viewModel; |
|||
Assert.Equal(view.ViewModel, viewModel); |
|||
Assert.Equal(view.DataContext, viewModel); |
|||
|
|||
view.DataContext = null; |
|||
Assert.Null(view.ViewModel); |
|||
Assert.Null(view.DataContext); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia.Controls; |
|||
using Avalonia.UnitTests; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class ReactiveWindowTest |
|||
{ |
|||
public class ExampleViewModel : ReactiveObject { } |
|||
|
|||
public class ExampleWindow : ReactiveWindow<ExampleViewModel> { } |
|||
|
|||
[Fact] |
|||
public void Data_Context_Should_Stay_In_Sync_With_Reactive_Window_View_Model() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
{ |
|||
var view = new ExampleWindow(); |
|||
var viewModel = new ExampleViewModel(); |
|||
Assert.Null(view.ViewModel); |
|||
|
|||
view.DataContext = viewModel; |
|||
Assert.Equal(view.ViewModel, viewModel); |
|||
Assert.Equal(view.DataContext, viewModel); |
|||
|
|||
view.DataContext = null; |
|||
Assert.Null(view.ViewModel); |
|||
Assert.Null(view.DataContext); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System.Linq; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Presenters; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.UnitTests; |
|||
using Avalonia.VisualTree; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class TransitioningContentControlTest |
|||
{ |
|||
[Fact] |
|||
public void Transitioning_Control_Should_Derive_Template_From_Content_Control() |
|||
{ |
|||
var target = new TransitioningContentControl(); |
|||
var stylable = (IStyledElement)target; |
|||
Assert.Equal(typeof(ContentControl),stylable.StyleKey); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Transitioning_Control_Template_Should_Be_Instantiated() |
|||
{ |
|||
var target = new TransitioningContentControl |
|||
{ |
|||
PageTransition = null, |
|||
Template = GetTemplate(), |
|||
Content = "Foo" |
|||
}; |
|||
target.ApplyTemplate(); |
|||
((ContentPresenter)target.Presenter).UpdateChild(); |
|||
|
|||
var child = ((IVisual)target).VisualChildren.Single(); |
|||
Assert.IsType<Border>(child); |
|||
child = child.VisualChildren.Single(); |
|||
Assert.IsType<ContentPresenter>(child); |
|||
child = child.VisualChildren.Single(); |
|||
Assert.IsType<TextBlock>(child); |
|||
} |
|||
|
|||
private FuncControlTemplate GetTemplate() |
|||
{ |
|||
return new FuncControlTemplate<ContentControl>(parent => |
|||
{ |
|||
return new Border |
|||
{ |
|||
Background = new Media.SolidColorBrush(0xffffffff), |
|||
Child = new ContentPresenter |
|||
{ |
|||
Name = "PART_ContentPresenter", |
|||
[~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], |
|||
[~ContentPresenter.ContentTemplateProperty] = parent[~ContentControl.ContentTemplateProperty], |
|||
} |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia.Controls; |
|||
using Avalonia.UnitTests; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class ViewModelViewHostTest |
|||
{ |
|||
public class FirstViewModel { } |
|||
|
|||
public class FirstView : ReactiveUserControl<FirstViewModel> { } |
|||
|
|||
public class SecondViewModel : ReactiveObject { } |
|||
|
|||
public class SecondView : ReactiveUserControl<SecondViewModel> { } |
|||
|
|||
public ViewModelViewHostTest() |
|||
{ |
|||
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); |
|||
Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor<FirstViewModel>)); |
|||
Locator.CurrentMutable.Register(() => new SecondView(), typeof(IViewFor<SecondViewModel>)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ViewModelViewHost_View_Should_Stay_In_Sync_With_ViewModel() |
|||
{ |
|||
var defaultContent = new TextBlock(); |
|||
var host = new ViewModelViewHost |
|||
{ |
|||
DefaultContent = defaultContent, |
|||
PageTransition = null |
|||
}; |
|||
|
|||
var root = new TestRoot |
|||
{ |
|||
Child = host |
|||
}; |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(TextBlock), host.Content.GetType()); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
|
|||
var first = new FirstViewModel(); |
|||
host.ViewModel = first; |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(FirstView), host.Content.GetType()); |
|||
Assert.Equal(first, ((FirstView)host.Content).DataContext); |
|||
Assert.Equal(first, ((FirstView)host.Content).ViewModel); |
|||
|
|||
var second = new SecondViewModel(); |
|||
host.ViewModel = second; |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(SecondView), host.Content.GetType()); |
|||
Assert.Equal(second, ((SecondView)host.Content).DataContext); |
|||
Assert.Equal(second, ((SecondView)host.Content).ViewModel); |
|||
|
|||
host.ViewModel = null; |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(TextBlock), host.Content.GetType()); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
|
|||
host.ViewModel = first; |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(FirstView), host.Content.GetType()); |
|||
Assert.Equal(first, ((FirstView)host.Content).DataContext); |
|||
Assert.Equal(first, ((FirstView)host.Content).ViewModel); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue