Skip to content

Commit d2e29b6

Browse files
authored
Implement "AdaptiveColumnsPanel" (#8)
1 parent a98c098 commit d2e29b6

5 files changed

Lines changed: 252 additions & 0 deletions

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Windows;
5+
using System.Windows.Controls;
6+
7+
namespace OpenSilver.ControlsKit
8+
{
9+
/// <summary>
10+
/// A responsive <see cref="Panel"/> that dynamically switches between layouts based on its width:
11+
/// when its width meets or exceeds <c>NoColumnsBelowWidth</c>, children are arranged into equal-width columns (one per visible child);
12+
/// otherwise, they are stacked vertically in a single column.
13+
/// Child horizontal and vertical alignments are respected within each allocated slot.
14+
/// </summary>
15+
public class AdaptiveColumnsPanel : Panel
16+
{
17+
/// <summary>
18+
/// When the available width is ≤ this threshold, children stack vertically.
19+
/// When > this threshold, children lay out in N equal-width columns (N = # of children).
20+
/// </summary>
21+
public static readonly DependencyProperty NoColumnsBelowWidthProperty =
22+
DependencyProperty.Register(
23+
nameof(NoColumnsBelowWidth),
24+
typeof(double),
25+
typeof(AdaptiveColumnsPanel),
26+
new FrameworkPropertyMetadata(
27+
500d,
28+
FrameworkPropertyMetadataOptions.AffectsMeasure));
29+
30+
/// <summary>
31+
/// When the available width is ≤ this threshold, children stack vertically.
32+
/// When > this threshold, children lay out in N equal-width columns (N = # of children).
33+
/// </summary>
34+
public double NoColumnsBelowWidth
35+
{
36+
get => (double)GetValue(NoColumnsBelowWidthProperty);
37+
set => SetValue(NoColumnsBelowWidthProperty, value);
38+
}
39+
40+
// Get visible children only once and as FrameworkElement directly
41+
private List<FrameworkElement> GetVisibleChildren() =>
42+
Children.OfType<FrameworkElement>()
43+
.Where(c => c.Visibility != Visibility.Collapsed)
44+
.ToList();
45+
46+
// Determine layout mode in one place
47+
private bool ShouldUseColumns(double availableWidth, int childCount) =>
48+
!double.IsInfinity(availableWidth) &&
49+
availableWidth > NoColumnsBelowWidth &&
50+
childCount > 0;
51+
52+
protected override Size MeasureOverride(Size availableSize)
53+
{
54+
double layoutWidth = double.IsNaN(this.Width) ? availableSize.Width : this.Width;
55+
var children = GetVisibleChildren();
56+
int count = children.Count;
57+
if (count == 0)
58+
{
59+
return base.MeasureOverride(availableSize);
60+
}
61+
62+
bool useColumns = ShouldUseColumns(layoutWidth, count);
63+
64+
if (!useColumns)
65+
{
66+
// Vertical stack mode
67+
double desiredW = 0, desiredH = 0;
68+
foreach (var child in children)
69+
{
70+
child.Measure(new Size(layoutWidth, double.PositiveInfinity));
71+
double mW = child.Margin.Left + child.Margin.Right;
72+
double mH = child.Margin.Top + child.Margin.Bottom;
73+
desiredW = Math.Max(desiredW, child.DesiredSize.Width + mW);
74+
desiredH += child.DesiredSize.Height + mH;
75+
}
76+
return new Size(desiredW, desiredH);
77+
}
78+
else
79+
{
80+
// Column mode
81+
double colW = layoutWidth / count;
82+
double maxChildH = 0;
83+
foreach (var child in children)
84+
{
85+
child.Measure(new Size(colW, double.PositiveInfinity));
86+
double mH = child.Margin.Top + child.Margin.Bottom;
87+
maxChildH = Math.Max(maxChildH, child.DesiredSize.Height + mH);
88+
}
89+
return new Size(layoutWidth, maxChildH);
90+
}
91+
}
92+
93+
protected override Size ArrangeOverride(Size finalSize)
94+
{
95+
var children = GetVisibleChildren();
96+
int count = children.Count;
97+
if (count == 0)
98+
{
99+
return base.ArrangeOverride(finalSize);
100+
}
101+
bool useColumns = ShouldUseColumns(finalSize.Width, count);
102+
103+
if (!useColumns)
104+
{
105+
// Vertical stack mode
106+
double y = 0;
107+
foreach (var child in children)
108+
{
109+
// Account for margins
110+
double marginLeft = child.Margin.Left;
111+
double marginRight = child.Margin.Right;
112+
double marginTop = child.Margin.Top;
113+
double marginBottom = child.Margin.Bottom;
114+
115+
// Calculate available width for this child
116+
double availableWidth = finalSize.Width - marginLeft - marginRight;
117+
118+
// Determine width based on alignment
119+
double width = (child.HorizontalAlignment == HorizontalAlignment.Stretch)
120+
? availableWidth
121+
: Math.Min(child.DesiredSize.Width, availableWidth);
122+
123+
// Calculate x position with alignment
124+
double x = marginLeft + GetHorizontalAlignmentOffset(availableWidth, width, child.HorizontalAlignment);
125+
126+
// Arrange the child
127+
child.Arrange(new Rect(x, y + marginTop, width, child.DesiredSize.Height));
128+
129+
// Move to next vertical position
130+
y += child.DesiredSize.Height + marginTop + marginBottom;
131+
}
132+
}
133+
else
134+
{
135+
// Column mode - calculate actual height needed
136+
double maxChildH = 0;
137+
foreach (var child in children)
138+
{
139+
double mH = child.Margin.Top + child.Margin.Bottom;
140+
maxChildH = Math.Max(maxChildH, child.DesiredSize.Height + mH);
141+
}
142+
143+
// Column width
144+
double colW = finalSize.Width / count;
145+
146+
for (int i = 0; i < count; i++)
147+
{
148+
var child = children[i];
149+
double marginLeft = child.Margin.Left;
150+
double marginRight = child.Margin.Right;
151+
double marginTop = child.Margin.Top;
152+
double marginBottom = child.Margin.Bottom;
153+
154+
// Available width for this column
155+
double availableWidth = colW - marginLeft - marginRight;
156+
157+
// Determine width based on alignment
158+
double width = (child.HorizontalAlignment == HorizontalAlignment.Stretch)
159+
? availableWidth
160+
: Math.Min(child.DesiredSize.Width, availableWidth);
161+
162+
// Calculate horizontal position
163+
double x = (i * colW) + marginLeft +
164+
GetHorizontalAlignmentOffset(availableWidth, width, child.HorizontalAlignment);
165+
166+
// Calculate height based on alignment
167+
double height = (child.VerticalAlignment == VerticalAlignment.Stretch)
168+
? maxChildH - marginTop - marginBottom
169+
: child.DesiredSize.Height;
170+
171+
// Calculate vertical position
172+
double availableHeight = maxChildH - marginTop - marginBottom;
173+
double y = marginTop + GetVerticalAlignmentOffset(availableHeight, height, child.VerticalAlignment);
174+
175+
// Arrange the child
176+
child.Arrange(new Rect(x, y, width, height));
177+
}
178+
179+
// Return the correct size
180+
return new Size(finalSize.Width, maxChildH);
181+
}
182+
183+
return finalSize;
184+
}
185+
186+
private double GetHorizontalAlignmentOffset(double container, double element, HorizontalAlignment align)
187+
{
188+
return align switch
189+
{
190+
HorizontalAlignment.Center => (container - element) / 2,
191+
HorizontalAlignment.Right => container - element,
192+
_ => 0 // Left or Stretch
193+
};
194+
}
195+
196+
private double GetVerticalAlignmentOffset(double container, double element, VerticalAlignment align)
197+
{
198+
return align switch
199+
{
200+
VerticalAlignment.Center => (container - element) / 2,
201+
VerticalAlignment.Bottom => container - element,
202+
_ => 0 // Top or Stretch
203+
};
204+
}
205+
}
206+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Page
2+
x:Class="FastControls.TestApp.Pages.TestAdaptiveColumnsPanel"
3+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:controlskit="clr-namespace:OpenSilver.ControlsKit;assembly=OpenSilver.ControlsKit.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" d:DesignWidth="512" d:DesignHeight="932" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d">
6+
<controlskit:AdaptiveColumnsPanel NoColumnsBelowWidth="400" VerticalAlignment="Top">
7+
<Border Height="100" CornerRadius="20" Margin="5" Background="#CC505E00">
8+
<TextBlock Text="Content 1" HorizontalAlignment="Center" Foreground="White" Margin="0,10,0,0"/>
9+
</Border>
10+
<Border Height="100" CornerRadius="20" Margin="5" Background="#CC7D0030">
11+
<TextBlock Text="Content 2" HorizontalAlignment="Center" Foreground="White" Margin="0,10,0,0"/>
12+
</Border>
13+
<Border Height="100" CornerRadius="20" Margin="5" Background="#CC00567A">
14+
<TextBlock Text="Content 3" HorizontalAlignment="Center" Foreground="White" Margin="0,10,0,0"/>
15+
</Border>
16+
</controlskit:AdaptiveColumnsPanel>
17+
</Page>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using DotNetForHtml5.Core;
2+
using System;
3+
using System.Collections.ObjectModel;
4+
using System.ComponentModel;
5+
using System.Diagnostics;
6+
using System.Linq;
7+
using System.Runtime.CompilerServices;
8+
using System.Threading.Tasks;
9+
using System.Windows;
10+
using System.Windows.Controls;
11+
using System.Windows.Data;
12+
using System.Windows.Media;
13+
14+
namespace FastControls.TestApp.Pages
15+
{
16+
public partial class TestAdaptiveColumnsPanel : Page
17+
{
18+
public TestAdaptiveColumnsPanel()
19+
{
20+
this.InitializeComponent();
21+
}
22+
23+
}
24+
}

src/TestApp/TestApp/Registry/TestRegistry.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ static TestRegistry()
1212
{
1313
new TreeItem("TestStaggeredPanel", "TestStaggeredPanel"),
1414
new TreeItem("FastCheckBox", "FastCheckBox"),
15+
new TreeItem("AdaptiveColumnsPanel", "AdaptiveColumnsPanel"),
1516
};
1617
}
1718
}

src/TestApp/TestApp/TestApp.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
<Page Include="Pages\FastCheckBox.xaml">
2121
<Generator>MSBuild:Compile</Generator>
2222
</Page>
23+
<Page Include="Pages\AdaptiveColumnsPanel.xaml">
24+
<Generator>MSBuild:Compile</Generator>
25+
</Page>
2326
<Page Include="Pages\TestStaggeredPanel.xaml">
2427
<Generator>MSBuild:Compile</Generator>
2528
</Page>
@@ -29,6 +32,7 @@
2932
<Compile Include="App.xaml.cs" />
3033
<Compile Include="MainPage.xaml.cs" />
3134
<Compile Include="Pages\FastCheckBox.xaml.cs" />
35+
<Compile Include="Pages\AdaptiveColumnsPanel.xaml.cs" />
3236
<Compile Include="Pages\TestStaggeredPanel.xaml.cs" />
3337
<Compile Include="Registry\TestRegistry.cs" />
3438
<Compile Include="Registry\TreeItem.cs" />

0 commit comments

Comments
 (0)