From e9927d5c2c696fb7fa04bd3233c67ff2a327ee99 Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Thu, 8 Jan 2026 17:24:35 +0200 Subject: [PATCH 01/26] Create GradientSlider component --- components/GradientSlider/OpenSolution.bat | 3 + .../GradientSlider/samples/Assets/icon.png | Bin 0 -> 2216 bytes .../GradientSlider/samples/Dependencies.props | 31 ++++ .../samples/GradientSlider.Samples.csproj | 10 ++ .../GradientSlider/samples/GradientSlider.md | 65 +++++++++ .../samples/GradientSliderCustomSample.xaml | 25 ++++ .../GradientSliderCustomSample.xaml.cs | 30 ++++ .../GradientSliderTemplatedSample.xaml | 16 +++ .../GradientSliderTemplatedSample.xaml.cs | 21 +++ ...dientSliderTemplatedStyleCustomSample.xaml | 26 ++++ ...ntSliderTemplatedStyleCustomSample.xaml.cs | 21 +++ .../GradientSliderXbindBackedSample.xaml | 16 +++ .../GradientSliderXbindBackedSample.xaml.cs | 21 +++ ...entSliderXbindBackedStyleCustomSample.xaml | 26 ++++ ...SliderXbindBackedStyleCustomSample.xaml.cs | 21 +++ ...olkit.WinUI.Controls.GradientSlider.csproj | 14 ++ .../GradientSlider/src/Dependencies.props | 31 ++++ .../GradientSlider/src/GradientSlider.cs | 108 ++++++++++++++ .../GradientSliderStyle_ClassicBinding.xaml | 62 ++++++++ .../src/GradientSliderStyle_xBind.xaml | 69 +++++++++ .../src/GradientSliderStyle_xBind.xaml.cs | 20 +++ .../src/GradientSlider_ClassicBinding.cs | 94 ++++++++++++ .../src/GradientSlider_xBind.cs | 71 ++++++++++ .../GradientSlider/src/MultiTarget.props | 9 ++ .../GradientSlider/src/Themes/Generic.xaml | 10 ++ .../tests/ExampleGradientSliderTestClass.cs | 134 ++++++++++++++++++ .../tests/ExampleGradientSliderTestPage.xaml | 14 ++ .../ExampleGradientSliderTestPage.xaml.cs | 16 +++ .../tests/GradientSlider.Tests.projitems | 23 +++ .../tests/GradientSlider.Tests.shproj | 13 ++ 30 files changed, 1020 insertions(+) create mode 100644 components/GradientSlider/OpenSolution.bat create mode 100644 components/GradientSlider/samples/Assets/icon.png create mode 100644 components/GradientSlider/samples/Dependencies.props create mode 100644 components/GradientSlider/samples/GradientSlider.Samples.csproj create mode 100644 components/GradientSlider/samples/GradientSlider.md create mode 100644 components/GradientSlider/samples/GradientSliderCustomSample.xaml create mode 100644 components/GradientSlider/samples/GradientSliderCustomSample.xaml.cs create mode 100644 components/GradientSlider/samples/GradientSliderTemplatedSample.xaml create mode 100644 components/GradientSlider/samples/GradientSliderTemplatedSample.xaml.cs create mode 100644 components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml create mode 100644 components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml.cs create mode 100644 components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml create mode 100644 components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml.cs create mode 100644 components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml create mode 100644 components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml.cs create mode 100644 components/GradientSlider/src/CommunityToolkit.WinUI.Controls.GradientSlider.csproj create mode 100644 components/GradientSlider/src/Dependencies.props create mode 100644 components/GradientSlider/src/GradientSlider.cs create mode 100644 components/GradientSlider/src/GradientSliderStyle_ClassicBinding.xaml create mode 100644 components/GradientSlider/src/GradientSliderStyle_xBind.xaml create mode 100644 components/GradientSlider/src/GradientSliderStyle_xBind.xaml.cs create mode 100644 components/GradientSlider/src/GradientSlider_ClassicBinding.cs create mode 100644 components/GradientSlider/src/GradientSlider_xBind.cs create mode 100644 components/GradientSlider/src/MultiTarget.props create mode 100644 components/GradientSlider/src/Themes/Generic.xaml create mode 100644 components/GradientSlider/tests/ExampleGradientSliderTestClass.cs create mode 100644 components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml create mode 100644 components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml.cs create mode 100644 components/GradientSlider/tests/GradientSlider.Tests.projitems create mode 100644 components/GradientSlider/tests/GradientSlider.Tests.shproj diff --git a/components/GradientSlider/OpenSolution.bat b/components/GradientSlider/OpenSolution.bat new file mode 100644 index 000000000..814a56d4b --- /dev/null +++ b/components/GradientSlider/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/GradientSlider/samples/Assets/icon.png b/components/GradientSlider/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/GradientSlider/samples/GradientSlider.Samples.csproj b/components/GradientSlider/samples/GradientSlider.Samples.csproj new file mode 100644 index 000000000..320e3ea06 --- /dev/null +++ b/components/GradientSlider/samples/GradientSlider.Samples.csproj @@ -0,0 +1,10 @@ + + + + + GradientSlider + + + + + diff --git a/components/GradientSlider/samples/GradientSlider.md b/components/GradientSlider/samples/GradientSlider.md new file mode 100644 index 000000000..a25067ddf --- /dev/null +++ b/components/GradientSlider/samples/GradientSlider.md @@ -0,0 +1,65 @@ +--- +title: GradientSlider +author: githubaccount +description: TODO: Your experiment's description here +keywords: GradientSlider, Control, Layout +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 0 +issue-id: 0 +icon: assets/icon.png +--- + + + + + + + + + +# GradientSlider + +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 GradientSliderCustomSample] + +## Templated Controls + +The Toolkit is built with templated controls. This provides developers a flexible way to restyle components +easily while still inheriting the general functionality a control provides. The examples below show +how a component can use a default style and then get overridden by the end developer. + +TODO: Two types of templated control building methods are shown. Delete these if you're building a custom component. +Otherwise, pick one method for your component and delete the files related to the unchosen `_ClassicBinding` or `_xBind` +classes (and the custom non-suffixed one as well). Then, rename your component to just be your component name. + +The `_ClassicBinding` class shows the traditional method used to develop components with best practices. + +### Implict style + +> [!SAMPLE GradientSliderTemplatedSample] + +### Custom style + +> [!SAMPLE GradientSliderTemplatedStyleCustomSample] + +## Templated Controls with x:Bind + +This is an _experimental_ new way to define components which allows for the use of x:Bind within the style. + +### Implict style + +> [!SAMPLE GradientSliderXbindBackedSample] + +### Custom style + +> [!SAMPLE GradientSliderXbindBackedStyleCustomSample] + diff --git a/components/GradientSlider/samples/GradientSliderCustomSample.xaml b/components/GradientSlider/samples/GradientSliderCustomSample.xaml new file mode 100644 index 000000000..a650e2449 --- /dev/null +++ b/components/GradientSlider/samples/GradientSliderCustomSample.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/components/GradientSlider/samples/GradientSliderCustomSample.xaml.cs b/components/GradientSlider/samples/GradientSliderCustomSample.xaml.cs new file mode 100644 index 000000000..3b8c47cdc --- /dev/null +++ b/components/GradientSlider/samples/GradientSliderCustomSample.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 GradientSliderExperiment.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(GradientSliderCustomSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(GradientSlider)} custom control.")] +public sealed partial class GradientSliderCustomSample : Page +{ + public GradientSliderCustomSample() + { + 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/GradientSlider/samples/GradientSliderTemplatedSample.xaml b/components/GradientSlider/samples/GradientSliderTemplatedSample.xaml new file mode 100644 index 000000000..1d787a284 --- /dev/null +++ b/components/GradientSlider/samples/GradientSliderTemplatedSample.xaml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/components/GradientSlider/samples/GradientSliderTemplatedSample.xaml.cs b/components/GradientSlider/samples/GradientSliderTemplatedSample.xaml.cs new file mode 100644 index 000000000..1b32aa358 --- /dev/null +++ b/components/GradientSlider/samples/GradientSliderTemplatedSample.xaml.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 GradientSliderExperiment.Samples; + +[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] +// Single values without a colon are used for both label and value. +// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). +[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, false, Title = "FontSize")] +[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] +[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] + +[ToolkitSample(id: nameof(GradientSliderTemplatedSample), "Templated control", description: "A sample for showing how to create and use a templated control.")] +public sealed partial class GradientSliderTemplatedSample : Page +{ + public GradientSliderTemplatedSample() + { + this.InitializeComponent(); + } +} diff --git a/components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml b/components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml new file mode 100644 index 000000000..0a38333ad --- /dev/null +++ b/components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml.cs b/components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml.cs new file mode 100644 index 000000000..677d61c46 --- /dev/null +++ b/components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml.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 GradientSliderExperiment.Samples; + +[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] +// Single values without a colon are used for both label and value. +// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). +[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, true, Title = "FontSize")] +[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] +[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] + +[ToolkitSample(id: nameof(GradientSliderTemplatedStyleCustomSample), "Templated control (restyled)", description: "A sample for showing how to create a use and templated control with a custom style.")] +public sealed partial class GradientSliderTemplatedStyleCustomSample : Page +{ + public GradientSliderTemplatedStyleCustomSample() + { + this.InitializeComponent(); + } +} diff --git a/components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml b/components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml new file mode 100644 index 000000000..80e35e3a2 --- /dev/null +++ b/components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml.cs b/components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml.cs new file mode 100644 index 000000000..64855833d --- /dev/null +++ b/components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml.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 GradientSliderExperiment.Samples; + +[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] +// Single values without a colon are used for both label and value. +// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). +[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, false, Title = "FontSize")] +[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] +[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] + +[ToolkitSample(id: nameof(GradientSliderXbindBackedSample), "Backed templated control", description: "A sample for showing how to create and use a templated control with a backed resource dictionary.")] +public sealed partial class GradientSliderXbindBackedSample : Page +{ + public GradientSliderXbindBackedSample() + { + this.InitializeComponent(); + } +} diff --git a/components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml b/components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml new file mode 100644 index 000000000..71663d01e --- /dev/null +++ b/components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml.cs b/components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml.cs new file mode 100644 index 000000000..9fd3d6269 --- /dev/null +++ b/components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml.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 GradientSliderExperiment.Samples; + +[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] +// Single values without a colon are used for both label and value. +// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). +[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, true, Title = "FontSize")] +[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] +[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] + +[ToolkitSample(id: nameof(GradientSliderXbindBackedStyleCustomSample), "Backed templated control (restyled)", description: "A sample for showing how to create and use a templated control with a backed resource dictionary and a custom style.")] +public sealed partial class GradientSliderXbindBackedStyleCustomSample : Page +{ + public GradientSliderXbindBackedStyleCustomSample() + { + this.InitializeComponent(); + } +} diff --git a/components/GradientSlider/src/CommunityToolkit.WinUI.Controls.GradientSlider.csproj b/components/GradientSlider/src/CommunityToolkit.WinUI.Controls.GradientSlider.csproj new file mode 100644 index 000000000..80ce29607 --- /dev/null +++ b/components/GradientSlider/src/CommunityToolkit.WinUI.Controls.GradientSlider.csproj @@ -0,0 +1,14 @@ + + + + + GradientSlider + This package contains GradientSlider. + + + CommunityToolkit.WinUI.Controls.GradientSliderRns + + + + + diff --git a/components/GradientSlider/src/Dependencies.props b/components/GradientSlider/src/Dependencies.props new file mode 100644 index 000000000..e622e1df4 --- /dev/null +++ b/components/GradientSlider/src/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/GradientSlider/src/GradientSlider.cs b/components/GradientSlider/src/GradientSlider.cs new file mode 100644 index 000000000..a87538479 --- /dev/null +++ b/components/GradientSlider/src/GradientSlider.cs @@ -0,0 +1,108 @@ +// 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; + +/// +/// This is an example control based off of the BoxPanel sample here: https://docs.microsoft.com/windows/apps/design/layout/boxpanel-example-custom-panel. If you need this similar sort of layout component for an application, see UniformGrid in the Toolkit. +/// It is provided as an example of how to inherit from another control like . +/// You can choose to start here or from the or example components. Remove unused components and rename as appropriate. +/// +public partial class GradientSlider : Panel +{ + /// + /// Identifies the property. + /// + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(GradientSlider), 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 GradientSlider 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/GradientSlider/src/GradientSliderStyle_ClassicBinding.xaml b/components/GradientSlider/src/GradientSliderStyle_ClassicBinding.xaml new file mode 100644 index 000000000..649fcd249 --- /dev/null +++ b/components/GradientSlider/src/GradientSliderStyle_ClassicBinding.xaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + 4,4,4,4 + + + + + + + + diff --git a/components/GradientSlider/src/GradientSliderStyle_xBind.xaml b/components/GradientSlider/src/GradientSliderStyle_xBind.xaml new file mode 100644 index 000000000..24103b7a4 --- /dev/null +++ b/components/GradientSlider/src/GradientSliderStyle_xBind.xaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + 4,4,4,4 + + + + + + + + diff --git a/components/GradientSlider/src/GradientSliderStyle_xBind.xaml.cs b/components/GradientSlider/src/GradientSliderStyle_xBind.xaml.cs new file mode 100644 index 000000000..32c62d8a6 --- /dev/null +++ b/components/GradientSlider/src/GradientSliderStyle_xBind.xaml.cs @@ -0,0 +1,20 @@ +// 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; + +/// +/// Backing code for this resource dictionary. +/// +public sealed partial class GradientSliderStyle_xBind : ResourceDictionary +{ + // NOTICE + // This file only exists to enable x:Bind in the resource dictionary. + // Do not add code here. + // Instead, add code-behind to your templated control. + public GradientSliderStyle_xBind() + { + this.InitializeComponent(); + } +} diff --git a/components/GradientSlider/src/GradientSlider_ClassicBinding.cs b/components/GradientSlider/src/GradientSlider_ClassicBinding.cs new file mode 100644 index 000000000..2872f433d --- /dev/null +++ b/components/GradientSlider/src/GradientSlider_ClassicBinding.cs @@ -0,0 +1,94 @@ +// 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; + +/// +/// An example templated control. +/// +[TemplatePart(Name = nameof(PART_HelloWorld), Type = typeof(TextBlock))] +public partial class GradientSlider_ClassicBinding : Control +{ + /// + /// Creates a new instance of the class. + /// + public GradientSlider_ClassicBinding() + { + this.DefaultStyleKey = typeof(GradientSlider_ClassicBinding); + } + + /// + /// The primary text block that displays "Hello world". + /// + protected TextBlock? PART_HelloWorld { get; private set; } + + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + // Detach all attached events when a new template is applied. + if (PART_HelloWorld is not null) + { + PART_HelloWorld.PointerEntered -= Element_PointerEntered; + } + + // Attach events when the template is applied and the control is loaded. + PART_HelloWorld = GetTemplateChild(nameof(PART_HelloWorld)) as TextBlock; + + if (PART_HelloWorld is not null) + { + PART_HelloWorld.PointerEntered += Element_PointerEntered; + } + } + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty ItemPaddingProperty = DependencyProperty.Register( + nameof(ItemPadding), + typeof(Thickness), + typeof(GradientSlider_ClassicBinding), + new PropertyMetadata(defaultValue: new Thickness(0))); + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( + nameof(MyProperty), + typeof(string), + typeof(GradientSlider_ClassicBinding), + new PropertyMetadata(defaultValue: string.Empty, (d, e) => ((GradientSlider_ClassicBinding)d).OnMyPropertyChanged((string)e.OldValue, (string)e.NewValue))); + + /// + /// Gets or sets an example string. A basic DependencyProperty example. + /// + public string MyProperty + { + get => (string)GetValue(MyPropertyProperty); + set => SetValue(MyPropertyProperty, value); + } + + /// + /// Gets or sets a padding for an item. A basic DependencyProperty example. + /// + public Thickness ItemPadding + { + get => (Thickness)GetValue(ItemPaddingProperty); + set => SetValue(ItemPaddingProperty, value); + } + + protected virtual void OnMyPropertyChanged(string oldValue, string newValue) + { + // Do something with the changed value. + } + + public void Element_PointerEntered(object sender, PointerRoutedEventArgs e) + { + if (sender is TextBlock text) + { + text.Opacity = 1; + } + } +} diff --git a/components/GradientSlider/src/GradientSlider_xBind.cs b/components/GradientSlider/src/GradientSlider_xBind.cs new file mode 100644 index 000000000..f87c6c90c --- /dev/null +++ b/components/GradientSlider/src/GradientSlider_xBind.cs @@ -0,0 +1,71 @@ +// 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; + +/// +/// An example templated control. +/// +public partial class GradientSlider_xBind: Control +{ + /// + /// Creates a new instance of the class. + /// + public GradientSlider_xBind() + { + this.DefaultStyleKey = typeof(GradientSlider_xBind); + + // Allows directly using this control as the x:DataType in the template. + this.DataContext = this; + } + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty ItemPaddingProperty = DependencyProperty.Register( + nameof(ItemPadding), + typeof(Thickness), + typeof(GradientSlider_xBind), + new PropertyMetadata(defaultValue: new Thickness(0))); + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( + nameof(MyProperty), + typeof(string), + typeof(GradientSlider_xBind), + new PropertyMetadata(defaultValue: string.Empty, (d, e) => ((GradientSlider_xBind)d).OnMyPropertyChanged((string)e.OldValue, (string)e.NewValue))); + + /// + /// Gets or sets an example string. A basic DependencyProperty example. + /// + public string MyProperty + { + get => (string)GetValue(MyPropertyProperty); + set => SetValue(MyPropertyProperty, value); + } + + /// + /// Gets or sets a padding for an item. A basic DependencyProperty example. + /// + public Thickness ItemPadding + { + get => (Thickness)GetValue(ItemPaddingProperty); + set => SetValue(ItemPaddingProperty, value); + } + + protected virtual void OnMyPropertyChanged(string oldValue, string newValue) + { + // Do something with the changed value. + } + + public void Element_PointerEntered(object sender, PointerRoutedEventArgs e) + { + if (sender is TextBlock text) + { + text.Opacity = 1; + } + } +} diff --git a/components/GradientSlider/src/MultiTarget.props b/components/GradientSlider/src/MultiTarget.props new file mode 100644 index 000000000..b11c19426 --- /dev/null +++ b/components/GradientSlider/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/GradientSlider/src/Themes/Generic.xaml b/components/GradientSlider/src/Themes/Generic.xaml new file mode 100644 index 000000000..1091389dd --- /dev/null +++ b/components/GradientSlider/src/Themes/Generic.xaml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/components/GradientSlider/tests/ExampleGradientSliderTestClass.cs b/components/GradientSlider/tests/ExampleGradientSliderTestClass.cs new file mode 100644 index 000000000..9d8253d7b --- /dev/null +++ b/components/GradientSlider/tests/ExampleGradientSliderTestClass.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 GradientSliderTests; + +[TestClass] +public partial class ExampleGradientSliderTestClass : VisualUITestBase +{ + // If you don't need access to UI objects directly or async code, use this pattern. + [TestMethod] + public void SimpleSynchronousExampleTest() + { + var assembly = typeof(GradientSlider).Assembly; + var type = assembly.GetType(typeof(GradientSlider).FullName ?? string.Empty); + + Assert.IsNotNull(type, "Could not find GradientSlider type."); + Assert.AreEqual(typeof(GradientSlider), type, "Type of GradientSlider 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 GradientSlider(); + 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(ExampleGradientSliderTestPage 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("GradientSliderControl"); + + Assert.IsNotNull(componentByName); + } + + // You can still do async work with a UIThreadTestMethod as well. + [UIThreadTestMethod] + public async Task SimpleAsyncUIExamplePageTest(ExampleGradientSliderTestPage 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 GradientSlider_ClassicBinding(); + 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 GradientSlider_ClassicBinding(); + 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 GradientSlider_ClassicBinding(); + 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/GradientSlider/tests/ExampleGradientSliderTestPage.xaml b/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml new file mode 100644 index 000000000..7556618bb --- /dev/null +++ b/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml.cs b/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml.cs new file mode 100644 index 000000000..0fe89ed24 --- /dev/null +++ b/components/GradientSlider/tests/ExampleGradientSliderTestPage.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 GradientSliderTests; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ExampleGradientSliderTestPage : Page +{ + public ExampleGradientSliderTestPage() + { + this.InitializeComponent(); + } +} diff --git a/components/GradientSlider/tests/GradientSlider.Tests.projitems b/components/GradientSlider/tests/GradientSlider.Tests.projitems new file mode 100644 index 000000000..57a9b8fb5 --- /dev/null +++ b/components/GradientSlider/tests/GradientSlider.Tests.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 2F1B2F6D-D1F4-4895-96CE-F8CB3B3AF87F + + + GradientSliderTests + + + + + ExampleGradientSliderTestPage.xaml + + + + + Designer + MSBuild:Compile + + + \ No newline at end of file diff --git a/components/GradientSlider/tests/GradientSlider.Tests.shproj b/components/GradientSlider/tests/GradientSlider.Tests.shproj new file mode 100644 index 000000000..027ce84ad --- /dev/null +++ b/components/GradientSlider/tests/GradientSlider.Tests.shproj @@ -0,0 +1,13 @@ + + + + 2F1B2F6D-D1F4-4895-96CE-F8CB3B3AF87F + 14.0 + + + + + + + + From 2c608e4b1adb18d9ddaa6359006708407e1d88e8 Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Thu, 8 Jan 2026 18:20:19 +0200 Subject: [PATCH 02/26] Cut template code to basics --- .../samples/GradientSlider.Samples.csproj | 5 + .../GradientSlider/samples/GradientSlider.md | 42 +------- .../samples/GradientSliderCustomSample.xaml | 25 ----- .../samples/GradientSliderSample.xaml | 14 +++ ...e.xaml.cs => GradientSliderSample.xaml.cs} | 8 +- .../GradientSliderTemplatedSample.xaml | 16 --- .../GradientSliderTemplatedSample.xaml.cs | 21 ---- ...dientSliderTemplatedStyleCustomSample.xaml | 26 ----- ...ntSliderTemplatedStyleCustomSample.xaml.cs | 21 ---- .../GradientSliderXbindBackedSample.xaml | 16 --- .../GradientSliderXbindBackedSample.xaml.cs | 21 ---- ...entSliderXbindBackedStyleCustomSample.xaml | 26 ----- ...SliderXbindBackedStyleCustomSample.xaml.cs | 21 ---- .../GradientSlider/src/GradientSlider.cs | 99 ++----------------- .../src/GradientSliderStyle.xaml | 24 +++++ .../GradientSliderStyle_ClassicBinding.xaml | 62 ------------ .../src/GradientSliderStyle_xBind.xaml | 69 ------------- .../src/GradientSliderStyle_xBind.xaml.cs | 20 ---- .../src/GradientSlider_ClassicBinding.cs | 94 ------------------ .../src/GradientSlider_xBind.cs | 71 ------------- .../GradientSlider/src/Themes/Generic.xaml | 5 +- .../tests/ExampleGradientSliderTestClass.cs | 10 +- .../tests/ExampleGradientSliderTestPage.xaml | 4 +- 23 files changed, 64 insertions(+), 656 deletions(-) delete mode 100644 components/GradientSlider/samples/GradientSliderCustomSample.xaml create mode 100644 components/GradientSlider/samples/GradientSliderSample.xaml rename components/GradientSlider/samples/{GradientSliderCustomSample.xaml.cs => GradientSliderSample.xaml.cs} (62%) delete mode 100644 components/GradientSlider/samples/GradientSliderTemplatedSample.xaml delete mode 100644 components/GradientSlider/samples/GradientSliderTemplatedSample.xaml.cs delete mode 100644 components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml delete mode 100644 components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml.cs delete mode 100644 components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml delete mode 100644 components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml.cs delete mode 100644 components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml delete mode 100644 components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml.cs create mode 100644 components/GradientSlider/src/GradientSliderStyle.xaml delete mode 100644 components/GradientSlider/src/GradientSliderStyle_ClassicBinding.xaml delete mode 100644 components/GradientSlider/src/GradientSliderStyle_xBind.xaml delete mode 100644 components/GradientSlider/src/GradientSliderStyle_xBind.xaml.cs delete mode 100644 components/GradientSlider/src/GradientSlider_ClassicBinding.cs delete mode 100644 components/GradientSlider/src/GradientSlider_xBind.cs diff --git a/components/GradientSlider/samples/GradientSlider.Samples.csproj b/components/GradientSlider/samples/GradientSlider.Samples.csproj index 320e3ea06..676ed9987 100644 --- a/components/GradientSlider/samples/GradientSlider.Samples.csproj +++ b/components/GradientSlider/samples/GradientSlider.Samples.csproj @@ -7,4 +7,9 @@ + + + GradientSliderSample.xaml + + diff --git a/components/GradientSlider/samples/GradientSlider.md b/components/GradientSlider/samples/GradientSlider.md index a25067ddf..197b06c43 100644 --- a/components/GradientSlider/samples/GradientSlider.md +++ b/components/GradientSlider/samples/GradientSlider.md @@ -22,44 +22,4 @@ icon: assets/icon.png # GradientSlider -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 GradientSliderCustomSample] - -## Templated Controls - -The Toolkit is built with templated controls. This provides developers a flexible way to restyle components -easily while still inheriting the general functionality a control provides. The examples below show -how a component can use a default style and then get overridden by the end developer. - -TODO: Two types of templated control building methods are shown. Delete these if you're building a custom component. -Otherwise, pick one method for your component and delete the files related to the unchosen `_ClassicBinding` or `_xBind` -classes (and the custom non-suffixed one as well). Then, rename your component to just be your component name. - -The `_ClassicBinding` class shows the traditional method used to develop components with best practices. - -### Implict style - -> [!SAMPLE GradientSliderTemplatedSample] - -### Custom style - -> [!SAMPLE GradientSliderTemplatedStyleCustomSample] - -## Templated Controls with x:Bind - -This is an _experimental_ new way to define components which allows for the use of x:Bind within the style. - -### Implict style - -> [!SAMPLE GradientSliderXbindBackedSample] - -### Custom style - -> [!SAMPLE GradientSliderXbindBackedStyleCustomSample] - +> [!SAMPLE GradientSliderSample] diff --git a/components/GradientSlider/samples/GradientSliderCustomSample.xaml b/components/GradientSlider/samples/GradientSliderCustomSample.xaml deleted file mode 100644 index a650e2449..000000000 --- a/components/GradientSlider/samples/GradientSliderCustomSample.xaml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/components/GradientSlider/samples/GradientSliderSample.xaml b/components/GradientSlider/samples/GradientSliderSample.xaml new file mode 100644 index 000000000..de732b752 --- /dev/null +++ b/components/GradientSlider/samples/GradientSliderSample.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/components/GradientSlider/samples/GradientSliderCustomSample.xaml.cs b/components/GradientSlider/samples/GradientSliderSample.xaml.cs similarity index 62% rename from components/GradientSlider/samples/GradientSliderCustomSample.xaml.cs rename to components/GradientSlider/samples/GradientSliderSample.xaml.cs index 3b8c47cdc..b0a4a644f 100644 --- a/components/GradientSlider/samples/GradientSliderCustomSample.xaml.cs +++ b/components/GradientSlider/samples/GradientSliderSample.xaml.cs @@ -9,13 +9,11 @@ namespace GradientSliderExperiment.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(GradientSliderCustomSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(GradientSlider)} custom control.")] -public sealed partial class GradientSliderCustomSample : Page +[ToolkitSample(id: nameof(GradientSliderSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(GradientSlider)} custom control.")] +public sealed partial class GradientSliderSample : Page { - public GradientSliderCustomSample() + public GradientSliderSample() { this.InitializeComponent(); } diff --git a/components/GradientSlider/samples/GradientSliderTemplatedSample.xaml b/components/GradientSlider/samples/GradientSliderTemplatedSample.xaml deleted file mode 100644 index 1d787a284..000000000 --- a/components/GradientSlider/samples/GradientSliderTemplatedSample.xaml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/components/GradientSlider/samples/GradientSliderTemplatedSample.xaml.cs b/components/GradientSlider/samples/GradientSliderTemplatedSample.xaml.cs deleted file mode 100644 index 1b32aa358..000000000 --- a/components/GradientSlider/samples/GradientSliderTemplatedSample.xaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 GradientSliderExperiment.Samples; - -[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] -// Single values without a colon are used for both label and value. -// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). -[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, false, Title = "FontSize")] -[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] -[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] - -[ToolkitSample(id: nameof(GradientSliderTemplatedSample), "Templated control", description: "A sample for showing how to create and use a templated control.")] -public sealed partial class GradientSliderTemplatedSample : Page -{ - public GradientSliderTemplatedSample() - { - this.InitializeComponent(); - } -} diff --git a/components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml b/components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml deleted file mode 100644 index 0a38333ad..000000000 --- a/components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml.cs b/components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml.cs deleted file mode 100644 index 677d61c46..000000000 --- a/components/GradientSlider/samples/GradientSliderTemplatedStyleCustomSample.xaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 GradientSliderExperiment.Samples; - -[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] -// Single values without a colon are used for both label and value. -// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). -[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, true, Title = "FontSize")] -[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] -[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] - -[ToolkitSample(id: nameof(GradientSliderTemplatedStyleCustomSample), "Templated control (restyled)", description: "A sample for showing how to create a use and templated control with a custom style.")] -public sealed partial class GradientSliderTemplatedStyleCustomSample : Page -{ - public GradientSliderTemplatedStyleCustomSample() - { - this.InitializeComponent(); - } -} diff --git a/components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml b/components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml deleted file mode 100644 index 80e35e3a2..000000000 --- a/components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml.cs b/components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml.cs deleted file mode 100644 index 64855833d..000000000 --- a/components/GradientSlider/samples/GradientSliderXbindBackedSample.xaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 GradientSliderExperiment.Samples; - -[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] -// Single values without a colon are used for both label and value. -// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). -[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, false, Title = "FontSize")] -[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] -[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] - -[ToolkitSample(id: nameof(GradientSliderXbindBackedSample), "Backed templated control", description: "A sample for showing how to create and use a templated control with a backed resource dictionary.")] -public sealed partial class GradientSliderXbindBackedSample : Page -{ - public GradientSliderXbindBackedSample() - { - this.InitializeComponent(); - } -} diff --git a/components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml b/components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml deleted file mode 100644 index 71663d01e..000000000 --- a/components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml.cs b/components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml.cs deleted file mode 100644 index 9fd3d6269..000000000 --- a/components/GradientSlider/samples/GradientSliderXbindBackedStyleCustomSample.xaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 GradientSliderExperiment.Samples; - -[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] -// Single values without a colon are used for both label and value. -// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). -[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, true, Title = "FontSize")] -[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] -[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] - -[ToolkitSample(id: nameof(GradientSliderXbindBackedStyleCustomSample), "Backed templated control (restyled)", description: "A sample for showing how to create and use a templated control with a backed resource dictionary and a custom style.")] -public sealed partial class GradientSliderXbindBackedStyleCustomSample : Page -{ - public GradientSliderXbindBackedStyleCustomSample() - { - this.InitializeComponent(); - } -} diff --git a/components/GradientSlider/src/GradientSlider.cs b/components/GradientSlider/src/GradientSlider.cs index a87538479..b7e11c134 100644 --- a/components/GradientSlider/src/GradientSlider.cs +++ b/components/GradientSlider/src/GradientSlider.cs @@ -5,104 +5,21 @@ namespace CommunityToolkit.WinUI.Controls; /// -/// This is an example control based off of the BoxPanel sample here: https://docs.microsoft.com/windows/apps/design/layout/boxpanel-example-custom-panel. If you need this similar sort of layout component for an application, see UniformGrid in the Toolkit. -/// It is provided as an example of how to inherit from another control like . -/// You can choose to start here or from the or example components. Remove unused components and rename as appropriate. +/// An example templated control. /// -public partial class GradientSlider : Panel +public partial class GradientSlider : Control { /// - /// Identifies the property. + /// Creates a new instance of the class. /// - public static readonly DependencyProperty OrientationProperty = - DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(GradientSlider), 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 GradientSlider 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) + public GradientSlider() { - // 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; + this.DefaultStyleKey = typeof(GradientSlider); } - protected override Size ArrangeOverride(Size finalSize) + /// + protected override void OnApplyTemplate() { - 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; + base.OnApplyTemplate(); } } diff --git a/components/GradientSlider/src/GradientSliderStyle.xaml b/components/GradientSlider/src/GradientSliderStyle.xaml new file mode 100644 index 000000000..43effdea1 --- /dev/null +++ b/components/GradientSlider/src/GradientSliderStyle.xaml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/components/GradientSlider/src/GradientSliderStyle_ClassicBinding.xaml b/components/GradientSlider/src/GradientSliderStyle_ClassicBinding.xaml deleted file mode 100644 index 649fcd249..000000000 --- a/components/GradientSlider/src/GradientSliderStyle_ClassicBinding.xaml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - 4,4,4,4 - - - - - - - - diff --git a/components/GradientSlider/src/GradientSliderStyle_xBind.xaml b/components/GradientSlider/src/GradientSliderStyle_xBind.xaml deleted file mode 100644 index 24103b7a4..000000000 --- a/components/GradientSlider/src/GradientSliderStyle_xBind.xaml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - 4,4,4,4 - - - - - - - - diff --git a/components/GradientSlider/src/GradientSliderStyle_xBind.xaml.cs b/components/GradientSlider/src/GradientSliderStyle_xBind.xaml.cs deleted file mode 100644 index 32c62d8a6..000000000 --- a/components/GradientSlider/src/GradientSliderStyle_xBind.xaml.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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; - -/// -/// Backing code for this resource dictionary. -/// -public sealed partial class GradientSliderStyle_xBind : ResourceDictionary -{ - // NOTICE - // This file only exists to enable x:Bind in the resource dictionary. - // Do not add code here. - // Instead, add code-behind to your templated control. - public GradientSliderStyle_xBind() - { - this.InitializeComponent(); - } -} diff --git a/components/GradientSlider/src/GradientSlider_ClassicBinding.cs b/components/GradientSlider/src/GradientSlider_ClassicBinding.cs deleted file mode 100644 index 2872f433d..000000000 --- a/components/GradientSlider/src/GradientSlider_ClassicBinding.cs +++ /dev/null @@ -1,94 +0,0 @@ -// 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; - -/// -/// An example templated control. -/// -[TemplatePart(Name = nameof(PART_HelloWorld), Type = typeof(TextBlock))] -public partial class GradientSlider_ClassicBinding : Control -{ - /// - /// Creates a new instance of the class. - /// - public GradientSlider_ClassicBinding() - { - this.DefaultStyleKey = typeof(GradientSlider_ClassicBinding); - } - - /// - /// The primary text block that displays "Hello world". - /// - protected TextBlock? PART_HelloWorld { get; private set; } - - /// - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - // Detach all attached events when a new template is applied. - if (PART_HelloWorld is not null) - { - PART_HelloWorld.PointerEntered -= Element_PointerEntered; - } - - // Attach events when the template is applied and the control is loaded. - PART_HelloWorld = GetTemplateChild(nameof(PART_HelloWorld)) as TextBlock; - - if (PART_HelloWorld is not null) - { - PART_HelloWorld.PointerEntered += Element_PointerEntered; - } - } - - /// - /// The backing for the property. - /// - public static readonly DependencyProperty ItemPaddingProperty = DependencyProperty.Register( - nameof(ItemPadding), - typeof(Thickness), - typeof(GradientSlider_ClassicBinding), - new PropertyMetadata(defaultValue: new Thickness(0))); - - /// - /// The backing for the property. - /// - public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( - nameof(MyProperty), - typeof(string), - typeof(GradientSlider_ClassicBinding), - new PropertyMetadata(defaultValue: string.Empty, (d, e) => ((GradientSlider_ClassicBinding)d).OnMyPropertyChanged((string)e.OldValue, (string)e.NewValue))); - - /// - /// Gets or sets an example string. A basic DependencyProperty example. - /// - public string MyProperty - { - get => (string)GetValue(MyPropertyProperty); - set => SetValue(MyPropertyProperty, value); - } - - /// - /// Gets or sets a padding for an item. A basic DependencyProperty example. - /// - public Thickness ItemPadding - { - get => (Thickness)GetValue(ItemPaddingProperty); - set => SetValue(ItemPaddingProperty, value); - } - - protected virtual void OnMyPropertyChanged(string oldValue, string newValue) - { - // Do something with the changed value. - } - - public void Element_PointerEntered(object sender, PointerRoutedEventArgs e) - { - if (sender is TextBlock text) - { - text.Opacity = 1; - } - } -} diff --git a/components/GradientSlider/src/GradientSlider_xBind.cs b/components/GradientSlider/src/GradientSlider_xBind.cs deleted file mode 100644 index f87c6c90c..000000000 --- a/components/GradientSlider/src/GradientSlider_xBind.cs +++ /dev/null @@ -1,71 +0,0 @@ -// 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; - -/// -/// An example templated control. -/// -public partial class GradientSlider_xBind: Control -{ - /// - /// Creates a new instance of the class. - /// - public GradientSlider_xBind() - { - this.DefaultStyleKey = typeof(GradientSlider_xBind); - - // Allows directly using this control as the x:DataType in the template. - this.DataContext = this; - } - - /// - /// The backing for the property. - /// - public static readonly DependencyProperty ItemPaddingProperty = DependencyProperty.Register( - nameof(ItemPadding), - typeof(Thickness), - typeof(GradientSlider_xBind), - new PropertyMetadata(defaultValue: new Thickness(0))); - - /// - /// The backing for the property. - /// - public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( - nameof(MyProperty), - typeof(string), - typeof(GradientSlider_xBind), - new PropertyMetadata(defaultValue: string.Empty, (d, e) => ((GradientSlider_xBind)d).OnMyPropertyChanged((string)e.OldValue, (string)e.NewValue))); - - /// - /// Gets or sets an example string. A basic DependencyProperty example. - /// - public string MyProperty - { - get => (string)GetValue(MyPropertyProperty); - set => SetValue(MyPropertyProperty, value); - } - - /// - /// Gets or sets a padding for an item. A basic DependencyProperty example. - /// - public Thickness ItemPadding - { - get => (Thickness)GetValue(ItemPaddingProperty); - set => SetValue(ItemPaddingProperty, value); - } - - protected virtual void OnMyPropertyChanged(string oldValue, string newValue) - { - // Do something with the changed value. - } - - public void Element_PointerEntered(object sender, PointerRoutedEventArgs e) - { - if (sender is TextBlock text) - { - text.Opacity = 1; - } - } -} diff --git a/components/GradientSlider/src/Themes/Generic.xaml b/components/GradientSlider/src/Themes/Generic.xaml index 1091389dd..b0218ee2c 100644 --- a/components/GradientSlider/src/Themes/Generic.xaml +++ b/components/GradientSlider/src/Themes/Generic.xaml @@ -1,10 +1,9 @@ - - - + diff --git a/components/GradientSlider/tests/ExampleGradientSliderTestClass.cs b/components/GradientSlider/tests/ExampleGradientSliderTestClass.cs index 9d8253d7b..5b6462bba 100644 --- a/components/GradientSlider/tests/ExampleGradientSliderTestClass.cs +++ b/components/GradientSlider/tests/ExampleGradientSliderTestClass.cs @@ -57,7 +57,7 @@ public void SimpleUIAttributeExampleTest() public void SimpleUIExamplePageTest(ExampleGradientSliderTestPage 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); @@ -74,7 +74,7 @@ public async Task SimpleAsyncUIExamplePageTest(ExampleGradientSliderTestPage pag // 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 GradientSlider_ClassicBinding(); + var component = new GradientSlider(); Assert.IsNotNull(component); }); } @@ -101,7 +101,7 @@ public async Task ComplexAsyncLoadUIExampleTest() { await EnqueueAsync(async () => { - var component = new GradientSlider_ClassicBinding(); + var component = new GradientSlider(); Assert.IsNotNull(component); Assert.IsFalse(component.IsLoaded); @@ -119,7 +119,7 @@ await EnqueueAsync(async () => [UIThreadTestMethod] public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest() { - var component = new GradientSlider_ClassicBinding(); + var component = new GradientSlider(); Assert.IsNotNull(component); Assert.IsFalse(component.IsLoaded); diff --git a/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml b/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml index 7556618bb..dbdc30833 100644 --- a/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml +++ b/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml @@ -1,4 +1,4 @@ - + - + From 1028e202fdcdc4640a1dbcf02bf2079fcfb99cb9 Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Thu, 8 Jan 2026 21:40:31 +0200 Subject: [PATCH 03/26] Added thumb and background population --- .../samples/GradientSliderSample.xaml | 11 +- .../samples/GradientSliderSample.xaml.cs | 8 -- .../src/GradientSlider.Events.cs | 22 ++++ .../src/GradientSlider.Properties.cs | 29 +++++ .../GradientSlider/src/GradientSlider.cs | 100 ++++++++++++++++++ .../src/GradientSliderStyle.xaml | 13 +-- .../GradientSlider/src/GradientSliderThumb.cs | 28 +++++ .../src/GradientSliderThumbStyle.xaml | 26 +++++ .../GradientSlider/src/Themes/Generic.xaml | 2 + 9 files changed, 224 insertions(+), 15 deletions(-) create mode 100644 components/GradientSlider/src/GradientSlider.Events.cs create mode 100644 components/GradientSlider/src/GradientSlider.Properties.cs create mode 100644 components/GradientSlider/src/GradientSliderThumb.cs create mode 100644 components/GradientSlider/src/GradientSliderThumbStyle.xaml diff --git a/components/GradientSlider/samples/GradientSliderSample.xaml b/components/GradientSlider/samples/GradientSliderSample.xaml index de732b752..a11637fd6 100644 --- a/components/GradientSlider/samples/GradientSliderSample.xaml +++ b/components/GradientSlider/samples/GradientSliderSample.xaml @@ -9,6 +9,15 @@ mc:Ignorable="d"> - + + + + + + + + + + diff --git a/components/GradientSlider/samples/GradientSliderSample.xaml.cs b/components/GradientSlider/samples/GradientSliderSample.xaml.cs index b0a4a644f..0d88ffaa8 100644 --- a/components/GradientSlider/samples/GradientSliderSample.xaml.cs +++ b/components/GradientSlider/samples/GradientSliderSample.xaml.cs @@ -17,12 +17,4 @@ public GradientSliderSample() { 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/GradientSlider/src/GradientSlider.Events.cs b/components/GradientSlider/src/GradientSlider.Events.cs new file mode 100644 index 000000000..9ea1389cd --- /dev/null +++ b/components/GradientSlider/src/GradientSlider.Events.cs @@ -0,0 +1,22 @@ +// 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 GradientSlider +{ + private void OnGradientStopOffsetChanged(DependencyObject d, DependencyProperty e) + { + if (d is not GradientStop stop) + return; + + if (!_stopThumbs.TryGetValue(stop, out var thumb)) + return; + + Canvas.SetLeft(thumb, stop.Offset); + } + + private void ContainerCanvas_SizeChanged(object sender, SizeChangedEventArgs e) + => SyncThumbs(); +} diff --git a/components/GradientSlider/src/GradientSlider.Properties.cs b/components/GradientSlider/src/GradientSlider.Properties.cs new file mode 100644 index 000000000..947e2d5dd --- /dev/null +++ b/components/GradientSlider/src/GradientSlider.Properties.cs @@ -0,0 +1,29 @@ +// 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 GradientSlider +{ + public static readonly DependencyProperty GradientStopsProperty = + DependencyProperty.Register(nameof(GradientStops), + typeof(GradientStopCollection), + typeof(GradientSlider), + new PropertyMetadata(new GradientStopCollection(), GradientStopsChangedCallback)); + + public GradientStopCollection GradientStops + { + get => (GradientStopCollection)GetValue(GradientStopsProperty); + set => SetValue(GradientStopsProperty, value); + } + + private static void GradientStopsChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not GradientSlider slider) + return; + + slider.RefreshThumbs(); + slider.SyncBackground(); + } +} diff --git a/components/GradientSlider/src/GradientSlider.cs b/components/GradientSlider/src/GradientSlider.cs index b7e11c134..ca3450367 100644 --- a/components/GradientSlider/src/GradientSlider.cs +++ b/components/GradientSlider/src/GradientSlider.cs @@ -2,13 +2,23 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.UI.Xaml.Shapes; + namespace CommunityToolkit.WinUI.Controls; /// /// An example templated control. /// +[TemplatePart(Name = "ContainerCanvas", Type = typeof(Canvas))] +[TemplatePart(Name = "BackgroundRectangle", Type = typeof(Rectangle))] public partial class GradientSlider : Control { + private readonly Dictionary _stopThumbs = []; + private readonly Dictionary _stopCallbacks = []; + + private Canvas? _containerCanvas; + private Rectangle? _backgroundRectangle; + /// /// Creates a new instance of the class. /// @@ -21,5 +31,95 @@ public GradientSlider() protected override void OnApplyTemplate() { base.OnApplyTemplate(); + + _containerCanvas = (Canvas)GetTemplateChild("ContainerCanvas"); + _backgroundRectangle = (Rectangle?)GetTemplateChild("BackgroundRectangle"); + + _containerCanvas.SizeChanged += this.ContainerCanvas_SizeChanged; + + RefreshThumbs(); + } + + private void AddStop(GradientStop stop) + { + if (_containerCanvas is null) + return; + + // Prepare a thumb for the gradient stop + var thumb = new GradientSliderThumb() + { + GradientStop = stop, + }; + + // Register callbacks + var callback = stop.RegisterPropertyChangedCallback(GradientStop.OffsetProperty, OnGradientStopOffsetChanged); + _stopCallbacks.Add(stop, callback); + thumb.Loaded += this.Thumb_Loaded; + + _stopThumbs.Add(stop, thumb); + _containerCanvas.Children.Add(thumb); + } + + private void RemoveStop(GradientStop stop) + { + if (_containerCanvas is null) + return; + + // Should this be an exception? + if (!_stopThumbs.TryGetValue(stop, out var thumb)) + return; + + stop.UnregisterPropertyChangedCallback(GradientStop.OffsetProperty, _stopCallbacks[stop]); + _stopCallbacks.Remove(stop); + + _containerCanvas.Children.Remove(thumb); + _stopThumbs.Remove(stop); + } + + private void RefreshThumbs() + { + ClearThumbs(); + foreach (var stop in GradientStops) + AddStop(stop); + + SyncBackground(); + } + + private void SyncThumbs() + { + foreach (var thumb in _stopThumbs.Values) + UpdateThumbPosition(thumb); + } + + private void ClearThumbs() + { + foreach (var (stop, thumb) in _stopThumbs) + RemoveStop(stop); + } + + private void SyncBackground() + { + if (_containerCanvas is null || _backgroundRectangle is null) + return; + + _backgroundRectangle.Fill = new LinearGradientBrush(GradientStops, 0); + } + + private void Thumb_Loaded(object sender, RoutedEventArgs e) + { + if (sender is not GradientSliderThumb thumb) + return; + + thumb.Loaded -= Thumb_Loaded; + UpdateThumbPosition(thumb); + } + + private void UpdateThumbPosition(GradientSliderThumb thumb) + { + if (_containerCanvas is null) + return; + + var dragWidth = _containerCanvas.ActualWidth - thumb.ActualWidth; + Canvas.SetLeft(thumb, thumb.GradientStop.Offset * dragWidth); } } diff --git a/components/GradientSlider/src/GradientSliderStyle.xaml b/components/GradientSlider/src/GradientSliderStyle.xaml index 43effdea1..64864e000 100644 --- a/components/GradientSlider/src/GradientSliderStyle.xaml +++ b/components/GradientSlider/src/GradientSliderStyle.xaml @@ -2,23 +2,24 @@ - - - - + diff --git a/components/GradientSlider/src/GradientSliderThumb.cs b/components/GradientSlider/src/GradientSliderThumb.cs new file mode 100644 index 000000000..81548a3ba --- /dev/null +++ b/components/GradientSlider/src/GradientSliderThumb.cs @@ -0,0 +1,28 @@ +// 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 sealed partial class GradientSliderThumb : Control +{ + public static readonly DependencyProperty GradientStopProperty = + DependencyProperty.Register(nameof(GradientStop), + typeof(GradientStop), + typeof(GradientSliderThumb), + new PropertyMetadata(null)); + + /// + /// Initializes a new instance of the class. + /// + public GradientSliderThumb() + { + DefaultStyleKey = typeof(GradientSliderThumb); + } + + public GradientStop GradientStop + { + get => (GradientStop)GetValue(GradientStopProperty); + set => SetValue(GradientStopProperty, value); + } +} diff --git a/components/GradientSlider/src/GradientSliderThumbStyle.xaml b/components/GradientSlider/src/GradientSliderThumbStyle.xaml new file mode 100644 index 000000000..2a1e050b5 --- /dev/null +++ b/components/GradientSlider/src/GradientSliderThumbStyle.xaml @@ -0,0 +1,26 @@ + + + + + + diff --git a/components/GradientSlider/src/Themes/Generic.xaml b/components/GradientSlider/src/Themes/Generic.xaml index b0218ee2c..614177e26 100644 --- a/components/GradientSlider/src/Themes/Generic.xaml +++ b/components/GradientSlider/src/Themes/Generic.xaml @@ -4,6 +4,8 @@ + + From a1f814f525121fb871cdb95ec0e4632ce2aa9d07 Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Thu, 8 Jan 2026 22:29:12 +0200 Subject: [PATCH 04/26] Styled GradientSliderThumb and added dragging events --- .../samples/GradientSliderSample.xaml | 2 +- .../src/GradientSlider.Events.cs | 7 +- .../src/GradientSliderThumb.Events.cs | 112 +++++++++++++++++ .../GradientSlider/src/GradientSliderThumb.cs | 20 +++ .../src/GradientSliderThumbStyle.xaml | 114 ++++++++++++++++-- 5 files changed, 239 insertions(+), 16 deletions(-) create mode 100644 components/GradientSlider/src/GradientSliderThumb.Events.cs diff --git a/components/GradientSlider/samples/GradientSliderSample.xaml b/components/GradientSlider/samples/GradientSliderSample.xaml index a11637fd6..ae00cd1e3 100644 --- a/components/GradientSlider/samples/GradientSliderSample.xaml +++ b/components/GradientSlider/samples/GradientSliderSample.xaml @@ -13,7 +13,7 @@ - + diff --git a/components/GradientSlider/src/GradientSlider.Events.cs b/components/GradientSlider/src/GradientSlider.Events.cs index 9ea1389cd..cb492c75a 100644 --- a/components/GradientSlider/src/GradientSlider.Events.cs +++ b/components/GradientSlider/src/GradientSlider.Events.cs @@ -8,13 +8,10 @@ public partial class GradientSlider { private void OnGradientStopOffsetChanged(DependencyObject d, DependencyProperty e) { - if (d is not GradientStop stop) + if (d is not GradientStop stop || !_stopThumbs.TryGetValue(stop, out var thumb)) return; - if (!_stopThumbs.TryGetValue(stop, out var thumb)) - return; - - Canvas.SetLeft(thumb, stop.Offset); + UpdateThumbPosition(thumb); } private void ContainerCanvas_SizeChanged(object sender, SizeChangedEventArgs e) diff --git a/components/GradientSlider/src/GradientSliderThumb.Events.cs b/components/GradientSlider/src/GradientSliderThumb.Events.cs new file mode 100644 index 000000000..5f7a22c52 --- /dev/null +++ b/components/GradientSlider/src/GradientSliderThumb.Events.cs @@ -0,0 +1,112 @@ +// 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 sealed partial class GradientSliderThumb : Control +{ + private bool _pointerOver; + private bool _pressed; + private bool _isDragging; + private Point _dragStartPosition; + private Point _lastPosition; + + public event DragStartedEventHandler? DragStarted; + + public event DragDeltaEventHandler? DragDelta; + + public event DragCompletedEventHandler? DragCompleted; + + private void GradientSliderThumb_PointerEntered(object sender, PointerRoutedEventArgs e) + { + _pointerOver = true; + + if (!_pressed) + { + VisualStateManager.GoToState(this, PointerOverState, true); + } + } + + private void GradientSliderThumb_PointerExited(object sender, PointerRoutedEventArgs e) + { + _pointerOver = false; + + if (!_pressed) + { + VisualStateManager.GoToState(this, NormalState, true); + } + } + + private void GradientSliderThumb_PointerPressed(object sender, PointerRoutedEventArgs e) + { + _pressed = true; + _isDragging = true; + + CapturePointer(e.Pointer); + + _dragStartPosition = e.GetCurrentPoint(this).Position; + _lastPosition = _dragStartPosition; + + DragStarted?.Invoke(this, + new DragStartedEventArgs(_dragStartPosition.X, _dragStartPosition.Y)); + + VisualStateManager.GoToState(this, PressedState, true); + } + + private void GradientSliderThumb_PointerMoved(object sender, PointerRoutedEventArgs e) + { + if (!_isDragging) + return; + + var position = e.GetCurrentPoint(this).Position; + + double deltaX = position.X - _lastPosition.X; + double deltaY = position.Y - _lastPosition.Y; + + _lastPosition = position; + + DragDelta?.Invoke(this, new DragDeltaEventArgs(deltaX, deltaY)); + } + + private void GradientSliderThumb_PointerReleased(object sender, PointerRoutedEventArgs e) + { + if (_isDragging) + { + var end = e.GetCurrentPoint(this).Position; + + double totalX = end.X - _dragStartPosition.X; + double totalY = end.Y - _dragStartPosition.Y; + + DragCompleted?.Invoke(this, + new DragCompletedEventArgs(totalX, totalY, false)); + } + + _isDragging = false; + _pressed = false; + + ReleasePointerCapture(e.Pointer); + + VisualStateManager.GoToState(this, _pointerOver ? PointerOverState : NormalState, true); + } + + private void GradientSliderThumb_PointerCanceled(object sender, PointerRoutedEventArgs e) + { + if (_isDragging) + { + DragCompleted?.Invoke(this, + new DragCompletedEventArgs(0, 0, true)); + } + + _isDragging = false; + _pressed = false; + + ReleasePointerCapture(e.Pointer); + VisualStateManager.GoToState(this, NormalState, true); + } + + private void GradientSliderThumb_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) + { + VisualStateManager.GoToState(this, IsEnabled ? NormalState : DisabledState, true); + } +} diff --git a/components/GradientSlider/src/GradientSliderThumb.cs b/components/GradientSlider/src/GradientSliderThumb.cs index 81548a3ba..6e42d5131 100644 --- a/components/GradientSlider/src/GradientSliderThumb.cs +++ b/components/GradientSlider/src/GradientSliderThumb.cs @@ -6,6 +6,12 @@ namespace CommunityToolkit.WinUI.Controls; public sealed partial class GradientSliderThumb : Control { + internal const string CommonStates = "CommonStates"; + internal const string NormalState = "Normal"; + internal const string PointerOverState = "PointerOver"; + internal const string PressedState = "Pressed"; + internal const string DisabledState = "Disabled"; + public static readonly DependencyProperty GradientStopProperty = DependencyProperty.Register(nameof(GradientStop), typeof(GradientStop), @@ -25,4 +31,18 @@ public GradientStop GradientStop get => (GradientStop)GetValue(GradientStopProperty); set => SetValue(GradientStopProperty, value); } + + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + PointerEntered += this.GradientSliderThumb_PointerEntered; + PointerExited += this.GradientSliderThumb_PointerExited; + PointerPressed += this.GradientSliderThumb_PointerPressed; + PointerMoved += this.GradientSliderThumb_PointerMoved; + PointerReleased += this.GradientSliderThumb_PointerReleased; + PointerCanceled += this.GradientSliderThumb_PointerCanceled; + IsEnabledChanged += this.GradientSliderThumb_IsEnabledChanged; + } } diff --git a/components/GradientSlider/src/GradientSliderThumbStyle.xaml b/components/GradientSlider/src/GradientSliderThumbStyle.xaml index 2a1e050b5..b3733573d 100644 --- a/components/GradientSlider/src/GradientSliderThumbStyle.xaml +++ b/components/GradientSlider/src/GradientSliderThumbStyle.xaml @@ -1,22 +1,116 @@ - + - + + + - From ca021d18f2f51645fd4d30b7f11d4fdfe109f49f Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Sat, 10 Jan 2026 03:37:20 +0200 Subject: [PATCH 15/26] XAML Styles --- .../src/GradientSliderStyle.xaml | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/components/GradientSlider/src/GradientSliderStyle.xaml b/components/GradientSlider/src/GradientSliderStyle.xaml index 9d929f8e4..1402594be 100644 --- a/components/GradientSlider/src/GradientSliderStyle.xaml +++ b/components/GradientSlider/src/GradientSliderStyle.xaml @@ -1,31 +1,33 @@ - - + + - - + + 0.5 -