From 99c07b3da1bbb8291e96851af776ec28fca53228 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Sat, 29 Nov 2025 12:28:28 +0200 Subject: [PATCH 01/14] StretchPanel initial commit --- components/StretchPanel/OpenSolution.bat | 3 + .../StretchPanel/samples/Assets/icon.png | Bin 0 -> 2216 bytes .../StretchPanel/samples/Dependencies.props | 31 ++++ .../samples/StretchPanel.Samples.csproj | 15 ++ .../StretchPanel/samples/StretchPanel.md | 32 +++++ .../samples/StretchPanelBasicSample.xaml | 25 ++++ .../samples/StretchPanelBasicSample.xaml.cs | 30 ++++ ...Toolkit.WinUI.Controls.StretchPanel.csproj | 14 ++ .../StretchPanel/src/Dependencies.props | 31 ++++ components/StretchPanel/src/MultiTarget.props | 9 ++ components/StretchPanel/src/StretchPanel.cs | 103 ++++++++++++++ .../tests/ExampleStretchPanelTestClass.cs | 134 ++++++++++++++++++ .../tests/ExampleStretchPanelTestPage.xaml | 14 ++ .../tests/ExampleStretchPanelTestPage.xaml.cs | 16 +++ .../tests/StretchPanel.Tests.projitems | 23 +++ .../tests/StretchPanel.Tests.shproj | 13 ++ tooling | 2 +- 17 files changed, 494 insertions(+), 1 deletion(-) create mode 100644 components/StretchPanel/OpenSolution.bat create mode 100644 components/StretchPanel/samples/Assets/icon.png create mode 100644 components/StretchPanel/samples/Dependencies.props create mode 100644 components/StretchPanel/samples/StretchPanel.Samples.csproj create mode 100644 components/StretchPanel/samples/StretchPanel.md create mode 100644 components/StretchPanel/samples/StretchPanelBasicSample.xaml create mode 100644 components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs create mode 100644 components/StretchPanel/src/CommunityToolkit.WinUI.Controls.StretchPanel.csproj create mode 100644 components/StretchPanel/src/Dependencies.props create mode 100644 components/StretchPanel/src/MultiTarget.props create mode 100644 components/StretchPanel/src/StretchPanel.cs create mode 100644 components/StretchPanel/tests/ExampleStretchPanelTestClass.cs create mode 100644 components/StretchPanel/tests/ExampleStretchPanelTestPage.xaml create mode 100644 components/StretchPanel/tests/ExampleStretchPanelTestPage.xaml.cs create mode 100644 components/StretchPanel/tests/StretchPanel.Tests.projitems create mode 100644 components/StretchPanel/tests/StretchPanel.Tests.shproj diff --git a/components/StretchPanel/OpenSolution.bat b/components/StretchPanel/OpenSolution.bat new file mode 100644 index 000000000..814a56d4b --- /dev/null +++ b/components/StretchPanel/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/StretchPanel/samples/Assets/icon.png b/components/StretchPanel/samples/Assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8435bcaa9fc371ca8e92db07ae596e0d57c8b9b0 GIT binary patch literal 2216 zcmV;Z2v_%sP);M1&0drDELIAGL9O(c600d`2O+f$vv5yP=YKs(Je9p7&Ka&|n*ecc!Pix~iV~>Yi6*wXL?Fq6O}_ef#!O2;q#X&d1-r zFW&c8*L5NOYWz*lA^xUE>kGNx-uL6v^z@vr)V_TA(vQ#Yoo1$I^Fv;IKkA`?U*Z6OoBe{XX@DLpL{F3+>RV2oP732rn@P@98F ziH~#f`tA7f<8t}(<$sLlUkdm_STvOKGYXY{9St3tGiu1>duN9F9aTe*kTXOCkoLYj zr)5cJP?i}f+kBoB8b~Q1rbJi`730DXGLx}aCV-6tC522QjZs)X03S%#=jXopQNe7G z@dlToiDbGDSy!a_DD)NYWiV?sGap0Dq-qgnA&~LHECw(NL8T=) z1ct(ga2;|14nD@qHwmVQ0;2|YUomW^!U#`7l>>&Bh;u)pT<|$jFoqk6%HTc~^RQ@( z5hZ5X^n7`vt!*nY9@rFRqF{^wF`}&H4I4JdfddC*W@e@$oS$Q04aThBn*gT3)URMp z>G{o@H*)RTHCb6%8B?H}yjcXcUm9p(K=nWD0vP!PaCP$@(k!31bkrIJ!2)-tl96*+&}@I6!3M8qT~Q2?u8 zcy@MHS+IzlwjvzRGx~+*l>(Gi6$!C8Jgi;2)=XVKe*CB(K745TS`)G2;nuBN9cqsP zh3?9y5yI0j3rZt%Q;V3i*L;-@bj}#EBDL zi-O{(`k1i!rOBH%ZPF-Qh^8PnZ{F0;pFa!x1_lMnUFbI&&8gFC}WVB;VU)DL&bgeyDBGT1Uw=yFE1xQ zlXdIXN%Xx|Sv6HKV+J+vFk{k20ni@>s(7s{7```cTQ%Vf8y`xM()#6VjL@l-2UZ)1 zMAlp(2n!()MJZ+A7DT29I(lXL!EfSTFx}nSy)d`h{3}%2w00Npq^YO)x9XqDcq0Kb`t5*xfQqpFjCI=5f3~jf78p^G}no2Fzdc;)IysSSZVr(fukQEprykDy< zqbZnxBN8(`!7W?1$e}}r1c|2>qh?{>d-v{@?c2BeJQ<2<$;_cXbnDiw*59|?yLU^< zSA=eeDMyH&Dn;O?U<@moWoql!uTK{z=_+aO*s+8Ar_R9^^Jcn6#~@GNDp>Q(&lq|B z{JGq_cdrQ3>E_6hBQiff?~J4|4F&T## ze2Ot?F7i1RStkmn!!cL^bKdFtd(+&zckeRXgNN#PA!`aD zjaC7J&2fZoFH{s(d8}#I6o7s`O)w?K`{bKiENsKV!h(MK^r(|LX> z*~rJxUHVoXzp-XhUqnbQUAiRq@89q5e?*H1Nj(o2FJ5%B>%JZY`Gw<0&+dhGuj!C7 z?uYbkO5XY{KIV*@{VRoX8s`fm<068)Y!=w) zn?l)IstDTf!(Iu(8i;vTaeMOuE%QV$RY7$1cP-aqS07(jX4W{bFHp!#3m}TTAaaF3L~n9Q)s^3xP5nw5 zM&HvBw5z{TbdA3!8Di)wIs`5S;W!fVdWDbiH|P}wlVfDMuB&#@so4j+uC6L7Q#M38 z*q?Pnc_gqfql3rn?qh)V@~B|(78+7U zKOCcocD&A`ELB-^?%cVvanNF%vtSH&^_LVuBqdkprWDoUyaLCf$s5zvc?rH#NGXk= qmFA_t_5FR}!i7I&wXL?Ful)xU?DJJ%Hwu*i0000 + + + + + + + + + + + + + + + + + + + + + diff --git a/components/StretchPanel/samples/StretchPanel.Samples.csproj b/components/StretchPanel/samples/StretchPanel.Samples.csproj new file mode 100644 index 000000000..27eaada09 --- /dev/null +++ b/components/StretchPanel/samples/StretchPanel.Samples.csproj @@ -0,0 +1,15 @@ + + + + + StretchPanel + + + + + + + StretchPanelBasicSample.xaml + + + diff --git a/components/StretchPanel/samples/StretchPanel.md b/components/StretchPanel/samples/StretchPanel.md new file mode 100644 index 000000000..b9c75a908 --- /dev/null +++ b/components/StretchPanel/samples/StretchPanel.md @@ -0,0 +1,32 @@ +--- +title: StretchPanel +author: githubaccount +description: TODO: Your experiment's description here +keywords: StretchPanel, Control, Layout +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 0 +issue-id: 0 +icon: assets/icon.png +--- + + + + + + + + + +# StretchPanel + +TODO: Fill in information about this experiment and how to get started here... + +## Custom Control + +You can inherit from an existing component as well, like `Panel`, this example shows a control without a +XAML Style that will be more light-weight to consume by an app developer: + +> [!Sample StretchPanelBasicSample] diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml b/components/StretchPanel/samples/StretchPanelBasicSample.xaml new file mode 100644 index 000000000..f6d5700ff --- /dev/null +++ b/components/StretchPanel/samples/StretchPanelBasicSample.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs b/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs new file mode 100644 index 000000000..2619e3b6b --- /dev/null +++ b/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Controls; + +namespace StretchPanelExperiment.Samples; + +/// +/// An example sample page of a custom control inheriting from Panel. +/// +[ToolkitSampleTextOption("TitleText", "This is a title", Title = "Input the text")] +[ToolkitSampleMultiChoiceOption("LayoutOrientation", "Horizontal", "Vertical", Title = "Orientation")] + +[ToolkitSample(id: nameof(StretchPanelBasicSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(StretchPanel)} custom control.")] +public sealed partial class StretchPanelBasicSample : Page +{ + public StretchPanelBasicSample() + { + this.InitializeComponent(); + } + + // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149 + public static Orientation ConvertStringToOrientation(string orientation) => orientation switch + { + "Vertical" => Orientation.Vertical, + "Horizontal" => Orientation.Horizontal, + _ => throw new System.NotImplementedException(), + }; +} diff --git a/components/StretchPanel/src/CommunityToolkit.WinUI.Controls.StretchPanel.csproj b/components/StretchPanel/src/CommunityToolkit.WinUI.Controls.StretchPanel.csproj new file mode 100644 index 000000000..78a283a99 --- /dev/null +++ b/components/StretchPanel/src/CommunityToolkit.WinUI.Controls.StretchPanel.csproj @@ -0,0 +1,14 @@ + + + + + StretchPanel + This package contains StretchPanel. + + + CommunityToolkit.WinUI.Controls.StretchPanelRns + + + + + diff --git a/components/StretchPanel/src/Dependencies.props b/components/StretchPanel/src/Dependencies.props new file mode 100644 index 000000000..e622e1df4 --- /dev/null +++ b/components/StretchPanel/src/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/StretchPanel/src/MultiTarget.props b/components/StretchPanel/src/MultiTarget.props new file mode 100644 index 000000000..b11c19426 --- /dev/null +++ b/components/StretchPanel/src/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android; + + \ No newline at end of file diff --git a/components/StretchPanel/src/StretchPanel.cs b/components/StretchPanel/src/StretchPanel.cs new file mode 100644 index 000000000..88943ceba --- /dev/null +++ b/components/StretchPanel/src/StretchPanel.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +public partial class StretchPanel : Panel +{ + /// + /// Identifies the property. + /// + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(StretchPanel), new PropertyMetadata(null, OnOrientationChanged)); + + /// + /// Gets the preference of the rows/columns when there are a non-square number of children. Defaults to Vertical. + /// + public Orientation Orientation + { + get { return (Orientation)GetValue(OrientationProperty); } + set { SetValue(OrientationProperty, value); } + } + + // Invalidate our layout when the property changes. + private static void OnOrientationChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args) + { + if (dependencyObject is StretchPanel panel) + { + panel.InvalidateMeasure(); + } + } + + // Store calculations we want to use between the Measure and Arrange methods. + int _columnCount; + double _cellWidth, _cellHeight; + + protected override Size MeasureOverride(Size availableSize) + { + // Determine the square that can contain this number of items. + var maxrc = (int)Math.Ceiling(Math.Sqrt(Children.Count)); + // Get an aspect ratio from availableSize, decides whether to trim row or column. + var aspectratio = availableSize.Width / availableSize.Height; + if (Orientation == Orientation.Vertical) { aspectratio = 1 / aspectratio; } + + int rowcount; + + // Now trim this square down to a rect, many times an entire row or column can be omitted. + if (aspectratio > 1) + { + rowcount = maxrc; + _columnCount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc; + } + else + { + rowcount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc; + _columnCount = maxrc; + } + + // Now that we have a column count, divide available horizontal, that's our cell width. + _cellWidth = (int)Math.Floor(availableSize.Width / _columnCount); + // Next get a cell height, same logic of dividing available vertical by rowcount. + _cellHeight = Double.IsInfinity(availableSize.Height) ? Double.PositiveInfinity : availableSize.Height / rowcount; + + double maxcellheight = 0; + + foreach (UIElement child in Children) + { + child.Measure(new Size(_cellWidth, _cellHeight)); + maxcellheight = (child.DesiredSize.Height > maxcellheight) ? child.DesiredSize.Height : maxcellheight; + } + + return LimitUnboundedSize(availableSize, maxcellheight); + } + + // This method limits the panel height when no limit is imposed by the panel's parent. + // That can happen to height if the panel is close to the root of main app window. + // In this case, base the height of a cell on the max height from desired size + // and base the height of the panel on that number times the #rows. + Size LimitUnboundedSize(Size input, double maxcellheight) + { + if (Double.IsInfinity(input.Height)) + { + input.Height = maxcellheight * _columnCount; + _cellHeight = maxcellheight; + } + return input; + } + + protected override Size ArrangeOverride(Size finalSize) + { + int count = 1; + double x, y; + foreach (UIElement child in Children) + { + x = (count - 1) % _columnCount * _cellWidth; + y = ((int)(count - 1) / _columnCount) * _cellHeight; + Point anchorPoint = new Point(x, y); + child.Arrange(new Rect(anchorPoint, child.DesiredSize)); + count++; + } + return finalSize; + } +} diff --git a/components/StretchPanel/tests/ExampleStretchPanelTestClass.cs b/components/StretchPanel/tests/ExampleStretchPanelTestClass.cs new file mode 100644 index 000000000..16fac7038 --- /dev/null +++ b/components/StretchPanel/tests/ExampleStretchPanelTestClass.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Tooling.TestGen; +using CommunityToolkit.Tests; +using CommunityToolkit.WinUI.Controls; + +namespace StretchPanelTests; + +[TestClass] +public partial class ExampleStretchPanelTestClass : VisualUITestBase +{ + // If you don't need access to UI objects directly or async code, use this pattern. + [TestMethod] + public void SimpleSynchronousExampleTest() + { + var assembly = typeof(StretchPanel).Assembly; + var type = assembly.GetType(typeof(StretchPanel).FullName ?? string.Empty); + + Assert.IsNotNull(type, "Could not find StretchPanel type."); + Assert.AreEqual(typeof(StretchPanel), type, "Type of StretchPanel does not match expected type."); + } + + // If you don't need access to UI objects directly, use this pattern. + [TestMethod] + public async Task SimpleAsyncExampleTest() + { + await Task.Delay(250); + + Assert.IsTrue(true); + } + + // Example that shows how to check for exception throwing. + [TestMethod] + public void SimpleExceptionCheckTest() + { + // If you need to check exceptions occur for invalid inputs, etc... + // Use Assert.ThrowsException to limit the scope to where you expect the error to occur. + // Otherwise, using the ExpectedException attribute could swallow or + // catch other issues in setup code. + Assert.ThrowsException(() => throw new NotImplementedException()); + } + + // The UIThreadTestMethod automatically dispatches to the UI for us to work with UI objects. + [UIThreadTestMethod] + public void SimpleUIAttributeExampleTest() + { + var component = new StretchPanel(); + Assert.IsNotNull(component); + } + + // The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter. + // This lets us actually test a control as it would behave within an actual application. + // The page will already be loaded by the time your test is called. + [UIThreadTestMethod] + public void SimpleUIExamplePageTest(ExampleStretchPanelTestPage page) + { + // You can use the Toolkit Visual Tree helpers here to find the component by type or name: + var component = page.FindDescendant(); + + Assert.IsNotNull(component); + + var componentByName = page.FindDescendant("StretchPanelControl"); + + Assert.IsNotNull(componentByName); + } + + // You can still do async work with a UIThreadTestMethod as well. + [UIThreadTestMethod] + public async Task SimpleAsyncUIExamplePageTest(ExampleStretchPanelTestPage page) + { + // This helper can be used to wait for a rendering pass to complete. + // Note, this is already done by loading a Page with the [UIThreadTestMethod] helper. + await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }); + + var component = page.FindDescendant(); + + Assert.IsNotNull(component); + } + + //// ----------------------------- ADVANCED TEST SCENARIOS ----------------------------- + + // If you need to use DataRow, you can use this pattern with the UI dispatch still. + // Otherwise, checkout the UIThreadTestMethod attribute above. + // See https://github.com/CommunityToolkit/Labs-Windows/issues/186 + [TestMethod] + public async Task ComplexAsyncUIExampleTest() + { + await EnqueueAsync(() => + { + var component = new StretchPanel(); + Assert.IsNotNull(component); + }); + } + + // If you want to load other content not within a XAML page using the UIThreadTestMethod above. + // Then you can do that using the Load/UnloadTestContentAsync methods. + [TestMethod] + public async Task ComplexAsyncLoadUIExampleTest() + { + await EnqueueAsync(async () => + { + var component = new StretchPanel(); + Assert.IsNotNull(component); + Assert.IsFalse(component.IsLoaded); + + await LoadTestContentAsync(component); + + Assert.IsTrue(component.IsLoaded); + + await UnloadTestContentAsync(component); + + Assert.IsFalse(component.IsLoaded); + }); + } + + // You can still use the UIThreadTestMethod to remove the extra layer for the dispatcher as well: + [UIThreadTestMethod] + public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest() + { + var component = new StretchPanel(); + Assert.IsNotNull(component); + Assert.IsFalse(component.IsLoaded); + + await LoadTestContentAsync(component); + + Assert.IsTrue(component.IsLoaded); + + await UnloadTestContentAsync(component); + + Assert.IsFalse(component.IsLoaded); + } +} diff --git a/components/StretchPanel/tests/ExampleStretchPanelTestPage.xaml b/components/StretchPanel/tests/ExampleStretchPanelTestPage.xaml new file mode 100644 index 000000000..4d9b76365 --- /dev/null +++ b/components/StretchPanel/tests/ExampleStretchPanelTestPage.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/components/StretchPanel/tests/ExampleStretchPanelTestPage.xaml.cs b/components/StretchPanel/tests/ExampleStretchPanelTestPage.xaml.cs new file mode 100644 index 000000000..c88957b04 --- /dev/null +++ b/components/StretchPanel/tests/ExampleStretchPanelTestPage.xaml.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace StretchPanelTests; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ExampleStretchPanelTestPage : Page +{ + public ExampleStretchPanelTestPage() + { + this.InitializeComponent(); + } +} diff --git a/components/StretchPanel/tests/StretchPanel.Tests.projitems b/components/StretchPanel/tests/StretchPanel.Tests.projitems new file mode 100644 index 000000000..021e2defd --- /dev/null +++ b/components/StretchPanel/tests/StretchPanel.Tests.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 1EFF9838-CA24-43C3-AA4F-0B321F74861B + + + StretchPanelTests + + + + + ExampleStretchPanelTestPage.xaml + + + + + Designer + MSBuild:Compile + + + \ No newline at end of file diff --git a/components/StretchPanel/tests/StretchPanel.Tests.shproj b/components/StretchPanel/tests/StretchPanel.Tests.shproj new file mode 100644 index 000000000..1b1e2d5fb --- /dev/null +++ b/components/StretchPanel/tests/StretchPanel.Tests.shproj @@ -0,0 +1,13 @@ + + + + 1EFF9838-CA24-43C3-AA4F-0B321F74861B + 14.0 + + + + + + + + diff --git a/tooling b/tooling index e7eb23621..fa4dd480f 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit e7eb23621735ea8d4a3191ac399848acc918f1ec +Subproject commit fa4dd480fda756e4d503bea17629eda6c89afae0 From e53c20688101ffb3360365c2f89fb9c3548b9bef Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Sat, 29 Nov 2025 15:06:24 +0200 Subject: [PATCH 02/14] Basic StretchPanel implementation --- .../samples/StretchPanelBasicSample.xaml | 64 +++- .../samples/StretchPanelBasicSample.xaml.cs | 1 - .../src/StretchPanel.Properties.cs | 109 +++++++ components/StretchPanel/src/StretchPanel.cs | 299 ++++++++++++++---- 4 files changed, 392 insertions(+), 81 deletions(-) create mode 100644 components/StretchPanel/src/StretchPanel.Properties.cs diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml b/components/StretchPanel/samples/StretchPanelBasicSample.xaml index f6d5700ff..5505a9bf3 100644 --- a/components/StretchPanel/samples/StretchPanelBasicSample.xaml +++ b/components/StretchPanel/samples/StretchPanelBasicSample.xaml @@ -8,18 +8,54 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs b/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs index 2619e3b6b..34d1be6f5 100644 --- a/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs +++ b/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs @@ -9,7 +9,6 @@ namespace StretchPanelExperiment.Samples; /// /// An example sample page of a custom control inheriting from Panel. /// -[ToolkitSampleTextOption("TitleText", "This is a title", Title = "Input the text")] [ToolkitSampleMultiChoiceOption("LayoutOrientation", "Horizontal", "Vertical", Title = "Orientation")] [ToolkitSample(id: nameof(StretchPanelBasicSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(StretchPanel)} custom control.")] diff --git a/components/StretchPanel/src/StretchPanel.Properties.cs b/components/StretchPanel/src/StretchPanel.Properties.cs new file mode 100644 index 000000000..73fb309e7 --- /dev/null +++ b/components/StretchPanel/src/StretchPanel.Properties.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +public partial class StretchPanel +{ + /// + /// An attached property for identifying the requested layout of a child within the panel. + /// + public static readonly DependencyProperty LayoutLengthProperty = + DependencyProperty.Register( + "LayoutLength", + typeof(GridLength), + typeof(StretchPanel), + new PropertyMetadata(GridLength.Auto)); + + /// + /// Backing for the property. + /// + public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( + nameof(Orientation), + typeof(Orientation), + typeof(StretchPanel), + new PropertyMetadata(default(Orientation), OnPropertyChanged)); + + /// + /// Backing for the property. + /// + /// The identifier for the dependency property. + public static readonly DependencyProperty WrapProperty = DependencyProperty.Register( + nameof(VerticalSpacing), + typeof(double), + typeof(StretchPanel), + new PropertyMetadata(default(double), OnPropertyChanged)); + + /// + /// Backing for the property. + /// + /// The identifier for the dependency property. + public static readonly DependencyProperty HorizontalSpacingProperty = DependencyProperty.Register( + nameof(HorizontalSpacing), + typeof(double), + typeof(StretchPanel), + new PropertyMetadata(default(double), OnPropertyChanged)); + + /// + /// Backing for the property. + /// + /// The identifier for the dependency property. + public static readonly DependencyProperty VerticalSpacingProperty = DependencyProperty.Register( + nameof(VerticalSpacing), + typeof(double), + typeof(StretchPanel), + new PropertyMetadata(default(double), OnPropertyChanged)); + + /// + /// Gets or sets the panel orientation. + /// + public Orientation Orientation + { + get => (Orientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + /// + /// Gets or sets whether or not the panel should wrap items. + /// + public bool Wrap + { + get => (bool)GetValue(WrapProperty); + set => SetValue(WrapProperty, value); + } + + /// + /// Gets or sets the horizontal spacing between items. + /// + public double HorizontalSpacing + { + get => (double)GetValue(HorizontalSpacingProperty); + set => SetValue(HorizontalSpacingProperty, value); + } + + /// + /// Gets or sets the vertical spacing between items. + /// + public double VerticalSpacing + { + get => (double)GetValue(VerticalSpacingProperty); + set => SetValue(VerticalSpacingProperty, value); + } + + /// + /// Gets the of an item in the . + /// + public static GridLength GetLayoutLength(DependencyObject obj) => (GridLength)obj.GetValue(LayoutLengthProperty); + + /// + /// Sets the of an item in the . + /// + public static void SetLayoutLength(DependencyObject obj, GridLength value) => obj.SetValue(LayoutLengthProperty, value); + + private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var panel = (StretchPanel)d; + panel.InvalidateMeasure(); + } +} diff --git a/components/StretchPanel/src/StretchPanel.cs b/components/StretchPanel/src/StretchPanel.cs index 88943ceba..411b34fb2 100644 --- a/components/StretchPanel/src/StretchPanel.cs +++ b/components/StretchPanel/src/StretchPanel.cs @@ -2,102 +2,269 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.ComponentModel.DataAnnotations; + namespace CommunityToolkit.WinUI.Controls; +/// +/// A panel that arranges its children in a grid-like fashion, stretching them to fill available space. +/// public partial class StretchPanel : Panel { - /// - /// Identifies the property. - /// - public static readonly DependencyProperty OrientationProperty = - DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(StretchPanel), new PropertyMetadata(null, OnOrientationChanged)); + private List? _rowSpecs; - /// - /// Gets the preference of the rows/columns when there are a non-square number of children. Defaults to Vertical. - /// - public Orientation Orientation + /// + protected override Size MeasureOverride(Size availableSize) { - get { return (Orientation)GetValue(OrientationProperty); } - set { SetValue(OrientationProperty, value); } - } + _rowSpecs = []; - // Invalidate our layout when the property changes. - private static void OnOrientationChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args) - { - if (dependencyObject is StretchPanel panel) + // Define XY/UV coordinate variables + var uvAvailableSize = new UVCoord(availableSize.Width, availableSize.Height, Orientation); + var uvSpacing = new UVCoord(HorizontalSpacing, VerticalSpacing, Orientation); + + double widestRow = 0; + double portionSizeCache = 0; + RowSpec currentRowSpec = default; + + var elements = Children.Where(static e => e.Visibility is Visibility.Visible); + + // Do nothing if the panel is empty + if (!elements.Any()) { - panel.InvalidateMeasure(); + return new Size(0, 0); } - } - // Store calculations we want to use between the Measure and Arrange methods. - int _columnCount; - double _cellWidth, _cellHeight; + foreach (var child in elements) + { + // Measure the child's desired size and get layout + child.Measure(availableSize); + var uvDesiredSize = new UVCoord(child.DesiredSize, Orientation); + var layoutLength = GetLayoutLength(child); - protected override Size MeasureOverride(Size availableSize) + // Attempt to add the child to the current row/column + var spec = new RowSpec(layoutLength, uvDesiredSize, ref portionSizeCache); + if (!currentRowSpec.TryAdd(spec, portionSizeCache, uvSpacing.U, uvAvailableSize.U)) + { + // Could not add to current row/column + // Start a new row/column + _rowSpecs.Add(currentRowSpec); + widestRow = Math.Max(widestRow, currentRowSpec.Measure(portionSizeCache, uvSpacing.U)); + currentRowSpec = spec; + portionSizeCache = 0; + } + } + + // Add the final row/column + _rowSpecs.Add(currentRowSpec); + widestRow = Math.Max(widestRow, currentRowSpec.Measure(portionSizeCache, uvSpacing.U)); + + // Determine if the desired alignment is stretched. + // Don't stretch if infinite space is available though. Attempting to divide infinite space will result in a crash. + bool stretch = Orientation switch + { + Orientation.Horizontal => HorizontalAlignment is HorizontalAlignment.Stretch && !double.IsInfinity(availableSize.Width), + Orientation.Vertical or _ => VerticalAlignment is VerticalAlignment.Stretch && !double.IsInfinity(availableSize.Height), + }; + + // Calculate final desired size + var uvSize = new UVCoord(0, 0, Orientation) + { + U = stretch ? uvAvailableSize.U : widestRow, + V = _rowSpecs.Sum(static rs => rs.MaxOffAxisSize) + (uvSpacing.V * (_rowSpecs.Count - 1)) + }; + + return uvSize.Size; + } + + /// + protected override Size ArrangeOverride(Size finalSize) { - // Determine the square that can contain this number of items. - var maxrc = (int)Math.Ceiling(Math.Sqrt(Children.Count)); - // Get an aspect ratio from availableSize, decides whether to trim row or column. - var aspectratio = availableSize.Width / availableSize.Height; - if (Orientation == Orientation.Vertical) { aspectratio = 1 / aspectratio; } + // Do nothing if there are no rows/columns + if (_rowSpecs is null || _rowSpecs.Count is 0) + return new Size(0, 0); - int rowcount; + // Create XY/UV coordinate variables + var pos = new UVCoord(0, 0, Orientation); + var uvFinalSize = new UVCoord(finalSize, Orientation); + var uvSpacing = new UVCoord(HorizontalSpacing, VerticalSpacing, Orientation); - // Now trim this square down to a rect, many times an entire row or column can be omitted. - if (aspectratio > 1) + var elements = Children.Where(static e => e.Visibility is Visibility.Visible); + foreach (var row in _rowSpecs) { - rowcount = maxrc; - _columnCount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc; + var spacingTotalSize = uvSpacing.U * (row.ItemsCount - 1); + var portionSize = (uvFinalSize.U - row.ReservedSpace - spacingTotalSize) / row.PortionsSum; + + for (int i = 0; i < row.ItemsCount; i++) + { + var child = elements.ElementAt(0); + elements = elements.Skip(1); + + // Sanity check + if (child is null) + { + return finalSize; + } + + // Get layout and desired size + var layoutLength = GetLayoutLength(child); + var uvDesiredSize = new UVCoord(child.DesiredSize, Orientation); + + // Determine the child's U size + double uSize = layoutLength.GridUnitType switch + { + GridUnitType.Auto => uvDesiredSize.U, + GridUnitType.Pixel => layoutLength.Value, + GridUnitType.Star => layoutLength.Value * portionSize, + _ => uvDesiredSize.U, + }; + + // Arrange the child + var size = new UVCoord(0, 0, Orientation) + { + U = uSize, + V = row.MaxOffAxisSize + }; + + // NOTE: The arrange method is still in X/Y coordinate system + child.Arrange(new Rect(pos.X, pos.Y, size.X, size.Y)); + + // Advance the position + pos.U += uSize + uvSpacing.U; + } + + // Advance to the next row/column + pos.U = 0; + pos.V += row.MaxOffAxisSize + uvSpacing.V; } - else + + return finalSize; + } + + private struct RowSpec + { + public RowSpec(GridLength layout, UVCoord desiredSize, ref double portionSize) { - rowcount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc; - _columnCount = maxrc; + switch (layout.GridUnitType) + { + case GridUnitType.Auto: + ReservedSpace = desiredSize.U; + break; + case GridUnitType.Pixel: + ReservedSpace = layout.Value; + break; + case GridUnitType.Star: + PortionsSum = layout.Value; + portionSize = Math.Max(portionSize, desiredSize.U / layout.Value); + break; + } + + MaxOffAxisSize = desiredSize.V; + ItemsCount = 1; } - // Now that we have a column count, divide available horizontal, that's our cell width. - _cellWidth = (int)Math.Floor(availableSize.Width / _columnCount); - // Next get a cell height, same logic of dividing available vertical by rowcount. - _cellHeight = Double.IsInfinity(availableSize.Height) ? Double.PositiveInfinity : availableSize.Height / rowcount; + /// + /// Gets the total reserved space for spacing in the row/column. + /// + /// + /// Items with a fixed size or auto size contribute to this value. + /// + public double ReservedSpace { get; private set; } + + /// + /// Gets the sum of portions in the row/column. + /// + /// + /// Items with a star-sized length contribute to this value. + /// + public double PortionsSum { get; private set; } - double maxcellheight = 0; + /// + /// Gets the maximum width/height of items in the row/column. + /// + /// + /// Width in vertical orientation, height in horizontal orientation. + /// + public double MaxOffAxisSize { get; private set; } - foreach (UIElement child in Children) + /// + /// Gets the number of items in the row/column. + /// + public int ItemsCount { get; private set; } + + public bool TryAdd(RowSpec addend, double portionSize, double spacing, double maxSize) { - child.Measure(new Size(_cellWidth, _cellHeight)); - maxcellheight = (child.DesiredSize.Height > maxcellheight) ? child.DesiredSize.Height : maxcellheight; + // Check if adding the new spec would exceed the maximum size + var reservedSum = ReservedSpace + addend.ReservedSpace; + var portionsSum = PortionsSum + addend.PortionsSum; + var itemsSum = ItemsCount + addend.ItemsCount; + if (reservedSum + (portionsSum * portionSize) + ((itemsSum - 1) * spacing) > maxSize) + return false; + + // Update the current spec to include the new spec + ReservedSpace = reservedSum; + PortionsSum = portionsSum; + MaxOffAxisSize = Math.Max(MaxOffAxisSize, addend.MaxOffAxisSize); + ItemsCount += addend.ItemsCount; + return true; } - return LimitUnboundedSize(availableSize, maxcellheight); + public double Measure(double portionSize, double spacing) => ReservedSpace + (PortionsSum * portionSize) + ((ItemsCount - 1) * spacing); } - // This method limits the panel height when no limit is imposed by the panel's parent. - // That can happen to height if the panel is close to the root of main app window. - // In this case, base the height of a cell on the max height from desired size - // and base the height of the panel on that number times the #rows. - Size LimitUnboundedSize(Size input, double maxcellheight) - { - if (Double.IsInfinity(input.Height)) + /// + /// A struct for mapping X/Y coordinates to an orientation adjusted U/V coordinate system. + /// + private struct UVCoord + { + private readonly bool _horizontal; + + public UVCoord(double x, double y, Orientation orientation) { - input.Height = maxcellheight * _columnCount; - _cellHeight = maxcellheight; + X = x; + Y = y; + _horizontal = orientation is Orientation.Horizontal; } - return input; - } - protected override Size ArrangeOverride(Size finalSize) - { - int count = 1; - double x, y; - foreach (UIElement child in Children) + public UVCoord(Size size, Orientation orientation) : this(size.Width, size.Height, orientation) { - x = (count - 1) % _columnCount * _cellWidth; - y = ((int)(count - 1) / _columnCount) * _cellHeight; - Point anchorPoint = new Point(x, y); - child.Arrange(new Rect(anchorPoint, child.DesiredSize)); - count++; } - return finalSize; + + public double X { get; set; } + + public double Y { get; set; } + + public double U + { + readonly get => _horizontal ? X : Y; + set + { + if (_horizontal) + { + X = value; + } + else + { + Y = value; + } + } + } + + public double V + { + readonly get => _horizontal ? Y : X; + set + { + if (_horizontal) + { + Y = value; + } + else + { + X = value; + } + } + } + + public readonly Size Size => new(X, Y); } } From be5f5903adff8306def280baf3a41e181680c075 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Sat, 29 Nov 2025 15:19:40 +0200 Subject: [PATCH 03/14] Removed Wrap property --- .../samples/StretchPanelBasicSample.xaml | 22 +++++++++---------- .../src/StretchPanel.Properties.cs | 19 ---------------- 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml b/components/StretchPanel/samples/StretchPanelBasicSample.xaml index 5505a9bf3..9d24623d8 100644 --- a/components/StretchPanel/samples/StretchPanelBasicSample.xaml +++ b/components/StretchPanel/samples/StretchPanelBasicSample.xaml @@ -24,37 +24,37 @@ - + - + - + - + - + - + - + - + - + - + - + diff --git a/components/StretchPanel/src/StretchPanel.Properties.cs b/components/StretchPanel/src/StretchPanel.Properties.cs index 73fb309e7..ca220c7f3 100644 --- a/components/StretchPanel/src/StretchPanel.Properties.cs +++ b/components/StretchPanel/src/StretchPanel.Properties.cs @@ -25,16 +25,6 @@ public partial class StretchPanel typeof(StretchPanel), new PropertyMetadata(default(Orientation), OnPropertyChanged)); - /// - /// Backing for the property. - /// - /// The identifier for the dependency property. - public static readonly DependencyProperty WrapProperty = DependencyProperty.Register( - nameof(VerticalSpacing), - typeof(double), - typeof(StretchPanel), - new PropertyMetadata(default(double), OnPropertyChanged)); - /// /// Backing for the property. /// @@ -64,15 +54,6 @@ public Orientation Orientation set => SetValue(OrientationProperty, value); } - /// - /// Gets or sets whether or not the panel should wrap items. - /// - public bool Wrap - { - get => (bool)GetValue(WrapProperty); - set => SetValue(WrapProperty, value); - } - /// /// Gets or sets the horizontal spacing between items. /// From c4b9ddc3ca712e653a302bce1492668182e6511f Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Sat, 29 Nov 2025 17:14:09 +0200 Subject: [PATCH 04/14] Improved alignment behavior --- .../samples/StretchPanelBasicSample.xaml | 7 +- .../samples/StretchPanelBasicSample.xaml.cs | 24 +++ components/StretchPanel/src/StretchPanel.cs | 181 ++++++++++++++---- 3 files changed, 172 insertions(+), 40 deletions(-) diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml b/components/StretchPanel/samples/StretchPanelBasicSample.xaml index 9d24623d8..ad716d188 100644 --- a/components/StretchPanel/samples/StretchPanelBasicSample.xaml +++ b/components/StretchPanel/samples/StretchPanelBasicSample.xaml @@ -20,9 +20,12 @@ - + + HorizontalSpacing="{x:Bind HorizontalSpacing, Mode=OneWay}" + VerticalSpacing="{x:Bind VerticalSpacing, Mode=OneWay}" + HorizontalAlignment="{x:Bind LayoutHorizontalAlignment, Mode=OneWay}" + VerticalAlignment="{x:Bind LayoutVerticalAlignment, Mode=OneWay}"> diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs b/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs index 34d1be6f5..f04201550 100644 --- a/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs +++ b/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs @@ -10,6 +10,10 @@ namespace StretchPanelExperiment.Samples; /// An example sample page of a custom control inheriting from Panel. /// [ToolkitSampleMultiChoiceOption("LayoutOrientation", "Horizontal", "Vertical", Title = "Orientation")] +[ToolkitSampleMultiChoiceOption("LayoutHorizontalAlignment", "Left", "Center", "Right", "Stretch", Title = "Horizontal Alignment")] +[ToolkitSampleMultiChoiceOption("LayoutVerticalAlignment", "Top", "Center", "Bottom", "Stretch", Title = "Vertical Alignment")] +[ToolkitSampleNumericOption("HorizontalSpacing", 8, 0, 16, Title = "Horizontal Spacing")] +[ToolkitSampleNumericOption("VerticalSpacing", 2, 0, 16, Title = "Vertical Spacing")] [ToolkitSample(id: nameof(StretchPanelBasicSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(StretchPanel)} custom control.")] public sealed partial class StretchPanelBasicSample : Page @@ -26,4 +30,24 @@ public StretchPanelBasicSample() "Horizontal" => Orientation.Horizontal, _ => throw new System.NotImplementedException(), }; + + // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149 + public static HorizontalAlignment ConvertStringToHorizontalAlignment(string alignment) => alignment switch + { + "Left" => HorizontalAlignment.Left, + "Center" => HorizontalAlignment.Center, + "Right" => HorizontalAlignment.Right, + "Stretch" => HorizontalAlignment.Stretch, + _ => throw new System.NotImplementedException(), + }; + + // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149 + public static VerticalAlignment ConvertStringToVerticalAlignment(string alignment) => alignment switch + { + "Top" => VerticalAlignment.Top, + "Center" => VerticalAlignment.Center, + "Bottom" => VerticalAlignment.Bottom, + "Stretch" => VerticalAlignment.Stretch, + _ => throw new System.NotImplementedException(), + }; } diff --git a/components/StretchPanel/src/StretchPanel.cs b/components/StretchPanel/src/StretchPanel.cs index 411b34fb2..b94179efc 100644 --- a/components/StretchPanel/src/StretchPanel.cs +++ b/components/StretchPanel/src/StretchPanel.cs @@ -22,8 +22,7 @@ protected override Size MeasureOverride(Size availableSize) var uvAvailableSize = new UVCoord(availableSize.Width, availableSize.Height, Orientation); var uvSpacing = new UVCoord(HorizontalSpacing, VerticalSpacing, Orientation); - double widestRow = 0; - double portionSizeCache = 0; + double largestRow = 0; RowSpec currentRowSpec = default; var elements = Children.Where(static e => e.Visibility is Visibility.Visible); @@ -42,34 +41,29 @@ protected override Size MeasureOverride(Size availableSize) var layoutLength = GetLayoutLength(child); // Attempt to add the child to the current row/column - var spec = new RowSpec(layoutLength, uvDesiredSize, ref portionSizeCache); - if (!currentRowSpec.TryAdd(spec, portionSizeCache, uvSpacing.U, uvAvailableSize.U)) + var spec = new RowSpec(layoutLength, uvDesiredSize); + if (!currentRowSpec.TryAdd(spec, uvSpacing.U, uvAvailableSize.U)) { // Could not add to current row/column // Start a new row/column _rowSpecs.Add(currentRowSpec); - widestRow = Math.Max(widestRow, currentRowSpec.Measure(portionSizeCache, uvSpacing.U)); + largestRow = Math.Max(largestRow, currentRowSpec.Measure(uvSpacing.U)); currentRowSpec = spec; - portionSizeCache = 0; } } // Add the final row/column _rowSpecs.Add(currentRowSpec); - widestRow = Math.Max(widestRow, currentRowSpec.Measure(portionSizeCache, uvSpacing.U)); + largestRow = Math.Max(largestRow, currentRowSpec.Measure(uvSpacing.U)); // Determine if the desired alignment is stretched. // Don't stretch if infinite space is available though. Attempting to divide infinite space will result in a crash. - bool stretch = Orientation switch - { - Orientation.Horizontal => HorizontalAlignment is HorizontalAlignment.Stretch && !double.IsInfinity(availableSize.Width), - Orientation.Vertical or _ => VerticalAlignment is VerticalAlignment.Stretch && !double.IsInfinity(availableSize.Height), - }; + bool stretch = GetAlignment() is Alignment.Stretch && !double.IsInfinity(uvAvailableSize.U); // Calculate final desired size var uvSize = new UVCoord(0, 0, Orientation) { - U = stretch ? uvAvailableSize.U : widestRow, + U = stretch ? uvAvailableSize.U : largestRow, V = _rowSpecs.Sum(static rs => rs.MaxOffAxisSize) + (uvSpacing.V * (_rowSpecs.Count - 1)) }; @@ -88,14 +82,40 @@ protected override Size ArrangeOverride(Size finalSize) var uvFinalSize = new UVCoord(finalSize, Orientation); var uvSpacing = new UVCoord(HorizontalSpacing, VerticalSpacing, Orientation); + // Adjust the starting position based on off-axis alignment + var contentHeight = _rowSpecs.Sum(static rs => rs.MaxOffAxisSize) + (uvSpacing.V * (_rowSpecs.Count - 1)); + pos.V = GetStartByAlignment(GetOffAlignment(), contentHeight, uvFinalSize.V); + var elements = Children.Where(static e => e.Visibility is Visibility.Visible); foreach (var row in _rowSpecs) { var spacingTotalSize = uvSpacing.U * (row.ItemsCount - 1); - var portionSize = (uvFinalSize.U - row.ReservedSpace - spacingTotalSize) / row.PortionsSum; + var portionSize = row.MinPortionSize; + + // Determine if the desired alignment is stretched. + bool stretch = GetAlignment() is Alignment.Stretch && !double.IsInfinity(uvFinalSize.U); + + // Calculate portion size if stretching + if (stretch) + { + portionSize = (uvFinalSize.U - row.ReservedSpace - spacingTotalSize) / row.PortionsSum; + } + + // Reset U position + pos.U = 0; + + // Adjust the starting position if not stretching + // Also do this if there are no star-sized items in the row/column + if (!stretch || row.PortionsSum is 0) + { + // Determine the offset based on alignment + var rowSize = row.Measure(uvSpacing.U); + pos.U = GetStartByAlignment(GetAlignment(), rowSize, uvFinalSize.U); + } for (int i = 0; i < row.ItemsCount; i++) { + // Get the next child var child = elements.ElementAt(0); elements = elements.Skip(1); @@ -133,16 +153,85 @@ protected override Size ArrangeOverride(Size finalSize) } // Advance to the next row/column - pos.U = 0; pos.V += row.MaxOffAxisSize + uvSpacing.V; } return finalSize; } + private static double GetStartByAlignment(Alignment alignment, double size, double availableSize) + { + return alignment switch + { + Alignment.Start => 0, + Alignment.Center => (availableSize / 2) - (size / 2), + Alignment.End => availableSize - size, + _ => 0, + }; + } + + private Alignment GetAlignment() + { + return Orientation switch + { + Orientation.Horizontal => HorizontalAlignment switch + { + HorizontalAlignment.Left => Alignment.Start, + HorizontalAlignment.Center => Alignment.Center, + HorizontalAlignment.Right => Alignment.End, + HorizontalAlignment.Stretch => Alignment.Stretch, + _ => Alignment.Start, + }, + Orientation.Vertical => VerticalAlignment switch + { + VerticalAlignment.Top => Alignment.Start, + VerticalAlignment.Center => Alignment.Center, + VerticalAlignment.Bottom => Alignment.End, + VerticalAlignment.Stretch => Alignment.Stretch, + _ => Alignment.Start, + }, + _ => Alignment.Start, + }; + } + + private Alignment GetOffAlignment() + { + return Orientation switch + { + Orientation.Horizontal => VerticalAlignment switch + { + VerticalAlignment.Top => Alignment.Start, + VerticalAlignment.Center => Alignment.Center, + VerticalAlignment.Bottom => Alignment.End, + VerticalAlignment.Stretch => Alignment.Stretch, + _ => Alignment.Start, + }, + Orientation.Vertical => HorizontalAlignment switch + { + HorizontalAlignment.Left => Alignment.Start, + HorizontalAlignment.Center => Alignment.Center, + HorizontalAlignment.Right => Alignment.End, + HorizontalAlignment.Stretch => Alignment.Stretch, + _ => Alignment.Start, + }, + _ => Alignment.Start, + }; + } + + private enum Alignment + { + Start, + Center, + End, + Stretch + } + + /// + /// A struct representing the specifications of a row or column in the panel. + /// private struct RowSpec { - public RowSpec(GridLength layout, UVCoord desiredSize, ref double portionSize) + public RowSpec(GridLength layout, UVCoord desiredSize) { switch (layout.GridUnitType) { @@ -154,7 +243,7 @@ public RowSpec(GridLength layout, UVCoord desiredSize, ref double portionSize) break; case GridUnitType.Star: PortionsSum = layout.Value; - portionSize = Math.Max(portionSize, desiredSize.U / layout.Value); + MinPortionSize = desiredSize.U / layout.Value; break; } @@ -186,52 +275,68 @@ public RowSpec(GridLength layout, UVCoord desiredSize, ref double portionSize) /// public double MaxOffAxisSize { get; private set; } + /// + /// Gets the minimum size of a portion in the row/column. + /// + public double MinPortionSize { get; private set; } + /// /// Gets the number of items in the row/column. /// public int ItemsCount { get; private set; } - public bool TryAdd(RowSpec addend, double portionSize, double spacing, double maxSize) + public bool TryAdd(RowSpec addend, double spacing, double maxSize) { // Check if adding the new spec would exceed the maximum size - var reservedSum = ReservedSpace + addend.ReservedSpace; - var portionsSum = PortionsSum + addend.PortionsSum; - var itemsSum = ItemsCount + addend.ItemsCount; - if (reservedSum + (portionsSum * portionSize) + ((itemsSum - 1) * spacing) > maxSize) + var sum = this + addend; + if (sum.Measure(spacing) > maxSize) return false; // Update the current spec to include the new spec - ReservedSpace = reservedSum; - PortionsSum = portionsSum; - MaxOffAxisSize = Math.Max(MaxOffAxisSize, addend.MaxOffAxisSize); - ItemsCount += addend.ItemsCount; + this = sum; return true; } - public double Measure(double portionSize, double spacing) => ReservedSpace + (PortionsSum * portionSize) + ((ItemsCount - 1) * spacing); + public readonly double Measure(double spacing) + { + var totalSpacing = (ItemsCount - 1) * spacing; + var totalSize = ReservedSpace + totalSpacing; + + // Add star-sized items if applicable + if (!double.IsNaN(MinPortionSize) && !double.IsInfinity(MinPortionSize)) + totalSize += MinPortionSize * PortionsSum; + + return totalSize; + } + + public static RowSpec operator +(RowSpec a, RowSpec b) + { + var combined = new RowSpec + { + ReservedSpace = a.ReservedSpace + b.ReservedSpace, + PortionsSum = a.PortionsSum + b.PortionsSum, + MinPortionSize = Math.Max(a.MinPortionSize, b.MinPortionSize), + MaxOffAxisSize = Math.Max(a.MaxOffAxisSize, b.MaxOffAxisSize), + ItemsCount = a.ItemsCount + b.ItemsCount + }; + return combined; + } } /// /// A struct for mapping X/Y coordinates to an orientation adjusted U/V coordinate system. /// - private struct UVCoord + private struct UVCoord(double x, double y, Orientation orientation) { - private readonly bool _horizontal; - - public UVCoord(double x, double y, Orientation orientation) - { - X = x; - Y = y; - _horizontal = orientation is Orientation.Horizontal; - } + private readonly bool _horizontal = orientation is Orientation.Horizontal; public UVCoord(Size size, Orientation orientation) : this(size.Width, size.Height, orientation) { } - public double X { get; set; } + public double X { get; set; } = x; - public double Y { get; set; } + public double Y { get; set; } = y; public double U { From dc2844ecf7db54184d3a8503f416ebfd64f38e86 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Sat, 29 Nov 2025 17:21:07 +0200 Subject: [PATCH 05/14] Improved Sample to better display 600px vertical sample --- components/StretchPanel/samples/StretchPanelBasicSample.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml b/components/StretchPanel/samples/StretchPanelBasicSample.xaml index ad716d188..4a51b7948 100644 --- a/components/StretchPanel/samples/StretchPanelBasicSample.xaml +++ b/components/StretchPanel/samples/StretchPanelBasicSample.xaml @@ -20,7 +20,7 @@ - + Date: Sat, 29 Nov 2025 18:14:49 +0200 Subject: [PATCH 06/14] Added FixedRowLengths property --- .../samples/StretchPanelBasicSample.xaml | 3 ++- .../samples/StretchPanelBasicSample.xaml.cs | 1 + .../src/StretchPanel.Properties.cs | 20 +++++++++++++++++-- components/StretchPanel/src/StretchPanel.cs | 12 ++++++----- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml b/components/StretchPanel/samples/StretchPanelBasicSample.xaml index 4a51b7948..2a93d8d27 100644 --- a/components/StretchPanel/samples/StretchPanelBasicSample.xaml +++ b/components/StretchPanel/samples/StretchPanelBasicSample.xaml @@ -25,7 +25,8 @@ HorizontalSpacing="{x:Bind HorizontalSpacing, Mode=OneWay}" VerticalSpacing="{x:Bind VerticalSpacing, Mode=OneWay}" HorizontalAlignment="{x:Bind LayoutHorizontalAlignment, Mode=OneWay}" - VerticalAlignment="{x:Bind LayoutVerticalAlignment, Mode=OneWay}"> + VerticalAlignment="{x:Bind LayoutVerticalAlignment, Mode=OneWay}" + FixedRowLengths="{x:Bind FixedRowLengths, Mode=OneWay}"> diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs b/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs index f04201550..22a253d63 100644 --- a/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs +++ b/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs @@ -14,6 +14,7 @@ namespace StretchPanelExperiment.Samples; [ToolkitSampleMultiChoiceOption("LayoutVerticalAlignment", "Top", "Center", "Bottom", "Stretch", Title = "Vertical Alignment")] [ToolkitSampleNumericOption("HorizontalSpacing", 8, 0, 16, Title = "Horizontal Spacing")] [ToolkitSampleNumericOption("VerticalSpacing", 2, 0, 16, Title = "Vertical Spacing")] +[ToolkitSampleBoolOption("FixedRowLengths", false, Title = "Fixed Row Lengths")] [ToolkitSample(id: nameof(StretchPanelBasicSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(StretchPanel)} custom control.")] public sealed partial class StretchPanelBasicSample : Page diff --git a/components/StretchPanel/src/StretchPanel.Properties.cs b/components/StretchPanel/src/StretchPanel.Properties.cs index ca220c7f3..02aa12cf1 100644 --- a/components/StretchPanel/src/StretchPanel.Properties.cs +++ b/components/StretchPanel/src/StretchPanel.Properties.cs @@ -28,7 +28,6 @@ public partial class StretchPanel /// /// Backing for the property. /// - /// The identifier for the dependency property. public static readonly DependencyProperty HorizontalSpacingProperty = DependencyProperty.Register( nameof(HorizontalSpacing), typeof(double), @@ -38,13 +37,21 @@ public partial class StretchPanel /// /// Backing for the property. /// - /// The identifier for the dependency property. public static readonly DependencyProperty VerticalSpacingProperty = DependencyProperty.Register( nameof(VerticalSpacing), typeof(double), typeof(StretchPanel), new PropertyMetadata(default(double), OnPropertyChanged)); + /// + /// Backing for the property. + /// + public static readonly DependencyProperty FixedRowLengthsProperty = DependencyProperty.Register( + nameof(FixedRowLengths), + typeof(bool), + typeof(StretchPanel), + new PropertyMetadata(default(bool), OnPropertyChanged)); + /// /// Gets or sets the panel orientation. /// @@ -72,6 +79,15 @@ public double VerticalSpacing set => SetValue(VerticalSpacingProperty, value); } + /// + /// Gets or sets whether or not all rows/columns should stretch to match the length of the longest. + /// + public bool FixedRowLengths + { + get => (bool)GetValue(FixedRowLengthsProperty); + set => SetValue(FixedRowLengthsProperty, value); + } + /// /// Gets the of an item in the . /// diff --git a/components/StretchPanel/src/StretchPanel.cs b/components/StretchPanel/src/StretchPanel.cs index b94179efc..3c70e366f 100644 --- a/components/StretchPanel/src/StretchPanel.cs +++ b/components/StretchPanel/src/StretchPanel.cs @@ -12,17 +12,18 @@ namespace CommunityToolkit.WinUI.Controls; public partial class StretchPanel : Panel { private List? _rowSpecs; + private double _longestRowSize = 0; /// protected override Size MeasureOverride(Size availableSize) { _rowSpecs = []; + _longestRowSize = 0; // Define XY/UV coordinate variables var uvAvailableSize = new UVCoord(availableSize.Width, availableSize.Height, Orientation); var uvSpacing = new UVCoord(HorizontalSpacing, VerticalSpacing, Orientation); - double largestRow = 0; RowSpec currentRowSpec = default; var elements = Children.Where(static e => e.Visibility is Visibility.Visible); @@ -47,14 +48,14 @@ protected override Size MeasureOverride(Size availableSize) // Could not add to current row/column // Start a new row/column _rowSpecs.Add(currentRowSpec); - largestRow = Math.Max(largestRow, currentRowSpec.Measure(uvSpacing.U)); + _longestRowSize = Math.Max(_longestRowSize, currentRowSpec.Measure(uvSpacing.U)); currentRowSpec = spec; } } // Add the final row/column _rowSpecs.Add(currentRowSpec); - largestRow = Math.Max(largestRow, currentRowSpec.Measure(uvSpacing.U)); + _longestRowSize = Math.Max(_longestRowSize, currentRowSpec.Measure(uvSpacing.U)); // Determine if the desired alignment is stretched. // Don't stretch if infinite space is available though. Attempting to divide infinite space will result in a crash. @@ -63,7 +64,7 @@ protected override Size MeasureOverride(Size availableSize) // Calculate final desired size var uvSize = new UVCoord(0, 0, Orientation) { - U = stretch ? uvAvailableSize.U : largestRow, + U = stretch ? uvAvailableSize.U : _longestRowSize, V = _rowSpecs.Sum(static rs => rs.MaxOffAxisSize) + (uvSpacing.V * (_rowSpecs.Count - 1)) }; @@ -96,7 +97,8 @@ protected override Size ArrangeOverride(Size finalSize) bool stretch = GetAlignment() is Alignment.Stretch && !double.IsInfinity(uvFinalSize.U); // Calculate portion size if stretching - if (stretch) + // Same logic applies for matching row lengths, since the size was determined during measure + if (stretch || FixedRowLengths) { portionSize = (uvFinalSize.U - row.ReservedSpace - spacingTotalSize) / row.PortionsSum; } From 62b5e20975f74b37ae015d2e77c257ec13ad8f62 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Sat, 29 Nov 2025 23:06:01 +0200 Subject: [PATCH 07/14] Added ForcedStretchMethod property and enum --- .../samples/StretchPanelBasicSample.xaml | 6 +- .../samples/StretchPanelBasicSample.xaml.cs | 12 ++++ .../StretchPanel/src/ForcedStretchMethod.cs | 36 ++++++++++++ .../src/StretchPanel.Properties.cs | 20 ++++++- components/StretchPanel/src/StretchPanel.cs | 56 +++++++++++++++++-- 5 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 components/StretchPanel/src/ForcedStretchMethod.cs diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml b/components/StretchPanel/samples/StretchPanelBasicSample.xaml index 2a93d8d27..96c54a931 100644 --- a/components/StretchPanel/samples/StretchPanelBasicSample.xaml +++ b/components/StretchPanel/samples/StretchPanelBasicSample.xaml @@ -26,7 +26,8 @@ VerticalSpacing="{x:Bind VerticalSpacing, Mode=OneWay}" HorizontalAlignment="{x:Bind LayoutHorizontalAlignment, Mode=OneWay}" VerticalAlignment="{x:Bind LayoutVerticalAlignment, Mode=OneWay}" - FixedRowLengths="{x:Bind FixedRowLengths, Mode=OneWay}"> + FixedRowLengths="{x:Bind FixedRowLengths, Mode=OneWay}" + ForcedStretchMethod="{x:Bind local:StretchPanelBasicSample.ConvertStringToForcedStretchMethod(LayoutForcedStretchMethod), Mode=OneWay}"> @@ -42,6 +43,9 @@ + + + diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs b/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs index 22a253d63..ea48b5a4f 100644 --- a/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs +++ b/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs @@ -15,6 +15,7 @@ namespace StretchPanelExperiment.Samples; [ToolkitSampleNumericOption("HorizontalSpacing", 8, 0, 16, Title = "Horizontal Spacing")] [ToolkitSampleNumericOption("VerticalSpacing", 2, 0, 16, Title = "Vertical Spacing")] [ToolkitSampleBoolOption("FixedRowLengths", false, Title = "Fixed Row Lengths")] +[ToolkitSampleMultiChoiceOption("LayoutForcedStretchMethod", "None", "First", "Last", "Equal", "Proportional", Title = "Forced Stretch Method")] [ToolkitSample(id: nameof(StretchPanelBasicSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(StretchPanel)} custom control.")] public sealed partial class StretchPanelBasicSample : Page @@ -51,4 +52,15 @@ public StretchPanelBasicSample() "Stretch" => VerticalAlignment.Stretch, _ => throw new System.NotImplementedException(), }; + + // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149 + public static ForcedStretchMethod ConvertStringToForcedStretchMethod(string stretchMethod) => stretchMethod switch + { + "None" => ForcedStretchMethod.None, + "First" => ForcedStretchMethod.First, + "Last" => ForcedStretchMethod.Last, + "Equal" => ForcedStretchMethod.Equal, + "Proportional" => ForcedStretchMethod.Proportional, + _ => throw new System.NotImplementedException(), + }; } diff --git a/components/StretchPanel/src/ForcedStretchMethod.cs b/components/StretchPanel/src/ForcedStretchMethod.cs new file mode 100644 index 000000000..ed8483840 --- /dev/null +++ b/components/StretchPanel/src/ForcedStretchMethod.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Describes the behavior of items in rows without a star-sized item. +/// +public enum ForcedStretchMethod +{ + /// + /// Items will never be streched beyond their desired size. + /// + None, + + /// + /// The first item in the row will be stretched to fill the row. + /// + First, + + /// + /// The last item in the row will be stretched to fill the row. + /// + Last, + + /// + /// Each item will be stretched to an equal size to fill the row. + /// + Equal, + + /// + /// Each item will be stretched proportional to their desired size to fill the row. + /// + Proportional, +} diff --git a/components/StretchPanel/src/StretchPanel.Properties.cs b/components/StretchPanel/src/StretchPanel.Properties.cs index 02aa12cf1..2c46f86c5 100644 --- a/components/StretchPanel/src/StretchPanel.Properties.cs +++ b/components/StretchPanel/src/StretchPanel.Properties.cs @@ -44,7 +44,7 @@ public partial class StretchPanel new PropertyMetadata(default(double), OnPropertyChanged)); /// - /// Backing for the property. + /// Backing for the property. /// public static readonly DependencyProperty FixedRowLengthsProperty = DependencyProperty.Register( nameof(FixedRowLengths), @@ -52,6 +52,15 @@ public partial class StretchPanel typeof(StretchPanel), new PropertyMetadata(default(bool), OnPropertyChanged)); + /// + /// Backing for the property. + /// + public static readonly DependencyProperty ForcedStretchMethodProperty = DependencyProperty.Register( + nameof(ForcedStretchMethod), + typeof(ForcedStretchMethod), + typeof(StretchPanel), + new PropertyMetadata(default(bool), OnPropertyChanged)); + /// /// Gets or sets the panel orientation. /// @@ -88,6 +97,15 @@ public bool FixedRowLengths set => SetValue(FixedRowLengthsProperty, value); } + /// + /// Gets or sets the method used to fill rows without a star-sized item. + /// + public ForcedStretchMethod ForcedStretchMethod + { + get => (ForcedStretchMethod)GetValue(ForcedStretchMethodProperty); + set => SetValue(ForcedStretchMethodProperty, value); + } + /// /// Gets the of an item in the . /// diff --git a/components/StretchPanel/src/StretchPanel.cs b/components/StretchPanel/src/StretchPanel.cs index 3c70e366f..1e071a54f 100644 --- a/components/StretchPanel/src/StretchPanel.cs +++ b/components/StretchPanel/src/StretchPanel.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.ComponentModel.DataAnnotations; - namespace CommunityToolkit.WinUI.Controls; /// @@ -91,6 +89,7 @@ protected override Size ArrangeOverride(Size finalSize) foreach (var row in _rowSpecs) { var spacingTotalSize = uvSpacing.U * (row.ItemsCount - 1); + var remainingSpace = uvFinalSize.U - row.ReservedSpace - spacingTotalSize; var portionSize = row.MinPortionSize; // Determine if the desired alignment is stretched. @@ -100,7 +99,7 @@ protected override Size ArrangeOverride(Size finalSize) // Same logic applies for matching row lengths, since the size was determined during measure if (stretch || FixedRowLengths) { - portionSize = (uvFinalSize.U - row.ReservedSpace - spacingTotalSize) / row.PortionsSum; + portionSize = remainingSpace / row.PortionsSum; } // Reset U position @@ -108,13 +107,30 @@ protected override Size ArrangeOverride(Size finalSize) // Adjust the starting position if not stretching // Also do this if there are no star-sized items in the row/column - if (!stretch || row.PortionsSum is 0) + // and no forced streching is in use. + if (!stretch || (row.PortionsSum is 0 && ForcedStretchMethod is ForcedStretchMethod.None)) { // Determine the offset based on alignment var rowSize = row.Measure(uvSpacing.U); pos.U = GetStartByAlignment(GetAlignment(), rowSize, uvFinalSize.U); } + // Set a flag for if the row is being forced to stretch + bool forceStrech = row.PortionsSum is 0 && ForcedStretchMethod is not ForcedStretchMethod.None; + + // Setup portionSize for forced stretching + if (forceStrech) + { + portionSize = ForcedStretchMethod switch + { + ForcedStretchMethod.First => remainingSpace + GetChildSize(elements.ElementAt(0)), + ForcedStretchMethod.Last => remainingSpace + GetChildSize(elements.ElementAt(row.ItemsCount - 1)), + ForcedStretchMethod.Equal => (uvFinalSize.U - spacingTotalSize) / row.ItemsCount, + ForcedStretchMethod.Equal or ForcedStretchMethod.Proportional => (uvFinalSize.U - spacingTotalSize) / row.ReservedSpace, + _ => row.MinPortionSize, + }; + } + for (int i = 0; i < row.ItemsCount; i++) { // Get the next child @@ -131,6 +147,26 @@ protected override Size ArrangeOverride(Size finalSize) var layoutLength = GetLayoutLength(child); var uvDesiredSize = new UVCoord(child.DesiredSize, Orientation); + // Override the layout based on the forced stretch method if necessary + if (forceStrech) + { + var oneStar = new GridLength(1, GridUnitType.Star); + layoutLength = ForcedStretchMethod switch + { + ForcedStretchMethod.First when i is 0 => oneStar, + ForcedStretchMethod.Last when i == (row.ItemsCount - 1) => oneStar, + ForcedStretchMethod.Equal => oneStar, + ForcedStretchMethod.Proportional => layoutLength.GridUnitType switch + { + GridUnitType.Auto => new GridLength(uvDesiredSize.U, GridUnitType.Star), + GridUnitType.Pixel or _ => new GridLength(layoutLength.Value, GridUnitType.Star), + }, + + // If the above conditions aren't met, do nothing + _ => layoutLength, + }; + } + // Determine the child's U size double uSize = layoutLength.GridUnitType switch { @@ -220,6 +256,18 @@ private Alignment GetOffAlignment() }; } + private double GetChildSize(UIElement child) + { + var childLayout = GetLayoutLength(child); + + return childLayout.GridUnitType switch + { + GridUnitType.Auto => new UVCoord(child.DesiredSize, Orientation).U, + GridUnitType.Pixel => childLayout.Value, + _ => 0, + }; + } + private enum Alignment { Start, From 5c8a3d86959e48a21748a937693376f7b61da0c1 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Sat, 29 Nov 2025 23:42:48 +0200 Subject: [PATCH 08/14] Fixed alignment issues for forced stretching --- components/StretchPanel/src/StretchPanel.cs | 178 +++++++++++--------- 1 file changed, 95 insertions(+), 83 deletions(-) diff --git a/components/StretchPanel/src/StretchPanel.cs b/components/StretchPanel/src/StretchPanel.cs index 1e071a54f..3cc19dcc7 100644 --- a/components/StretchPanel/src/StretchPanel.cs +++ b/components/StretchPanel/src/StretchPanel.cs @@ -88,48 +88,9 @@ protected override Size ArrangeOverride(Size finalSize) var elements = Children.Where(static e => e.Visibility is Visibility.Visible); foreach (var row in _rowSpecs) { - var spacingTotalSize = uvSpacing.U * (row.ItemsCount - 1); - var remainingSpace = uvFinalSize.U - row.ReservedSpace - spacingTotalSize; - var portionSize = row.MinPortionSize; - - // Determine if the desired alignment is stretched. - bool stretch = GetAlignment() is Alignment.Stretch && !double.IsInfinity(uvFinalSize.U); - - // Calculate portion size if stretching - // Same logic applies for matching row lengths, since the size was determined during measure - if (stretch || FixedRowLengths) - { - portionSize = remainingSpace / row.PortionsSum; - } - - // Reset U position - pos.U = 0; - - // Adjust the starting position if not stretching - // Also do this if there are no star-sized items in the row/column - // and no forced streching is in use. - if (!stretch || (row.PortionsSum is 0 && ForcedStretchMethod is ForcedStretchMethod.None)) - { - // Determine the offset based on alignment - var rowSize = row.Measure(uvSpacing.U); - pos.U = GetStartByAlignment(GetAlignment(), rowSize, uvFinalSize.U); - } - - // Set a flag for if the row is being forced to stretch - bool forceStrech = row.PortionsSum is 0 && ForcedStretchMethod is not ForcedStretchMethod.None; - - // Setup portionSize for forced stretching - if (forceStrech) - { - portionSize = ForcedStretchMethod switch - { - ForcedStretchMethod.First => remainingSpace + GetChildSize(elements.ElementAt(0)), - ForcedStretchMethod.Last => remainingSpace + GetChildSize(elements.ElementAt(row.ItemsCount - 1)), - ForcedStretchMethod.Equal => (uvFinalSize.U - spacingTotalSize) / row.ItemsCount, - ForcedStretchMethod.Equal or ForcedStretchMethod.Proportional => (uvFinalSize.U - spacingTotalSize) / row.ReservedSpace, - _ => row.MinPortionSize, - }; - } + // Setup the row/column for arrangement + var (uPos, portionSize, forceStrech) = SetupArrangeRow(row, uvFinalSize, uvSpacing, elements); + pos.U = uPos; for (int i = 0; i < row.ItemsCount; i++) { @@ -143,51 +104,14 @@ protected override Size ArrangeOverride(Size finalSize) return finalSize; } - // Get layout and desired size - var layoutLength = GetLayoutLength(child); - var uvDesiredSize = new UVCoord(child.DesiredSize, Orientation); - - // Override the layout based on the forced stretch method if necessary - if (forceStrech) - { - var oneStar = new GridLength(1, GridUnitType.Star); - layoutLength = ForcedStretchMethod switch - { - ForcedStretchMethod.First when i is 0 => oneStar, - ForcedStretchMethod.Last when i == (row.ItemsCount - 1) => oneStar, - ForcedStretchMethod.Equal => oneStar, - ForcedStretchMethod.Proportional => layoutLength.GridUnitType switch - { - GridUnitType.Auto => new GridLength(uvDesiredSize.U, GridUnitType.Star), - GridUnitType.Pixel or _ => new GridLength(layoutLength.Value, GridUnitType.Star), - }, - - // If the above conditions aren't met, do nothing - _ => layoutLength, - }; - } - - // Determine the child's U size - double uSize = layoutLength.GridUnitType switch - { - GridUnitType.Auto => uvDesiredSize.U, - GridUnitType.Pixel => layoutLength.Value, - GridUnitType.Star => layoutLength.Value * portionSize, - _ => uvDesiredSize.U, - }; - - // Arrange the child - var size = new UVCoord(0, 0, Orientation) - { - U = uSize, - V = row.MaxOffAxisSize - }; + // Determine the child's size + var size = GetChildSize(child, i, row, portionSize, forceStrech); // NOTE: The arrange method is still in X/Y coordinate system child.Arrange(new Rect(pos.X, pos.Y, size.X, size.Y)); // Advance the position - pos.U += uSize + uvSpacing.U; + pos.U += size.U + uvSpacing.U; } // Advance to the next row/column @@ -196,6 +120,94 @@ protected override Size ArrangeOverride(Size finalSize) return finalSize; } + private (double uPos, double portionSize, bool forceStretch) SetupArrangeRow(RowSpec row, UVCoord uvFinalSize, UVCoord uvSpacing, IEnumerable elements) + { + double uPos = 0; + + var spacingTotalSize = uvSpacing.U * (row.ItemsCount - 1); + var remainingSpace = uvFinalSize.U - row.ReservedSpace - spacingTotalSize; + var portionSize = row.MinPortionSize; + + // Determine if the desired alignment is stretched. + bool stretch = GetAlignment() is Alignment.Stretch && !double.IsInfinity(uvFinalSize.U); + + // Calculate portion size if stretching + // Same logic applies for matching row lengths, since the size was determined during measure + if (stretch || FixedRowLengths) + { + portionSize = remainingSpace / row.PortionsSum; + } + + // Adjust the starting position if not stretching + // Also do this if there are no star-sized items in the row/column + // and no forced streching is in use. + if (!(stretch || FixedRowLengths) || (row.PortionsSum is 0 && ForcedStretchMethod is ForcedStretchMethod.None)) + { + var rowSize = row.Measure(uvSpacing.U); + uPos = GetStartByAlignment(GetAlignment(), rowSize, uvFinalSize.U); + } + + // Set a flag for if the row is being forced to stretch + bool forceStretch = row.PortionsSum is 0 && ForcedStretchMethod is not ForcedStretchMethod.None; + + // Setup portionSize for forced stretching + if (forceStretch) + { + portionSize = ForcedStretchMethod switch + { + ForcedStretchMethod.First => remainingSpace + GetChildSize(elements.ElementAt(0)), + ForcedStretchMethod.Last => remainingSpace + GetChildSize(elements.ElementAt(row.ItemsCount - 1)), + ForcedStretchMethod.Equal => (uvFinalSize.U - spacingTotalSize) / row.ItemsCount, + ForcedStretchMethod.Equal or ForcedStretchMethod.Proportional => (uvFinalSize.U - spacingTotalSize) / row.ReservedSpace, + _ => row.MinPortionSize, + }; + } + + return (uPos, portionSize, forceStretch); + } + + private UVCoord GetChildSize(UIElement child, int rowIndex, RowSpec row, double portionSize, bool forceStretch) + { + // Get layout and desired size + var layoutLength = GetLayoutLength(child); + var uvDesiredSize = new UVCoord(child.DesiredSize, Orientation); + + // Override the layout based on the forced stretch method if necessary + if (forceStretch) + { + var oneStar = new GridLength(1, GridUnitType.Star); + layoutLength = ForcedStretchMethod switch + { + ForcedStretchMethod.First when rowIndex is 0 => oneStar, + ForcedStretchMethod.Last when rowIndex == (row.ItemsCount - 1) => oneStar, + ForcedStretchMethod.Equal => oneStar, + ForcedStretchMethod.Proportional => layoutLength.GridUnitType switch + { + GridUnitType.Auto => new GridLength(uvDesiredSize.U, GridUnitType.Star), + GridUnitType.Pixel or _ => new GridLength(layoutLength.Value, GridUnitType.Star), + }, + + // If the above conditions aren't met, do nothing + _ => layoutLength, + }; + } + + // Determine the child's U size + double uSize = layoutLength.GridUnitType switch + { + GridUnitType.Auto => uvDesiredSize.U, + GridUnitType.Pixel => layoutLength.Value, + GridUnitType.Star => layoutLength.Value * portionSize, + _ => uvDesiredSize.U, + }; + + // Return the final size + return new UVCoord(0, 0, Orientation) + { + U = uSize, + V = row.MaxOffAxisSize + }; + } private static double GetStartByAlignment(Alignment alignment, double size, double availableSize) { @@ -391,7 +403,7 @@ public UVCoord(Size size, Orientation orientation) : this(size.Width, size.Heigh public double U { readonly get => _horizontal ? X : Y; - set + set { if (_horizontal) { From a76e62f72b6e5142509d072448072287dd3492a8 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Sun, 30 Nov 2025 14:58:46 +0200 Subject: [PATCH 09/14] Refactored Arrange to operate by rows --- .../StretchPanel/src/StretchPanel.Structs.cs | 155 ++++++++++ components/StretchPanel/src/StretchPanel.cs | 273 +++++------------- 2 files changed, 230 insertions(+), 198 deletions(-) create mode 100644 components/StretchPanel/src/StretchPanel.Structs.cs diff --git a/components/StretchPanel/src/StretchPanel.Structs.cs b/components/StretchPanel/src/StretchPanel.Structs.cs new file mode 100644 index 000000000..cd4db1cf1 --- /dev/null +++ b/components/StretchPanel/src/StretchPanel.Structs.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +public partial class StretchPanel +{ + /// + /// A struct representing the specifications of a row or column in the panel. + /// + private struct RowSpec + { + public RowSpec(GridLength layout, UVCoord desiredSize) + { + switch (layout.GridUnitType) + { + case GridUnitType.Auto: + ReservedSpace = desiredSize.U; + break; + case GridUnitType.Pixel: + ReservedSpace = layout.Value; + break; + case GridUnitType.Star: + PortionsSum = layout.Value; + MinPortionSize = desiredSize.U / layout.Value; + break; + } + + MaxOffAxisSize = desiredSize.V; + ItemsCount = 1; + } + + /// + /// Gets the total reserved space for spacing in the row/column. + /// + /// + /// Items with a fixed size or auto size contribute to this value. + /// + public double ReservedSpace { get; private set; } + + /// + /// Gets the sum of portions in the row/column. + /// + /// + /// Items with a star-sized length contribute to this value. + /// + public double PortionsSum { get; private set; } + + /// + /// Gets the maximum width/height of items in the row/column. + /// + /// + /// Width in vertical orientation, height in horizontal orientation. + /// + public double MaxOffAxisSize { get; private set; } + + /// + /// Gets the minimum size of a portion in the row/column. + /// + public double MinPortionSize { get; private set; } + + /// + /// Gets the number of items in the row/column. + /// + public int ItemsCount { get; private set; } + + public bool TryAdd(RowSpec addend, double spacing, double maxSize) + { + // Check if adding the new spec would exceed the maximum size + var sum = this + addend; + if (sum.Measure(spacing) > maxSize) + return false; + + // Update the current spec to include the new spec + this = sum; + return true; + } + + public readonly double Measure(double spacing) + { + var totalSpacing = (ItemsCount - 1) * spacing; + var totalSize = ReservedSpace + totalSpacing; + + // Add star-sized items if applicable + if (!double.IsNaN(MinPortionSize) && !double.IsInfinity(MinPortionSize)) + totalSize += MinPortionSize * PortionsSum; + + return totalSize; + } + + public static RowSpec operator +(RowSpec a, RowSpec b) + { + var combined = new RowSpec + { + ReservedSpace = a.ReservedSpace + b.ReservedSpace, + PortionsSum = a.PortionsSum + b.PortionsSum, + MinPortionSize = Math.Max(a.MinPortionSize, b.MinPortionSize), + MaxOffAxisSize = Math.Max(a.MaxOffAxisSize, b.MaxOffAxisSize), + ItemsCount = a.ItemsCount + b.ItemsCount + }; + return combined; + } + } + + /// + /// A struct for mapping X/Y coordinates to an orientation adjusted U/V coordinate system. + /// + private struct UVCoord(double x, double y, Orientation orientation) + { + private readonly bool _horizontal = orientation is Orientation.Horizontal; + + public UVCoord(Size size, Orientation orientation) : this(size.Width, size.Height, orientation) + { + } + + public double X { get; set; } = x; + + public double Y { get; set; } = y; + + public double U + { + readonly get => _horizontal ? X : Y; + set + { + if (_horizontal) + { + X = value; + } + else + { + Y = value; + } + } + } + + public double V + { + readonly get => _horizontal ? Y : X; + set + { + if (_horizontal) + { + Y = value; + } + else + { + X = value; + } + } + } + + public readonly Size Size => new(X, Y); + } +} diff --git a/components/StretchPanel/src/StretchPanel.cs b/components/StretchPanel/src/StretchPanel.cs index 3cc19dcc7..1281880c9 100644 --- a/components/StretchPanel/src/StretchPanel.cs +++ b/components/StretchPanel/src/StretchPanel.cs @@ -55,14 +55,10 @@ protected override Size MeasureOverride(Size availableSize) _rowSpecs.Add(currentRowSpec); _longestRowSize = Math.Max(_longestRowSize, currentRowSpec.Measure(uvSpacing.U)); - // Determine if the desired alignment is stretched. - // Don't stretch if infinite space is available though. Attempting to divide infinite space will result in a crash. - bool stretch = GetAlignment() is Alignment.Stretch && !double.IsInfinity(uvAvailableSize.U); - // Calculate final desired size var uvSize = new UVCoord(0, 0, Orientation) { - U = stretch ? uvAvailableSize.U : _longestRowSize, + U = IsMainAxisStretch(uvAvailableSize.U) ? uvAvailableSize.U : _longestRowSize, V = _rowSpecs.Sum(static rs => rs.MaxOffAxisSize) + (uvSpacing.V * (_rowSpecs.Count - 1)) }; @@ -85,66 +81,43 @@ protected override Size ArrangeOverride(Size finalSize) var contentHeight = _rowSpecs.Sum(static rs => rs.MaxOffAxisSize) + (uvSpacing.V * (_rowSpecs.Count - 1)); pos.V = GetStartByAlignment(GetOffAlignment(), contentHeight, uvFinalSize.V); - var elements = Children.Where(static e => e.Visibility is Visibility.Visible); + var childQueue = new Queue(Children.Where(static e => e.Visibility is Visibility.Visible)); + foreach (var row in _rowSpecs) { - // Setup the row/column for arrangement - var (uPos, portionSize, forceStrech) = SetupArrangeRow(row, uvFinalSize, uvSpacing, elements); - pos.U = uPos; - - for (int i = 0; i < row.ItemsCount; i++) - { - // Get the next child - var child = elements.ElementAt(0); - elements = elements.Skip(1); - - // Sanity check - if (child is null) - { - return finalSize; - } - - // Determine the child's size - var size = GetChildSize(child, i, row, portionSize, forceStrech); - - // NOTE: The arrange method is still in X/Y coordinate system - child.Arrange(new Rect(pos.X, pos.Y, size.X, size.Y)); - - // Advance the position - pos.U += size.U + uvSpacing.U; - } - - // Advance to the next row/column - pos.V += row.MaxOffAxisSize + uvSpacing.V; + // Arrange the row/column + ArrangeRow(ref pos, row, uvFinalSize, uvSpacing, childQueue); } return finalSize; } - private (double uPos, double portionSize, bool forceStretch) SetupArrangeRow(RowSpec row, UVCoord uvFinalSize, UVCoord uvSpacing, IEnumerable elements) - { - double uPos = 0; + private void ArrangeRow(ref UVCoord pos, RowSpec row, UVCoord uvFinalSize, UVCoord uvSpacing, Queue childQueue) + { var spacingTotalSize = uvSpacing.U * (row.ItemsCount - 1); var remainingSpace = uvFinalSize.U - row.ReservedSpace - spacingTotalSize; var portionSize = row.MinPortionSize; // Determine if the desired alignment is stretched. - bool stretch = GetAlignment() is Alignment.Stretch && !double.IsInfinity(uvFinalSize.U); + // Or if fixed row lengths are in use. + bool stretch = IsMainAxisStretch(uvFinalSize.U) || FixedRowLengths; // Calculate portion size if stretching // Same logic applies for matching row lengths, since the size was determined during measure - if (stretch || FixedRowLengths) + if (stretch) { portionSize = remainingSpace / row.PortionsSum; } + // Reset the starting U position + pos.U = 0; + // Adjust the starting position if not stretching - // Also do this if there are no star-sized items in the row/column - // and no forced streching is in use. - if (!(stretch || FixedRowLengths) || (row.PortionsSum is 0 && ForcedStretchMethod is ForcedStretchMethod.None)) + // Also do this if there are no star-sized items in the row/column and no forced streching is in use. + if (!stretch || (row.PortionsSum is 0 && ForcedStretchMethod is ForcedStretchMethod.None)) { var rowSize = row.Measure(uvSpacing.U); - uPos = GetStartByAlignment(GetAlignment(), rowSize, uvFinalSize.U); + pos.U = GetStartByAlignment(GetAlignment(), rowSize, uvFinalSize.U); } // Set a flag for if the row is being forced to stretch @@ -155,18 +128,56 @@ protected override Size ArrangeOverride(Size finalSize) { portionSize = ForcedStretchMethod switch { - ForcedStretchMethod.First => remainingSpace + GetChildSize(elements.ElementAt(0)), - ForcedStretchMethod.Last => remainingSpace + GetChildSize(elements.ElementAt(row.ItemsCount - 1)), - ForcedStretchMethod.Equal => (uvFinalSize.U - spacingTotalSize) / row.ItemsCount, - ForcedStretchMethod.Equal or ForcedStretchMethod.Proportional => (uvFinalSize.U - spacingTotalSize) / row.ReservedSpace, + // The first child's size will be overridden to 1* + // Change portion size to fill remaining space plus its original size + ForcedStretchMethod.First => + remainingSpace + GetChildSize(childQueue.Peek()), + + // The last child's size will be overridden to 1* + // Change portion size to fill remaining space plus its original size + ForcedStretchMethod.Last => + remainingSpace + GetChildSize(childQueue.ElementAt(row.ItemsCount - 1)), + + // All children's sizes will be overridden to 1* + // Change portion size to evenly distribute remaining space + ForcedStretchMethod.Equal => + (uvFinalSize.U - spacingTotalSize) / row.ItemsCount, + + // All children's sizes will be overridden to star sizes proportional to their original size + // Change portion size to distribute remaining space proportionally + ForcedStretchMethod.Proportional => + (uvFinalSize.U - spacingTotalSize) / row.ReservedSpace, + + // Default case (should not be hit) _ => row.MinPortionSize, }; } - return (uPos, portionSize, forceStretch); + // Arrange each child in the row/column + for (int i = 0; i < row.ItemsCount; i++) + { + // Get the next child + var child = childQueue.Dequeue(); + + // Sanity check + if (child is null) + return; + + // Determine the child's size + var size = GetChildSize(child, i, row, portionSize, forceStretch); + + // NOTE: The arrange method is still in X/Y coordinate system + child.Arrange(new Rect(pos.X, pos.Y, size.X, size.Y)); + + // Advance the position + pos.U += size.U + uvSpacing.U; + } + + // Advance to the next row/column + pos.V += row.MaxOffAxisSize + uvSpacing.V; } - private UVCoord GetChildSize(UIElement child, int rowIndex, RowSpec row, double portionSize, bool forceStretch) + private UVCoord GetChildSize(UIElement child, int indexInRow, RowSpec row, double portionSize, bool forceStretch) { // Get layout and desired size var layoutLength = GetLayoutLength(child); @@ -178,9 +189,16 @@ private UVCoord GetChildSize(UIElement child, int rowIndex, RowSpec row, double var oneStar = new GridLength(1, GridUnitType.Star); layoutLength = ForcedStretchMethod switch { - ForcedStretchMethod.First when rowIndex is 0 => oneStar, - ForcedStretchMethod.Last when rowIndex == (row.ItemsCount - 1) => oneStar, + // Override the first item's layout to 1* + ForcedStretchMethod.First when indexInRow is 0 => oneStar, + + // Override the last item's layout to 1* + ForcedStretchMethod.Last when indexInRow == (row.ItemsCount - 1) => oneStar, + + // Override all item's layouts to 1* ForcedStretchMethod.Equal => oneStar, + + // Override all item's layouts to star sizes proportional to their original size ForcedStretchMethod.Proportional => layoutLength.GridUnitType switch { GridUnitType.Auto => new GridLength(uvDesiredSize.U, GridUnitType.Star), @@ -268,6 +286,12 @@ private Alignment GetOffAlignment() }; } + /// + /// Determine if the desired alignment is stretched. + /// Don't stretch if infinite space is available though. Attempting to divide infinite space will result in a crash. + /// + private bool IsMainAxisStretch(double availableSize) => GetAlignment() is Alignment.Stretch && !double.IsInfinity(availableSize); + private double GetChildSize(UIElement child) { var childLayout = GetLayoutLength(child); @@ -287,151 +311,4 @@ private enum Alignment End, Stretch } - - /// - /// A struct representing the specifications of a row or column in the panel. - /// - private struct RowSpec - { - public RowSpec(GridLength layout, UVCoord desiredSize) - { - switch (layout.GridUnitType) - { - case GridUnitType.Auto: - ReservedSpace = desiredSize.U; - break; - case GridUnitType.Pixel: - ReservedSpace = layout.Value; - break; - case GridUnitType.Star: - PortionsSum = layout.Value; - MinPortionSize = desiredSize.U / layout.Value; - break; - } - - MaxOffAxisSize = desiredSize.V; - ItemsCount = 1; - } - - /// - /// Gets the total reserved space for spacing in the row/column. - /// - /// - /// Items with a fixed size or auto size contribute to this value. - /// - public double ReservedSpace { get; private set; } - - /// - /// Gets the sum of portions in the row/column. - /// - /// - /// Items with a star-sized length contribute to this value. - /// - public double PortionsSum { get; private set; } - - /// - /// Gets the maximum width/height of items in the row/column. - /// - /// - /// Width in vertical orientation, height in horizontal orientation. - /// - public double MaxOffAxisSize { get; private set; } - - /// - /// Gets the minimum size of a portion in the row/column. - /// - public double MinPortionSize { get; private set; } - - /// - /// Gets the number of items in the row/column. - /// - public int ItemsCount { get; private set; } - - public bool TryAdd(RowSpec addend, double spacing, double maxSize) - { - // Check if adding the new spec would exceed the maximum size - var sum = this + addend; - if (sum.Measure(spacing) > maxSize) - return false; - - // Update the current spec to include the new spec - this = sum; - return true; - } - - public readonly double Measure(double spacing) - { - var totalSpacing = (ItemsCount - 1) * spacing; - var totalSize = ReservedSpace + totalSpacing; - - // Add star-sized items if applicable - if (!double.IsNaN(MinPortionSize) && !double.IsInfinity(MinPortionSize)) - totalSize += MinPortionSize * PortionsSum; - - return totalSize; - } - - public static RowSpec operator +(RowSpec a, RowSpec b) - { - var combined = new RowSpec - { - ReservedSpace = a.ReservedSpace + b.ReservedSpace, - PortionsSum = a.PortionsSum + b.PortionsSum, - MinPortionSize = Math.Max(a.MinPortionSize, b.MinPortionSize), - MaxOffAxisSize = Math.Max(a.MaxOffAxisSize, b.MaxOffAxisSize), - ItemsCount = a.ItemsCount + b.ItemsCount - }; - return combined; - } - } - - /// - /// A struct for mapping X/Y coordinates to an orientation adjusted U/V coordinate system. - /// - private struct UVCoord(double x, double y, Orientation orientation) - { - private readonly bool _horizontal = orientation is Orientation.Horizontal; - - public UVCoord(Size size, Orientation orientation) : this(size.Width, size.Height, orientation) - { - } - - public double X { get; set; } = x; - - public double Y { get; set; } = y; - - public double U - { - readonly get => _horizontal ? X : Y; - set - { - if (_horizontal) - { - X = value; - } - else - { - Y = value; - } - } - } - - public double V - { - readonly get => _horizontal ? Y : X; - set - { - if (_horizontal) - { - Y = value; - } - else - { - X = value; - } - } - } - - public readonly Size Size => new(X, Y); - } } From 379769c1291de9dbc7cfd518c0caf8db1ec7425e Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Mon, 1 Dec 2025 01:08:12 +0200 Subject: [PATCH 10/14] Added Markdown Docs for the StretchPanel --- .../StretchPanel/samples/StretchPanel.md | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/components/StretchPanel/samples/StretchPanel.md b/components/StretchPanel/samples/StretchPanel.md index b9c75a908..c9dc5eb48 100644 --- a/components/StretchPanel/samples/StretchPanel.md +++ b/components/StretchPanel/samples/StretchPanel.md @@ -1,32 +1,53 @@ --- title: StretchPanel -author: githubaccount -description: TODO: Your experiment's description here +author: Avid29 +description: A highly flexible panel that arranges its child elements according to GridLength definitions, allowing for proportional sizing and stretching behavior. keywords: StretchPanel, Control, Layout dev_langs: - csharp -category: Controls -subcategory: Layout +category: Layouts +subcategory: Panel discussion-id: 0 issue-id: 0 icon: assets/icon.png --- - - - - - - - - # StretchPanel -TODO: Fill in information about this experiment and how to get started here... +The StretchPanel is a panel that arranges its child elements according to GridLength definitions. -## Custom Control +When stretched along the main axis, the child elements with star-sized GridLength values will proportionally occupy the available space. -You can inherit from an existing component as well, like `Panel`, this example shows a control without a -XAML Style that will be more light-weight to consume by an app developer: +When not stretched along the main axis, star-sized child elements will be the smallest size possible while maintaining proportional sizing relative to each other and ensuring that all child elements are fully visible. > [!Sample StretchPanelBasicSample] + +## Properties + +### Fixed Row Length + +When `FixedRowLengths` is enabled, all rows/columns will to stretch to the size of the largest row/column in the panel. When this is not enabled, rows/columns will size to their content individually. + +### Forced Stretch Method + +The `ForcedStretchMethod` property allows you to specify how the StretchPanel should handle stretching in rows without star-sized definitions. + +#### None + +When set to `None`, the StretchPanel will not stretch rows/columns that do not have star-sized definitions. When the alignment is set to stretch, and even when fixed row lengths is enabled, the rows/columns without star-sized definitions will size to their content. + +#### First + +When set the `First`, the StretchPanel will stretch the first item in the row/column to occupy the remaining space when needed to comply with stretch alignment. + +#### Last + +When set to `Last`, the StretchPanel will stretch the last item in the row/column to occupy the remaining space when needed to comply with stretch alignment. + +#### Equal + +When set to `Equal`, the StretchPanel will stretch all items in the row/column to occupy the equal space throughout the row when needed to comply with stretch alignment. + +#### Proportional + +When set to `Proportional`, the StretchPanel will stretch all items in the row/column proportionally to their defined size to occupy the remaining space when needed to comply with stretch alignment. From 4977f8733dd436584419331454968b8665f43776 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Mon, 1 Dec 2025 21:15:10 +0200 Subject: [PATCH 11/14] Added OverflowBehaviors for StretchPanel --- .../samples/StretchPanelBasicSample.xaml | 3 ++- .../samples/StretchPanelBasicSample.xaml.cs | 9 ++++++++ .../StretchPanel/src/OverflowBehavior.cs | 21 +++++++++++++++++++ .../src/StretchPanel.Properties.cs | 20 +++++++++++++++++- components/StretchPanel/src/StretchPanel.cs | 14 +++++++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 components/StretchPanel/src/OverflowBehavior.cs diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml b/components/StretchPanel/samples/StretchPanelBasicSample.xaml index 96c54a931..a200f9fdb 100644 --- a/components/StretchPanel/samples/StretchPanelBasicSample.xaml +++ b/components/StretchPanel/samples/StretchPanelBasicSample.xaml @@ -27,7 +27,8 @@ HorizontalAlignment="{x:Bind LayoutHorizontalAlignment, Mode=OneWay}" VerticalAlignment="{x:Bind LayoutVerticalAlignment, Mode=OneWay}" FixedRowLengths="{x:Bind FixedRowLengths, Mode=OneWay}" - ForcedStretchMethod="{x:Bind local:StretchPanelBasicSample.ConvertStringToForcedStretchMethod(LayoutForcedStretchMethod), Mode=OneWay}"> + ForcedStretchMethod="{x:Bind local:StretchPanelBasicSample.ConvertStringToForcedStretchMethod(LayoutForcedStretchMethod), Mode=OneWay}" + OverflowBehavior="{x:Bind local:StretchPanelBasicSample.ConvertStringToOverflowBehavior(LayoutOverflowBehavior), Mode=OneWay}"> diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs b/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs index ea48b5a4f..dbe1542ed 100644 --- a/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs +++ b/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs @@ -16,6 +16,7 @@ namespace StretchPanelExperiment.Samples; [ToolkitSampleNumericOption("VerticalSpacing", 2, 0, 16, Title = "Vertical Spacing")] [ToolkitSampleBoolOption("FixedRowLengths", false, Title = "Fixed Row Lengths")] [ToolkitSampleMultiChoiceOption("LayoutForcedStretchMethod", "None", "First", "Last", "Equal", "Proportional", Title = "Forced Stretch Method")] +[ToolkitSampleMultiChoiceOption("LayoutOverflowBehavior", "Wrap", "Drop", Title = "Overflow Behavior")] [ToolkitSample(id: nameof(StretchPanelBasicSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(StretchPanel)} custom control.")] public sealed partial class StretchPanelBasicSample : Page @@ -63,4 +64,12 @@ public StretchPanelBasicSample() "Proportional" => ForcedStretchMethod.Proportional, _ => throw new System.NotImplementedException(), }; + + // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149 + public static OverflowBehavior ConvertStringToOverflowBehavior(string overflowBehavior) => overflowBehavior switch + { + "Wrap" => OverflowBehavior.Wrap, + "Drop" => OverflowBehavior.Drop, + _ => throw new System.NotImplementedException(), + }; } diff --git a/components/StretchPanel/src/OverflowBehavior.cs b/components/StretchPanel/src/OverflowBehavior.cs new file mode 100644 index 000000000..ebd22c0ca --- /dev/null +++ b/components/StretchPanel/src/OverflowBehavior.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Describes the behavior of items that exceed the available space in the panel. +/// +public enum OverflowBehavior +{ + /// + /// When an item exceeds the available space, it will be moved to a new row or column. + /// + Wrap, + + /// + /// Items which do not fit within the available space will be removed from the layout. + /// + Drop, +} diff --git a/components/StretchPanel/src/StretchPanel.Properties.cs b/components/StretchPanel/src/StretchPanel.Properties.cs index 2c46f86c5..f1826982b 100644 --- a/components/StretchPanel/src/StretchPanel.Properties.cs +++ b/components/StretchPanel/src/StretchPanel.Properties.cs @@ -59,7 +59,16 @@ public partial class StretchPanel nameof(ForcedStretchMethod), typeof(ForcedStretchMethod), typeof(StretchPanel), - new PropertyMetadata(default(bool), OnPropertyChanged)); + new PropertyMetadata(default(ForcedStretchMethod), OnPropertyChanged)); + + /// + /// Backing for the property. + /// + public static readonly DependencyProperty OverflowBehaviorProperty = DependencyProperty.Register( + nameof(OverflowBehavior), + typeof(OverflowBehavior), + typeof(StretchPanel), + new PropertyMetadata(default(OverflowBehavior), OnPropertyChanged)); /// /// Gets or sets the panel orientation. @@ -106,6 +115,15 @@ public ForcedStretchMethod ForcedStretchMethod set => SetValue(ForcedStretchMethodProperty, value); } + /// + /// Gets or sets how the panel handles content overflowing the available space. + /// + public OverflowBehavior OverflowBehavior + { + get => (OverflowBehavior)GetValue(OverflowBehaviorProperty); + set => SetValue(OverflowBehaviorProperty, value); + } + /// /// Gets the of an item in the . /// diff --git a/components/StretchPanel/src/StretchPanel.cs b/components/StretchPanel/src/StretchPanel.cs index 1281880c9..615824dc5 100644 --- a/components/StretchPanel/src/StretchPanel.cs +++ b/components/StretchPanel/src/StretchPanel.cs @@ -43,6 +43,10 @@ protected override Size MeasureOverride(Size availableSize) var spec = new RowSpec(layoutLength, uvDesiredSize); if (!currentRowSpec.TryAdd(spec, uvSpacing.U, uvAvailableSize.U)) { + // If the overflow behavior is drop, just end the row here. + if (OverflowBehavior is OverflowBehavior.Drop) + break; + // Could not add to current row/column // Start a new row/column _rowSpecs.Add(currentRowSpec); @@ -62,6 +66,9 @@ protected override Size MeasureOverride(Size availableSize) V = _rowSpecs.Sum(static rs => rs.MaxOffAxisSize) + (uvSpacing.V * (_rowSpecs.Count - 1)) }; + // Clamp to available size and return + uvSize.U = Math.Min(uvSize.U, uvAvailableSize.U); + uvSize.V = Math.Min(uvSize.V, uvAvailableSize.V); return uvSize.Size; } @@ -89,6 +96,13 @@ protected override Size ArrangeOverride(Size finalSize) ArrangeRow(ref pos, row, uvFinalSize, uvSpacing, childQueue); } + // "Arrange" remaning children by rendering them with zero size + while (childQueue.TryDequeue(out var child)) + { + // Arrange with zero size + child.Arrange(new Rect(0, 0, 0, 0)); + } + return finalSize; } From a05b71d35cb59dc391420a6446308278e57f9020 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Wed, 3 Dec 2025 13:28:51 +0200 Subject: [PATCH 12/14] Renamed to WrapPanel2 --- .../StretchPanel/samples/StretchPanel.md | 53 ----------------- .../OpenSolution.bat | 0 .../samples/Assets/icon.png | Bin .../samples/Dependencies.props | 0 .../samples/WrapPanel2.Samples.csproj} | 6 +- components/WrapPanel2/samples/WrapPanel2.md | 54 ++++++++++++++++++ .../samples/WrapPanel2BasicSample.xaml} | 36 ++++++------ .../samples/WrapPanel2BasicSample.xaml.cs} | 8 +-- ...Toolkit.WinUI.Controls.StretchPanel.csproj | 6 +- .../src/Dependencies.props | 0 .../src/ForcedStretchMethod.cs | 0 .../src/MultiTarget.props | 0 .../src/OverflowBehavior.cs | 0 .../src/WrapPanel2.Properties.cs} | 22 +++---- .../src/WrapPanel2.Structs.cs} | 2 +- .../src/WrapPanel2.cs} | 2 +- .../tests/ExampleWrapPanel2TestClass.cs} | 30 +++++----- .../tests/ExampleWrapPanel2TestPage.xaml} | 4 +- .../tests/ExampleWrapPanel2TestPage.xaml.cs} | 6 +- .../tests/WrapPanel2.Tests.projitems} | 10 ++-- .../tests/WrapPanel2.Tests.shproj} | 2 +- 21 files changed, 121 insertions(+), 120 deletions(-) delete mode 100644 components/StretchPanel/samples/StretchPanel.md rename components/{StretchPanel => WrapPanel2}/OpenSolution.bat (100%) rename components/{StretchPanel => WrapPanel2}/samples/Assets/icon.png (100%) rename components/{StretchPanel => WrapPanel2}/samples/Dependencies.props (100%) rename components/{StretchPanel/samples/StretchPanel.Samples.csproj => WrapPanel2/samples/WrapPanel2.Samples.csproj} (68%) create mode 100644 components/WrapPanel2/samples/WrapPanel2.md rename components/{StretchPanel/samples/StretchPanelBasicSample.xaml => WrapPanel2/samples/WrapPanel2BasicSample.xaml} (68%) rename components/{StretchPanel/samples/StretchPanelBasicSample.xaml.cs => WrapPanel2/samples/WrapPanel2BasicSample.xaml.cs} (91%) rename components/{StretchPanel => WrapPanel2}/src/CommunityToolkit.WinUI.Controls.StretchPanel.csproj (70%) rename components/{StretchPanel => WrapPanel2}/src/Dependencies.props (100%) rename components/{StretchPanel => WrapPanel2}/src/ForcedStretchMethod.cs (100%) rename components/{StretchPanel => WrapPanel2}/src/MultiTarget.props (100%) rename components/{StretchPanel => WrapPanel2}/src/OverflowBehavior.cs (100%) rename components/{StretchPanel/src/StretchPanel.Properties.cs => WrapPanel2/src/WrapPanel2.Properties.cs} (93%) rename components/{StretchPanel/src/StretchPanel.Structs.cs => WrapPanel2/src/WrapPanel2.Structs.cs} (99%) rename components/{StretchPanel/src/StretchPanel.cs => WrapPanel2/src/WrapPanel2.cs} (99%) rename components/{StretchPanel/tests/ExampleStretchPanelTestClass.cs => WrapPanel2/tests/ExampleWrapPanel2TestClass.cs} (80%) rename components/{StretchPanel/tests/ExampleStretchPanelTestPage.xaml => WrapPanel2/tests/ExampleWrapPanel2TestPage.xaml} (84%) rename components/{StretchPanel/tests/ExampleStretchPanelTestPage.xaml.cs => WrapPanel2/tests/ExampleWrapPanel2TestPage.xaml.cs} (73%) rename components/{StretchPanel/tests/StretchPanel.Tests.projitems => WrapPanel2/tests/WrapPanel2.Tests.projitems} (62%) rename components/{StretchPanel/tests/StretchPanel.Tests.shproj => WrapPanel2/tests/WrapPanel2.Tests.shproj} (93%) diff --git a/components/StretchPanel/samples/StretchPanel.md b/components/StretchPanel/samples/StretchPanel.md deleted file mode 100644 index c9dc5eb48..000000000 --- a/components/StretchPanel/samples/StretchPanel.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: StretchPanel -author: Avid29 -description: A highly flexible panel that arranges its child elements according to GridLength definitions, allowing for proportional sizing and stretching behavior. -keywords: StretchPanel, Control, Layout -dev_langs: - - csharp -category: Layouts -subcategory: Panel -discussion-id: 0 -issue-id: 0 -icon: assets/icon.png ---- - -# StretchPanel - -The StretchPanel is a panel that arranges its child elements according to GridLength definitions. - -When stretched along the main axis, the child elements with star-sized GridLength values will proportionally occupy the available space. - -When not stretched along the main axis, star-sized child elements will be the smallest size possible while maintaining proportional sizing relative to each other and ensuring that all child elements are fully visible. - -> [!Sample StretchPanelBasicSample] - -## Properties - -### Fixed Row Length - -When `FixedRowLengths` is enabled, all rows/columns will to stretch to the size of the largest row/column in the panel. When this is not enabled, rows/columns will size to their content individually. - -### Forced Stretch Method - -The `ForcedStretchMethod` property allows you to specify how the StretchPanel should handle stretching in rows without star-sized definitions. - -#### None - -When set to `None`, the StretchPanel will not stretch rows/columns that do not have star-sized definitions. When the alignment is set to stretch, and even when fixed row lengths is enabled, the rows/columns without star-sized definitions will size to their content. - -#### First - -When set the `First`, the StretchPanel will stretch the first item in the row/column to occupy the remaining space when needed to comply with stretch alignment. - -#### Last - -When set to `Last`, the StretchPanel will stretch the last item in the row/column to occupy the remaining space when needed to comply with stretch alignment. - -#### Equal - -When set to `Equal`, the StretchPanel will stretch all items in the row/column to occupy the equal space throughout the row when needed to comply with stretch alignment. - -#### Proportional - -When set to `Proportional`, the StretchPanel will stretch all items in the row/column proportionally to their defined size to occupy the remaining space when needed to comply with stretch alignment. diff --git a/components/StretchPanel/OpenSolution.bat b/components/WrapPanel2/OpenSolution.bat similarity index 100% rename from components/StretchPanel/OpenSolution.bat rename to components/WrapPanel2/OpenSolution.bat diff --git a/components/StretchPanel/samples/Assets/icon.png b/components/WrapPanel2/samples/Assets/icon.png similarity index 100% rename from components/StretchPanel/samples/Assets/icon.png rename to components/WrapPanel2/samples/Assets/icon.png diff --git a/components/StretchPanel/samples/Dependencies.props b/components/WrapPanel2/samples/Dependencies.props similarity index 100% rename from components/StretchPanel/samples/Dependencies.props rename to components/WrapPanel2/samples/Dependencies.props diff --git a/components/StretchPanel/samples/StretchPanel.Samples.csproj b/components/WrapPanel2/samples/WrapPanel2.Samples.csproj similarity index 68% rename from components/StretchPanel/samples/StretchPanel.Samples.csproj rename to components/WrapPanel2/samples/WrapPanel2.Samples.csproj index 27eaada09..a8cc6056f 100644 --- a/components/StretchPanel/samples/StretchPanel.Samples.csproj +++ b/components/WrapPanel2/samples/WrapPanel2.Samples.csproj @@ -2,14 +2,14 @@ - StretchPanel + WrapPanel2 - - StretchPanelBasicSample.xaml + + WrapPanel2BasicSample.xaml diff --git a/components/WrapPanel2/samples/WrapPanel2.md b/components/WrapPanel2/samples/WrapPanel2.md new file mode 100644 index 000000000..a80b15b8c --- /dev/null +++ b/components/WrapPanel2/samples/WrapPanel2.md @@ -0,0 +1,54 @@ +--- +title: WrapPannel2 +author: Avid29 +description: A labs-component candidate for a new WrapPanel implementation. +keywords: WrapPanel, Control, Layout +dev_langs: + - csharp +category: Layouts +subcategory: Panel +discussion-id: 762 +issue-id: 763 +icon: assets/icon.png +--- + +# WrapPanel2 + +The WrapPanel2 is an experiment for a new WrapPanel API using GridLength definitions to define the item's desired sizings. + +When stretched along the main axis, the child elements with star-sized GridLength values will proportionally occupy the available space. + +When not stretched along the main axis, star-sized child elements will be the smallest size possible while maintaining proportional sizing relative to each other and ensuring that all child elements are fully visible. + + +> [!Sample WrapPanel2BasicSample] + +## Properties + +### Fixed Row Length + +When `FixedRowLengths` is enabled, all rows/columns will to stretch to the size of the largest row/column in the panel. When this is not enabled, rows/columns will size to their content individually. + +### Forced Stretch Method + +The `ForcedStretchMethod` property allows you to specify how the panel should handle stretching in rows without star-sized definitions. + +#### None + +When set to `None`, this panel will not stretch rows/columns that do not have star-sized definitions. When the alignment is set to stretch, and even when fixed row lengths is enabled, the rows/columns without star-sized definitions will size to their content. + +#### First + +When set the `First`, this panel will stretch the first item in the row/column to occupy the remaining space when needed to comply with stretch alignment. + +#### Last + +When set to `Last`, this panel will stretch the last item in the row/column to occupy the remaining space when needed to comply with stretch alignment. + +#### Equal + +When set to `Equal`, this panel will stretch all items in the row/column to occupy the equal space throughout the row when needed to comply with stretch alignment. + +#### Proportional + +When set to `Proportional`, this panel will stretch all items in the row/column proportionally to their defined size to occupy the remaining space when needed to comply with stretch alignment. diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml b/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml similarity index 68% rename from components/StretchPanel/samples/StretchPanelBasicSample.xaml rename to components/WrapPanel2/samples/WrapPanel2BasicSample.xaml index a200f9fdb..19e06156c 100644 --- a/components/StretchPanel/samples/StretchPanelBasicSample.xaml +++ b/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml @@ -1,10 +1,10 @@ - @@ -21,50 +21,50 @@ - - + ForcedStretchMethod="{x:Bind local:WrapPanel2BasicSample.ConvertStringToForcedStretchMethod(LayoutForcedStretchMethod), Mode=OneWay}" + OverflowBehavior="{x:Bind local:WrapPanel2BasicSample.ConvertStringToOverflowBehavior(LayoutOverflowBehavior), Mode=OneWay}"> + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs b/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml.cs similarity index 91% rename from components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs rename to components/WrapPanel2/samples/WrapPanel2BasicSample.xaml.cs index dbe1542ed..4c8f21472 100644 --- a/components/StretchPanel/samples/StretchPanelBasicSample.xaml.cs +++ b/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml.cs @@ -4,7 +4,7 @@ using CommunityToolkit.WinUI.Controls; -namespace StretchPanelExperiment.Samples; +namespace WrapPanel2Experiment.Samples; /// /// An example sample page of a custom control inheriting from Panel. @@ -18,10 +18,10 @@ namespace StretchPanelExperiment.Samples; [ToolkitSampleMultiChoiceOption("LayoutForcedStretchMethod", "None", "First", "Last", "Equal", "Proportional", Title = "Forced Stretch Method")] [ToolkitSampleMultiChoiceOption("LayoutOverflowBehavior", "Wrap", "Drop", Title = "Overflow Behavior")] -[ToolkitSample(id: nameof(StretchPanelBasicSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(StretchPanel)} custom control.")] -public sealed partial class StretchPanelBasicSample : Page +[ToolkitSample(id: nameof(WrapPanel2BasicSample), "WrapPanel2 Basic Sample", description: $"A sample for showing how to use a {nameof(WrapPanel2)} panel.")] +public sealed partial class WrapPanel2BasicSample : Page { - public StretchPanelBasicSample() + public WrapPanel2BasicSample() { this.InitializeComponent(); } diff --git a/components/StretchPanel/src/CommunityToolkit.WinUI.Controls.StretchPanel.csproj b/components/WrapPanel2/src/CommunityToolkit.WinUI.Controls.StretchPanel.csproj similarity index 70% rename from components/StretchPanel/src/CommunityToolkit.WinUI.Controls.StretchPanel.csproj rename to components/WrapPanel2/src/CommunityToolkit.WinUI.Controls.StretchPanel.csproj index 78a283a99..fef145482 100644 --- a/components/StretchPanel/src/CommunityToolkit.WinUI.Controls.StretchPanel.csproj +++ b/components/WrapPanel2/src/CommunityToolkit.WinUI.Controls.StretchPanel.csproj @@ -2,11 +2,11 @@ - StretchPanel - This package contains StretchPanel. + WrapPanel2 + This package contains WrapPanel2. - CommunityToolkit.WinUI.Controls.StretchPanelRns + CommunityToolkit.WinUI.Controls.WrapPanel2Rns diff --git a/components/StretchPanel/src/Dependencies.props b/components/WrapPanel2/src/Dependencies.props similarity index 100% rename from components/StretchPanel/src/Dependencies.props rename to components/WrapPanel2/src/Dependencies.props diff --git a/components/StretchPanel/src/ForcedStretchMethod.cs b/components/WrapPanel2/src/ForcedStretchMethod.cs similarity index 100% rename from components/StretchPanel/src/ForcedStretchMethod.cs rename to components/WrapPanel2/src/ForcedStretchMethod.cs diff --git a/components/StretchPanel/src/MultiTarget.props b/components/WrapPanel2/src/MultiTarget.props similarity index 100% rename from components/StretchPanel/src/MultiTarget.props rename to components/WrapPanel2/src/MultiTarget.props diff --git a/components/StretchPanel/src/OverflowBehavior.cs b/components/WrapPanel2/src/OverflowBehavior.cs similarity index 100% rename from components/StretchPanel/src/OverflowBehavior.cs rename to components/WrapPanel2/src/OverflowBehavior.cs diff --git a/components/StretchPanel/src/StretchPanel.Properties.cs b/components/WrapPanel2/src/WrapPanel2.Properties.cs similarity index 93% rename from components/StretchPanel/src/StretchPanel.Properties.cs rename to components/WrapPanel2/src/WrapPanel2.Properties.cs index f1826982b..7a59d8914 100644 --- a/components/StretchPanel/src/StretchPanel.Properties.cs +++ b/components/WrapPanel2/src/WrapPanel2.Properties.cs @@ -4,7 +4,7 @@ namespace CommunityToolkit.WinUI.Controls; -public partial class StretchPanel +public partial class WrapPanel2 { /// /// An attached property for identifying the requested layout of a child within the panel. @@ -13,7 +13,7 @@ public partial class StretchPanel DependencyProperty.Register( "LayoutLength", typeof(GridLength), - typeof(StretchPanel), + typeof(WrapPanel2), new PropertyMetadata(GridLength.Auto)); /// @@ -22,7 +22,7 @@ public partial class StretchPanel public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( nameof(Orientation), typeof(Orientation), - typeof(StretchPanel), + typeof(WrapPanel2), new PropertyMetadata(default(Orientation), OnPropertyChanged)); /// @@ -31,7 +31,7 @@ public partial class StretchPanel public static readonly DependencyProperty HorizontalSpacingProperty = DependencyProperty.Register( nameof(HorizontalSpacing), typeof(double), - typeof(StretchPanel), + typeof(WrapPanel2), new PropertyMetadata(default(double), OnPropertyChanged)); /// @@ -40,7 +40,7 @@ public partial class StretchPanel public static readonly DependencyProperty VerticalSpacingProperty = DependencyProperty.Register( nameof(VerticalSpacing), typeof(double), - typeof(StretchPanel), + typeof(WrapPanel2), new PropertyMetadata(default(double), OnPropertyChanged)); /// @@ -49,7 +49,7 @@ public partial class StretchPanel public static readonly DependencyProperty FixedRowLengthsProperty = DependencyProperty.Register( nameof(FixedRowLengths), typeof(bool), - typeof(StretchPanel), + typeof(WrapPanel2), new PropertyMetadata(default(bool), OnPropertyChanged)); /// @@ -58,7 +58,7 @@ public partial class StretchPanel public static readonly DependencyProperty ForcedStretchMethodProperty = DependencyProperty.Register( nameof(ForcedStretchMethod), typeof(ForcedStretchMethod), - typeof(StretchPanel), + typeof(WrapPanel2), new PropertyMetadata(default(ForcedStretchMethod), OnPropertyChanged)); /// @@ -67,7 +67,7 @@ public partial class StretchPanel public static readonly DependencyProperty OverflowBehaviorProperty = DependencyProperty.Register( nameof(OverflowBehavior), typeof(OverflowBehavior), - typeof(StretchPanel), + typeof(WrapPanel2), new PropertyMetadata(default(OverflowBehavior), OnPropertyChanged)); /// @@ -125,18 +125,18 @@ public OverflowBehavior OverflowBehavior } /// - /// Gets the of an item in the . + /// Gets the of an item in the . /// public static GridLength GetLayoutLength(DependencyObject obj) => (GridLength)obj.GetValue(LayoutLengthProperty); /// - /// Sets the of an item in the . + /// Sets the of an item in the . /// public static void SetLayoutLength(DependencyObject obj, GridLength value) => obj.SetValue(LayoutLengthProperty, value); private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - var panel = (StretchPanel)d; + var panel = (WrapPanel2)d; panel.InvalidateMeasure(); } } diff --git a/components/StretchPanel/src/StretchPanel.Structs.cs b/components/WrapPanel2/src/WrapPanel2.Structs.cs similarity index 99% rename from components/StretchPanel/src/StretchPanel.Structs.cs rename to components/WrapPanel2/src/WrapPanel2.Structs.cs index cd4db1cf1..2c423730c 100644 --- a/components/StretchPanel/src/StretchPanel.Structs.cs +++ b/components/WrapPanel2/src/WrapPanel2.Structs.cs @@ -4,7 +4,7 @@ namespace CommunityToolkit.WinUI.Controls; -public partial class StretchPanel +public partial class WrapPanel2 { /// /// A struct representing the specifications of a row or column in the panel. diff --git a/components/StretchPanel/src/StretchPanel.cs b/components/WrapPanel2/src/WrapPanel2.cs similarity index 99% rename from components/StretchPanel/src/StretchPanel.cs rename to components/WrapPanel2/src/WrapPanel2.cs index 615824dc5..48d9f3758 100644 --- a/components/StretchPanel/src/StretchPanel.cs +++ b/components/WrapPanel2/src/WrapPanel2.cs @@ -7,7 +7,7 @@ namespace CommunityToolkit.WinUI.Controls; /// /// A panel that arranges its children in a grid-like fashion, stretching them to fill available space. /// -public partial class StretchPanel : Panel +public partial class WrapPanel2 : Panel { private List? _rowSpecs; private double _longestRowSize = 0; diff --git a/components/StretchPanel/tests/ExampleStretchPanelTestClass.cs b/components/WrapPanel2/tests/ExampleWrapPanel2TestClass.cs similarity index 80% rename from components/StretchPanel/tests/ExampleStretchPanelTestClass.cs rename to components/WrapPanel2/tests/ExampleWrapPanel2TestClass.cs index 16fac7038..5a41388c8 100644 --- a/components/StretchPanel/tests/ExampleStretchPanelTestClass.cs +++ b/components/WrapPanel2/tests/ExampleWrapPanel2TestClass.cs @@ -6,20 +6,20 @@ using CommunityToolkit.Tests; using CommunityToolkit.WinUI.Controls; -namespace StretchPanelTests; +namespace WrapPanel2Tests; [TestClass] -public partial class ExampleStretchPanelTestClass : VisualUITestBase +public partial class ExampleWrapPanel2TestClass : VisualUITestBase { // If you don't need access to UI objects directly or async code, use this pattern. [TestMethod] public void SimpleSynchronousExampleTest() { - var assembly = typeof(StretchPanel).Assembly; - var type = assembly.GetType(typeof(StretchPanel).FullName ?? string.Empty); + var assembly = typeof(WrapPanel2).Assembly; + var type = assembly.GetType(typeof(WrapPanel2).FullName ?? string.Empty); - Assert.IsNotNull(type, "Could not find StretchPanel type."); - Assert.AreEqual(typeof(StretchPanel), type, "Type of StretchPanel does not match expected type."); + Assert.IsNotNull(type, "Could not find WrapPanel2 type."); + Assert.AreEqual(typeof(WrapPanel2), type, "Type of WrapPanel2 does not match expected type."); } // If you don't need access to UI objects directly, use this pattern. @@ -46,7 +46,7 @@ public void SimpleExceptionCheckTest() [UIThreadTestMethod] public void SimpleUIAttributeExampleTest() { - var component = new StretchPanel(); + var component = new WrapPanel2(); Assert.IsNotNull(component); } @@ -54,27 +54,27 @@ public void SimpleUIAttributeExampleTest() // This lets us actually test a control as it would behave within an actual application. // The page will already be loaded by the time your test is called. [UIThreadTestMethod] - public void SimpleUIExamplePageTest(ExampleStretchPanelTestPage page) + public void SimpleUIExamplePageTest(ExampleWrapPanel2TestPage page) { // You can use the Toolkit Visual Tree helpers here to find the component by type or name: - var component = page.FindDescendant(); + var component = page.FindDescendant(); Assert.IsNotNull(component); - var componentByName = page.FindDescendant("StretchPanelControl"); + var componentByName = page.FindDescendant("WrapPanel2Control"); Assert.IsNotNull(componentByName); } // You can still do async work with a UIThreadTestMethod as well. [UIThreadTestMethod] - public async Task SimpleAsyncUIExamplePageTest(ExampleStretchPanelTestPage page) + public async Task SimpleAsyncUIExamplePageTest(ExampleWrapPanel2TestPage page) { // This helper can be used to wait for a rendering pass to complete. // Note, this is already done by loading a Page with the [UIThreadTestMethod] helper. await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }); - var component = page.FindDescendant(); + var component = page.FindDescendant(); Assert.IsNotNull(component); } @@ -89,7 +89,7 @@ public async Task ComplexAsyncUIExampleTest() { await EnqueueAsync(() => { - var component = new StretchPanel(); + var component = new WrapPanel2(); Assert.IsNotNull(component); }); } @@ -101,7 +101,7 @@ public async Task ComplexAsyncLoadUIExampleTest() { await EnqueueAsync(async () => { - var component = new StretchPanel(); + var component = new WrapPanel2(); Assert.IsNotNull(component); Assert.IsFalse(component.IsLoaded); @@ -119,7 +119,7 @@ await EnqueueAsync(async () => [UIThreadTestMethod] public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest() { - var component = new StretchPanel(); + var component = new WrapPanel2(); Assert.IsNotNull(component); Assert.IsFalse(component.IsLoaded); diff --git a/components/StretchPanel/tests/ExampleStretchPanelTestPage.xaml b/components/WrapPanel2/tests/ExampleWrapPanel2TestPage.xaml similarity index 84% rename from components/StretchPanel/tests/ExampleStretchPanelTestPage.xaml rename to components/WrapPanel2/tests/ExampleWrapPanel2TestPage.xaml index 4d9b76365..5e5b69b41 100644 --- a/components/StretchPanel/tests/ExampleStretchPanelTestPage.xaml +++ b/components/WrapPanel2/tests/ExampleWrapPanel2TestPage.xaml @@ -1,5 +1,5 @@ - - + diff --git a/components/StretchPanel/tests/ExampleStretchPanelTestPage.xaml.cs b/components/WrapPanel2/tests/ExampleWrapPanel2TestPage.xaml.cs similarity index 73% rename from components/StretchPanel/tests/ExampleStretchPanelTestPage.xaml.cs rename to components/WrapPanel2/tests/ExampleWrapPanel2TestPage.xaml.cs index c88957b04..0272a9498 100644 --- a/components/StretchPanel/tests/ExampleStretchPanelTestPage.xaml.cs +++ b/components/WrapPanel2/tests/ExampleWrapPanel2TestPage.xaml.cs @@ -2,14 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace StretchPanelTests; +namespace WrapPanel2Tests; /// /// An empty page that can be used on its own or navigated to within a Frame. /// -public sealed partial class ExampleStretchPanelTestPage : Page +public sealed partial class ExampleWrapPanel2TestPage : Page { - public ExampleStretchPanelTestPage() + public ExampleWrapPanel2TestPage() { this.InitializeComponent(); } diff --git a/components/StretchPanel/tests/StretchPanel.Tests.projitems b/components/WrapPanel2/tests/WrapPanel2.Tests.projitems similarity index 62% rename from components/StretchPanel/tests/StretchPanel.Tests.projitems rename to components/WrapPanel2/tests/WrapPanel2.Tests.projitems index 021e2defd..5dd673b98 100644 --- a/components/StretchPanel/tests/StretchPanel.Tests.projitems +++ b/components/WrapPanel2/tests/WrapPanel2.Tests.projitems @@ -6,16 +6,16 @@ 1EFF9838-CA24-43C3-AA4F-0B321F74861B - StretchPanelTests + WrapPanel2Tests - - - ExampleStretchPanelTestPage.xaml + + + ExampleWrapPanel2TestPage.xaml - + Designer MSBuild:Compile diff --git a/components/StretchPanel/tests/StretchPanel.Tests.shproj b/components/WrapPanel2/tests/WrapPanel2.Tests.shproj similarity index 93% rename from components/StretchPanel/tests/StretchPanel.Tests.shproj rename to components/WrapPanel2/tests/WrapPanel2.Tests.shproj index 1b1e2d5fb..8db9f9ff5 100644 --- a/components/StretchPanel/tests/StretchPanel.Tests.shproj +++ b/components/WrapPanel2/tests/WrapPanel2.Tests.shproj @@ -8,6 +8,6 @@ - + From f02d885c70df130fdd99b6cf513647160b6eb8b4 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Wed, 3 Dec 2025 13:35:37 +0200 Subject: [PATCH 13/14] Ran XAML styles --- .../samples/WrapPanel2BasicSample.xaml | 83 +++++++++++-------- 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml b/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml index 19e06156c..0a87c7079 100644 --- a/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml +++ b/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml @@ -1,4 +1,4 @@ - + - - - - + + + + - - + + - - + + - - + + - - + + - - + + - + - + - + - + - + - + From 7716c088aad0903c13572c76f4d670f914b926dc Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Wed, 3 Dec 2025 13:45:34 +0200 Subject: [PATCH 14/14] Replaced Horizontal/Vertical Spacing with Item/Line spacing --- .../samples/WrapPanel2BasicSample.xaml | 6 ++--- .../samples/WrapPanel2BasicSample.xaml.cs | 4 +-- .../WrapPanel2/src/WrapPanel2.Properties.cs | 26 +++++++++---------- components/WrapPanel2/src/WrapPanel2.cs | 24 ++++++++--------- 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml b/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml index 0a87c7079..2718d035d 100644 --- a/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml +++ b/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml @@ -26,10 +26,10 @@ VerticalAlignment="{x:Bind LayoutVerticalAlignment, Mode=OneWay}" FixedRowLengths="{x:Bind FixedRowLengths, Mode=OneWay}" ForcedStretchMethod="{x:Bind local:WrapPanel2BasicSample.ConvertStringToForcedStretchMethod(LayoutForcedStretchMethod), Mode=OneWay}" - HorizontalSpacing="{x:Bind HorizontalSpacing, Mode=OneWay}" + ItemSpacing="{x:Bind ItemSpacing, Mode=OneWay}" + LineSpacing="{x:Bind LineSpacing, Mode=OneWay}" Orientation="{x:Bind local:WrapPanel2BasicSample.ConvertStringToOrientation(LayoutOrientation), Mode=OneWay}" - OverflowBehavior="{x:Bind local:WrapPanel2BasicSample.ConvertStringToOverflowBehavior(LayoutOverflowBehavior), Mode=OneWay}" - VerticalSpacing="{x:Bind VerticalSpacing, Mode=OneWay}"> + OverflowBehavior="{x:Bind local:WrapPanel2BasicSample.ConvertStringToOverflowBehavior(LayoutOverflowBehavior), Mode=OneWay}"> diff --git a/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml.cs b/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml.cs index 4c8f21472..e1196aee5 100644 --- a/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml.cs +++ b/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml.cs @@ -12,8 +12,8 @@ namespace WrapPanel2Experiment.Samples; [ToolkitSampleMultiChoiceOption("LayoutOrientation", "Horizontal", "Vertical", Title = "Orientation")] [ToolkitSampleMultiChoiceOption("LayoutHorizontalAlignment", "Left", "Center", "Right", "Stretch", Title = "Horizontal Alignment")] [ToolkitSampleMultiChoiceOption("LayoutVerticalAlignment", "Top", "Center", "Bottom", "Stretch", Title = "Vertical Alignment")] -[ToolkitSampleNumericOption("HorizontalSpacing", 8, 0, 16, Title = "Horizontal Spacing")] -[ToolkitSampleNumericOption("VerticalSpacing", 2, 0, 16, Title = "Vertical Spacing")] +[ToolkitSampleNumericOption("ItemSpacing", 8, 0, 16, Title = "Item Spacing")] +[ToolkitSampleNumericOption("LineSpacing", 2, 0, 16, Title = "Line Spacing")] [ToolkitSampleBoolOption("FixedRowLengths", false, Title = "Fixed Row Lengths")] [ToolkitSampleMultiChoiceOption("LayoutForcedStretchMethod", "None", "First", "Last", "Equal", "Proportional", Title = "Forced Stretch Method")] [ToolkitSampleMultiChoiceOption("LayoutOverflowBehavior", "Wrap", "Drop", Title = "Overflow Behavior")] diff --git a/components/WrapPanel2/src/WrapPanel2.Properties.cs b/components/WrapPanel2/src/WrapPanel2.Properties.cs index 7a59d8914..ed28b429f 100644 --- a/components/WrapPanel2/src/WrapPanel2.Properties.cs +++ b/components/WrapPanel2/src/WrapPanel2.Properties.cs @@ -26,19 +26,19 @@ public partial class WrapPanel2 new PropertyMetadata(default(Orientation), OnPropertyChanged)); /// - /// Backing for the property. + /// Backing for the property. /// - public static readonly DependencyProperty HorizontalSpacingProperty = DependencyProperty.Register( - nameof(HorizontalSpacing), + public static readonly DependencyProperty ItemSpacingProperty = DependencyProperty.Register( + nameof(ItemSpacing), typeof(double), typeof(WrapPanel2), new PropertyMetadata(default(double), OnPropertyChanged)); /// - /// Backing for the property. + /// Backing for the property. /// - public static readonly DependencyProperty VerticalSpacingProperty = DependencyProperty.Register( - nameof(VerticalSpacing), + public static readonly DependencyProperty lineSpacingProperty = DependencyProperty.Register( + nameof(LineSpacing), typeof(double), typeof(WrapPanel2), new PropertyMetadata(default(double), OnPropertyChanged)); @@ -80,21 +80,21 @@ public Orientation Orientation } /// - /// Gets or sets the horizontal spacing between items. + /// Gets or sets the spacing between items. /// - public double HorizontalSpacing + public double ItemSpacing { - get => (double)GetValue(HorizontalSpacingProperty); - set => SetValue(HorizontalSpacingProperty, value); + get => (double)GetValue(ItemSpacingProperty); + set => SetValue(ItemSpacingProperty, value); } /// /// Gets or sets the vertical spacing between items. /// - public double VerticalSpacing + public double LineSpacing { - get => (double)GetValue(VerticalSpacingProperty); - set => SetValue(VerticalSpacingProperty, value); + get => (double)GetValue(lineSpacingProperty); + set => SetValue(lineSpacingProperty, value); } /// diff --git a/components/WrapPanel2/src/WrapPanel2.cs b/components/WrapPanel2/src/WrapPanel2.cs index 48d9f3758..ba2bf1a50 100644 --- a/components/WrapPanel2/src/WrapPanel2.cs +++ b/components/WrapPanel2/src/WrapPanel2.cs @@ -20,7 +20,6 @@ protected override Size MeasureOverride(Size availableSize) // Define XY/UV coordinate variables var uvAvailableSize = new UVCoord(availableSize.Width, availableSize.Height, Orientation); - var uvSpacing = new UVCoord(HorizontalSpacing, VerticalSpacing, Orientation); RowSpec currentRowSpec = default; @@ -41,7 +40,7 @@ protected override Size MeasureOverride(Size availableSize) // Attempt to add the child to the current row/column var spec = new RowSpec(layoutLength, uvDesiredSize); - if (!currentRowSpec.TryAdd(spec, uvSpacing.U, uvAvailableSize.U)) + if (!currentRowSpec.TryAdd(spec, ItemSpacing, uvAvailableSize.U)) { // If the overflow behavior is drop, just end the row here. if (OverflowBehavior is OverflowBehavior.Drop) @@ -50,20 +49,20 @@ protected override Size MeasureOverride(Size availableSize) // Could not add to current row/column // Start a new row/column _rowSpecs.Add(currentRowSpec); - _longestRowSize = Math.Max(_longestRowSize, currentRowSpec.Measure(uvSpacing.U)); + _longestRowSize = Math.Max(_longestRowSize, currentRowSpec.Measure(ItemSpacing)); currentRowSpec = spec; } } // Add the final row/column _rowSpecs.Add(currentRowSpec); - _longestRowSize = Math.Max(_longestRowSize, currentRowSpec.Measure(uvSpacing.U)); + _longestRowSize = Math.Max(_longestRowSize, currentRowSpec.Measure(ItemSpacing)); // Calculate final desired size var uvSize = new UVCoord(0, 0, Orientation) { U = IsMainAxisStretch(uvAvailableSize.U) ? uvAvailableSize.U : _longestRowSize, - V = _rowSpecs.Sum(static rs => rs.MaxOffAxisSize) + (uvSpacing.V * (_rowSpecs.Count - 1)) + V = _rowSpecs.Sum(static rs => rs.MaxOffAxisSize) + (LineSpacing * (_rowSpecs.Count - 1)) }; // Clamp to available size and return @@ -82,10 +81,9 @@ protected override Size ArrangeOverride(Size finalSize) // Create XY/UV coordinate variables var pos = new UVCoord(0, 0, Orientation); var uvFinalSize = new UVCoord(finalSize, Orientation); - var uvSpacing = new UVCoord(HorizontalSpacing, VerticalSpacing, Orientation); // Adjust the starting position based on off-axis alignment - var contentHeight = _rowSpecs.Sum(static rs => rs.MaxOffAxisSize) + (uvSpacing.V * (_rowSpecs.Count - 1)); + var contentHeight = _rowSpecs.Sum(static rs => rs.MaxOffAxisSize) + (LineSpacing * (_rowSpecs.Count - 1)); pos.V = GetStartByAlignment(GetOffAlignment(), contentHeight, uvFinalSize.V); var childQueue = new Queue(Children.Where(static e => e.Visibility is Visibility.Visible)); @@ -93,7 +91,7 @@ protected override Size ArrangeOverride(Size finalSize) foreach (var row in _rowSpecs) { // Arrange the row/column - ArrangeRow(ref pos, row, uvFinalSize, uvSpacing, childQueue); + ArrangeRow(ref pos, row, uvFinalSize, childQueue); } // "Arrange" remaning children by rendering them with zero size @@ -106,9 +104,9 @@ protected override Size ArrangeOverride(Size finalSize) return finalSize; } - private void ArrangeRow(ref UVCoord pos, RowSpec row, UVCoord uvFinalSize, UVCoord uvSpacing, Queue childQueue) + private void ArrangeRow(ref UVCoord pos, RowSpec row, UVCoord uvFinalSize, Queue childQueue) { - var spacingTotalSize = uvSpacing.U * (row.ItemsCount - 1); + var spacingTotalSize = ItemSpacing * (row.ItemsCount - 1); var remainingSpace = uvFinalSize.U - row.ReservedSpace - spacingTotalSize; var portionSize = row.MinPortionSize; @@ -130,7 +128,7 @@ private void ArrangeRow(ref UVCoord pos, RowSpec row, UVCoord uvFinalSize, UVCoo // Also do this if there are no star-sized items in the row/column and no forced streching is in use. if (!stretch || (row.PortionsSum is 0 && ForcedStretchMethod is ForcedStretchMethod.None)) { - var rowSize = row.Measure(uvSpacing.U); + var rowSize = row.Measure(ItemSpacing); pos.U = GetStartByAlignment(GetAlignment(), rowSize, uvFinalSize.U); } @@ -184,11 +182,11 @@ private void ArrangeRow(ref UVCoord pos, RowSpec row, UVCoord uvFinalSize, UVCoo child.Arrange(new Rect(pos.X, pos.Y, size.X, size.Y)); // Advance the position - pos.U += size.U + uvSpacing.U; + pos.U += size.U + ItemSpacing; } // Advance to the next row/column - pos.V += row.MaxOffAxisSize + uvSpacing.V; + pos.V += row.MaxOffAxisSize + LineSpacing; } private UVCoord GetChildSize(UIElement child, int indexInRow, RowSpec row, double portionSize, bool forceStretch)