diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index d812818ed8..fa4fd7dd07 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -56,6 +56,7 @@ + diff --git a/samples/ControlCatalog/Pages/RelativePanelPage.axaml b/samples/ControlCatalog/Pages/RelativePanelPage.axaml new file mode 100644 index 0000000000..3657d52bd9 --- /dev/null +++ b/samples/ControlCatalog/Pages/RelativePanelPage.axaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs b/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs new file mode 100644 index 0000000000..11d0a5152e --- /dev/null +++ b/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class RelativePanelPage : UserControl + { + public RelativePanelPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs new file mode 100644 index 0000000000..f93de5ca15 --- /dev/null +++ b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs @@ -0,0 +1,546 @@ +using Avalonia.Layout; + +#nullable enable + +namespace Avalonia.Controls +{ + public partial class RelativePanel + { + private static void OnAlignPropertiesChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + if (d is Layoutable layoutable && layoutable.Parent is Layoutable layoutableParent) + { + layoutableParent.InvalidateArrange(); + } + } + + static RelativePanel() + { + ClipToBoundsProperty.OverrideDefaultValue(true); + + AboveProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignBottomWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignBottomWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignHorizontalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignHorizontalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignLeftWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignLeftWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignRightWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignRightWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignTopWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignTopWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignVerticalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignVerticalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + BelowProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + LeftOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + LeftOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + } + + /// + /// Gets the value of the RelativePanel.Above XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.Above XAML attached property value of the specified object. + /// (The element to position this element above.) + /// + [ResolveByName] + public static object GetAbove(AvaloniaObject obj) + { + return (object)obj.GetValue(AboveProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to position this element above.) + public static void SetAbove(AvaloniaObject obj, object value) + { + obj.SetValue(AboveProperty, value); + } + + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty AboveProperty = + AvaloniaProperty.RegisterAttached("Above", typeof(RelativePanel)); + + + /// + /// Gets the value of the RelativePanel.AlignBottomWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignBottomWithPanel XAML attached property value of the specified + /// object. (true to align this element's bottom edge with the panel's bottom edge; + /// otherwise, false.) + /// + public static bool GetAlignBottomWithPanel(AvaloniaObject obj) + { + return (bool)obj.GetValue(AlignBottomWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to align this element's bottom edge with the panel's + /// bottom edge; otherwise, false.) + /// + public static void SetAlignBottomWithPanel(AvaloniaObject obj, bool value) + { + obj.SetValue(AlignBottomWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly AttachedProperty AlignBottomWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignBottomWithPanel", typeof(RelativePanel)); + + /// + /// Gets the value of the RelativePanel.AlignBottomWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignBottomWith XAML attached property value of the specified object. + /// (The element to align this element's bottom edge with.) + /// + [ResolveByName] + public static object GetAlignBottomWith(AvaloniaObject obj) + { + return (object)obj.GetValue(AlignBottomWithProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's bottom edge with.) + public static void SetAlignBottomWith(AvaloniaObject obj, object value) + { + obj.SetValue(AlignBottomWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty AlignBottomWithProperty = + AvaloniaProperty.RegisterAttached("AlignBottomWith", typeof(RelativePanel)); + + /// + /// Gets the value of the RelativePanel.AlignHorizontalCenterWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignHorizontalCenterWithPanel XAML attached property value + /// of the specified object. (true to horizontally center this element in the panel; + /// otherwise, false.) + /// + public static bool GetAlignHorizontalCenterWithPanel(AvaloniaObject obj) + { + return (bool)obj.GetValue(AlignHorizontalCenterWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to horizontally center this element in the panel; otherwise, + /// false.) + /// + public static void SetAlignHorizontalCenterWithPanel(AvaloniaObject obj, bool value) + { + obj.SetValue(AlignHorizontalCenterWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly AttachedProperty AlignHorizontalCenterWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignHorizontalCenterWithPanel", typeof(RelativePanel), false); + + /// + /// Gets the value of the RelativePanel.AlignHorizontalCenterWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignHorizontalCenterWith XAML attached property value of the + /// specified object. (The element to align this element's horizontal center with.) + /// + [ResolveByName] + public static object GetAlignHorizontalCenterWith(AvaloniaObject obj) + { + return (object)obj.GetValue(AlignHorizontalCenterWithProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's horizontal center with.) + public static void SetAlignHorizontalCenterWith(AvaloniaObject obj, object value) + { + obj.SetValue(AlignHorizontalCenterWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty AlignHorizontalCenterWithProperty = + AvaloniaProperty.RegisterAttached("AlignHorizontalCenterWith", typeof(object), typeof(RelativePanel)); + + /// + /// Gets the value of the RelativePanel.AlignLeftWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignLeftWithPanel XAML attached property value of the specified + /// object. (true to align this element's left edge with the panel's left edge; otherwise, + /// false.) + /// + public static bool GetAlignLeftWithPanel(AvaloniaObject obj) + { + return (bool)obj.GetValue(AlignLeftWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to align this element's left edge with the panel's left + /// edge; otherwise, false.) + /// + public static void SetAlignLeftWithPanel(AvaloniaObject obj, bool value) + { + obj.SetValue(AlignLeftWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly AttachedProperty AlignLeftWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignLeftWithPanel", typeof(RelativePanel), false); + + + /// + /// Gets the value of the RelativePanel.AlignLeftWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignLeftWith XAML attached property value of the specified + /// object. (The element to align this element's left edge with.) + /// + [ResolveByName] + public static object GetAlignLeftWith(AvaloniaObject obj) + { + return (object)obj.GetValue(AlignLeftWithProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's left edge with.) + public static void SetAlignLeftWith(AvaloniaObject obj, object value) + { + obj.SetValue(AlignLeftWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty AlignLeftWithProperty = + AvaloniaProperty.RegisterAttached("AlignLeftWith"); + + + /// + /// Gets the value of the RelativePanel.AlignRightWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignRightWithPanel XAML attached property value of the specified + /// object. (true to align this element's right edge with the panel's right edge; + /// otherwise, false.) + /// + public static bool GetAlignRightWithPanel(AvaloniaObject obj) + { + return (bool)obj.GetValue(AlignRightWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to align this element's right edge with the panel's right + /// edge; otherwise, false.) + /// + public static void SetAlignRightWithPanel(AvaloniaObject obj, bool value) + { + obj.SetValue(AlignRightWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly AttachedProperty AlignRightWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignRightWithPanel", false); + + /// + /// Gets the value of the RelativePanel.AlignRightWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignRightWith XAML attached property value of the specified + /// object. (The element to align this element's right edge with.) + /// + [ResolveByName] + public static object GetAlignRightWith(AvaloniaObject obj) + { + return (object)obj.GetValue(AlignRightWithProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignRightWith XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's right edge with.) + public static void SetAlignRightWith(AvaloniaObject obj, object value) + { + obj.SetValue(AlignRightWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty AlignRightWithProperty = + AvaloniaProperty.RegisterAttached("AlignRightWith"); + + /// + /// Gets the value of the RelativePanel.AlignTopWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignTopWithPanel XAML attached property value of the specified + /// object. (true to align this element's top edge with the panel's top edge; otherwise, + /// false.) + /// + public static bool GetAlignTopWithPanel(AvaloniaObject obj) + { + return (bool)obj.GetValue(AlignTopWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignTopWithPanel XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to align this element's top edge with the panel's top + /// edge; otherwise, false.) + /// + public static void SetAlignTopWithPanel(AvaloniaObject obj, bool value) + { + obj.SetValue(AlignTopWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly AttachedProperty AlignTopWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignTopWithPanel", false); + + /// + /// Gets the value of the RelativePanel.AlignTopWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// The value to set. (The element to align this element's top edge with.) + [ResolveByName] + public static object GetAlignTopWith(AvaloniaObject obj) + { + return (object)obj.GetValue(AlignTopWithProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignTopWith XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's top edge with.) + public static void SetAlignTopWith(AvaloniaObject obj, object value) + { + obj.SetValue(AlignTopWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty AlignTopWithProperty = + AvaloniaProperty.RegisterAttached("AlignTopWith"); + + /// + /// Gets the value of the RelativePanel.AlignVerticalCenterWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignVerticalCenterWithPanel XAML attached property value of + /// the specified object. (true to vertically center this element in the panel; otherwise, + /// false.) + /// + public static bool GetAlignVerticalCenterWithPanel(AvaloniaObject obj) + { + return (bool)obj.GetValue(AlignVerticalCenterWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignVerticalCenterWithPanel XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to vertically center this element in the panel; otherwise, + /// false.) + /// + public static void SetAlignVerticalCenterWithPanel(AvaloniaObject obj, bool value) + { + obj.SetValue(AlignVerticalCenterWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly AttachedProperty AlignVerticalCenterWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignVerticalCenterWithPanel", false); + + /// + /// Gets the value of the RelativePanel.AlignVerticalCenterWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// The value to set. (The element to align this element's vertical center with.) + [ResolveByName] + public static object GetAlignVerticalCenterWith(AvaloniaObject obj) + { + return (object)obj.GetValue(AlignVerticalCenterWithProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignVerticalCenterWith XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's horizontal center with.) + + public static void SetAlignVerticalCenterWith(AvaloniaObject obj, object value) + { + obj.SetValue(AlignVerticalCenterWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly AttachedProperty AlignVerticalCenterWithProperty = + AvaloniaProperty.RegisterAttached("AlignVerticalCenterWith"); + + /// + /// Gets the value of the RelativePanel.Below XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.Below XAML attached property value of the specified object. + /// (The element to position this element below.) + /// + [ResolveByName] + public static object GetBelow(AvaloniaObject obj) + { + return (object)obj.GetValue(BelowProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to position this element below.) + + public static void SetBelow(AvaloniaObject obj, object value) + { + obj.SetValue(BelowProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty BelowProperty = + AvaloniaProperty.RegisterAttached("Below"); + + /// + /// Gets the value of the RelativePanel.LeftOf XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.LeftOf XAML attached property value of the specified object. + /// (The element to position this element to the left of.) + /// + [ResolveByName] + public static object GetLeftOf(AvaloniaObject obj) + { + return (object)obj.GetValue(LeftOfProperty); + } + + /// + /// Sets the value of the RelativePanel.LeftOf XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to position this element to the left of.) + public static void SetLeftOf(AvaloniaObject obj, object value) + { + obj.SetValue(LeftOfProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty LeftOfProperty = + AvaloniaProperty.RegisterAttached("LeftOf"); + + /// + /// Gets the value of the RelativePanel.RightOf XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.RightOf XAML attached property value of the specified object. + /// (The element to position this element to the right of.) + /// + [ResolveByName] + public static object GetRightOf(AvaloniaObject obj) + { + return (object)obj.GetValue(RightOfProperty); + } + + /// + /// Sets the value of the RelativePanel.RightOf XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to position this element to the right of.) + public static void SetRightOf(AvaloniaObject obj, object value) + { + obj.SetValue(RightOfProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty RightOfProperty = + AvaloniaProperty.RegisterAttached("RightOf"); + } +} diff --git a/src/Avalonia.Controls/RelativePanel.cs b/src/Avalonia.Controls/RelativePanel.cs new file mode 100644 index 0000000000..033a5559f5 --- /dev/null +++ b/src/Avalonia.Controls/RelativePanel.cs @@ -0,0 +1,353 @@ +/// Ported from https://github.com/HandyOrg/HandyControl/blob/master/src/Shared/HandyControl_Shared/Controls/Panel/RelativePanel.cs +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Layout; + +#nullable enable + +namespace Avalonia.Controls +{ + public partial class RelativePanel : Panel + { + private readonly Graph _childGraph; + + public RelativePanel() => _childGraph = new Graph(); + + protected override Size MeasureOverride(Size availableSize) + { + foreach (var child in Children) + { + child?.Measure(availableSize); + } + + return availableSize; + } + + protected override Size ArrangeOverride(Size arrangeSize) + { + _childGraph.Reset(arrangeSize); + + foreach (var child in Children.OfType()) + { + if (child == null) + continue; + + var node = _childGraph.AddNode(child); + + node.AlignLeftWithNode = _childGraph.AddLink(node, GetDependencyElement(AlignLeftWithProperty, child)); + node.AlignTopWithNode = _childGraph.AddLink(node, GetDependencyElement(AlignTopWithProperty, child)); + node.AlignRightWithNode = _childGraph.AddLink(node, GetDependencyElement(AlignRightWithProperty, child)); + node.AlignBottomWithNode = _childGraph.AddLink(node, GetDependencyElement(AlignBottomWithProperty, child)); + + node.LeftOfNode = _childGraph.AddLink(node, GetDependencyElement(LeftOfProperty, child)); + node.AboveNode = _childGraph.AddLink(node, GetDependencyElement(AboveProperty, child)); + node.RightOfNode = _childGraph.AddLink(node, GetDependencyElement(RightOfProperty, child)); + node.BelowNode = _childGraph.AddLink(node, GetDependencyElement(BelowProperty, child)); + + node.AlignHorizontalCenterWith = _childGraph.AddLink(node, GetDependencyElement(AlignHorizontalCenterWithProperty, child)); + node.AlignVerticalCenterWith = _childGraph.AddLink(node, GetDependencyElement(AlignVerticalCenterWithProperty, child)); + } + + if (_childGraph.CheckCyclic()) + { + throw new Exception("RelativePanel error: Circular dependency detected. Layout could not complete."); + } + + var size = new Size(); + + foreach (var child in Children) + { + if (child.Bounds.Bottom > size.Height) + { + size = size.WithHeight(child.Bounds.Bottom); + } + + if (child.Bounds.Right > size.Width) + { + size = size.WithWidth(child.Bounds.Right); + } + } + + if (VerticalAlignment == VerticalAlignment.Stretch) + { + size = size.WithHeight(arrangeSize.Height); + } + + if (HorizontalAlignment == HorizontalAlignment.Stretch) + { + size = size.WithWidth(arrangeSize.Width); + } + + return size; + } + + private Layoutable? GetDependencyElement(AvaloniaProperty property, AvaloniaObject child) + { + var dependency = child.GetValue(property); + + if (dependency is Layoutable layoutable) + { + if (Children.Contains((ILayoutable)layoutable)) + return layoutable; + + throw new ArgumentException($"RelativePanel error: Element does not exist in the current context: {property.Name}"); + } + + return null; + } + + private class GraphNode + { + public Point Position { get; set; } + + public bool Arranged { get; set; } + + public Layoutable Element { get; } + + public HashSet OutgoingNodes { get; } + + public GraphNode? AlignLeftWithNode { get; set; } + + public GraphNode? AlignTopWithNode { get; set; } + + public GraphNode? AlignRightWithNode { get; set; } + + public GraphNode? AlignBottomWithNode { get; set; } + + public GraphNode? LeftOfNode { get; set; } + + public GraphNode? AboveNode { get; set; } + + public GraphNode? RightOfNode { get; set; } + + public GraphNode? BelowNode { get; set; } + + public GraphNode? AlignHorizontalCenterWith { get; set; } + + public GraphNode? AlignVerticalCenterWith { get; set; } + + public GraphNode(Layoutable element) + { + OutgoingNodes = new HashSet(); + Element = element; + } + } + + private class Graph + { + private readonly Dictionary _nodeDic; + + private Size _arrangeSize; + + public Graph() + { + _nodeDic = new Dictionary(); + } + + public GraphNode? AddLink(GraphNode from, Layoutable? to) + { + if (to == null) + return null; + + GraphNode nodeTo; + if (_nodeDic.ContainsKey(to)) + { + nodeTo = _nodeDic[to]; + } + else + { + nodeTo = new GraphNode(to); + _nodeDic[to] = nodeTo; + } + + from.OutgoingNodes.Add(nodeTo); + return nodeTo; + } + + public GraphNode AddNode(Layoutable value) + { + if (!_nodeDic.ContainsKey(value)) + { + var node = new GraphNode(value); + _nodeDic.Add(value, node); + return node; + } + + return _nodeDic[value]; + } + + public void Reset(Size arrangeSize) + { + _arrangeSize = arrangeSize; + _nodeDic.Clear(); + } + + public bool CheckCyclic() => CheckCyclic(_nodeDic.Values, null); + + private bool CheckCyclic(IEnumerable nodes, HashSet? set) + { + set ??= new HashSet(); + + foreach (var node in nodes) + { + if (!node.Arranged && node.OutgoingNodes.Count == 0) + { + ArrangeChild(node, true); + continue; + } + + if (node.OutgoingNodes.All(item => item.Arranged)) + { + ArrangeChild(node); + continue; + } + + if (!set.Add(node.Element)) + return true; + + return CheckCyclic(node.OutgoingNodes, set); + } + + return false; + } + + private void ArrangeChild(GraphNode node, bool ignoneSibling = false) + { + var child = node.Element; + var childSize = child.DesiredSize; + var childPos = new Point(); + + if (GetAlignHorizontalCenterWithPanel(child)) + { + childPos = childPos.WithX((_arrangeSize.Width - childSize.Width) / 2); + } + + if (GetAlignVerticalCenterWithPanel(child)) + { + childPos = childPos.WithY((_arrangeSize.Height - childSize.Height) / 2); + } + + var alignLeftWithPanel = GetAlignLeftWithPanel(child); + var alignTopWithPanel = GetAlignTopWithPanel(child); + var alignRightWithPanel = GetAlignRightWithPanel(child); + var alignBottomWithPanel = GetAlignBottomWithPanel(child); + + if (!ignoneSibling) + { + if (node.LeftOfNode != null) + { + childPos = childPos.WithX(node.LeftOfNode.Position.X - childSize.Width); + } + + if (node.AboveNode != null) + { + childPos = childPos.WithY(node.AboveNode.Position.Y - childSize.Height); + } + + if (node.RightOfNode != null) + { + childPos = childPos.WithX(node.RightOfNode.Position.X + node.RightOfNode.Element.DesiredSize.Width); + } + + if (node.BelowNode != null) + { + childPos = childPos.WithY(node.BelowNode.Position.Y + node.BelowNode.Element.DesiredSize.Height); + } + + if (node.AlignHorizontalCenterWith != null) + { + childPos = childPos.WithX(node.AlignHorizontalCenterWith.Position.X + + (node.AlignHorizontalCenterWith.Element.DesiredSize.Width - childSize.Width) / 2); + } + + if (node.AlignVerticalCenterWith != null) + { + childPos = childPos.WithY(node.AlignVerticalCenterWith.Position.Y + + (node.AlignVerticalCenterWith.Element.DesiredSize.Height - childSize.Height) / 2); + } + + if (node.AlignLeftWithNode != null) + { + childPos = childPos.WithX(node.AlignLeftWithNode.Position.X); + } + + if (node.AlignTopWithNode != null) + { + childPos = childPos.WithY(node.AlignTopWithNode.Position.Y); + } + + if (node.AlignRightWithNode != null) + { + childPos = childPos.WithX(node.AlignRightWithNode.Element.DesiredSize.Width + node.AlignRightWithNode.Position.X - childSize.Width); + } + + if (node.AlignBottomWithNode != null) + { + childPos = childPos.WithY(node.AlignBottomWithNode.Element.DesiredSize.Height + node.AlignBottomWithNode.Position.Y - childSize.Height); + } + } + + if (alignLeftWithPanel) + { + if (node.AlignRightWithNode != null) + { + childPos = childPos.WithX((node.AlignRightWithNode.Element.DesiredSize.Width + node.AlignRightWithNode.Position.X - childSize.Width) / 2); + } + else + { + childPos = childPos.WithX(0); + } + } + + if (alignTopWithPanel) + { + if (node.AlignBottomWithNode != null) + { + childPos = childPos.WithY((node.AlignBottomWithNode.Element.DesiredSize.Height + node.AlignBottomWithNode.Position.Y - childSize.Height) / 2); + } + else + { + childPos = childPos.WithY(0); + } + } + + if (alignRightWithPanel) + { + if (alignLeftWithPanel) + { + childPos = childPos.WithX((_arrangeSize.Width - childSize.Width) / 2); + } + else if (node.AlignLeftWithNode == null) + { + childPos = childPos.WithX(_arrangeSize.Width - childSize.Width); + } + else + { + childPos = childPos.WithX((_arrangeSize.Width + node.AlignLeftWithNode.Position.X - childSize.Width) / 2); + } + } + + if (alignBottomWithPanel) + { + if (alignTopWithPanel) + { + childPos = childPos.WithY((_arrangeSize.Height - childSize.Height) / 2); + } + else if (node.AlignTopWithNode == null) + { + childPos = childPos.WithY(_arrangeSize.Height - childSize.Height); + } + else + { + childPos = childPos.WithY((_arrangeSize.Height + node.AlignTopWithNode.Position.Y - childSize.Height) / 2); + } + } + + child.Arrange(new Rect(childPos.X, childPos.Y, childSize.Width, childSize.Height)); + node.Position = childPos; + node.Arranged = true; + } + } + } +} diff --git a/src/Avalonia.Controls/ResolveByNameAttribute.cs b/src/Avalonia.Controls/ResolveByNameAttribute.cs new file mode 100644 index 0000000000..a00e3f8a1b --- /dev/null +++ b/src/Avalonia.Controls/ResolveByNameAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Controls +{ + public class ResolveByNameAttribute : Attribute + { + + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index da345835c8..3979312ce0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -25,6 +25,7 @@ + @@ -57,6 +58,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResolveByNameExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResolveByNameExtension.cs new file mode 100644 index 0000000000..8561aa898b --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResolveByNameExtension.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Controls; +using Avalonia.Data.Core; + +#nullable enable + +namespace Avalonia.Markup.Xaml.MarkupExtensions +{ + public class ResolveByNameExtension + { + public ResolveByNameExtension(string name) + { + Name = name; + } + + public string Name { get; } + + public object? ProvideValue(IServiceProvider serviceProvider) + { + var nameScope = serviceProvider.GetService(); + + var value = nameScope.FindAsync(Name); + + if(value.IsCompleted) + return value.GetResult(); + + var provideValueTarget = serviceProvider.GetService(); + var target = provideValueTarget.TargetObject; + + if (provideValueTarget.TargetProperty is IPropertyInfo property) + value.OnCompleted(() => property.Set(target, value.GetResult())); + + return null; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs index f3b8559d45..abff763bb1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -40,22 +40,21 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions // Targeted - InsertBefore( new AvaloniaXamlIlTransformInstanceAttachedProperties(), new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers()); - InsertAfter(new AvaloniaXamlIlAvaloniaPropertyResolver()); - - + InsertAfter( + new AvaloniaXamlIlAvaloniaPropertyResolver()); - InsertBefore( + InsertBefore( new AvaloniaXamlIlBindingPathParser(), new AvaloniaXamlIlSelectorTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlPropertyPathTransformer(), new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), - new AvaloniaXamlIlTransitionsTypeMetadataTransformer() + new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), + new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() ); // After everything else diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlResolveByNameMarkupExtensionReplacer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlResolveByNameMarkupExtensionReplacer.cs new file mode 100644 index 0000000000..c0ac841b7f --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlResolveByNameMarkupExtensionReplacer.cs @@ -0,0 +1,49 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; + +#nullable enable +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlResolveByNameMarkupExtensionReplacer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (!(node is XamlAstXamlPropertyValueNode propertyValueNode)) return node; + + if (!(propertyValueNode.Property is XamlAstClrProperty clrProperty)) return node; + + IEnumerable attributes = propertyValueNode.Property.GetClrProperty().CustomAttributes; + + if (propertyValueNode.Property is XamlAstClrProperty referenceNode && + referenceNode.Getter != null) + { + attributes = attributes.Concat(referenceNode.Getter.CustomAttributes); + } + + if (attributes.All(attribute => attribute.Type.FullName != "Avalonia.Controls.ResolveByNameAttribute")) + return node; + + if (propertyValueNode.Values.Count != 1 || !(propertyValueNode.Values.First() is XamlAstTextNode)) + return node; + + var newNode = new XamlAstObjectNode( + propertyValueNode.Values[0], + new XamlAstClrTypeReference(propertyValueNode.Values[0], + context.GetAvaloniaTypes().ResolveByNameExtension, true)) + { + Arguments = new List { propertyValueNode.Values[0] } + }; + + if (XamlTransformHelpers.TryConvertMarkupExtension(context, newNode, out var extensionNode)) + { + propertyValueNode.Values[0] = extensionNode; + } + + return node; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs index 8e194e9385..d78ceeb918 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs @@ -174,6 +174,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers other is GetterMethod m && m.Name == Name && m.DeclaringType.Equals(DeclaringType); public IXamlType ReturnType => Parent.PropertyType; public IReadOnlyList Parameters { get; } + + public IReadOnlyList CustomAttributes => DeclaringType.CustomAttributes; + public void EmitCall(IXamlILEmitter emitter) { var method = Parent._avaloniaObject diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index bf1fb4a6fc..58ea11aa8f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -37,6 +37,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType CompiledBindingPathBuilder { get; } public IXamlType CompiledBindingPath { get; } public IXamlType CompiledBindingExtension { get; } + + public IXamlType ResolveByNameExtension { get; } + public IXamlType DataTemplate { get; } public IXamlType IItemsPresenterHost { get; } public IXamlType ReflectionBindingExtension { get; } @@ -92,6 +95,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers CompiledBindingPathBuilder = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPathBuilder"); CompiledBindingPath = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPath"); CompiledBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension"); + ResolveByNameExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ResolveByNameExtension"); DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate"); IItemsPresenterHost = cfg.TypeSystem.GetType("Avalonia.Controls.Presenters.IItemsPresenterHost"); ReflectionBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension"); diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 0028377ce7..7b8b3013bd 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 0028377ce7c7dc21f9fe71b45f62a95991b1ab58 +Subproject commit 7b8b3013bd42e1992838a525c991f44191da55be diff --git a/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs b/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs new file mode 100644 index 0000000000..4248e643eb --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs @@ -0,0 +1,59 @@ +using Avalonia.Controls.Shapes; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class RelativePanelTests + { + [Fact] + public void Lays_Out_1_Child_Next_the_other() + { + var rect1 = new Rectangle { Height = 20, Width = 20 }; + var rect2 = new Rectangle { Height = 20, Width = 20 }; + + var target = new RelativePanel + { + VerticalAlignment = Layout.VerticalAlignment.Top, + HorizontalAlignment = Layout.HorizontalAlignment.Left, + Children = + { + rect1, rect2 + } + }; + + RelativePanel.SetAlignLeftWithPanel(rect1 , true); + RelativePanel.SetRightOf(rect2, rect1); + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(40, 20), target.Bounds.Size); + Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); + Assert.Equal(new Rect(20, 0, 20, 20), target.Children[1].Bounds); + } + + public void Lays_Out_1_Child_Below_the_other() + { + var rect1 = new Rectangle { Height = 20, Width = 20 }; + var rect2 = new Rectangle { Height = 20, Width = 20 }; + + var target = new RelativePanel + { + VerticalAlignment = Layout.VerticalAlignment.Top, + HorizontalAlignment = Layout.HorizontalAlignment.Left, + Children = + { + rect1, rect2 + } + }; + + RelativePanel.SetAlignLeftWithPanel(rect1, true); + RelativePanel.SetBelow(rect2, rect1); + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(20, 40), target.Bounds.Size); + Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); + Assert.Equal(new Rect(0, 20, 20, 20), target.Children[1].Bounds); + } + } +}