committed by
GitHub
226 changed files with 9260 additions and 5777 deletions
@ -0,0 +1 @@ |
|||
open_collective: avalonia |
|||
@ -0,0 +1,99 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Input; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Immutable; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class PointersPage : Control |
|||
{ |
|||
class PointerInfo |
|||
{ |
|||
public Point Point { get; set; } |
|||
public Color Color { get; set; } |
|||
} |
|||
|
|||
private static Color[] AllColors = new[] |
|||
{ |
|||
Colors.Aqua, |
|||
Colors.Beige, |
|||
Colors.Chartreuse, |
|||
Colors.Coral, |
|||
Colors.Fuchsia, |
|||
Colors.Crimson, |
|||
Colors.Lavender, |
|||
Colors.Orange, |
|||
Colors.Orchid, |
|||
Colors.ForestGreen, |
|||
Colors.SteelBlue, |
|||
Colors.PapayaWhip, |
|||
Colors.PaleVioletRed, |
|||
Colors.Goldenrod, |
|||
Colors.Maroon, |
|||
Colors.Moccasin, |
|||
Colors.Navy, |
|||
Colors.Wheat, |
|||
Colors.Violet, |
|||
Colors.Sienna, |
|||
Colors.Indigo, |
|||
Colors.Honeydew |
|||
}; |
|||
|
|||
private Dictionary<IPointer, PointerInfo> _pointers = new Dictionary<IPointer, PointerInfo>(); |
|||
|
|||
public PointersPage() |
|||
{ |
|||
ClipToBounds = true; |
|||
} |
|||
|
|||
void UpdatePointer(PointerEventArgs e) |
|||
{ |
|||
if (!_pointers.TryGetValue(e.Pointer, out var info)) |
|||
{ |
|||
if (e.RoutedEvent == PointerMovedEvent) |
|||
return; |
|||
var colors = AllColors.Except(_pointers.Values.Select(c => c.Color)).ToArray(); |
|||
var color = colors[new Random().Next(0, colors.Length - 1)]; |
|||
_pointers[e.Pointer] = info = new PointerInfo {Color = color}; |
|||
} |
|||
|
|||
info.Point = e.GetPosition(this); |
|||
InvalidateVisual(); |
|||
} |
|||
|
|||
protected override void OnPointerPressed(PointerPressedEventArgs e) |
|||
{ |
|||
UpdatePointer(e); |
|||
e.Pointer.Capture(this); |
|||
base.OnPointerPressed(e); |
|||
} |
|||
|
|||
protected override void OnPointerMoved(PointerEventArgs e) |
|||
{ |
|||
UpdatePointer(e); |
|||
base.OnPointerMoved(e); |
|||
} |
|||
|
|||
protected override void OnPointerReleased(PointerReleasedEventArgs e) |
|||
{ |
|||
_pointers.Remove(e.Pointer); |
|||
InvalidateVisual(); |
|||
} |
|||
|
|||
public override void Render(DrawingContext context) |
|||
{ |
|||
context.FillRectangle(Brushes.Transparent, new Rect(default, Bounds.Size)); |
|||
foreach (var pt in _pointers.Values) |
|||
{ |
|||
var brush = new ImmutableSolidColorBrush(pt.Color); |
|||
context.DrawGeometry(brush, null, new EllipseGeometry(new Rect(pt.Point.X - 75, pt.Point.Y - 75, |
|||
150, 150))); |
|||
} |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -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,734 @@ |
|||
// 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.
|
|||
/// SharedSizeGroup property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<string> SharedSizeGroupProperty = |
|||
AvaloniaProperty.Register<DefinitionBase, string>(nameof(SharedSizeGroup), inherits: true); |
|||
public string SharedSizeGroup |
|||
{ |
|||
get { return (string)GetValue(SharedSizeGroupProperty); } |
|||
set { SetValue(SharedSizeGroupProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the name of the shared size group of the column or row.
|
|||
/// Callback to notify about entering model tree.
|
|||
/// </summary>
|
|||
public string SharedSizeGroup |
|||
internal void OnEnterParentTree() |
|||
{ |
|||
get { return GetValue(SharedSizeGroupProperty); } |
|||
set { SetValue(SharedSizeGroupProperty, value); } |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <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; |
|||
} |
|||
|
|||
/// <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.
|
|||
/// </remarks>
|
|||
internal static void OnIsSharedSizeScopePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if ((bool)e.NewValue) |
|||
{ |
|||
SharedSizeScope sharedStatesCollection = new SharedSizeScope(); |
|||
d.SetValue(PrivateSharedSizeScopeProperty, sharedStatesCollection); |
|||
} |
|||
else |
|||
{ |
|||
d.ClearValue(PrivateSharedSizeScopeProperty); |
|||
} |
|||
} |
|||
|
|||
/// <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; } |
|||
|
|||
internal Grid Parent { get; set; } |
|||
|
|||
/// <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); |
|||
} |
|||
|
|||
private static void OnSharedSizeGroupPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
DefinitionBase definition = (DefinitionBase)d; |
|||
|
|||
if (definition.Parent != null) |
|||
{ |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <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."); |
|||
} |
|||
|
|||
/// <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.Parent != null) |
|||
{ |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <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); } |
|||
} |
|||
|
|||
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
|
|||
|
|||
[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; |
|||
} |
|||
|
|||
// the scope this state belongs to
|
|||
private readonly SharedSizeScope _sharedSizeScope; |
|||
|
|||
// Id of the shared size group this object is servicing
|
|||
private readonly string _sharedSizeGroupId; |
|||
|
|||
// Registry of participating definitions
|
|||
private readonly List<DefinitionBase> _registry; |
|||
|
|||
// Instance event handler for layout updated event
|
|||
private readonly EventHandler _layoutUpdated; |
|||
|
|||
// Control for which layout updated event handler is registered
|
|||
private Control _layoutUpdatedHost; |
|||
|
|||
// "true" when broadcasting of invalidation is needed
|
|||
private bool _broadcastInvalidation; |
|||
|
|||
// "true" when _userSize is up to date
|
|||
private bool _userSizeValid; |
|||
|
|||
// shared state
|
|||
private GridLength _userSize; |
|||
|
|||
// shared state
|
|||
private double _minSize; |
|||
} |
|||
|
|||
/// <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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,60 @@ |
|||
// 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 |
|||
{ |
|||
public DefinitionList() |
|||
{ |
|||
ResetBehavior = ResetBehavior.Remove; |
|||
CollectionChanged += OnCollectionChanged; |
|||
} |
|||
|
|||
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,18 @@ |
|||
namespace Avalonia.Input |
|||
{ |
|||
public interface IPointer |
|||
{ |
|||
int Id { get; } |
|||
void Capture(IInputElement control); |
|||
IInputElement Captured { get; } |
|||
PointerType Type { get; } |
|||
bool IsPrimary { get; } |
|||
|
|||
} |
|||
|
|||
public enum PointerType |
|||
{ |
|||
Mouse, |
|||
Touch |
|||
} |
|||
} |
|||
@ -1,18 +1,20 @@ |
|||
// 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.VisualTree; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
public interface IPointerDevice : IInputDevice |
|||
{ |
|||
[Obsolete("Use IPointer")] |
|||
IInputElement Captured { get; } |
|||
|
|||
|
|||
[Obsolete("Use IPointer")] |
|||
void Capture(IInputElement control); |
|||
|
|||
[Obsolete("Use PointerEventArgs.GetPosition")] |
|||
Point GetPosition(IVisual relativeTo); |
|||
|
|||
void SceneInvalidated(IInputRoot root, Rect rect); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,73 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
public class Pointer : IPointer, IDisposable |
|||
{ |
|||
private static int s_NextFreePointerId = 1000; |
|||
public static int GetNextFreeId() => s_NextFreePointerId++; |
|||
|
|||
public Pointer(int id, PointerType type, bool isPrimary) |
|||
{ |
|||
Id = id; |
|||
Type = type; |
|||
IsPrimary = isPrimary; |
|||
} |
|||
|
|||
public int Id { get; } |
|||
|
|||
IInputElement FindCommonParent(IInputElement control1, IInputElement control2) |
|||
{ |
|||
if (control1 == null || control2 == null) |
|||
return null; |
|||
var seen = new HashSet<IInputElement>(control1.GetSelfAndVisualAncestors().OfType<IInputElement>()); |
|||
return control2.GetSelfAndVisualAncestors().OfType<IInputElement>().FirstOrDefault(seen.Contains); |
|||
} |
|||
|
|||
protected virtual void PlatformCapture(IInputElement element) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void Capture(IInputElement control) |
|||
{ |
|||
if (Captured != null) |
|||
Captured.DetachedFromVisualTree -= OnCaptureDetached; |
|||
var oldCapture = control; |
|||
Captured = control; |
|||
PlatformCapture(control); |
|||
if (oldCapture != null) |
|||
{ |
|||
var commonParent = FindCommonParent(control, oldCapture); |
|||
foreach (var notifyTarget in oldCapture.GetSelfAndVisualAncestors().OfType<IInputElement>()) |
|||
{ |
|||
if (notifyTarget == commonParent) |
|||
break; |
|||
notifyTarget.RaiseEvent(new PointerCaptureLostEventArgs(notifyTarget, this)); |
|||
} |
|||
} |
|||
|
|||
if (Captured != null) |
|||
Captured.DetachedFromVisualTree += OnCaptureDetached; |
|||
} |
|||
|
|||
IInputElement GetNextCapture(IVisual parent) => |
|||
parent as IInputElement ?? parent.GetVisualAncestors().OfType<IInputElement>().FirstOrDefault(); |
|||
|
|||
private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
Capture(GetNextCapture(e.Parent)); |
|||
} |
|||
|
|||
|
|||
public IInputElement Captured { get; private set; } |
|||
|
|||
public PointerType Type { get; } |
|||
public bool IsPrimary { get; } |
|||
public void Dispose() => Capture(null); |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
namespace Avalonia.Input |
|||
{ |
|||
public sealed class PointerPoint |
|||
{ |
|||
public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties) |
|||
{ |
|||
Pointer = pointer; |
|||
Position = position; |
|||
Properties = properties; |
|||
} |
|||
public IPointer Pointer { get; } |
|||
public PointerPointProperties Properties { get; } |
|||
public Point Position { get; } |
|||
} |
|||
|
|||
public sealed class PointerPointProperties |
|||
{ |
|||
public bool IsLeftButtonPressed { get; set; } |
|||
public bool IsMiddleButtonPressed { get; set; } |
|||
public bool IsRightButtonPressed { get; set; } |
|||
|
|||
public PointerPointProperties() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public PointerPointProperties(InputModifiers modifiers) |
|||
{ |
|||
IsLeftButtonPressed = modifiers.HasFlag(InputModifiers.LeftMouseButton); |
|||
IsMiddleButtonPressed = modifiers.HasFlag(InputModifiers.MiddleMouseButton); |
|||
IsRightButtonPressed = modifiers.HasFlag(InputModifiers.RightMouseButton); |
|||
} |
|||
|
|||
public MouseButton GetObsoleteMouseButton() |
|||
{ |
|||
if (IsLeftButtonPressed) |
|||
return MouseButton.Left; |
|||
if (IsMiddleButtonPressed) |
|||
return MouseButton.Middle; |
|||
if (IsRightButtonPressed) |
|||
return MouseButton.Right; |
|||
return MouseButton.None; |
|||
} |
|||
} |
|||
} |
|||
@ -1,10 +1,22 @@ |
|||
// 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.Interactivity; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
public class PointerWheelEventArgs : PointerEventArgs |
|||
{ |
|||
public Vector Delta { get; set; } |
|||
|
|||
public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, |
|||
Point rootVisualPosition, ulong timestamp, |
|||
PointerPointProperties properties, InputModifiers modifiers, Vector delta) |
|||
: base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, |
|||
timestamp, properties, modifiers) |
|||
{ |
|||
Delta = delta; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,15 @@ |
|||
namespace Avalonia.Input.Raw |
|||
{ |
|||
public class RawTouchEventArgs : RawPointerEventArgs |
|||
{ |
|||
public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root, |
|||
RawPointerEventType type, Point position, InputModifiers inputModifiers, |
|||
long touchPointId) |
|||
: base(device, timestamp, root, type, position, inputModifiers) |
|||
{ |
|||
TouchPointId = touchPointId; |
|||
} |
|||
|
|||
public long TouchPointId { get; set; } |
|||
} |
|||
} |
|||
@ -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,74 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
/// <summary>
|
|||
/// Handles raw touch events
|
|||
/// <remarks>
|
|||
/// This class is supposed to be used on per-toplevel basis, don't use a shared one
|
|||
/// </remarks>
|
|||
/// </summary>
|
|||
public class TouchDevice : IInputDevice |
|||
{ |
|||
Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>(); |
|||
|
|||
static InputModifiers GetModifiers(InputModifiers modifiers, bool left) |
|||
{ |
|||
var mask = (InputModifiers)0x7fffffff ^ InputModifiers.LeftMouseButton ^ InputModifiers.MiddleMouseButton ^ |
|||
InputModifiers.RightMouseButton; |
|||
modifiers &= mask; |
|||
if (left) |
|||
modifiers |= InputModifiers.LeftMouseButton; |
|||
return modifiers; |
|||
} |
|||
|
|||
public void ProcessRawEvent(RawInputEventArgs ev) |
|||
{ |
|||
var args = (RawTouchEventArgs)ev; |
|||
if (!_pointers.TryGetValue(args.TouchPointId, out var pointer)) |
|||
{ |
|||
if (args.Type == RawPointerEventType.TouchEnd) |
|||
return; |
|||
var hit = args.Root.InputHitTest(args.Position); |
|||
|
|||
_pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), |
|||
PointerType.Touch, _pointers.Count == 0); |
|||
pointer.Capture(hit); |
|||
} |
|||
|
|||
|
|||
var target = pointer.Captured ?? args.Root; |
|||
if (args.Type == RawPointerEventType.TouchBegin) |
|||
{ |
|||
target.RaiseEvent(new PointerPressedEventArgs(target, pointer, |
|||
args.Root, args.Position, ev.Timestamp, |
|||
new PointerPointProperties(GetModifiers(args.InputModifiers, pointer.IsPrimary)), |
|||
GetModifiers(args.InputModifiers, false))); |
|||
} |
|||
|
|||
if (args.Type == RawPointerEventType.TouchEnd) |
|||
{ |
|||
_pointers.Remove(args.TouchPointId); |
|||
using (pointer) |
|||
{ |
|||
target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, |
|||
args.Root, args.Position, ev.Timestamp, |
|||
new PointerPointProperties(GetModifiers(args.InputModifiers, false)), |
|||
GetModifiers(args.InputModifiers, pointer.IsPrimary), |
|||
pointer.IsPrimary ? MouseButton.Left : MouseButton.None)); |
|||
} |
|||
} |
|||
|
|||
if (args.Type == RawPointerEventType.TouchUpdate) |
|||
{ |
|||
var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary); |
|||
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, |
|||
args.Position, ev.Timestamp, new PointerPointProperties(modifiers), modifiers)); |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue