From fca777bf91794d253165c07d554698a5e6629854 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Jul 2020 18:21:45 -0300 Subject: [PATCH] fixes for relativepanel --- src/Avalonia.Controls/RelativePanel.cs | 541 +++++++++++++++++-------- 1 file changed, 374 insertions(+), 167 deletions(-) diff --git a/src/Avalonia.Controls/RelativePanel.cs b/src/Avalonia.Controls/RelativePanel.cs index 07137e3d1a..a5743f4153 100644 --- a/src/Avalonia.Controls/RelativePanel.cs +++ b/src/Avalonia.Controls/RelativePanel.cs @@ -8,35 +8,61 @@ using Avalonia.Layout; namespace Avalonia.Controls { + public static partial class Extensions + { + /// + /// Returns a value that indicates whether the specified value is not a number (). + /// + /// A double-precision floating-point number. + /// true if evaluates to ; otherwise, false. + public static bool IsNaN(this double d) + { + return double.IsNaN(d); + } + + public static IEnumerable Do(this IEnumerable source, Action predicate) + { + var enumerable = source as IList ?? source.ToList(); + foreach (var item in enumerable) + { + predicate.Invoke(item); + } + + return enumerable; + } + } + public partial class RelativePanel : Panel { private readonly Graph _childGraph; public RelativePanel() => _childGraph = new Graph(); + - protected override Size MeasureOverride(Size availableSize) + private Layoutable? GetDependencyElement(AvaloniaProperty property, AvaloniaObject child) { - var maxSize = new Size(); + var dependency = child.GetValue(property); - foreach (var child in Children.OfType()) + if (dependency is Layoutable layoutable) { - child.Measure(availableSize); - maxSize = maxSize.WithWidth(Math.Max(maxSize.Width, child.DesiredSize.Width)); - maxSize = maxSize.WithHeight(Math.Max(maxSize.Height, child.DesiredSize.Height)); + if (Children.Contains((ILayoutable)layoutable)) + return layoutable; + + throw new ArgumentException($"RelativePanel error: Element does not exist in the current context: {property.Name}"); } - return maxSize; + return null; } - protected override Size ArrangeOverride(Size arrangeSize) + protected override Size MeasureOverride(Size availableSize) { - _childGraph.Reset(arrangeSize); + #region Calc DesiredSize - foreach (var child in Children.OfType()) + _childGraph.Clear(); + foreach (Layoutable child in Children) { if (child == null) continue; - var node = _childGraph.AddNode(child); node.AlignLeftWithNode = _childGraph.AddLink(node, GetDependencyElement(AlignLeftWithProperty, child)); @@ -50,106 +76,175 @@ namespace Avalonia.Controls node.BelowNode = _childGraph.AddLink(node, GetDependencyElement(BelowProperty, child)); node.AlignHorizontalCenterWith = _childGraph.AddLink(node, GetDependencyElement(AlignHorizontalCenterWithProperty, child)); - node.AlignVerticalCenterWith = _childGraph.AddLink(node, GetDependencyElement(AlignVerticalCenterWithProperty, child)); - } + node.AlignVerticalCenterWith = _childGraph.AddLink(node, GetDependencyElement(AlignVerticalCenterWithProperty, child)); - if (_childGraph.CheckCyclic()) - { - throw new Exception("RelativePanel error: Circular dependency detected. Layout could not complete."); } + _childGraph.Measure(availableSize); - var size = new Size(); + #endregion - foreach (var child in Children) - { - if (child.Bounds.Bottom > size.Height) - { - size = size.WithHeight(child.Bounds.Bottom); - } + #region Calc AvailableSize - if (child.Bounds.Right > size.Width) - { - size = size.WithWidth(child.Bounds.Right); - } - } + _childGraph.Reset(); + var boundingSize = _childGraph.GetBoundingSize(Width.IsNaN(), Height.IsNaN()); + _childGraph.Reset(); + _childGraph.Measure(boundingSize); + return boundingSize; - if (VerticalAlignment == VerticalAlignment.Stretch) - { - size = size.WithHeight(arrangeSize.Height); - } - - if (HorizontalAlignment == HorizontalAlignment.Stretch) - { - size = size.WithWidth(arrangeSize.Width); - } + #endregion + } - return size; + protected override Size ArrangeOverride(Size arrangeSize) + { + _childGraph.GetNodes().Do(node => node.Arrange(arrangeSize)); + return arrangeSize; } - private Layoutable? GetDependencyElement(AvaloniaProperty property, AvaloniaObject child) + private class GraphNode { - var dependency = child.GetValue(property); + public bool Measured { get; set; } - if (dependency is Layoutable layoutable) - { - if (Children.Contains((ILayoutable)layoutable)) - return layoutable; + public Layoutable Element { get; } - throw new ArgumentException($"RelativePanel error: Element does not exist in the current context: {property.Name}"); - } + private bool HorizontalOffsetFlag { get; set; } - return null; - } + private bool VerticalOffsetFlag { get; set; } - private class GraphNode - { - public Point Position { get; set; } + private Size BoundingSize { get; set; } - public bool Arranged { get; set; } + public Size OriginDesiredSize { get; set; } - public Layoutable Element { get; } + public double Left { get; set; } = double.NaN; + + public double Top { get; set; } = double.NaN; + + public double Right { get; set; } = double.NaN; + + public double Bottom { get; set; } = double.NaN; public HashSet OutgoingNodes { get; } - public GraphNode? AlignLeftWithNode { get; set; } + public GraphNode AlignLeftWithNode { get; set; } - public GraphNode? AlignTopWithNode { get; set; } + public GraphNode AlignTopWithNode { get; set; } - public GraphNode? AlignRightWithNode { get; set; } + public GraphNode AlignRightWithNode { get; set; } - public GraphNode? AlignBottomWithNode { get; set; } + public GraphNode AlignBottomWithNode { get; set; } - public GraphNode? LeftOfNode { get; set; } + public GraphNode LeftOfNode { get; set; } - public GraphNode? AboveNode { get; set; } + public GraphNode AboveNode { get; set; } - public GraphNode? RightOfNode { get; set; } + public GraphNode RightOfNode { get; set; } - public GraphNode? BelowNode { get; set; } + public GraphNode BelowNode { get; set; } - public GraphNode? AlignHorizontalCenterWith { get; set; } + public GraphNode AlignHorizontalCenterWith { get; set; } - public GraphNode? AlignVerticalCenterWith { get; set; } + public GraphNode AlignVerticalCenterWith { get; set; } public GraphNode(Layoutable element) { OutgoingNodes = new HashSet(); Element = element; } + + public void Arrange(Size arrangeSize) => Element.Arrange(new Rect(Left, Top, Math.Max(arrangeSize.Width - Left - Right, 0), Math.Max(arrangeSize.Height - Top - Bottom, 0))); + + public void Reset() + { + Left = double.NaN; + Top = double.NaN; + Right = double.NaN; + Bottom = double.NaN; + Measured = false; + } + + public Size GetBoundingSize() + { + if (Measured) + return BoundingSize; + + if (!OutgoingNodes.Any()) + { + BoundingSize = Element.DesiredSize; + Measured = true; + } + else + { + BoundingSize = GetBoundingSize(this, Element.DesiredSize, OutgoingNodes); + Measured = true; + } + + return BoundingSize; + } + + private static Size GetBoundingSize(GraphNode prevNode, Size prevSize, IEnumerable nodes) + { + foreach (var node in nodes) + { + if (node.Measured || !node.OutgoingNodes.Any()) + { + if (prevNode.LeftOfNode != null && prevNode.LeftOfNode == node || + prevNode.RightOfNode != null && prevNode.RightOfNode == node) + { + prevSize = prevSize.WithWidth(prevSize.Width + node.BoundingSize.Width); + if (GetAlignHorizontalCenterWithPanel(node.Element) || node.HorizontalOffsetFlag) + { + prevSize = prevSize.WithWidth(prevSize.Width + prevNode.OriginDesiredSize.Width); + prevNode.HorizontalOffsetFlag = true; + } + if (node.VerticalOffsetFlag) + { + prevNode.VerticalOffsetFlag = true; + } + } + + if (prevNode.AboveNode != null && prevNode.AboveNode == node || + prevNode.BelowNode != null && prevNode.BelowNode == node) + { + prevSize = prevSize.WithHeight(prevSize.Height + node.BoundingSize.Height); + if (GetAlignVerticalCenterWithPanel(node.Element) || node.VerticalOffsetFlag) + { + prevSize = prevSize.WithHeight(prevSize.Height + node.OriginDesiredSize.Height); + prevNode.VerticalOffsetFlag = true; + } + if (node.HorizontalOffsetFlag) + { + prevNode.HorizontalOffsetFlag = true; + } + } + } + else + { + return GetBoundingSize(node, prevSize, node.OutgoingNodes); + } + } + + return prevSize; + } } private class Graph { private readonly Dictionary _nodeDic; - private Size _arrangeSize; + private Size AvailableSize { get; set; } - public Graph() + public Graph() => _nodeDic = new Dictionary(); + + public IEnumerable GetNodes() => _nodeDic.Values; + + public void Clear() { - _nodeDic = new Dictionary(); + AvailableSize = new Size(); + _nodeDic.Clear(); } - public GraphNode? AddLink(GraphNode from, Layoutable? to) + public void Reset() => _nodeDic.Values.Do(node => node.Reset()); + + public GraphNode AddLink(GraphNode from, Layoutable to) { if (to == null) return null; @@ -181,183 +276,295 @@ namespace Avalonia.Controls return _nodeDic[value]; } - public void Reset(Size arrangeSize) + public void Measure(Size availableSize) { - _arrangeSize = arrangeSize; - _nodeDic.Clear(); + AvailableSize = availableSize; + Measure(_nodeDic.Values, null); } - public bool CheckCyclic() => CheckCyclic(_nodeDic.Values, null, null); - - private bool CheckCyclic(IEnumerable nodes, GraphNode? waitNode, HashSet? set) + private void Measure(IEnumerable nodes, HashSet set) { - if (set == null) - { - set = new HashSet(); - } + set ??= new HashSet(); foreach (var node in nodes) { - if (!node.Arranged && node.OutgoingNodes.Count == 0) + /* + * 该节点无任何依赖,所以从这里开始计算元素位置。 + * 因为无任何依赖,所以忽略同级元素 + */ + if (!node.Measured && !node.OutgoingNodes.Any()) { - ArrangeChild(node, true); + MeasureChild(node); continue; } - if (node.OutgoingNodes.All(item => item.Arranged)) + // 判断依赖元素是否全部排列完毕 + if (node.OutgoingNodes.All(item => item.Measured)) { - ArrangeChild(node); + MeasureChild(node); continue; } + // 判断是否有循环 if (!set.Add(node.Element)) - return true; + throw new Exception("RelativePanel error: Circular dependency detected. Layout could not complete."); - return CheckCyclic(node.OutgoingNodes, node.Arranged ? null : node, set); - } + // 没有循环,且有依赖,则继续往下 + Measure(node.OutgoingNodes, set); - if (waitNode != null) - { - ArrangeChild(waitNode); + if (!node.Measured) + { + MeasureChild(node); + } } - return false; } - private void ArrangeChild(GraphNode node, bool ignoneSibling = false) + private void MeasureChild(GraphNode node) { var child = node.Element; - var childSize = child.DesiredSize; - var childPos = new Point(); + child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + node.OriginDesiredSize = child.DesiredSize; - if (GetAlignHorizontalCenterWithPanel(child)) + var alignLeftWithPanel = GetAlignLeftWithPanel(child); + var alignTopWithPanel = GetAlignTopWithPanel(child); + var alignRightWithPanel = GetAlignRightWithPanel(child); + var alignBottomWithPanel = GetAlignBottomWithPanel(child); + + #region Panel alignment + + if (alignLeftWithPanel) + node.Left = 0; + if (alignTopWithPanel) + node.Top = 0; + if (alignRightWithPanel) + node.Right = 0; + if (alignBottomWithPanel) + node.Bottom = 0; + + #endregion + + #region Sibling alignment + + if (node.AlignLeftWithNode != null) { - childPos = childPos.WithX((_arrangeSize.Width - childSize.Width) / 2); + node.Left = node.Left.IsNaN() ? node.AlignLeftWithNode.Left : node.AlignLeftWithNode.Left * 0.5; } - if (GetAlignVerticalCenterWithPanel(child)) + if (node.AlignTopWithNode != null) { - childPos = childPos.WithY((_arrangeSize.Height - childSize.Height) / 2); + node.Top = node.Top.IsNaN() ? node.AlignTopWithNode.Top : node.AlignTopWithNode.Top * 0.5; } - var alignLeftWithPanel = GetAlignLeftWithPanel(child); - var alignTopWithPanel = GetAlignTopWithPanel(child); - var alignRightWithPanel = GetAlignRightWithPanel(child); - var alignBottomWithPanel = GetAlignBottomWithPanel(child); + if (node.AlignRightWithNode != null) + { + node.Right = node.Right.IsNaN() + ? node.AlignRightWithNode.Right + : node.AlignRightWithNode.Right * 0.5; + } - if (!ignoneSibling) + if (node.AlignBottomWithNode != null) { - if (node.LeftOfNode != null) - { - childPos = childPos.WithX(node.LeftOfNode.Position.X - childSize.Width); - } + node.Bottom = node.Bottom.IsNaN() + ? node.AlignBottomWithNode.Bottom + : node.AlignBottomWithNode.Bottom * 0.5; + } - if (node.AboveNode != null) - { - childPos = childPos.WithY(node.AboveNode.Position.Y - childSize.Height); - } + #endregion + + #region Measure + + var availableHeight = AvailableSize.Height - node.Top - node.Bottom; + if (availableHeight.IsNaN()) + { + availableHeight = AvailableSize.Height; - if (node.RightOfNode != null) + if (!node.Top.IsNaN() && node.Bottom.IsNaN()) { - childPos = childPos.WithX(node.RightOfNode.Position.X + node.RightOfNode.Element.DesiredSize.Width); + availableHeight -= node.Top; } - - if (node.BelowNode != null) + else if (node.Top.IsNaN() && !node.Bottom.IsNaN()) { - childPos = childPos.WithY(node.BelowNode.Position.Y + node.BelowNode.Element.DesiredSize.Height); + availableHeight -= node.Bottom; } + } - if (node.AlignHorizontalCenterWith != null) + var availableWidth = AvailableSize.Width - node.Left - node.Right; + if (availableWidth.IsNaN()) + { + availableWidth = AvailableSize.Width; + + if (!node.Left.IsNaN() && node.Right.IsNaN()) { - childPos = childPos.WithX(node.AlignHorizontalCenterWith.Position.X + - (node.AlignHorizontalCenterWith.Element.DesiredSize.Width - childSize.Width) / 2); + availableWidth -= node.Left; } - - if (node.AlignVerticalCenterWith != null) + else if (node.Left.IsNaN() && !node.Right.IsNaN()) { - childPos = childPos.WithY(node.AlignVerticalCenterWith.Position.Y + - (node.AlignVerticalCenterWith.Element.DesiredSize.Height - childSize.Height) / 2); + availableWidth -= node.Right; } + } + + child.Measure(new Size(Math.Max(availableWidth, 0), Math.Max(availableHeight, 0))); + var childSize = child.DesiredSize; + + #endregion - if (node.AlignLeftWithNode != null) + #region Sibling positional + + if (node.LeftOfNode != null && node.Left.IsNaN()) + { + node.Left = node.LeftOfNode.Left - childSize.Width; + } + + if (node.AboveNode != null && node.Top.IsNaN()) + { + node.Top = node.AboveNode.Top - childSize.Height; + } + + if (node.RightOfNode != null) + { + if (node.Right.IsNaN()) { - childPos = childPos.WithX(node.AlignLeftWithNode.Position.X); + node.Right = node.RightOfNode.Right - childSize.Width; } - if (node.AlignTopWithNode != null) + if (node.Left.IsNaN()) { - childPos = childPos.WithY(node.AlignTopWithNode.Position.Y); + node.Left = AvailableSize.Width - node.RightOfNode.Right; } + } - if (node.AlignRightWithNode != null) + if (node.BelowNode != null) + { + if (node.Bottom.IsNaN()) { - childPos = childPos.WithX(node.AlignRightWithNode.Element.DesiredSize.Width + node.AlignRightWithNode.Position.X - childSize.Width); + node.Bottom = node.BelowNode.Bottom - childSize.Height; } - if (node.AlignBottomWithNode != null) + if (node.Top.IsNaN()) { - childPos = childPos.WithY(node.AlignBottomWithNode.Element.DesiredSize.Height + node.AlignBottomWithNode.Position.Y - childSize.Height); + node.Top = AvailableSize.Height - node.BelowNode.Bottom; } } - if (alignLeftWithPanel) + #endregion + + #region Sibling-center alignment + + if (node.AlignHorizontalCenterWith != null) { - if (node.AlignRightWithNode != null) - { - childPos = childPos.WithX((node.AlignRightWithNode.Element.DesiredSize.Width + node.AlignRightWithNode.Position.X - childSize.Width) / 2); - } + var halfWidthLeft = (AvailableSize.Width + node.AlignHorizontalCenterWith.Left - node.AlignHorizontalCenterWith.Right - childSize.Width) * 0.5; + var halfWidthRight = (AvailableSize.Width - node.AlignHorizontalCenterWith.Left + node.AlignHorizontalCenterWith.Right - childSize.Width) * 0.5; + + if (node.Left.IsNaN()) + node.Left = halfWidthLeft; else - { - childPos = childPos.WithX(0); - } + node.Left = (node.Left + halfWidthLeft) * 0.5; + + if (node.Right.IsNaN()) + node.Right = halfWidthRight; + else + node.Right = (node.Right + halfWidthRight) * 0.5; } - if (alignTopWithPanel) + if (node.AlignVerticalCenterWith != null) { - if (node.AlignBottomWithNode != null) - { - childPos = childPos.WithY((node.AlignBottomWithNode.Element.DesiredSize.Height + node.AlignBottomWithNode.Position.Y - childSize.Height) / 2); - } + var halfHeightTop = (AvailableSize.Height + node.AlignVerticalCenterWith.Top - node.AlignVerticalCenterWith.Bottom - childSize.Height) * 0.5; + var halfHeightBottom = (AvailableSize.Height - node.AlignVerticalCenterWith.Top + node.AlignVerticalCenterWith.Bottom - childSize.Height) * 0.5; + + if (node.Top.IsNaN()) + node.Top = halfHeightTop; else - { - childPos = childPos.WithY(0); - } + node.Top = (node.Top + halfHeightTop) * 0.5; + + if (node.Bottom.IsNaN()) + node.Bottom = halfHeightBottom; + else + node.Bottom = (node.Bottom + halfHeightBottom) * 0.5; } - if (alignRightWithPanel) + #endregion + + #region Panel-center alignment + + if (GetAlignHorizontalCenterWithPanel(child)) { - if (alignLeftWithPanel) - { - childPos = childPos.WithX((_arrangeSize.Width - childSize.Width) / 2); - } - else if (node.AlignLeftWithNode == null) - { - childPos = childPos.WithX(_arrangeSize.Width - childSize.Width); - } + var halfSubWidth = (AvailableSize.Width - childSize.Width) * 0.5; + + if (node.Left.IsNaN()) + node.Left = halfSubWidth; else - { - childPos = childPos.WithX((_arrangeSize.Width + node.AlignLeftWithNode.Position.X - childSize.Width) / 2); - } + node.Left = (node.Left + halfSubWidth) * 0.5; + + if (node.Right.IsNaN()) + node.Right = halfSubWidth; + else + node.Right = (node.Right + halfSubWidth) * 0.5; } - if (alignBottomWithPanel) + if (GetAlignVerticalCenterWithPanel(child)) { - if (alignTopWithPanel) - { - childPos = childPos.WithY((_arrangeSize.Height - childSize.Height) / 2); - } - else if (node.AlignTopWithNode == null) + var halfSubHeight = (AvailableSize.Height - childSize.Height) * 0.5; + + if (node.Top.IsNaN()) + node.Top = halfSubHeight; + else + node.Top = (node.Top + halfSubHeight) * 0.5; + + if (node.Bottom.IsNaN()) + node.Bottom = halfSubHeight; + else + node.Bottom = (node.Bottom + halfSubHeight) * 0.5; + } + + #endregion + + if (node.Left.IsNaN()) + { + if (!node.Right.IsNaN()) + node.Left = AvailableSize.Width - node.Right - childSize.Width; + else { - childPos = childPos.WithY(_arrangeSize.Height - childSize.Height); + node.Left = 0; + node.Right = AvailableSize.Width - childSize.Width; } + } + else if (!node.Left.IsNaN() && node.Right.IsNaN()) + { + node.Right = AvailableSize.Width - node.Left - childSize.Width; + } + + if (node.Top.IsNaN()) + { + if (!node.Bottom.IsNaN()) + node.Top = AvailableSize.Height - node.Bottom - childSize.Height; else { - childPos = childPos.WithY((_arrangeSize.Height + node.AlignTopWithNode.Position.Y - childSize.Height) / 2); + node.Top = 0; + node.Bottom = AvailableSize.Height - childSize.Height; } } + else if (!node.Top.IsNaN() && node.Bottom.IsNaN()) + { + node.Bottom = AvailableSize.Height - node.Top - childSize.Height; + } + + node.Measured = true; + } + + public Size GetBoundingSize(bool calcWidth, bool calcHeight) + { + var boundingSize = new Size(); + + foreach (var node in _nodeDic.Values) + { + var size = node.GetBoundingSize(); + boundingSize = boundingSize.WithWidth(Math.Max(boundingSize.Width, size.Width)); + boundingSize = boundingSize.WithHeight(Math.Max(boundingSize.Height, size.Height)); + } - child.Arrange(new Rect(childPos.X, childPos.Y, childSize.Width, childSize.Height)); - node.Position = childPos; - node.Arranged = true; + boundingSize = boundingSize.WithWidth(calcWidth ? boundingSize.Width : AvailableSize.Width); + boundingSize = boundingSize.WithHeight(calcHeight ? boundingSize.Height : AvailableSize.Height); + return boundingSize; } } }