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.
|
// This source file is adapted from the Windows Presentation Foundation project.
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
// (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 |
namespace Avalonia.Controls |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Base class for <see cref="ColumnDefinition"/> and <see cref="RowDefinition"/>.
|
/// DefinitionBase provides core functionality used internally by Grid
|
||||
|
/// and ColumnDefinitionCollection / RowDefinitionCollection
|
||||
/// </summary>
|
/// </summary>
|
||||
public class DefinitionBase : AvaloniaObject |
public abstract class DefinitionBase : AvaloniaObject |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Defines the <see cref="SharedSizeGroup"/> property.
|
/// SharedSizeGroup property.
|
||||
/// </summary>
|
/// </summary>
|
||||
public static readonly StyledProperty<string> SharedSizeGroupProperty = |
public string SharedSizeGroup |
||||
AvaloniaProperty.Register<DefinitionBase, string>(nameof(SharedSizeGroup), inherits: true); |
{ |
||||
|
get { return (string)GetValue(SharedSizeGroupProperty); } |
||||
|
set { SetValue(SharedSizeGroupProperty, value); } |
||||
|
} |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Gets or sets the name of the shared size group of the column or row.
|
/// Callback to notify about entering model tree.
|
||||
/// </summary>
|
/// </summary>
|
||||
public string SharedSizeGroup |
internal void OnEnterParentTree() |
||||
{ |
{ |
||||
get { return GetValue(SharedSizeGroupProperty); } |
this.InheritanceParent = Parent; |
||||
set { SetValue(SharedSizeGroupProperty, value); } |
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.
|
// 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.
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
using Avalonia.VisualTree; |
using Avalonia.VisualTree; |
||||
|
|
||||
namespace Avalonia.Input |
namespace Avalonia.Input |
||||
{ |
{ |
||||
public interface IPointerDevice : IInputDevice |
public interface IPointerDevice : IInputDevice |
||||
{ |
{ |
||||
|
[Obsolete("Use IPointer")] |
||||
IInputElement Captured { get; } |
IInputElement Captured { get; } |
||||
|
|
||||
|
[Obsolete("Use IPointer")] |
||||
void Capture(IInputElement control); |
void Capture(IInputElement control); |
||||
|
|
||||
|
[Obsolete("Use PointerEventArgs.GetPosition")] |
||||
Point GetPosition(IVisual relativeTo); |
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.
|
// 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.
|
// 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 |
namespace Avalonia.Input |
||||
{ |
{ |
||||
public class PointerWheelEventArgs : PointerEventArgs |
public class PointerWheelEventArgs : PointerEventArgs |
||||
{ |
{ |
||||
public Vector Delta { get; set; } |
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