using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Utilities; using static System.Math; namespace Nitrox.Launcher.Models.Controls; /// /// Panel that arranges stretchable child controls to fit min width, up to the limit of . /// Code inspired by Avalonia's WrapPanel /// (https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Controls/WrapPanel.cs). /// /// /// Looks similar to YouTube video layout. /// public class FittingWrapPanel : Panel, INavigableContainer { public static readonly StyledProperty MinItemWidthProperty = AvaloniaProperty.Register(nameof(MinItemWidth), 100); public double MinItemWidth { get => GetValue(MinItemWidthProperty); set => SetValue(MinItemWidthProperty, value); } static FittingWrapPanel() { AffectsMeasure(MinItemWidthProperty); } /// protected override Size MeasureOverride(Size constraint) { UVSize curLineSize = new(); UVSize panelSize = new(); UVSize uvConstraint = new(constraint.Width, constraint.Height); int itemsPerRow = (int)Min(constraint.Width / MinItemWidth, Max(Children.Count, 1)); double adjustedWidth = constraint.Width / itemsPerRow; for (int i = 0, count = Children.Count; i < count; i++) { Control child = Children[i]; child.Measure(new Size(adjustedWidth, constraint.Height)); UVSize sz = new(adjustedWidth, child.DesiredSize.Height); if (MathUtilities.GreaterThan(curLineSize.Width + sz.Width, uvConstraint.Width)) // Need to switch to another line { panelSize = new UVSize { Width = Max(curLineSize.Width, panelSize.Width), Height = panelSize.Height + curLineSize.Height }; curLineSize = sz; if (MathUtilities.GreaterThan(sz.Width, uvConstraint.Width)) // The element is wider then the constraint - give it a separate line { panelSize = new UVSize { Width = Max(sz.Width, panelSize.Width), Height = panelSize.Height + sz.Height }; curLineSize = new UVSize(); } } else // Continue to accumulate a line { curLineSize = new UVSize { Width = curLineSize.Width + sz.Width, Height = Max(sz.Height, curLineSize.Height) }; } } panelSize = new UVSize { Width = Max(curLineSize.Width, panelSize.Width), Height = panelSize.Height + curLineSize.Height }; return new Size(panelSize.Width, panelSize.Height); } /// protected override Size ArrangeOverride(Size finalSize) { int firstInLine = 0; double accumulatedV = 0; UVSize curLineSize = new(); UVSize uvFinalSize = new(finalSize.Width, finalSize.Height); int itemsPerRow = (int)Min(finalSize.Width / MinItemWidth, Max(Children.Count, 1)); double adjustedWidth = finalSize.Width / itemsPerRow; for (int i = 0; i < Children.Count; i++) { Control child = Children[i]; UVSize sz = new(adjustedWidth, child.DesiredSize.Height); if (MathUtilities.GreaterThan(curLineSize.Width + sz.Width, uvFinalSize.Width)) // Need to switch to another line { ArrangeLine(accumulatedV, curLineSize.Height, firstInLine, i, adjustedWidth); accumulatedV += curLineSize.Height; curLineSize = sz; if (MathUtilities.GreaterThan(sz.Width, uvFinalSize.Width)) // The element is wider then the constraint - give it a separate line { ArrangeLine(accumulatedV, sz.Height, i, ++i, adjustedWidth); accumulatedV += sz.Height; curLineSize = new UVSize(); } firstInLine = i; } else // Continue to accumulate a line { curLineSize = new UVSize { Width = curLineSize.Width + sz.Width, Height = Max(sz.Height, curLineSize.Height) }; } } if (firstInLine < Children.Count) { ArrangeLine(accumulatedV, curLineSize.Height, firstInLine, Children.Count, adjustedWidth); } return finalSize; } /// /// Gets the next control in the specified direction. /// /// The movement direction. /// The control from which movement begins. /// Whether to wrap around when the first or last item is reached. /// The control. IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from, bool wrap) { Avalonia.Controls.Controls children = Children; int index = from is not null ? Children.IndexOf((Control)from) : -1; switch (direction) { case NavigationDirection.First: index = 0; break; case NavigationDirection.Last: index = children.Count - 1; break; case NavigationDirection.Next: ++index; break; case NavigationDirection.Previous: --index; break; case NavigationDirection.Left: index -= 1; break; case NavigationDirection.Right: index += 1; break; case NavigationDirection.Up: case NavigationDirection.Down: index = -1; break; } if (index >= 0 && index < children.Count) { return children[index]; } return null; } private void ArrangeLine(double v, double lineV, int start, int end, double itemU) { Avalonia.Controls.Controls children = Children; double u = 0; for (int i = start; i < end; i++) { Control child = children[i]; child.Arrange(new Rect(u, v, itemU, lineV)); u += itemU; } } private readonly struct UVSize { internal UVSize(double width, double height) { Width = width; Height = height; } public double Width { get; init; } internal double Height { get; init; } } }