diff --git a/Toolkit.UI.Controls.Avalonia/ResponsiveGrid/ResponsiveGrid.cs b/Toolkit.UI.Controls.Avalonia/ResponsiveGrid/ResponsiveGrid.cs new file mode 100644 index 0000000..cbe92ea --- /dev/null +++ b/Toolkit.UI.Controls.Avalonia/ResponsiveGrid/ResponsiveGrid.cs @@ -0,0 +1,330 @@ +using Avalonia; +using Avalonia.Controls; + +namespace Toolkit.UI.Controls.Avalonia +{ + public class ResponsiveGrid : Grid + { + public static readonly AvaloniaProperty ActualColumnProperty = + AvaloniaProperty.RegisterAttached("ActualColumn", 0); + + public static readonly AvaloniaProperty ActualRowProperty = + AvaloniaProperty.RegisterAttached("ActualRow", 0); + + public static readonly StyledProperty BreakPointsProperty = + AvaloniaProperty.Register(nameof(Thresholds)); + + public static readonly AvaloniaProperty LargeOffsetProperty = + AvaloniaProperty.RegisterAttached("LargeOffset", 0); + + public static readonly AvaloniaProperty LargePullProperty = + AvaloniaProperty.RegisterAttached("LargePull", 0); + + public static readonly AvaloniaProperty LargePushProperty = + AvaloniaProperty.RegisterAttached("LargePush", 0); + + public static readonly AvaloniaProperty LargeProperty = + AvaloniaProperty.RegisterAttached("Large", 0); + + public static readonly StyledProperty MaxDivisionProperty = + AvaloniaProperty.Register(nameof(MaxDivision), 12); + + public static readonly AvaloniaProperty MediumOffsetProperty = + AvaloniaProperty.RegisterAttached("MediumOffset", 0); + + public static readonly AvaloniaProperty MediumPullProperty = + AvaloniaProperty.RegisterAttached("MediumPull", 0); + + public static readonly AvaloniaProperty MediumPushProperty = + AvaloniaProperty.RegisterAttached("MediumPush", 0); + + public static readonly AvaloniaProperty MediumProperty = + AvaloniaProperty.RegisterAttached("Medium", 0); + + public static readonly AvaloniaProperty SmallOffsetProperty = + AvaloniaProperty.RegisterAttached("SmallOffset", 0); + + public static readonly AvaloniaProperty SmallPullProperty = + AvaloniaProperty.RegisterAttached("SmallPull", 0); + + public static readonly AvaloniaProperty SmallPushProperty = + AvaloniaProperty.RegisterAttached("SmallPush", 0); + + public static readonly AvaloniaProperty SmallProperty = + AvaloniaProperty.RegisterAttached("Small", 0); + + public static readonly AvaloniaProperty ExtraSmallOffsetProperty = + AvaloniaProperty.RegisterAttached("ExtraSmallOffset", 0); + + public static readonly AvaloniaProperty ExtraSmallPullProperty = + AvaloniaProperty.RegisterAttached("ExtraSmallPull", 0); + + public static readonly AvaloniaProperty ExtraSmallPushProperty = + AvaloniaProperty.RegisterAttached("ExtraSmallPush", 0); + + public static readonly AvaloniaProperty ExtraSmallProperty = + AvaloniaProperty.RegisterAttached("ExtraSmall", 0); + + static ResponsiveGrid() + { + AffectsMeasure( + MaxDivisionProperty, + BreakPointsProperty, + LargeProperty, + MediumProperty, + SmallProperty, + ExtraSmallProperty, + LargeOffsetProperty, + LargePullProperty, + LargePushProperty, + MediumOffsetProperty, + MediumPullProperty, + MediumPushProperty, + SmallOffsetProperty, + SmallPullProperty, + SmallPushProperty, + ExtraSmallOffsetProperty, + ExtraSmallPullProperty, + ExtraSmallPushProperty + ); + } + + public ResponsiveGrid() + { + MaxDivision = 12; + Thresholds = new SizeThresholds(); + } + + public int MaxDivision + { + get => GetValue(MaxDivisionProperty); + set => SetValue(MaxDivisionProperty, value); + } + + public SizeThresholds Thresholds + { + get => GetValue(BreakPointsProperty); + set => SetValue(BreakPointsProperty, value); + } + + public static int GetActualColumn(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(ActualColumnProperty); + + public static int GetActualRow(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(ActualRowProperty); + + public static int GetLarge(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(LargeProperty); + + public static int GetLargeOffset(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(LargeOffsetProperty); + + public static int GetLargePull(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(LargePullProperty); + + public static int GetLargePush(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(LargePushProperty); + + public static int GetMedium(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(MediumProperty); + + public static int GetMediumOffset(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(MediumOffsetProperty); + + public static int GetMediumPull(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(MediumPullProperty); + + public static int GetMediumPush(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(MediumPushProperty); + + public static int GetSmall(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(SmallProperty); + + public static int GetSmallOffset(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(SmallOffsetProperty); + + public static int GetSmallPull(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(SmallPullProperty); + + public static int GetSmallPush(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(SmallPushProperty); + + public static int GetExtraSmall(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(ExtraSmallProperty); + + public static int GetExtraSmallOffset(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(ExtraSmallOffsetProperty); + + public static int GetExtraSmallPull(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(ExtraSmallPullProperty); + + public static int GetExtraSmallPush(AvaloniaObject avaloniaObject) => + avaloniaObject.GetValue(ExtraSmallPushProperty); + + public static void SetLarge(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(LargeProperty, value); + + public static void SetLargeOffset(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(LargeOffsetProperty, value); + + public static void SetLargePull(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(LargePullProperty, value); + + public static void SetLargePush(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(LargePushProperty, value); + + public static void SetMedium(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(MediumProperty, value); + + public static void SetMediumOffset(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(MediumOffsetProperty, value); + + public static void SetMediumPull(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(MediumPullProperty, value); + + public static void SetMediumPush(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(MediumPushProperty, value); + + public static void SetSmall(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(SmallProperty, value); + + public static void SetSmallOffset(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(SmallOffsetProperty, value); + + public static void SetSmallPull(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(SmallPullProperty, value); + + public static void SetSmallPush(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(SmallPushProperty, value); + + public static void SetExtraSmall(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(ExtraSmallProperty, value); + + public static void SetExtraSmallOffset(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(ExtraSmallOffsetProperty, value); + + public static void SetExtraSmallPull(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(ExtraSmallPullProperty, value); + + public static void SetExtraSmallPush(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(ExtraSmallPushProperty, value); + + protected static void SetActualColumn(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(ActualColumnProperty, value); + + protected static void SetActualRow(AvaloniaObject avaloniaObject, int value) => + avaloniaObject.SetValue(ActualRowProperty, value); + + protected override Size ArrangeOverride(Size finalSize) + { + double columnWidth = finalSize.Width / MaxDivision; + + IEnumerable> groupedRows = Children.OfType().GroupBy(GetActualRow); + + double yOffset = 0; + foreach (IGrouping row in groupedRows) + { + double maxRowHeight = row.Max(control => control.DesiredSize.Height); + + foreach (Control element in row) + { + int column = GetActualColumn(element); + int span = GetSpan(element, finalSize.Width); + + Rect rect = new(column * columnWidth, yOffset, span * columnWidth, maxRowHeight); + element.Arrange(rect); + } + + yOffset += maxRowHeight; + } + + return finalSize; + } + + protected int GetOffset(Control control, double width) + { + int GetXS() => Math.Max(0, GetExtraSmallOffset(control)); + int GetSM() => Math.Max(GetXS(), GetSmallOffset(control)); + int GetMD() => Math.Max(GetSM(), GetMediumOffset(control)); + int GetLG() => Math.Max(GetMD(), GetLargeOffset(control)); + + int span = width < Thresholds.ExtraSmallToSmall ? GetXS() : width < Thresholds.SmallToMedium ? GetSM() : width < Thresholds.MediumToLarge ? GetMD() : GetLG(); + return Math.Min(span, MaxDivision); + } + + protected int GetPull(Control control, double width) + { + int GetXS() => Math.Max(0, GetExtraSmallPull(control)); + int GetSM() => Math.Max(GetXS(), GetSmallPull(control)); + int GetMD() => Math.Max(GetSM(), GetMediumPull(control)); + int GetLG() => Math.Max(GetMD(), GetLargePull(control)); + + int span = width < Thresholds.ExtraSmallToSmall ? GetXS() : width < Thresholds.SmallToMedium ? GetSM() : width < Thresholds.MediumToLarge ? GetMD() : GetLG(); + return Math.Min(span, MaxDivision); + } + + protected int GetPush(Control control, double width) + { + int GetXS() => Math.Max(0, GetExtraSmallPush(control)); + int GetSM() => Math.Max(GetXS(), GetSmallPush(control)); + int GetMD() => Math.Max(GetSM(), GetMediumPush(control)); + int GetLG() => Math.Max(GetMD(), GetLargePush(control)); + + int span = width < Thresholds.ExtraSmallToSmall ? GetXS() : width < Thresholds.SmallToMedium ? GetSM() : width < Thresholds.MediumToLarge ? GetMD() : GetLG(); + return Math.Min(span, MaxDivision); + } + + protected int GetSpan(Control control, double width) + { + int GetXS() => Math.Max(0, GetExtraSmall(control)); + int GetSM() => Math.Max(GetXS(), GetSmall(control)); + int GetMD() => Math.Max(GetSM(), GetMedium(control)); + int GetLG() => Math.Max(GetMD(), GetLarge(control)); + + int span = width < Thresholds.ExtraSmallToSmall ? GetXS() : width < Thresholds.SmallToMedium ? GetSM() : width < Thresholds.MediumToLarge ? GetMD() : GetLG(); + return Math.Min(span, MaxDivision); + } + + protected override Size MeasureOverride(Size availableSize) + { + int count = 0; + int currentRow = 0; + + double availableWidth = double.IsPositiveInfinity(availableSize.Width) + ? double.PositiveInfinity + : availableSize.Width / MaxDivision; + + foreach (Control control in Children.OfType()) + { + if (control.IsVisible) + { + int span = GetSpan(control, availableSize.Width); + int offset = GetOffset(control, availableSize.Width); + int push = GetPush(control, availableSize.Width); + int pull = GetPull(control, availableSize.Width); + + if (count + span + offset > MaxDivision) + { + currentRow++; + count = 0; + } + + SetActualColumn(control, count + offset + push - pull); + SetActualRow(control, currentRow); + + count += span + offset; + + Size size = new(availableWidth * span, double.PositiveInfinity); + control.Measure(size); + } + } + + IEnumerable> groupedRows = Children.OfType().GroupBy(GetActualRow); + + Size totalSize = new(groupedRows.Max(rows => rows.Sum(control => control.DesiredSize.Width)), + groupedRows.Sum(rows => rows.Max(control => control.DesiredSize.Height))); + + return totalSize; + } + } +} diff --git a/Toolkit.UI.Controls.Avalonia/ResponsiveGrid/SizeThresholds.cs b/Toolkit.UI.Controls.Avalonia/ResponsiveGrid/SizeThresholds.cs new file mode 100644 index 0000000..12219a6 --- /dev/null +++ b/Toolkit.UI.Controls.Avalonia/ResponsiveGrid/SizeThresholds.cs @@ -0,0 +1,36 @@ +using Avalonia; +using System.ComponentModel; + +namespace Toolkit.UI.Controls.Avalonia +{ + [TypeConverter(typeof(SizeThresholdsTypeConverter))] + public class SizeThresholds : AvaloniaObject + { + public static readonly StyledProperty MediumToLargeProperty = + AvaloniaProperty.Register("MediumToLarge", 1200.0); + + public static readonly StyledProperty SmallToMediumProperty = + AvaloniaProperty.Register("SmallToMedium", 992.0); + + public static readonly StyledProperty ExtraSmallToSmallProperty = + AvaloniaProperty.Register("ExtraSmallToSmall", 768.0); + + public double MediumToLarge + { + get => (double)GetValue(MediumToLargeProperty); + set => SetValue(MediumToLargeProperty, value); + } + + public double SmallToMedium + { + get => (double)GetValue(SmallToMediumProperty); + set => SetValue(SmallToMediumProperty, value); + } + + public double ExtraSmallToSmall + { + get => (double)GetValue(ExtraSmallToSmallProperty); + set => SetValue(ExtraSmallToSmallProperty, value); + } + } +} diff --git a/Toolkit.UI.Controls.Avalonia/ResponsiveGrid/SizeThresholdsTypeConverter.cs b/Toolkit.UI.Controls.Avalonia/ResponsiveGrid/SizeThresholdsTypeConverter.cs new file mode 100644 index 0000000..99c87ec --- /dev/null +++ b/Toolkit.UI.Controls.Avalonia/ResponsiveGrid/SizeThresholdsTypeConverter.cs @@ -0,0 +1,44 @@ +using System.ComponentModel; +using System.Globalization; + +namespace Toolkit.UI.Controls.Avalonia +{ + public class SizeThresholdsTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type? sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext? context, + CultureInfo? culture, object value) + { + if (value is not string text) + { + return new SizeThresholds(); + } + + List values = text.Split(',') + .Select(o => o.Trim()) + .Select(o => int.TryParse(o, out var result) ? result : 0) + .ToList(); + + if (values.Count != 3) + { + return new SizeThresholds + { + ExtraSmallToSmall = 768, + SmallToMedium = 992, + MediumToLarge = 1200 + }; + } + + return new SizeThresholds + { + ExtraSmallToSmall = values[0], + SmallToMedium = values[1], + MediumToLarge = values[2] + }; + } + } +}