From 69675bff0d42c3bf68986652d82aebf47800890c Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:30:07 -0800 Subject: [PATCH 01/34] Create base Adorners project from base template --- components/Adorners/OpenSolution.bat | 3 + .../Adorners/samples/Adorners.Samples.csproj | 10 ++ components/Adorners/samples/Adorners.md | 65 +++++++++ .../samples/AdornersCustomSample.xaml | 25 ++++ .../samples/AdornersCustomSample.xaml.cs | 30 ++++ .../samples/AdornersTemplatedSample.xaml | 16 +++ .../samples/AdornersTemplatedSample.xaml.cs | 21 +++ .../AdornersTemplatedStyleCustomSample.xaml | 26 ++++ ...AdornersTemplatedStyleCustomSample.xaml.cs | 21 +++ .../samples/AdornersXbindBackedSample.xaml | 16 +++ .../samples/AdornersXbindBackedSample.xaml.cs | 21 +++ .../AdornersXbindBackedStyleCustomSample.xaml | 26 ++++ ...ornersXbindBackedStyleCustomSample.xaml.cs | 21 +++ components/Adorners/samples/Assets/icon.png | Bin 0 -> 2216 bytes .../Adorners/samples/Dependencies.props | 31 ++++ components/Adorners/src/Adorners.cs | 108 ++++++++++++++ .../src/AdornersStyle_ClassicBinding.xaml | 62 ++++++++ .../Adorners/src/AdornersStyle_xBind.xaml | 69 +++++++++ .../Adorners/src/AdornersStyle_xBind.xaml.cs | 20 +++ .../Adorners/src/Adorners_ClassicBinding.cs | 94 ++++++++++++ components/Adorners/src/Adorners_xBind.cs | 71 ++++++++++ ...nityToolkit.WinUI.Controls.Adorners.csproj | 14 ++ components/Adorners/src/Dependencies.props | 31 ++++ components/Adorners/src/MultiTarget.props | 9 ++ components/Adorners/src/Themes/Generic.xaml | 10 ++ .../Adorners/tests/Adorners.Tests.projitems | 23 +++ .../Adorners/tests/Adorners.Tests.shproj | 13 ++ .../tests/ExampleAdornersTestClass.cs | 134 ++++++++++++++++++ .../tests/ExampleAdornersTestPage.xaml | 14 ++ .../tests/ExampleAdornersTestPage.xaml.cs | 16 +++ 30 files changed, 1020 insertions(+) create mode 100644 components/Adorners/OpenSolution.bat create mode 100644 components/Adorners/samples/Adorners.Samples.csproj create mode 100644 components/Adorners/samples/Adorners.md create mode 100644 components/Adorners/samples/AdornersCustomSample.xaml create mode 100644 components/Adorners/samples/AdornersCustomSample.xaml.cs create mode 100644 components/Adorners/samples/AdornersTemplatedSample.xaml create mode 100644 components/Adorners/samples/AdornersTemplatedSample.xaml.cs create mode 100644 components/Adorners/samples/AdornersTemplatedStyleCustomSample.xaml create mode 100644 components/Adorners/samples/AdornersTemplatedStyleCustomSample.xaml.cs create mode 100644 components/Adorners/samples/AdornersXbindBackedSample.xaml create mode 100644 components/Adorners/samples/AdornersXbindBackedSample.xaml.cs create mode 100644 components/Adorners/samples/AdornersXbindBackedStyleCustomSample.xaml create mode 100644 components/Adorners/samples/AdornersXbindBackedStyleCustomSample.xaml.cs create mode 100644 components/Adorners/samples/Assets/icon.png create mode 100644 components/Adorners/samples/Dependencies.props create mode 100644 components/Adorners/src/Adorners.cs create mode 100644 components/Adorners/src/AdornersStyle_ClassicBinding.xaml create mode 100644 components/Adorners/src/AdornersStyle_xBind.xaml create mode 100644 components/Adorners/src/AdornersStyle_xBind.xaml.cs create mode 100644 components/Adorners/src/Adorners_ClassicBinding.cs create mode 100644 components/Adorners/src/Adorners_xBind.cs create mode 100644 components/Adorners/src/CommunityToolkit.WinUI.Controls.Adorners.csproj create mode 100644 components/Adorners/src/Dependencies.props create mode 100644 components/Adorners/src/MultiTarget.props create mode 100644 components/Adorners/src/Themes/Generic.xaml create mode 100644 components/Adorners/tests/Adorners.Tests.projitems create mode 100644 components/Adorners/tests/Adorners.Tests.shproj create mode 100644 components/Adorners/tests/ExampleAdornersTestClass.cs create mode 100644 components/Adorners/tests/ExampleAdornersTestPage.xaml create mode 100644 components/Adorners/tests/ExampleAdornersTestPage.xaml.cs diff --git a/components/Adorners/OpenSolution.bat b/components/Adorners/OpenSolution.bat new file mode 100644 index 000000000..814a56d4b --- /dev/null +++ b/components/Adorners/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/Adorners/samples/Adorners.Samples.csproj b/components/Adorners/samples/Adorners.Samples.csproj new file mode 100644 index 000000000..c772be49d --- /dev/null +++ b/components/Adorners/samples/Adorners.Samples.csproj @@ -0,0 +1,10 @@ + + + + + Adorners + + + + + diff --git a/components/Adorners/samples/Adorners.md b/components/Adorners/samples/Adorners.md new file mode 100644 index 000000000..3a45c432f --- /dev/null +++ b/components/Adorners/samples/Adorners.md @@ -0,0 +1,65 @@ +--- +title: Adorners +author: githubaccount +description: TODO: Your experiment's description here +keywords: Adorners, Control, Layout +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 0 +issue-id: 0 +icon: assets/icon.png +--- + + + + + + + + + +# Adorners + +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 AdornersCustomSample] + +## 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 AdornersTemplatedSample] + +### Custom style + +> [!SAMPLE AdornersTemplatedStyleCustomSample] + +## 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 AdornersXbindBackedSample] + +### Custom style + +> [!SAMPLE AdornersXbindBackedStyleCustomSample] + diff --git a/components/Adorners/samples/AdornersCustomSample.xaml b/components/Adorners/samples/AdornersCustomSample.xaml new file mode 100644 index 000000000..729fab6c7 --- /dev/null +++ b/components/Adorners/samples/AdornersCustomSample.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/components/Adorners/samples/AdornersCustomSample.xaml.cs b/components/Adorners/samples/AdornersCustomSample.xaml.cs new file mode 100644 index 000000000..2a2fe5dd1 --- /dev/null +++ b/components/Adorners/samples/AdornersCustomSample.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 AdornersExperiment.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(AdornersCustomSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(Adorners)} custom control.")] +public sealed partial class AdornersCustomSample : Page +{ + public AdornersCustomSample() + { + 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/Adorners/samples/AdornersTemplatedSample.xaml b/components/Adorners/samples/AdornersTemplatedSample.xaml new file mode 100644 index 000000000..f6a256c01 --- /dev/null +++ b/components/Adorners/samples/AdornersTemplatedSample.xaml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/components/Adorners/samples/AdornersTemplatedSample.xaml.cs b/components/Adorners/samples/AdornersTemplatedSample.xaml.cs new file mode 100644 index 000000000..0ca5e2df3 --- /dev/null +++ b/components/Adorners/samples/AdornersTemplatedSample.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 AdornersExperiment.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(AdornersTemplatedSample), "Templated control", description: "A sample for showing how to create and use a templated control.")] +public sealed partial class AdornersTemplatedSample : Page +{ + public AdornersTemplatedSample() + { + this.InitializeComponent(); + } +} diff --git a/components/Adorners/samples/AdornersTemplatedStyleCustomSample.xaml b/components/Adorners/samples/AdornersTemplatedStyleCustomSample.xaml new file mode 100644 index 000000000..ba9c58f29 --- /dev/null +++ b/components/Adorners/samples/AdornersTemplatedStyleCustomSample.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/components/Adorners/samples/AdornersTemplatedStyleCustomSample.xaml.cs b/components/Adorners/samples/AdornersTemplatedStyleCustomSample.xaml.cs new file mode 100644 index 000000000..c50f4ad07 --- /dev/null +++ b/components/Adorners/samples/AdornersTemplatedStyleCustomSample.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 AdornersExperiment.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(AdornersTemplatedStyleCustomSample), "Templated control (restyled)", description: "A sample for showing how to create a use and templated control with a custom style.")] +public sealed partial class AdornersTemplatedStyleCustomSample : Page +{ + public AdornersTemplatedStyleCustomSample() + { + this.InitializeComponent(); + } +} diff --git a/components/Adorners/samples/AdornersXbindBackedSample.xaml b/components/Adorners/samples/AdornersXbindBackedSample.xaml new file mode 100644 index 000000000..ee3f4b3ae --- /dev/null +++ b/components/Adorners/samples/AdornersXbindBackedSample.xaml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/components/Adorners/samples/AdornersXbindBackedSample.xaml.cs b/components/Adorners/samples/AdornersXbindBackedSample.xaml.cs new file mode 100644 index 000000000..f9e97c5ff --- /dev/null +++ b/components/Adorners/samples/AdornersXbindBackedSample.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 AdornersExperiment.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(AdornersXbindBackedSample), "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 AdornersXbindBackedSample : Page +{ + public AdornersXbindBackedSample() + { + this.InitializeComponent(); + } +} diff --git a/components/Adorners/samples/AdornersXbindBackedStyleCustomSample.xaml b/components/Adorners/samples/AdornersXbindBackedStyleCustomSample.xaml new file mode 100644 index 000000000..c0726cf4a --- /dev/null +++ b/components/Adorners/samples/AdornersXbindBackedStyleCustomSample.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/components/Adorners/samples/AdornersXbindBackedStyleCustomSample.xaml.cs b/components/Adorners/samples/AdornersXbindBackedStyleCustomSample.xaml.cs new file mode 100644 index 000000000..9315506ee --- /dev/null +++ b/components/Adorners/samples/AdornersXbindBackedStyleCustomSample.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 AdornersExperiment.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(AdornersXbindBackedStyleCustomSample), "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 AdornersXbindBackedStyleCustomSample : Page +{ + public AdornersXbindBackedStyleCustomSample() + { + this.InitializeComponent(); + } +} diff --git a/components/Adorners/samples/Assets/icon.png b/components/Adorners/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/Adorners/src/Adorners.cs b/components/Adorners/src/Adorners.cs new file mode 100644 index 000000000..c3f2a2b11 --- /dev/null +++ b/components/Adorners/src/Adorners.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 Adorners : Panel +{ + /// + /// Identifies the property. + /// + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(Adorners), 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 Adorners 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/Adorners/src/AdornersStyle_ClassicBinding.xaml b/components/Adorners/src/AdornersStyle_ClassicBinding.xaml new file mode 100644 index 000000000..5728f1cdc --- /dev/null +++ b/components/Adorners/src/AdornersStyle_ClassicBinding.xaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + 4,4,4,4 + + + + + + + + diff --git a/components/Adorners/src/AdornersStyle_xBind.xaml b/components/Adorners/src/AdornersStyle_xBind.xaml new file mode 100644 index 000000000..497404f72 --- /dev/null +++ b/components/Adorners/src/AdornersStyle_xBind.xaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + 4,4,4,4 + + + + + + + + diff --git a/components/Adorners/src/AdornersStyle_xBind.xaml.cs b/components/Adorners/src/AdornersStyle_xBind.xaml.cs new file mode 100644 index 000000000..7e625f7be --- /dev/null +++ b/components/Adorners/src/AdornersStyle_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 AdornersStyle_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 AdornersStyle_xBind() + { + this.InitializeComponent(); + } +} diff --git a/components/Adorners/src/Adorners_ClassicBinding.cs b/components/Adorners/src/Adorners_ClassicBinding.cs new file mode 100644 index 000000000..7dbeb0984 --- /dev/null +++ b/components/Adorners/src/Adorners_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 Adorners_ClassicBinding : Control +{ + /// + /// Creates a new instance of the class. + /// + public Adorners_ClassicBinding() + { + this.DefaultStyleKey = typeof(Adorners_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(Adorners_ClassicBinding), + new PropertyMetadata(defaultValue: new Thickness(0))); + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( + nameof(MyProperty), + typeof(string), + typeof(Adorners_ClassicBinding), + new PropertyMetadata(defaultValue: string.Empty, (d, e) => ((Adorners_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/Adorners/src/Adorners_xBind.cs b/components/Adorners/src/Adorners_xBind.cs new file mode 100644 index 000000000..b3d082aa7 --- /dev/null +++ b/components/Adorners/src/Adorners_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 Adorners_xBind: Control +{ + /// + /// Creates a new instance of the class. + /// + public Adorners_xBind() + { + this.DefaultStyleKey = typeof(Adorners_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(Adorners_xBind), + new PropertyMetadata(defaultValue: new Thickness(0))); + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( + nameof(MyProperty), + typeof(string), + typeof(Adorners_xBind), + new PropertyMetadata(defaultValue: string.Empty, (d, e) => ((Adorners_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/Adorners/src/CommunityToolkit.WinUI.Controls.Adorners.csproj b/components/Adorners/src/CommunityToolkit.WinUI.Controls.Adorners.csproj new file mode 100644 index 000000000..913309c0a --- /dev/null +++ b/components/Adorners/src/CommunityToolkit.WinUI.Controls.Adorners.csproj @@ -0,0 +1,14 @@ + + + + + Adorners + This package contains Adorners. + + + CommunityToolkit.WinUI.Controls.AdornersRns + + + + + diff --git a/components/Adorners/src/Dependencies.props b/components/Adorners/src/Dependencies.props new file mode 100644 index 000000000..e622e1df4 --- /dev/null +++ b/components/Adorners/src/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Adorners/src/MultiTarget.props b/components/Adorners/src/MultiTarget.props new file mode 100644 index 000000000..b11c19426 --- /dev/null +++ b/components/Adorners/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/Adorners/src/Themes/Generic.xaml b/components/Adorners/src/Themes/Generic.xaml new file mode 100644 index 000000000..e7db8864b --- /dev/null +++ b/components/Adorners/src/Themes/Generic.xaml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/components/Adorners/tests/Adorners.Tests.projitems b/components/Adorners/tests/Adorners.Tests.projitems new file mode 100644 index 000000000..9b22684ce --- /dev/null +++ b/components/Adorners/tests/Adorners.Tests.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 95E1FE34-BF6D-4E5C-8F44-51C8B9348212 + + + AdornersTests + + + + + ExampleAdornersTestPage.xaml + + + + + Designer + MSBuild:Compile + + + \ No newline at end of file diff --git a/components/Adorners/tests/Adorners.Tests.shproj b/components/Adorners/tests/Adorners.Tests.shproj new file mode 100644 index 000000000..95aeb7961 --- /dev/null +++ b/components/Adorners/tests/Adorners.Tests.shproj @@ -0,0 +1,13 @@ + + + + 95E1FE34-BF6D-4E5C-8F44-51C8B9348212 + 14.0 + + + + + + + + diff --git a/components/Adorners/tests/ExampleAdornersTestClass.cs b/components/Adorners/tests/ExampleAdornersTestClass.cs new file mode 100644 index 000000000..3526f5cff --- /dev/null +++ b/components/Adorners/tests/ExampleAdornersTestClass.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 AdornersTests; + +[TestClass] +public partial class ExampleAdornersTestClass : VisualUITestBase +{ + // If you don't need access to UI objects directly or async code, use this pattern. + [TestMethod] + public void SimpleSynchronousExampleTest() + { + var assembly = typeof(Adorners).Assembly; + var type = assembly.GetType(typeof(Adorners).FullName ?? string.Empty); + + Assert.IsNotNull(type, "Could not find Adorners type."); + Assert.AreEqual(typeof(Adorners), type, "Type of Adorners 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 Adorners(); + 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(ExampleAdornersTestPage 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("AdornersControl"); + + Assert.IsNotNull(componentByName); + } + + // You can still do async work with a UIThreadTestMethod as well. + [UIThreadTestMethod] + public async Task SimpleAsyncUIExamplePageTest(ExampleAdornersTestPage 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 Adorners_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 Adorners_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 Adorners_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/Adorners/tests/ExampleAdornersTestPage.xaml b/components/Adorners/tests/ExampleAdornersTestPage.xaml new file mode 100644 index 000000000..fe7b4bca6 --- /dev/null +++ b/components/Adorners/tests/ExampleAdornersTestPage.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/components/Adorners/tests/ExampleAdornersTestPage.xaml.cs b/components/Adorners/tests/ExampleAdornersTestPage.xaml.cs new file mode 100644 index 000000000..5022a14c9 --- /dev/null +++ b/components/Adorners/tests/ExampleAdornersTestPage.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 AdornersTests; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ExampleAdornersTestPage : Page +{ + public ExampleAdornersTestPage() + { + this.InitializeComponent(); + } +} From 8ad91dbd2ccdd26bab2ccb9eab09f023c2b7413d Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:36:40 -0800 Subject: [PATCH 02/34] [WIP] Initial updating of old Adorners branch import Original Branch: https://github.com/michael-hawker/Labs-Windows/tree/llama/adorners/labs/Adorners (This was on an older interation of Labs infrastructure, so would make rebasing/updating branch difficult, easier to just start new component and copy over.) Only changes were to modernize atop latest Toolkit packages instead of including copied helper code. Have more updates to build atop this from XAML Studio after, plus some new updates to API surface, and docs for WPF comparisons. --- components/Adorners/samples/Adorners.md | 56 ++--- .../samples/AdornersCustomSample.xaml | 25 --- .../samples/AdornersCustomSample.xaml.cs | 30 --- .../samples/AdornersInfoBadgeSample.xaml | 24 +++ .../samples/AdornersInfoBadgeSample.xaml.cs | 16 ++ .../samples/AdornersTemplatedSample.xaml | 16 -- .../samples/AdornersTemplatedSample.xaml.cs | 21 -- .../AdornersTemplatedStyleCustomSample.xaml | 26 --- ...AdornersTemplatedStyleCustomSample.xaml.cs | 21 -- .../samples/AdornersXbindBackedSample.xaml | 16 -- .../samples/AdornersXbindBackedSample.xaml.cs | 21 -- .../AdornersXbindBackedStyleCustomSample.xaml | 26 --- ...ornersXbindBackedStyleCustomSample.xaml.cs | 21 -- .../ElementHighlightAdornerSample.xaml | 60 ++++++ .../ElementHighlightAdornerSample.xaml.cs | 19 ++ .../samples/InfoBadgeWithoutAdorner.xaml | 23 ++ .../samples/InfoBadgeWithoutAdorner.xaml.cs | 16 ++ components/Adorners/src/AdornerDecorator.cs | 39 ++++ components/Adorners/src/AdornerDecorator.xaml | 40 ++++ components/Adorners/src/AdornerLayer.cs | 198 ++++++++++++++++++ components/Adorners/src/Adorners.cs | 108 ---------- .../src/AdornersStyle_ClassicBinding.xaml | 62 ------ .../Adorners/src/AdornersStyle_xBind.xaml | 69 ------ .../Adorners/src/AdornersStyle_xBind.xaml.cs | 20 -- .../Adorners/src/Adorners_ClassicBinding.cs | 94 --------- components/Adorners/src/Adorners_xBind.cs | 71 ------- ...=> CommunityToolkit.WinUI.Adorners.csproj} | 2 +- components/Adorners/src/Dependencies.props | 22 +- ...meworkElementExtensions.WaitUntilLoaded.cs | 44 ++++ components/Adorners/src/Themes/Generic.xaml | 8 +- .../tests/ExampleAdornersTestClass.cs | 18 +- .../tests/ExampleAdornersTestPage.xaml | 2 +- 32 files changed, 516 insertions(+), 718 deletions(-) delete mode 100644 components/Adorners/samples/AdornersCustomSample.xaml delete mode 100644 components/Adorners/samples/AdornersCustomSample.xaml.cs create mode 100644 components/Adorners/samples/AdornersInfoBadgeSample.xaml create mode 100644 components/Adorners/samples/AdornersInfoBadgeSample.xaml.cs delete mode 100644 components/Adorners/samples/AdornersTemplatedSample.xaml delete mode 100644 components/Adorners/samples/AdornersTemplatedSample.xaml.cs delete mode 100644 components/Adorners/samples/AdornersTemplatedStyleCustomSample.xaml delete mode 100644 components/Adorners/samples/AdornersTemplatedStyleCustomSample.xaml.cs delete mode 100644 components/Adorners/samples/AdornersXbindBackedSample.xaml delete mode 100644 components/Adorners/samples/AdornersXbindBackedSample.xaml.cs delete mode 100644 components/Adorners/samples/AdornersXbindBackedStyleCustomSample.xaml delete mode 100644 components/Adorners/samples/AdornersXbindBackedStyleCustomSample.xaml.cs create mode 100644 components/Adorners/samples/ElementHighlightAdornerSample.xaml create mode 100644 components/Adorners/samples/ElementHighlightAdornerSample.xaml.cs create mode 100644 components/Adorners/samples/InfoBadgeWithoutAdorner.xaml create mode 100644 components/Adorners/samples/InfoBadgeWithoutAdorner.xaml.cs create mode 100644 components/Adorners/src/AdornerDecorator.cs create mode 100644 components/Adorners/src/AdornerDecorator.xaml create mode 100644 components/Adorners/src/AdornerLayer.cs delete mode 100644 components/Adorners/src/Adorners.cs delete mode 100644 components/Adorners/src/AdornersStyle_ClassicBinding.xaml delete mode 100644 components/Adorners/src/AdornersStyle_xBind.xaml delete mode 100644 components/Adorners/src/AdornersStyle_xBind.xaml.cs delete mode 100644 components/Adorners/src/Adorners_ClassicBinding.cs delete mode 100644 components/Adorners/src/Adorners_xBind.cs rename components/Adorners/src/{CommunityToolkit.WinUI.Controls.Adorners.csproj => CommunityToolkit.WinUI.Adorners.csproj} (85%) create mode 100644 components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs diff --git a/components/Adorners/samples/Adorners.md b/components/Adorners/samples/Adorners.md index 3a45c432f..159596d05 100644 --- a/components/Adorners/samples/Adorners.md +++ b/components/Adorners/samples/Adorners.md @@ -1,65 +1,45 @@ --- title: Adorners -author: githubaccount -description: TODO: Your experiment's description here +author: michael-hawker +description: Adorners let you overlay content on top of your XAML components in a separate layer on top of everything else. keywords: Adorners, Control, Layout dev_langs: - csharp category: Controls subcategory: Layout -discussion-id: 0 +discussion-id: 278 issue-id: 0 icon: assets/icon.png --- - - - - - - - - # Adorners -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 AdornersCustomSample] - -## Templated Controls +Adorners allow a developer to overlay any content on top of another UI element in a separate layer that resides on top of everything else. -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. +## Background -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. +Adorners originally existed in WPF as a main integration part as part of the framework. [You can read more about how they worked in WPF here.](https://learn.microsoft.com/dotnet/desktop/wpf/controls/adorners-overview) -The `_ClassicBinding` class shows the traditional method used to develop components with best practices. +UWP/WinUI unfortunately never ported this integration point into the new framework, this experiment hopes to fill that gap with a similar and modernized version of the API surface. -### Implict style +### Without Adorners -> [!SAMPLE AdornersTemplatedSample] +Imagine a scenario where you have a button or tab that checks a user's e-mail, and you'd like it to display the number of new e-mails that have arrived. -### Custom style +You could try and incorporate a [`InfoBadge`](https://learn.microsoft.com/windows/apps/design/controls/info-badge) into your Visual Tree in order to display this as part of your icon, but that requires you to modify quite a bit of your content, as in this example: -> [!SAMPLE AdornersTemplatedStyleCustomSample] +> [!SAMPLE InfoBadgeWithoutAdorner] -## Templated Controls with x:Bind +It also by default gets confined to the perimeter of the button and clipped, as seen above. -This is an _experimental_ new way to define components which allows for the use of x:Bind within the style. +### With Adorners -### Implict style +However, with an Adorner instead, you can abstract this behavior from the content of your control. You can even more easily place the notification outside the bounds of the original element, like so: -> [!SAMPLE AdornersXbindBackedSample] +> [!SAMPLE AdornersInfoBadgeSample] -### Custom style +## Highlight Example -> [!SAMPLE AdornersXbindBackedStyleCustomSample] +Adorners can be used in a variety of scenarios. For instance, if you wanted to highlight an element and show it's alignment to other elements in a creativity app. +> [!SAMPLE ElementHighlightAdornerSample] diff --git a/components/Adorners/samples/AdornersCustomSample.xaml b/components/Adorners/samples/AdornersCustomSample.xaml deleted file mode 100644 index 729fab6c7..000000000 --- a/components/Adorners/samples/AdornersCustomSample.xaml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/components/Adorners/samples/AdornersCustomSample.xaml.cs b/components/Adorners/samples/AdornersCustomSample.xaml.cs deleted file mode 100644 index 2a2fe5dd1..000000000 --- a/components/Adorners/samples/AdornersCustomSample.xaml.cs +++ /dev/null @@ -1,30 +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. - -using CommunityToolkit.WinUI.Controls; - -namespace AdornersExperiment.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(AdornersCustomSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(Adorners)} custom control.")] -public sealed partial class AdornersCustomSample : Page -{ - public AdornersCustomSample() - { - 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/Adorners/samples/AdornersInfoBadgeSample.xaml b/components/Adorners/samples/AdornersInfoBadgeSample.xaml new file mode 100644 index 000000000..3d6b8468e --- /dev/null +++ b/components/Adorners/samples/AdornersInfoBadgeSample.xaml @@ -0,0 +1,24 @@ + + + + + diff --git a/components/Adorners/samples/AdornersInfoBadgeSample.xaml.cs b/components/Adorners/samples/AdornersInfoBadgeSample.xaml.cs new file mode 100644 index 000000000..2c5a47783 --- /dev/null +++ b/components/Adorners/samples/AdornersInfoBadgeSample.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 AdornersExperiment.Samples; + +[ToolkitSampleBoolOption("IsAdornerVisible", true, Title = "Is Adorner Visible")] + +[ToolkitSample(id: nameof(AdornersInfoBadgeSample), "InfoBadge w/ Adorner", description: "A sample for showing how add an infobadge to a component via an Adorner.")] +public sealed partial class AdornersInfoBadgeSample : Page +{ + public AdornersInfoBadgeSample() + { + this.InitializeComponent(); + } +} diff --git a/components/Adorners/samples/AdornersTemplatedSample.xaml b/components/Adorners/samples/AdornersTemplatedSample.xaml deleted file mode 100644 index f6a256c01..000000000 --- a/components/Adorners/samples/AdornersTemplatedSample.xaml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/components/Adorners/samples/AdornersTemplatedSample.xaml.cs b/components/Adorners/samples/AdornersTemplatedSample.xaml.cs deleted file mode 100644 index 0ca5e2df3..000000000 --- a/components/Adorners/samples/AdornersTemplatedSample.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 AdornersExperiment.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(AdornersTemplatedSample), "Templated control", description: "A sample for showing how to create and use a templated control.")] -public sealed partial class AdornersTemplatedSample : Page -{ - public AdornersTemplatedSample() - { - this.InitializeComponent(); - } -} diff --git a/components/Adorners/samples/AdornersTemplatedStyleCustomSample.xaml b/components/Adorners/samples/AdornersTemplatedStyleCustomSample.xaml deleted file mode 100644 index ba9c58f29..000000000 --- a/components/Adorners/samples/AdornersTemplatedStyleCustomSample.xaml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/components/Adorners/samples/AdornersTemplatedStyleCustomSample.xaml.cs b/components/Adorners/samples/AdornersTemplatedStyleCustomSample.xaml.cs deleted file mode 100644 index c50f4ad07..000000000 --- a/components/Adorners/samples/AdornersTemplatedStyleCustomSample.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 AdornersExperiment.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(AdornersTemplatedStyleCustomSample), "Templated control (restyled)", description: "A sample for showing how to create a use and templated control with a custom style.")] -public sealed partial class AdornersTemplatedStyleCustomSample : Page -{ - public AdornersTemplatedStyleCustomSample() - { - this.InitializeComponent(); - } -} diff --git a/components/Adorners/samples/AdornersXbindBackedSample.xaml b/components/Adorners/samples/AdornersXbindBackedSample.xaml deleted file mode 100644 index ee3f4b3ae..000000000 --- a/components/Adorners/samples/AdornersXbindBackedSample.xaml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/components/Adorners/samples/AdornersXbindBackedSample.xaml.cs b/components/Adorners/samples/AdornersXbindBackedSample.xaml.cs deleted file mode 100644 index f9e97c5ff..000000000 --- a/components/Adorners/samples/AdornersXbindBackedSample.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 AdornersExperiment.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(AdornersXbindBackedSample), "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 AdornersXbindBackedSample : Page -{ - public AdornersXbindBackedSample() - { - this.InitializeComponent(); - } -} diff --git a/components/Adorners/samples/AdornersXbindBackedStyleCustomSample.xaml b/components/Adorners/samples/AdornersXbindBackedStyleCustomSample.xaml deleted file mode 100644 index c0726cf4a..000000000 --- a/components/Adorners/samples/AdornersXbindBackedStyleCustomSample.xaml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/components/Adorners/samples/AdornersXbindBackedStyleCustomSample.xaml.cs b/components/Adorners/samples/AdornersXbindBackedStyleCustomSample.xaml.cs deleted file mode 100644 index 9315506ee..000000000 --- a/components/Adorners/samples/AdornersXbindBackedStyleCustomSample.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 AdornersExperiment.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(AdornersXbindBackedStyleCustomSample), "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 AdornersXbindBackedStyleCustomSample : Page -{ - public AdornersXbindBackedStyleCustomSample() - { - this.InitializeComponent(); - } -} diff --git a/components/Adorners/samples/ElementHighlightAdornerSample.xaml b/components/Adorners/samples/ElementHighlightAdornerSample.xaml new file mode 100644 index 000000000..048991b91 --- /dev/null +++ b/components/Adorners/samples/ElementHighlightAdornerSample.xaml @@ -0,0 +1,60 @@ + + + + + + + + diff --git a/components/Adorners/samples/ElementHighlightAdornerSample.xaml.cs b/components/Adorners/samples/ElementHighlightAdornerSample.xaml.cs new file mode 100644 index 000000000..6d60cd912 --- /dev/null +++ b/components/Adorners/samples/ElementHighlightAdornerSample.xaml.cs @@ -0,0 +1,19 @@ +// 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 AdornersExperiment.Samples; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +[ToolkitSampleBoolOption("IsAdornerVisible", false, Title = "Is Adorner Visible")] + +[ToolkitSample(id: nameof(ElementHighlightAdornerSample), "Highlighting an Element w/ Adorner", description: "A sample for showing how to highlight an element's bounds with an Adorner.")] +public sealed partial class ElementHighlightAdornerSample : Page +{ + public ElementHighlightAdornerSample() + { + this.InitializeComponent(); + } +} diff --git a/components/Adorners/samples/InfoBadgeWithoutAdorner.xaml b/components/Adorners/samples/InfoBadgeWithoutAdorner.xaml new file mode 100644 index 000000000..31bbe7796 --- /dev/null +++ b/components/Adorners/samples/InfoBadgeWithoutAdorner.xaml @@ -0,0 +1,23 @@ + + + + + diff --git a/components/Adorners/samples/InfoBadgeWithoutAdorner.xaml.cs b/components/Adorners/samples/InfoBadgeWithoutAdorner.xaml.cs new file mode 100644 index 000000000..247603cdb --- /dev/null +++ b/components/Adorners/samples/InfoBadgeWithoutAdorner.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 AdornersExperiment.Samples; + +[ToolkitSampleBoolOption("IsNotificationVisible", true, Title = "Is Notification Visible")] + +[ToolkitSample(id: nameof(InfoBadgeWithoutAdorner), "InfoBadge w/o Adorner", description: "A sample for showing how one adds an infobadge to a component without an Adorner (from WinUI Gallery app).")] +public sealed partial class InfoBadgeWithoutAdorner : Page +{ + public InfoBadgeWithoutAdorner() + { + this.InitializeComponent(); + } +} diff --git a/components/Adorners/src/AdornerDecorator.cs b/components/Adorners/src/AdornerDecorator.cs new file mode 100644 index 000000000..0907144a1 --- /dev/null +++ b/components/Adorners/src/AdornerDecorator.cs @@ -0,0 +1,39 @@ +// 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; + +/// +/// Helper class to hold content with an . +/// +[TemplatePart(Name = PartAdornerLayer, Type = typeof(AdornerLayer))] +[ContentProperty(Name = nameof(Child))] +public sealed class AdornerDecorator : Control +{ + private const string PartAdornerLayer = "AdornerLayer"; + + public UIElement Child + { + get { return (UIElement)GetValue(ContentProperty); } + set { SetValue(ContentProperty, value); } + } + + // Using a DependencyProperty as the backing store for Content. This enables animation, styling, binding, etc... + public static readonly DependencyProperty ContentProperty = + DependencyProperty.Register(nameof(Child), typeof(UIElement), typeof(AdornerDecorator), new PropertyMetadata(null)); + + public AdornerLayer? AdornerLayer { get; private set; } + + public AdornerDecorator() + { + this.DefaultStyleKey = typeof(AdornerDecorator); + } + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + AdornerLayer = GetTemplateChild(PartAdornerLayer) as AdornerLayer; + } +} diff --git a/components/Adorners/src/AdornerDecorator.xaml b/components/Adorners/src/AdornerDecorator.xaml new file mode 100644 index 000000000..fc0a0a572 --- /dev/null +++ b/components/Adorners/src/AdornerDecorator.xaml @@ -0,0 +1,40 @@ + + + + diff --git a/components/Adorners/src/AdornerLayer.cs b/components/Adorners/src/AdornerLayer.cs new file mode 100644 index 000000000..6ca90bd40 --- /dev/null +++ b/components/Adorners/src/AdornerLayer.cs @@ -0,0 +1,198 @@ +// 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; + +/// +/// An adornment layer which can hold content to show on top of other components. If none is specified, one will be injected into your app content for you. +/// +public partial class AdornerLayer : Canvas +{ + public static UIElement GetXaml(FrameworkElement obj) + { + return (UIElement)obj.GetValue(XamlProperty); + } + + public static void SetXaml(FrameworkElement obj, UIElement value) + { + obj.SetValue(XamlProperty, value); + } + + public static readonly DependencyProperty XamlProperty = + DependencyProperty.RegisterAttached("Xaml", typeof(UIElement), typeof(AdornerLayer), new PropertyMetadata(null, OnXamlPropertyChanged)); + + private static async void OnXamlPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args) + { + if (dependencyObject is FrameworkElement fe) + { + if (!fe.IsLoaded || fe.Parent is null) + { + fe.Loaded += XamlPropertyFrameworkElement_Loaded; + } + else if (args.NewValue is UIElement adorner) + { + var layer = await GetAdornerLayerAsync(fe); + + if (layer is not null) + { + AttachAdorner(layer, fe, adorner); + } + } + + // TODO: Handle removing Adorner + } + } + + private static async void XamlPropertyFrameworkElement_Loaded(object sender, RoutedEventArgs e) + { + if (sender is FrameworkElement fe) + { + fe.Loaded -= XamlPropertyFrameworkElement_Loaded; + + var layer = await GetAdornerLayerAsync(fe); + + if (layer is not null) + { + AttachAdorner(layer, fe, GetXaml(fe)); + } + } + } + + /// + /// Retrieves the closest (or creates an) for the given element. If awaited, the retrieved adorner layer is guaranteed to be loaded. This is to assist adorners with being able to be positioned in relation to the loaded element. + /// There may be multiple s within an application, as each should have one to enable relational scrolling along content that may be outside of the viewport. + /// + /// Element to adorn. + /// Loaded responsible for that element. + public static async Task GetAdornerLayerAsync(FrameworkElement adornedElement) + { + // 1. Find Adorner Layer for element or top-most element + FrameworkElement? lastElement = null; + + var adornerLayerOrTopMostElement = adornedElement.FindAscendant((element) => + { + lastElement = element; // TODO: should this be after our if, does it matter? + + if (element is AdornerDecorator) + { + return true; + } + else if (element is AdornerLayer) + { + return true; + } + else if (element is ScrollViewer scoller) + { + return true; + } + // TODO: Need to figure out porting new DO toolkit helpers to Uno, only needed for custom adorner layer placement... + /*else + { + // TODO: Use BreadthFirst Search w/ Depth Limited? + var child = element.FindFirstLevelDescendants(); + + if (child != null) + { + lastElement = child; + return true; + } + }*/ + + return false; + }) ?? lastElement; + + // Check cases where we may have found a child that we want to use instead of the element returned by search. + if (lastElement is AdornerLayer || lastElement is AdornerDecorator) + { + adornerLayerOrTopMostElement = lastElement; + } + + if (adornerLayerOrTopMostElement is AdornerDecorator decorator) + { + await decorator.WaitUntilLoadedAsync(); + + return decorator.AdornerLayer; + } + else if (adornerLayerOrTopMostElement is AdornerLayer layer) + { + await layer.WaitUntilLoadedAsync(); + + // If we just have an adorner layer now, we're done! + return layer; + } + else + { + // TODO: Windows.UI.Xaml.Internal.RootScrollViewer is a maybe different and what was causing issues before I looked for ScrollViewers along the way? + // It's an internal unexposed type, so maybe it inherits from ScrollViewer? Not sure yet, but might need to detect and + // do something different here? + + // ScrollViewers need AdornerLayers so they can provide adorners that scroll with the adorned elements (as it worked in WPF). + // Note: ScrollViewers and the Window were the main AdornerLayer integration points in WPF. + if (adornerLayerOrTopMostElement is ScrollViewer scroller) + { + var content = scroller.Content as FrameworkElement; + + // Extra code for RootScrollViewer TODO: Can we detect this better? + if (scroller.Parent == null) + { + //// XamlMarkupHelper.UnloadObject doesn't work here (throws an invalid value exception) does content need a name? + // TODO: Figure out this scenario? + throw new NotImplementedException("RootScrollViewer attachment isn't supported, add a AdornerDecorator or ScrollViewer manually to the top-level of your application."); + } + + scroller.Content = null; + + var layerContainer = new AdornerDecorator() + { + Child = content!, + }; + + scroller.Content = layerContainer; + + await layerContainer.WaitUntilLoadedAsync(); + + return layerContainer.AdornerLayer; + } + // Grid seems like the easiest place for us to inject AdornerLayers automatically at the top-level (if needed) - not sure how common this will be? + else if (adornerLayerOrTopMostElement is Grid grid) + { + // TODO: Not sure how we want to handle AdornerDecorator in this scenario... + var adornerLayer = new AdornerLayer(); + + // TODO: Handle if grid row/columns change. + Grid.SetRowSpan(adornerLayer, grid.RowDefinitions.Count); + Grid.SetColumnSpan(adornerLayer, grid.ColumnDefinitions.Count); + grid.Children.Add(adornerLayer); + + await adornerLayer.WaitUntilLoadedAsync(); + + return adornerLayer; + } + } + + return null; + } + + // TODO: Temp helper? Build into 'Adorner' base class? + private static void AttachAdorner(AdornerLayer layer, FrameworkElement adornedElement, UIElement adorner) + { + // Add adorner XAML content to the Adorner Layer + + var border = new Border() + { + Child = adorner, + Width = adornedElement.ActualWidth, // TODO: Register/tie to size of element better for changes. + Height = adornedElement.ActualHeight, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch + }; + + var coord = layer.CoordinatesTo(adornedElement); + + Canvas.SetLeft(border, coord.X); + Canvas.SetTop(border, coord.Y); + + layer.Children.Add(border); + } +} diff --git a/components/Adorners/src/Adorners.cs b/components/Adorners/src/Adorners.cs deleted file mode 100644 index c3f2a2b11..000000000 --- a/components/Adorners/src/Adorners.cs +++ /dev/null @@ -1,108 +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; - -/// -/// 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 Adorners : Panel -{ - /// - /// Identifies the property. - /// - public static readonly DependencyProperty OrientationProperty = - DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(Adorners), 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 Adorners 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/Adorners/src/AdornersStyle_ClassicBinding.xaml b/components/Adorners/src/AdornersStyle_ClassicBinding.xaml deleted file mode 100644 index 5728f1cdc..000000000 --- a/components/Adorners/src/AdornersStyle_ClassicBinding.xaml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - 4,4,4,4 - - - - - - - - diff --git a/components/Adorners/src/AdornersStyle_xBind.xaml b/components/Adorners/src/AdornersStyle_xBind.xaml deleted file mode 100644 index 497404f72..000000000 --- a/components/Adorners/src/AdornersStyle_xBind.xaml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - 4,4,4,4 - - - - - - - - diff --git a/components/Adorners/src/AdornersStyle_xBind.xaml.cs b/components/Adorners/src/AdornersStyle_xBind.xaml.cs deleted file mode 100644 index 7e625f7be..000000000 --- a/components/Adorners/src/AdornersStyle_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 AdornersStyle_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 AdornersStyle_xBind() - { - this.InitializeComponent(); - } -} diff --git a/components/Adorners/src/Adorners_ClassicBinding.cs b/components/Adorners/src/Adorners_ClassicBinding.cs deleted file mode 100644 index 7dbeb0984..000000000 --- a/components/Adorners/src/Adorners_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 Adorners_ClassicBinding : Control -{ - /// - /// Creates a new instance of the class. - /// - public Adorners_ClassicBinding() - { - this.DefaultStyleKey = typeof(Adorners_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(Adorners_ClassicBinding), - new PropertyMetadata(defaultValue: new Thickness(0))); - - /// - /// The backing for the property. - /// - public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( - nameof(MyProperty), - typeof(string), - typeof(Adorners_ClassicBinding), - new PropertyMetadata(defaultValue: string.Empty, (d, e) => ((Adorners_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/Adorners/src/Adorners_xBind.cs b/components/Adorners/src/Adorners_xBind.cs deleted file mode 100644 index b3d082aa7..000000000 --- a/components/Adorners/src/Adorners_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 Adorners_xBind: Control -{ - /// - /// Creates a new instance of the class. - /// - public Adorners_xBind() - { - this.DefaultStyleKey = typeof(Adorners_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(Adorners_xBind), - new PropertyMetadata(defaultValue: new Thickness(0))); - - /// - /// The backing for the property. - /// - public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( - nameof(MyProperty), - typeof(string), - typeof(Adorners_xBind), - new PropertyMetadata(defaultValue: string.Empty, (d, e) => ((Adorners_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/Adorners/src/CommunityToolkit.WinUI.Controls.Adorners.csproj b/components/Adorners/src/CommunityToolkit.WinUI.Adorners.csproj similarity index 85% rename from components/Adorners/src/CommunityToolkit.WinUI.Controls.Adorners.csproj rename to components/Adorners/src/CommunityToolkit.WinUI.Adorners.csproj index 913309c0a..fcb853361 100644 --- a/components/Adorners/src/CommunityToolkit.WinUI.Controls.Adorners.csproj +++ b/components/Adorners/src/CommunityToolkit.WinUI.Adorners.csproj @@ -3,7 +3,7 @@ Adorners - This package contains Adorners. + This package contains Adorners. A Modern WinUI XAML based take on WPF Adorners. CommunityToolkit.WinUI.Controls.AdornersRns diff --git a/components/Adorners/src/Dependencies.props b/components/Adorners/src/Dependencies.props index e622e1df4..12520a351 100644 --- a/components/Adorners/src/Dependencies.props +++ b/components/Adorners/src/Dependencies.props @@ -9,23 +9,13 @@ For UWP / WinAppSDK / Uno packages, place the package references here. --> - - - + + + - - - - - - - - - - - - - + + + diff --git a/components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs b/components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs new file mode 100644 index 000000000..4d63f7159 --- /dev/null +++ b/components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs @@ -0,0 +1,44 @@ +// 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; + +public static partial class FrameworkElementExtensions +{ + /// + /// A extension which can be used in asynchronous scenarios to + /// wait until an element has loaded before proceeding using a + /// that listens to the event. In the event the element + /// is already loaded (), the method will return immediately. + /// + /// The element to await loading. + /// + /// True if the element is loaded. + public static Task WaitUntilLoadedAsync(this FrameworkElement element, TaskCreationOptions? options = null) + { + if (element.IsLoaded && element.Parent != null) + { + return Task.FromResult(true); + } + + var taskCompletionSource = options.HasValue ? new TaskCompletionSource(options.Value) + : new TaskCompletionSource(); + try + { + void LoadedCallback(object sender, RoutedEventArgs args) + { + element.Loaded -= LoadedCallback; + taskCompletionSource.SetResult(true); + } + + element.Loaded += LoadedCallback; + } + catch (Exception e) + { + taskCompletionSource.SetException(e); + } + + return taskCompletionSource.Task; + } +} diff --git a/components/Adorners/src/Themes/Generic.xaml b/components/Adorners/src/Themes/Generic.xaml index e7db8864b..d721f8e3a 100644 --- a/components/Adorners/src/Themes/Generic.xaml +++ b/components/Adorners/src/Themes/Generic.xaml @@ -1,10 +1,6 @@  - + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - - + - diff --git a/components/Adorners/tests/ExampleAdornersTestClass.cs b/components/Adorners/tests/ExampleAdornersTestClass.cs index 3526f5cff..acf716aae 100644 --- a/components/Adorners/tests/ExampleAdornersTestClass.cs +++ b/components/Adorners/tests/ExampleAdornersTestClass.cs @@ -15,11 +15,11 @@ public partial class ExampleAdornersTestClass : VisualUITestBase [TestMethod] public void SimpleSynchronousExampleTest() { - var assembly = typeof(Adorners).Assembly; - var type = assembly.GetType(typeof(Adorners).FullName ?? string.Empty); + var assembly = typeof(AdornerLayer).Assembly; + var type = assembly.GetType(typeof(AdornerLayer).FullName ?? string.Empty); Assert.IsNotNull(type, "Could not find Adorners type."); - Assert.AreEqual(typeof(Adorners), type, "Type of Adorners does not match expected type."); + Assert.AreEqual(typeof(AdornerLayer), type, "Type of Adorners 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 Adorners(); + var component = new AdornerLayer(); Assert.IsNotNull(component); } @@ -57,7 +57,7 @@ public void SimpleUIAttributeExampleTest() public void SimpleUIExamplePageTest(ExampleAdornersTestPage 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(ExampleAdornersTestPage page) // 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 Adorners_ClassicBinding(); + var component = new AdornerLayer(); Assert.IsNotNull(component); }); } @@ -101,7 +101,7 @@ public async Task ComplexAsyncLoadUIExampleTest() { await EnqueueAsync(async () => { - var component = new Adorners_ClassicBinding(); + var component = new AdornerLayer(); Assert.IsNotNull(component); Assert.IsFalse(component.IsLoaded); @@ -119,7 +119,7 @@ await EnqueueAsync(async () => [UIThreadTestMethod] public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest() { - var component = new Adorners_ClassicBinding(); + var component = new AdornerLayer(); Assert.IsNotNull(component); Assert.IsFalse(component.IsLoaded); diff --git a/components/Adorners/tests/ExampleAdornersTestPage.xaml b/components/Adorners/tests/ExampleAdornersTestPage.xaml index fe7b4bca6..70538ef63 100644 --- a/components/Adorners/tests/ExampleAdornersTestPage.xaml +++ b/components/Adorners/tests/ExampleAdornersTestPage.xaml @@ -9,6 +9,6 @@ mc:Ignorable="d"> - + From ba0274005cc8c0ee9775051e93c68ae4bb0ba0c3 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 18 Nov 2025 23:33:31 -0800 Subject: [PATCH 03/34] Fix build time error with XAML Compilers from overloaded namespace/class extension for FrameworkElementExtensions --- components/Adorners/src/AdornerLayer.cs | 2 ++ .../src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/components/Adorners/src/AdornerLayer.cs b/components/Adorners/src/AdornerLayer.cs index 6ca90bd40..b95aecd74 100644 --- a/components/Adorners/src/AdornerLayer.cs +++ b/components/Adorners/src/AdornerLayer.cs @@ -2,6 +2,8 @@ // 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.Future; + namespace CommunityToolkit.WinUI; /// diff --git a/components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs b/components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs index 4d63f7159..322f8abab 100644 --- a/components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs +++ b/components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs @@ -2,7 +2,7 @@ // 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; +namespace CommunityToolkit.WinUI.Future; public static partial class FrameworkElementExtensions { From e7de2811f29b4413c1f9714302659409b2730db6 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 18 Nov 2025 23:51:57 -0800 Subject: [PATCH 04/34] Bring over Adorner improvements from XAML Studio Includes initial support for adorners to adjust to window/content resizing --- components/Adorners/src/AdornerDecorator.cs | 2 +- components/Adorners/src/AdornerDecorator.xaml | 2 +- components/Adorners/src/AdornerLayer.cs | 66 +++++++++++++++++-- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/components/Adorners/src/AdornerDecorator.cs b/components/Adorners/src/AdornerDecorator.cs index 0907144a1..f67096b78 100644 --- a/components/Adorners/src/AdornerDecorator.cs +++ b/components/Adorners/src/AdornerDecorator.cs @@ -5,7 +5,7 @@ namespace CommunityToolkit.WinUI; /// -/// Helper class to hold content with an . +/// Helper class to hold content with an . Use this to wrap another and direct where the should sit. This class is helpful to constrain the or in cases where an appropriate location for the layer can't be automatically determined. /// [TemplatePart(Name = PartAdornerLayer, Type = typeof(AdornerLayer))] [ContentProperty(Name = nameof(Child))] diff --git a/components/Adorners/src/AdornerDecorator.xaml b/components/Adorners/src/AdornerDecorator.xaml index fc0a0a572..dbf92423c 100644 --- a/components/Adorners/src/AdornerDecorator.xaml +++ b/components/Adorners/src/AdornerDecorator.xaml @@ -20,7 +20,7 @@ Basically we need something that arranges the content and the adorner layer within the same space, we put the AdornerLayer below so it will appear atop all content within the decorated region. --> - + + /// Sets the of a . Use this to attach any as an adorner to another . Requires that an is available in the visual tree above the adorned element. + /// + /// The to adorn. + /// The to attach as an adorner. public static void SetXaml(FrameworkElement obj, UIElement value) { obj.SetValue(XamlProperty, value); } + /// + /// Identifies the Xaml Attached Property. + /// public static readonly DependencyProperty XamlProperty = DependencyProperty.RegisterAttached("Xaml", typeof(UIElement), typeof(AdornerLayer), new PropertyMetadata(null, OnXamlPropertyChanged)); + public AdornerLayer() + { + SizeChanged += AdornerLayer_SizeChanged; + } + + private void AdornerLayer_SizeChanged(object sender, SizeChangedEventArgs e) + { + foreach (var adorner in Children) + { + if (adorner is Border border && border.Tag is FrameworkElement adornedElement) + { + border.Width = adornedElement.ActualWidth; + border.Height = adornedElement.ActualHeight; + + var coord = this.CoordinatesTo(adornedElement); + + Canvas.SetLeft(border, coord.X); + Canvas.SetTop(border, coord.Y); + } + } + } + private static async void OnXamlPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args) { if (dependencyObject is FrameworkElement fe) @@ -41,8 +71,15 @@ private static async void OnXamlPropertyChanged(DependencyObject dependencyObjec AttachAdorner(layer, fe, adorner); } } + else if (args.NewValue == null && args.OldValue is UIElement oldAdorner) + { + var layer = await GetAdornerLayerAsync(fe); - // TODO: Handle removing Adorner + if (layer is not null) + { + RemoveAdorner(layer, oldAdorner); + } + } } } @@ -56,7 +93,11 @@ private static async void XamlPropertyFrameworkElement_Loaded(object sender, Rou if (layer is not null) { - AttachAdorner(layer, fe, GetXaml(fe)); + var adorner = GetXaml(fe); + + if (adorner == null) return; + + AttachAdorner(layer, fe, adorner); } } } @@ -71,7 +112,7 @@ private static async void XamlPropertyFrameworkElement_Loaded(object sender, Rou { // 1. Find Adorner Layer for element or top-most element FrameworkElement? lastElement = null; - + var adornerLayerOrTopMostElement = adornedElement.FindAscendant((element) => { lastElement = element; // TODO: should this be after our if, does it matter? @@ -84,7 +125,7 @@ private static async void XamlPropertyFrameworkElement_Loaded(object sender, Rou { return true; } - else if (element is ScrollViewer scoller) + else if (element is ScrollViewer) { return true; } @@ -157,7 +198,7 @@ private static async void XamlPropertyFrameworkElement_Loaded(object sender, Rou return layerContainer.AdornerLayer; } // Grid seems like the easiest place for us to inject AdornerLayers automatically at the top-level (if needed) - not sure how common this will be? - else if (adornerLayerOrTopMostElement is Grid grid) + else if (adornerLayerOrTopMostElement is Grid grid) { // TODO: Not sure how we want to handle AdornerDecorator in this scenario... var adornerLayer = new AdornerLayer(); @@ -187,7 +228,8 @@ private static void AttachAdorner(AdornerLayer layer, FrameworkElement adornedEl Width = adornedElement.ActualWidth, // TODO: Register/tie to size of element better for changes. Height = adornedElement.ActualHeight, HorizontalAlignment = HorizontalAlignment.Stretch, - VerticalAlignment = VerticalAlignment.Stretch + VerticalAlignment = VerticalAlignment.Stretch, + Tag = adornedElement, }; var coord = layer.CoordinatesTo(adornedElement); @@ -197,4 +239,16 @@ private static void AttachAdorner(AdornerLayer layer, FrameworkElement adornedEl layer.Children.Add(border); } + + private static void RemoveAdorner(AdornerLayer layer, UIElement adorner) + { + var border = adorner.FindAscendant(); + + if (border != null) + { + layer.Children.Remove(border); + + VisualTreeHelper.DisconnectChildrenRecursive(border); + } + } } From 9fa0dc46e5a41d1658652eb7a4e537508debc590 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Wed, 19 Nov 2025 00:04:14 -0800 Subject: [PATCH 05/34] Add some notes on differences with WPF and TODOs --- components/Adorners/samples/Adorners.md | 26 ++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/components/Adorners/samples/Adorners.md b/components/Adorners/samples/Adorners.md index 159596d05..c25e2e0d9 100644 --- a/components/Adorners/samples/Adorners.md +++ b/components/Adorners/samples/Adorners.md @@ -18,9 +18,7 @@ Adorners allow a developer to overlay any content on top of another UI element i ## Background -Adorners originally existed in WPF as a main integration part as part of the framework. [You can read more about how they worked in WPF here.](https://learn.microsoft.com/dotnet/desktop/wpf/controls/adorners-overview) - -UWP/WinUI unfortunately never ported this integration point into the new framework, this experiment hopes to fill that gap with a similar and modernized version of the API surface. +Adorners originally existed in WPF as a main integration part as part of the framework. [You can read more about how they worked in WPF here.](https://learn.microsoft.com/dotnet/desktop/wpf/controls/adorners-overview) See more about the commonalities and differences to WinUI adorners in the migration section below. ### Without Adorners @@ -30,7 +28,7 @@ You could try and incorporate a [`InfoBadge`](https://learn.microsoft.com/window > [!SAMPLE InfoBadgeWithoutAdorner] -It also by default gets confined to the perimeter of the button and clipped, as seen above. +It also, by default, gets confined to the perimeter of the button and clipped, as seen above. ### With Adorners @@ -40,6 +38,24 @@ However, with an Adorner instead, you can abstract this behavior from the conten ## Highlight Example -Adorners can be used in a variety of scenarios. For instance, if you wanted to highlight an element and show it's alignment to other elements in a creativity app. +Adorners can be used in a variety of scenarios. For instance, if you wanted to highlight an element and show it's alignment to other elements in a creativity app: > [!SAMPLE ElementHighlightAdornerSample] + +## TODO: Resize Example + +Another common use case for adorners is to allow a user to resize a visual element. + +// TODO: Make a simple example here for this soon... + +## Migrating from WPF + +The WinUI Adorner API surface adapts many similar names and concepts as WPF Adorners; however, WinUI Adorners are XAML based and make use of the attached properties to make using Adorners much simpler, like Behaviors. Where as defining Adorners in WPF required custom drawing routines. It's possible to replicate many similar scenarios with this new API surface and make better use of XAML features like data binding; however, it will mean rewriting any existing WPF code. + +### Concepts + +The `AdornerLayer` is still an element of the visual tree which resides atop other content within your app and is the parent of all adorners. In WPF, this is usually already automatically a component of your app or `ScrollViewer`. Like WPF, adorners parent's in the visual tree will be the `AdornerLayer` and not the adorned element. + +The `AdornerDecorator` provides a similar purpose to that of its WPF counterpart, it will host an `AdornerLayer`. The main difference with the WinUI API is that the `AdornerDecorator` will wrap your contained content vs. in WPF it sat as a sibling to your content. We feel this makes it easier to use and ensure your adorned elements reside atop your adorned content, it also makes it easier to find within the Visual Tree for performance reasons. + +TODO: Adorner class info... From d22d7e4c2dee83cdf9c26e5a258f8386e9e984bc Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:34:25 -0800 Subject: [PATCH 06/34] Fix for WindowsAppSDK Interface error (from CsWinRT) Co-authored-by: Ahmed I had missed the warning from CsWinRT, big thanks to @ahmed605 for helping debug this issue in the Windows App Community Discord! --- components/Adorners/src/AdornerDecorator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Adorners/src/AdornerDecorator.cs b/components/Adorners/src/AdornerDecorator.cs index f67096b78..6fcd3bd33 100644 --- a/components/Adorners/src/AdornerDecorator.cs +++ b/components/Adorners/src/AdornerDecorator.cs @@ -9,7 +9,7 @@ namespace CommunityToolkit.WinUI; /// [TemplatePart(Name = PartAdornerLayer, Type = typeof(AdornerLayer))] [ContentProperty(Name = nameof(Child))] -public sealed class AdornerDecorator : Control +public sealed partial class AdornerDecorator : Control { private const string PartAdornerLayer = "AdornerLayer"; From 0b0b108033c7f2ec5ee3ca44d456befb57d6908b Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:36:37 -0800 Subject: [PATCH 07/34] Apply XAML Styler... --- components/Adorners/src/AdornerDecorator.xaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/Adorners/src/AdornerDecorator.xaml b/components/Adorners/src/AdornerDecorator.xaml index dbf92423c..05dbd08ba 100644 --- a/components/Adorners/src/AdornerDecorator.xaml +++ b/components/Adorners/src/AdornerDecorator.xaml @@ -1,4 +1,4 @@ - @@ -20,7 +20,8 @@ Basically we need something that arranges the content and the adorner layer within the same space, we put the AdornerLayer below so it will appear atop all content within the decorated region. --> - + Date: Sat, 22 Nov 2025 23:42:35 -0800 Subject: [PATCH 08/34] Add more xmldoc comments for AdornerLayer and AdornerDecorator --- components/Adorners/src/AdornerDecorator.cs | 16 ++++++++++++++-- components/Adorners/src/AdornerLayer.cs | 15 ++++++++++++++- ...FrameworkElementExtensions.WaitUntilLoaded.cs | 3 +++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/components/Adorners/src/AdornerDecorator.cs b/components/Adorners/src/AdornerDecorator.cs index 6fcd3bd33..f82ac7f19 100644 --- a/components/Adorners/src/AdornerDecorator.cs +++ b/components/Adorners/src/AdornerDecorator.cs @@ -13,23 +13,35 @@ public sealed partial class AdornerDecorator : Control { private const string PartAdornerLayer = "AdornerLayer"; + /// + /// Gets or sets the single child element of the . + /// public UIElement Child { get { return (UIElement)GetValue(ContentProperty); } set { SetValue(ContentProperty, value); } } - // Using a DependencyProperty as the backing store for Content. This enables animation, styling, binding, etc... + /// + /// Identifies the dependency property. + /// public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Child), typeof(UIElement), typeof(AdornerDecorator), new PropertyMetadata(null)); - public AdornerLayer? AdornerLayer { get; private set; } + /// + /// Gets the contained within this . + /// + internal AdornerLayer? AdornerLayer { get; private set; } + /// + /// Constructs a new instance of . + /// public AdornerDecorator() { this.DefaultStyleKey = typeof(AdornerDecorator); } + /// protected override void OnApplyTemplate() { base.OnApplyTemplate(); diff --git a/components/Adorners/src/AdornerLayer.cs b/components/Adorners/src/AdornerLayer.cs index f07ea3e57..612981958 100644 --- a/components/Adorners/src/AdornerLayer.cs +++ b/components/Adorners/src/AdornerLayer.cs @@ -7,10 +7,18 @@ namespace CommunityToolkit.WinUI; /// -/// An adornment layer which can hold content to show on top of other components. If none is specified, one will be injected into your app content for you. +/// An adornment layer which can hold content to show on top of other components. +/// If none is specified, one will be injected into your app content for you. +/// If a suitable location can't be automatically found, you can also use an +/// to specify where the should be placed. /// public partial class AdornerLayer : Canvas { + /// + /// Gets the of a . Use this to retrieve any attached adorner from another . + /// + /// The to retrieve the adorner from. + /// The attached as an adorner. public static UIElement GetXaml(FrameworkElement obj) { return (UIElement)obj.GetValue(XamlProperty); @@ -32,6 +40,9 @@ public static void SetXaml(FrameworkElement obj, UIElement value) public static readonly DependencyProperty XamlProperty = DependencyProperty.RegisterAttached("Xaml", typeof(UIElement), typeof(AdornerLayer), new PropertyMetadata(null, OnXamlPropertyChanged)); + /// + /// Constructs a new instance of . + /// public AdornerLayer() { SizeChanged += AdornerLayer_SizeChanged; @@ -248,7 +259,9 @@ private static void RemoveAdorner(AdornerLayer layer, UIElement adorner) { layer.Children.Remove(border); +#if !HAS_UNO VisualTreeHelper.DisconnectChildrenRecursive(border); +#endif } } } diff --git a/components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs b/components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs index 322f8abab..34d39c1fc 100644 --- a/components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs +++ b/components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs @@ -4,6 +4,9 @@ namespace CommunityToolkit.WinUI.Future; +/// +/// Helper extensions for . +/// public static partial class FrameworkElementExtensions { /// From 4eeb38c521f9b487a48612f4d26304210aa69982 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Sun, 23 Nov 2025 01:09:53 -0800 Subject: [PATCH 09/34] Add proper `Adorner` wrapper class for coordination of size/layout with `AdornerLayer` and customization point Add extra info about this to the doc TODO: need to test passing in a custom/subclassed Adorner still --- components/Adorners/samples/Adorners.md | 10 +- components/Adorners/src/Adorner.cs | 98 +++++++++++++++++++ components/Adorners/src/Adorner.xaml | 32 ++++++ components/Adorners/src/AdornerLayer.cs | 57 +++++------ .../CommunityToolkit.WinUI.Adorners.csproj | 1 + components/Adorners/src/Dependencies.props | 2 + components/Adorners/src/Themes/Generic.xaml | 3 +- .../tests/ExampleAdornersTestPage.xaml | 6 +- 8 files changed, 170 insertions(+), 39 deletions(-) create mode 100644 components/Adorners/src/Adorner.cs create mode 100644 components/Adorners/src/Adorner.xaml diff --git a/components/Adorners/samples/Adorners.md b/components/Adorners/samples/Adorners.md index c25e2e0d9..390d43ee0 100644 --- a/components/Adorners/samples/Adorners.md +++ b/components/Adorners/samples/Adorners.md @@ -18,7 +18,7 @@ Adorners allow a developer to overlay any content on top of another UI element i ## Background -Adorners originally existed in WPF as a main integration part as part of the framework. [You can read more about how they worked in WPF here.](https://learn.microsoft.com/dotnet/desktop/wpf/controls/adorners-overview) See more about the commonalities and differences to WinUI adorners in the migration section below. +Adorners originally existed in WPF as an extension part of the framework. [You can read more about how they worked in WPF here.](https://learn.microsoft.com/dotnet/desktop/wpf/controls/adorners-overview) See more about the commonalities and differences to WinUI adorners in the migration section below. ### Without Adorners @@ -46,16 +46,16 @@ Adorners can be used in a variety of scenarios. For instance, if you wanted to h Another common use case for adorners is to allow a user to resize a visual element. -// TODO: Make a simple example here for this soon... +// TODO: Make an example here for this w/ custom Adorner class... ## Migrating from WPF -The WinUI Adorner API surface adapts many similar names and concepts as WPF Adorners; however, WinUI Adorners are XAML based and make use of the attached properties to make using Adorners much simpler, like Behaviors. Where as defining Adorners in WPF required custom drawing routines. It's possible to replicate many similar scenarios with this new API surface and make better use of XAML features like data binding; however, it will mean rewriting any existing WPF code. +The WinUI Adorner API surface adapts many similar names and concepts as WPF Adorners; however, WinUI Adorners are XAML based and make use of the attached properties to make using Adorners much simpler, like Behaviors. Where as defining Adorners in WPF required custom drawing routines. It's possible to replicate many similar scenarios with this new API surface and make better use of XAML features like data binding and styling; however, it will mean rewriting any existing WPF code. ### Concepts -The `AdornerLayer` is still an element of the visual tree which resides atop other content within your app and is the parent of all adorners. In WPF, this is usually already automatically a component of your app or `ScrollViewer`. Like WPF, adorners parent's in the visual tree will be the `AdornerLayer` and not the adorned element. +The `AdornerLayer` is still an element of the visual tree which resides atop other content within your app and is the parent of all adorners. In WPF, this is usually already automatically a component of your app or `ScrollViewer`. Like WPF, adorners parent's in the visual tree will be the `AdornerLayer` and not the adorned element. The WinUI-based `AdornerLayer` will automatically be inserted in many common scenarios, otherwise, an `AdornerDecorator` may still be used to direct the placement of the `AdornerLayer` within the Visual Tree. The `AdornerDecorator` provides a similar purpose to that of its WPF counterpart, it will host an `AdornerLayer`. The main difference with the WinUI API is that the `AdornerDecorator` will wrap your contained content vs. in WPF it sat as a sibling to your content. We feel this makes it easier to use and ensure your adorned elements reside atop your adorned content, it also makes it easier to find within the Visual Tree for performance reasons. -TODO: Adorner class info... +The `Adorner` class in WinUI is now a XAML-based element that can contain any content you wish to overlay atop your adorned element. In WPF, this was a non-visual class that required custom drawing logic to render the adorner's content. This change allows for easier creation of adorners using XAML, data binding, and styling. Many similar concepts and properties still exist between the two, like a reference to the `AdornedElement`. Any loose XAML attached via the `AdornerLayer.Xaml` attached property is automatically wrapped within a basic `Adorner` container. You can either restyle or subclass the `Adorner` class in order to better encapsulate logic of a custom `Adorner` for your specific scenario, like a behavior, as shown above. diff --git a/components/Adorners/src/Adorner.cs b/components/Adorners/src/Adorner.cs new file mode 100644 index 000000000..57072584e --- /dev/null +++ b/components/Adorners/src/Adorner.cs @@ -0,0 +1,98 @@ +using CommunityToolkit.WinUI.Helpers; + +namespace CommunityToolkit.WinUI; + +/// +/// A class which represents a that decorates a . +/// +/// +/// An adorner is a custom element which is bound to a specific and can +/// provide additional visual cues to the user. Adorners are rendered in an +/// , a special layer that is on top of the adorned element or a collection +/// of adorned elements. Rendering of an adorner is independent of the UIElement it is bound to. An +/// adorner is typically positioned relative to the element it is bound to based on the upper-left +/// coordinate origin of the adorned element. +/// +/// Note: The parent of an is always an and not the element being adorned. +/// +public partial class Adorner : ContentControl +{ + /// + /// Gets the element being adorned by this . + /// + public UIElement? AdornedElement + { + get; + internal set + { + var oldvalue = field; + field = value; + OnAdornedElementChanged(oldvalue, value); + } + } + + private void OnAdornedElementChanged(UIElement? oldvalue, UIElement? newvalue) + { + if (oldvalue is not null + && oldvalue is FrameworkElement oldfe) + { + // TODO: Should we explicitly detach the WEL here? + } + + if (newvalue is not null + && newvalue is FrameworkElement newfe) + { + // Track changes to the AdornedElement's size + var weakPropertyChangedListenerSize = new WeakEventListener(this) + { + OnEventAction = static (instance, source, eventArgs) => instance.OnSizeChanged(source, eventArgs), + OnDetachAction = (weakEventListener) => newfe.SizeChanged -= weakEventListener.OnEvent // Use Local References Only + }; + newfe.SizeChanged += weakPropertyChangedListenerSize.OnEvent; + + // Track changes to the AdornedElement's layout + // Note: This is pretty spammy, thinking we don't need this? + /*var weakPropertyChangedListenerLayout = new WeakEventListener(this) + { + OnEventAction = static (instance, source, eventArgs) => instance.OnLayoutUpdated(source, eventArgs), + OnDetachAction = (weakEventListener) => newfe.LayoutUpdated -= weakEventListener.OnEvent // Use Local References Only + }; + newfe.LayoutUpdated += weakPropertyChangedListenerLayout.OnEvent;*/ + + // Initial size & layout update + OnSizeChanged(null, null!); + OnLayoutUpdated(null, null!); + } + } + + private void OnSizeChanged(object? sender, SizeChangedEventArgs e) + { + if (AdornedElement is null) return; + + Width = AdornedElement.ActualSize.X; + Height = AdornedElement.ActualSize.Y; + } + + internal void OnLayoutUpdated(object? sender, object e) + { + // Note: Also called by the parent AdornerLayer when its size changes + if (AdornerLayer is not null + && AdornedElement is not null) + { + var coord = AdornerLayer.CoordinatesTo(AdornedElement); + + Canvas.SetLeft(this, coord.X); + Canvas.SetTop(this, coord.Y); + } + } + + internal AdornerLayer? AdornerLayer { get; set; } + + /// + /// Constructs a new instance of . + /// + public Adorner() + { + this.DefaultStyleKey = typeof(Adorner); + } +} diff --git a/components/Adorners/src/Adorner.xaml b/components/Adorners/src/Adorner.xaml new file mode 100644 index 000000000..09bea3a0d --- /dev/null +++ b/components/Adorners/src/Adorner.xaml @@ -0,0 +1,32 @@ + + + + diff --git a/components/Adorners/src/AdornerLayer.cs b/components/Adorners/src/AdornerLayer.cs index 612981958..214d7d03e 100644 --- a/components/Adorners/src/AdornerLayer.cs +++ b/components/Adorners/src/AdornerLayer.cs @@ -50,17 +50,12 @@ public AdornerLayer() private void AdornerLayer_SizeChanged(object sender, SizeChangedEventArgs e) { - foreach (var adorner in Children) + foreach (var adornerXaml in Children) { - if (adorner is Border border && border.Tag is FrameworkElement adornedElement) + if (adornerXaml is Adorner adorner) { - border.Width = adornedElement.ActualWidth; - border.Height = adornedElement.ActualHeight; - - var coord = this.CoordinatesTo(adornedElement); - - Canvas.SetLeft(border, coord.X); - Canvas.SetTop(border, coord.Y); + // Notify each adorner that our general layout has updated. + adorner.OnLayoutUpdated(null, EventArgs.Empty); } } } @@ -229,38 +224,40 @@ private static async void XamlPropertyFrameworkElement_Loaded(object sender, Rou } // TODO: Temp helper? Build into 'Adorner' base class? - private static void AttachAdorner(AdornerLayer layer, FrameworkElement adornedElement, UIElement adorner) + private static void AttachAdorner(AdornerLayer layer, FrameworkElement adornedElement, UIElement adornerXaml) { - // Add adorner XAML content to the Adorner Layer - - var border = new Border() + if (adornerXaml is Adorner adorner) { - Child = adorner, - Width = adornedElement.ActualWidth, // TODO: Register/tie to size of element better for changes. - Height = adornedElement.ActualHeight, - HorizontalAlignment = HorizontalAlignment.Stretch, - VerticalAlignment = VerticalAlignment.Stretch, - Tag = adornedElement, - }; - - var coord = layer.CoordinatesTo(adornedElement); + // We already have an adorner type, use it directly. + } + else + { + adorner = new Adorner() + { + Content = adornerXaml, + }; + } - Canvas.SetLeft(border, coord.X); - Canvas.SetTop(border, coord.Y); + // Add adorner XAML content to the Adorner Layer + adorner.AdornerLayer = layer; + adorner.AdornedElement = adornedElement; - layer.Children.Add(border); + layer.Children.Add(adorner); } - private static void RemoveAdorner(AdornerLayer layer, UIElement adorner) + private static void RemoveAdorner(AdornerLayer layer, UIElement adornerXaml) { - var border = adorner.FindAscendant(); + var adorner = adornerXaml.FindAscendant(); - if (border != null) + if (adorner != null) { - layer.Children.Remove(border); + adorner.AdornedElement = null; + adorner.AdornerLayer = null; + + layer.Children.Remove(adorner); #if !HAS_UNO - VisualTreeHelper.DisconnectChildrenRecursive(border); + VisualTreeHelper.DisconnectChildrenRecursive(adorner); #endif } } diff --git a/components/Adorners/src/CommunityToolkit.WinUI.Adorners.csproj b/components/Adorners/src/CommunityToolkit.WinUI.Adorners.csproj index fcb853361..548041b9e 100644 --- a/components/Adorners/src/CommunityToolkit.WinUI.Adorners.csproj +++ b/components/Adorners/src/CommunityToolkit.WinUI.Adorners.csproj @@ -7,6 +7,7 @@ CommunityToolkit.WinUI.Controls.AdornersRns + preview diff --git a/components/Adorners/src/Dependencies.props b/components/Adorners/src/Dependencies.props index 12520a351..2d0ca4fff 100644 --- a/components/Adorners/src/Dependencies.props +++ b/components/Adorners/src/Dependencies.props @@ -12,10 +12,12 @@ + + diff --git a/components/Adorners/src/Themes/Generic.xaml b/components/Adorners/src/Themes/Generic.xaml index d721f8e3a..42a3fc901 100644 --- a/components/Adorners/src/Themes/Generic.xaml +++ b/components/Adorners/src/Themes/Generic.xaml @@ -1,6 +1,7 @@ - + diff --git a/components/Adorners/tests/ExampleAdornersTestPage.xaml b/components/Adorners/tests/ExampleAdornersTestPage.xaml index 70538ef63..c099117ce 100644 --- a/components/Adorners/tests/ExampleAdornersTestPage.xaml +++ b/components/Adorners/tests/ExampleAdornersTestPage.xaml @@ -1,14 +1,14 @@ - + - + From 1dca1c9d61a1681308ccfd1665e8979a2127a91d Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Sun, 23 Nov 2025 01:23:46 -0800 Subject: [PATCH 10/34] Add another Adorner example with a TabViewItem InfoBadge adorner TODO: Need to handle the Tab closing (i.e. AdornedElement unloads/disappears should also detach/remove adorner from AdornerLayer) --- components/Adorners/samples/Adorners.md | 4 +++ .../samples/AdornersTabBadgeSample.xaml | 26 +++++++++++++++++++ .../samples/AdornersTabBadgeSample.xaml.cs | 16 ++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 components/Adorners/samples/AdornersTabBadgeSample.xaml create mode 100644 components/Adorners/samples/AdornersTabBadgeSample.xaml.cs diff --git a/components/Adorners/samples/Adorners.md b/components/Adorners/samples/Adorners.md index 390d43ee0..3d37ae82b 100644 --- a/components/Adorners/samples/Adorners.md +++ b/components/Adorners/samples/Adorners.md @@ -36,6 +36,10 @@ However, with an Adorner instead, you can abstract this behavior from the conten > [!SAMPLE AdornersInfoBadgeSample] +You can see how Adorners react to more dynamic content, like a TabViewItem here: + +> [!SAMPLE AdornersTabBadgeSample] + ## Highlight Example Adorners can be used in a variety of scenarios. For instance, if you wanted to highlight an element and show it's alignment to other elements in a creativity app: diff --git a/components/Adorners/samples/AdornersTabBadgeSample.xaml b/components/Adorners/samples/AdornersTabBadgeSample.xaml new file mode 100644 index 000000000..5503667a3 --- /dev/null +++ b/components/Adorners/samples/AdornersTabBadgeSample.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/components/Adorners/samples/AdornersTabBadgeSample.xaml.cs b/components/Adorners/samples/AdornersTabBadgeSample.xaml.cs new file mode 100644 index 000000000..6b5e8690c --- /dev/null +++ b/components/Adorners/samples/AdornersTabBadgeSample.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 AdornersExperiment.Samples; + +[ToolkitSampleBoolOption("IsAdornerVisible", true, Title = "Is Adorner Visible")] + +[ToolkitSample(id: nameof(AdornersTabBadgeSample), "InfoBadge w/ Adorner in TabView", description: "A sample for showing how add an InfoBadge to a TabViewItem via an Adorner.")] +public sealed partial class AdornersTabBadgeSample : Page +{ + public AdornersTabBadgeSample() + { + this.InitializeComponent(); + } +} From 277890a74510c1fa30092fd87b933f5c027d0493 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Sun, 23 Nov 2025 01:26:35 -0800 Subject: [PATCH 11/34] Fix XAML Styling again... --- components/Adorners/tests/ExampleAdornersTestPage.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/Adorners/tests/ExampleAdornersTestPage.xaml b/components/Adorners/tests/ExampleAdornersTestPage.xaml index c099117ce..5bac4081a 100644 --- a/components/Adorners/tests/ExampleAdornersTestPage.xaml +++ b/components/Adorners/tests/ExampleAdornersTestPage.xaml @@ -1,10 +1,10 @@ - + From b03bdc3ce6171ef6bb6485222df443fe41eecc0d Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:13:04 -0800 Subject: [PATCH 12/34] Add more to TabViewItem Adorner sample Animations + handling unloading (close) --- .../samples/AdornersTabBadgeSample.xaml | 30 +++++++++++++++++-- .../samples/AdornersTabBadgeSample.xaml.cs | 5 ++++ .../Adorners/samples/Dependencies.props | 26 +++++----------- components/Adorners/src/Adorner.cs | 15 ++++++++++ components/Adorners/src/AdornerLayer.cs | 4 +-- 5 files changed, 58 insertions(+), 22 deletions(-) diff --git a/components/Adorners/samples/AdornersTabBadgeSample.xaml b/components/Adorners/samples/AdornersTabBadgeSample.xaml index 5503667a3..c09c32a29 100644 --- a/components/Adorners/samples/AdornersTabBadgeSample.xaml +++ b/components/Adorners/samples/AdornersTabBadgeSample.xaml @@ -2,13 +2,14 @@ - + + Value="3"> + + + + + + + + + + + + + + diff --git a/components/Adorners/samples/AdornersTabBadgeSample.xaml.cs b/components/Adorners/samples/AdornersTabBadgeSample.xaml.cs index 6b5e8690c..9661cf7a1 100644 --- a/components/Adorners/samples/AdornersTabBadgeSample.xaml.cs +++ b/components/Adorners/samples/AdornersTabBadgeSample.xaml.cs @@ -13,4 +13,9 @@ public AdornersTabBadgeSample() { this.InitializeComponent(); } + + private void TabView_TabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args) + { + sender.TabItems.Remove(args.Tab); + } } diff --git a/components/Adorners/samples/Dependencies.props b/components/Adorners/samples/Dependencies.props index e622e1df4..4a797a706 100644 --- a/components/Adorners/samples/Dependencies.props +++ b/components/Adorners/samples/Dependencies.props @@ -9,23 +9,13 @@ For UWP / WinAppSDK / Uno packages, place the package references here. --> - - - - + + + + - - - - - - - - - - - - - - + + + + diff --git a/components/Adorners/src/Adorner.cs b/components/Adorners/src/Adorner.cs index 57072584e..2623e3b35 100644 --- a/components/Adorners/src/Adorner.cs +++ b/components/Adorners/src/Adorner.cs @@ -62,6 +62,14 @@ private void OnAdornedElementChanged(UIElement? oldvalue, UIElement? newvalue) // Initial size & layout update OnSizeChanged(null, null!); OnLayoutUpdated(null, null!); + + // Track if AdornedElement is unloaded + var weakPropertyChangedListenerUnloaded = new WeakEventListener(this) + { + OnEventAction = static (instance, source, eventArgs) => instance.OnUnloaded(source, eventArgs), + OnDetachAction = (weakEventListener) => newfe.Unloaded -= weakEventListener.OnEvent // Use Local References Only + }; + newfe.Unloaded += weakPropertyChangedListenerUnloaded.OnEvent; } } @@ -86,6 +94,13 @@ internal void OnLayoutUpdated(object? sender, object e) } } + private void OnUnloaded(object source, RoutedEventArgs eventArgs) + { + if (AdornerLayer is null) return; + + AdornerLayer.RemoveAdorner(AdornerLayer, this); + } + internal AdornerLayer? AdornerLayer { get; set; } /// diff --git a/components/Adorners/src/AdornerLayer.cs b/components/Adorners/src/AdornerLayer.cs index 214d7d03e..2ef9410ff 100644 --- a/components/Adorners/src/AdornerLayer.cs +++ b/components/Adorners/src/AdornerLayer.cs @@ -245,9 +245,9 @@ private static void AttachAdorner(AdornerLayer layer, FrameworkElement adornedEl layer.Children.Add(adorner); } - private static void RemoveAdorner(AdornerLayer layer, UIElement adornerXaml) + internal static void RemoveAdorner(AdornerLayer layer, UIElement adornerXaml) { - var adorner = adornerXaml.FindAscendant(); + var adorner = adornerXaml.FindAscendantOrSelf(); if (adorner != null) { From 2bce0e355dd8d791db18904294e6ae02c4f94e10 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:21:58 -0800 Subject: [PATCH 13/34] Add Data Binding to Adorner TabViewItem sample --- components/Adorners/samples/Adorners.md | 4 +++- components/Adorners/samples/AdornersTabBadgeSample.xaml | 2 +- components/Adorners/samples/AdornersTabBadgeSample.xaml.cs | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/components/Adorners/samples/Adorners.md b/components/Adorners/samples/Adorners.md index 3d37ae82b..6271cc21d 100644 --- a/components/Adorners/samples/Adorners.md +++ b/components/Adorners/samples/Adorners.md @@ -36,10 +36,12 @@ However, with an Adorner instead, you can abstract this behavior from the conten > [!SAMPLE AdornersInfoBadgeSample] -You can see how Adorners react to more dynamic content, like a TabViewItem here: +You can see how Adorners react to more dynamic content with this more complete example here: > [!SAMPLE AdornersTabBadgeSample] +The above example shows how to leverage XAML animations and data binding alongside the XAML-based Adorner with a `TabViewItem` which can also move or disappear. + ## Highlight Example Adorners can be used in a variety of scenarios. For instance, if you wanted to highlight an element and show it's alignment to other elements in a creativity app: diff --git a/components/Adorners/samples/AdornersTabBadgeSample.xaml b/components/Adorners/samples/AdornersTabBadgeSample.xaml index c09c32a29..261319959 100644 --- a/components/Adorners/samples/AdornersTabBadgeSample.xaml +++ b/components/Adorners/samples/AdornersTabBadgeSample.xaml @@ -18,7 +18,7 @@ IsHitTestVisible="False" Opacity="0.9" Visibility="{x:Bind IsAdornerVisible, Mode=OneWay}" - Value="3"> + Value="{x:Bind (x:Int32)BadgeValue, Mode=OneWay}"> Date: Sun, 23 Nov 2025 02:40:01 -0800 Subject: [PATCH 14/34] Add missing header --- components/Adorners/src/Adorner.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/Adorners/src/Adorner.cs b/components/Adorners/src/Adorner.cs index 2623e3b35..cf99b86ca 100644 --- a/components/Adorners/src/Adorner.cs +++ b/components/Adorners/src/Adorner.cs @@ -1,3 +1,7 @@ +// 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.Helpers; namespace CommunityToolkit.WinUI; From 3f79dbac4a00e848c73a578a1d8ee0889fe3c6c3 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Sun, 23 Nov 2025 03:07:35 -0800 Subject: [PATCH 15/34] Fix UWP build of Sample --- components/Adorners/samples/AdornersTabBadgeSample.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Adorners/samples/AdornersTabBadgeSample.xaml.cs b/components/Adorners/samples/AdornersTabBadgeSample.xaml.cs index 05b7db1bb..a4c0f5ad1 100644 --- a/components/Adorners/samples/AdornersTabBadgeSample.xaml.cs +++ b/components/Adorners/samples/AdornersTabBadgeSample.xaml.cs @@ -15,7 +15,7 @@ public AdornersTabBadgeSample() this.InitializeComponent(); } - private void TabView_TabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args) + private void TabView_TabCloseRequested(MUXC.TabView sender, MUXC.TabViewTabCloseRequestedEventArgs args) { sender.TabItems.Remove(args.Tab); } From 79ca271da9381d7694fecf53cab905213090e827 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:54:14 -0800 Subject: [PATCH 16/34] Add an initial custom adorner sample Provide OnAttached/OnDetached helper methods and Typed abstract class --- .../Adorners/samples/Adorners.Samples.csproj | 3 +- components/Adorners/samples/Adorners.md | 10 +++ .../samples/InPlaceTextEditorAdorner.xaml | 65 +++++++++++++++ .../samples/InPlaceTextEditorAdorner.xaml.cs | 17 ++++ .../InPlaceTextEditorAdornerSample.xaml | 80 ++++++++++++++++++ .../InPlaceTextEditorAdornerSample.xaml.cs | 81 +++++++++++++++++++ components/Adorners/src/Adorner.cs | 20 +++++ components/Adorners/src/AdornerOfT.cs | 29 +++++++ components/Adorners/src/Dependencies.props | 2 + 9 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 components/Adorners/samples/InPlaceTextEditorAdorner.xaml create mode 100644 components/Adorners/samples/InPlaceTextEditorAdorner.xaml.cs create mode 100644 components/Adorners/samples/InPlaceTextEditorAdornerSample.xaml create mode 100644 components/Adorners/samples/InPlaceTextEditorAdornerSample.xaml.cs create mode 100644 components/Adorners/src/AdornerOfT.cs diff --git a/components/Adorners/samples/Adorners.Samples.csproj b/components/Adorners/samples/Adorners.Samples.csproj index c772be49d..f62617366 100644 --- a/components/Adorners/samples/Adorners.Samples.csproj +++ b/components/Adorners/samples/Adorners.Samples.csproj @@ -1,8 +1,9 @@ - + Adorners + preview diff --git a/components/Adorners/samples/Adorners.md b/components/Adorners/samples/Adorners.md index 6271cc21d..3991cfa67 100644 --- a/components/Adorners/samples/Adorners.md +++ b/components/Adorners/samples/Adorners.md @@ -48,6 +48,16 @@ Adorners can be used in a variety of scenarios. For instance, if you wanted to h > [!SAMPLE ElementHighlightAdornerSample] +The above examples highlights how adorners are sized and positioned directly atop the adorned element. This allows for relative positioning of elements within the context of the Adorner's visuals in relation to the Adorned Element itself. + +## Custom Adorner Example + +Adorners can be subclassed in order to encapsulate specific logic and/or styling for your scenario. For instance, you may want to create a custom Adorner that allows a user to edit a piece of text in place: + +> [!SAMPLE InPlaceTextEditorAdornerSample] + +Adorners are templated controls, but you can use a class-backed resource dictionary to better enable usage of x:Bind for easier creation. + ## TODO: Resize Example Another common use case for adorners is to allow a user to resize a visual element. diff --git a/components/Adorners/samples/InPlaceTextEditorAdorner.xaml b/components/Adorners/samples/InPlaceTextEditorAdorner.xaml new file mode 100644 index 000000000..34f4166d5 --- /dev/null +++ b/components/Adorners/samples/InPlaceTextEditorAdorner.xaml @@ -0,0 +1,65 @@ + + + + + diff --git a/components/Adorners/samples/InPlaceTextEditorAdorner.xaml.cs b/components/Adorners/samples/InPlaceTextEditorAdorner.xaml.cs new file mode 100644 index 000000000..51f32bd55 --- /dev/null +++ b/components/Adorners/samples/InPlaceTextEditorAdorner.xaml.cs @@ -0,0 +1,17 @@ +// 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 AdornersExperiment.Samples; + +public sealed partial class InPlaceTextEditorAdornerResources : 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 InPlaceTextEditorAdornerResources() + { + this.InitializeComponent(); + } +} diff --git a/components/Adorners/samples/InPlaceTextEditorAdornerSample.xaml b/components/Adorners/samples/InPlaceTextEditorAdornerSample.xaml new file mode 100644 index 000000000..8d2ec92ba --- /dev/null +++ b/components/Adorners/samples/InPlaceTextEditorAdornerSample.xaml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + diff --git a/components/Adorners/samples/InPlaceTextEditorAdornerSample.xaml.cs b/components/Adorners/samples/InPlaceTextEditorAdornerSample.xaml.cs new file mode 100644 index 000000000..515c03fe9 --- /dev/null +++ b/components/Adorners/samples/InPlaceTextEditorAdornerSample.xaml.cs @@ -0,0 +1,81 @@ +// 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; +using Windows.Foundation.Metadata; + +namespace AdornersExperiment.Samples; + +[ToolkitSample(id: nameof(InPlaceTextEditorAdornerSample), "In place text editor Adorner", description: "A sample for showing how add a popup TextBox component via an Adorner of a TextBlock.")] +public sealed partial class InPlaceTextEditorAdornerSample : Page +{ + public InPlaceTextEditorAdornerSample() + { + this.InitializeComponent(); + } +} + +public sealed partial class InPlaceTextEditorAdorner : Adorner +{ + /// + /// Gets or sets whether the popup is open. + /// + public bool IsPopupOpen + { + get { return (bool)GetValue(IsPopupOpenProperty); } + set { SetValue(IsPopupOpenProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsPopupOpenProperty = + DependencyProperty.Register("IsPopupOpen", typeof(bool), typeof(InPlaceTextEditorAdorner), new PropertyMetadata(false)); + + private string _originalText = string.Empty; + + public InPlaceTextEditorAdorner() + { + this.DefaultStyleKey = typeof(InPlaceTextEditorAdorner); + + // Uno workaround + DataContext = this; + } + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + } + + protected override void OnAttached() + { + base.OnAttached(); + + AdornedElement?.Tapped += AdornedElement_Tapped; + } + + protected override void OnDetaching() + { + base.OnDetaching(); + + AdornedElement?.Tapped -= AdornedElement_Tapped; + } + + private void AdornedElement_Tapped(object sender, TappedRoutedEventArgs e) + { + _originalText = AdornedElement?.Text ?? string.Empty; + IsPopupOpen = true; + } + + public void ConfirmButton_Click(object sender, RoutedEventArgs e) + { + IsPopupOpen = false; + } + + public void CloseButton_Click(object sender, RoutedEventArgs e) + { + AdornedElement?.Text = _originalText; + IsPopupOpen = false; + } +} diff --git a/components/Adorners/src/Adorner.cs b/components/Adorners/src/Adorner.cs index cf99b86ca..fa94415cf 100644 --- a/components/Adorners/src/Adorner.cs +++ b/components/Adorners/src/Adorner.cs @@ -74,6 +74,8 @@ private void OnAdornedElementChanged(UIElement? oldvalue, UIElement? newvalue) OnDetachAction = (weakEventListener) => newfe.Unloaded -= weakEventListener.OnEvent // Use Local References Only }; newfe.Unloaded += weakPropertyChangedListenerUnloaded.OnEvent; + + OnAttached(); } } @@ -102,6 +104,8 @@ private void OnUnloaded(object source, RoutedEventArgs eventArgs) { if (AdornerLayer is null) return; + OnDetaching(); + AdornerLayer.RemoveAdorner(AdornerLayer, this); } @@ -114,4 +118,20 @@ public Adorner() { this.DefaultStyleKey = typeof(Adorner); } + + /// + /// Called after the is attached to the . + /// + /// + /// Override this method in a subclass to initiate functionality of the . + /// + protected virtual void OnAttached() { } + + /// + /// Called when the is being detached from the . + /// + /// + /// Override this method to unhook functionality from the . + /// + protected virtual void OnDetaching() { } } diff --git a/components/Adorners/src/AdornerOfT.cs b/components/Adorners/src/AdornerOfT.cs new file mode 100644 index 000000000..e9e375a13 --- /dev/null +++ b/components/Adorners/src/AdornerOfT.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; + +/// +/// A base class for s allowing for explicit types. +/// +/// The object type to attach to +public abstract class Adorner : Adorner where T : UIElement +{ + /// + public new T? AdornedElement + { + get { return base.AdornedElement as T; } + } + + /// + protected override void OnAttached() + { + base.OnAttached(); + + if (this.AdornedElement is null) + { + throw new InvalidOperationException($"AdornedElement {base.AdornedElement?.GetType().FullName} is not of type {typeof(T).FullName}"); + } + } +} diff --git a/components/Adorners/src/Dependencies.props b/components/Adorners/src/Dependencies.props index 2d0ca4fff..5acb20497 100644 --- a/components/Adorners/src/Dependencies.props +++ b/components/Adorners/src/Dependencies.props @@ -11,12 +11,14 @@ + + From 4999a38ac90ce1010c06b5d71886f0cfdaea434e Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 25 Nov 2025 23:51:21 -0800 Subject: [PATCH 17/34] Move more complex InPlaceTextEditor Sample to its own subdirectory (need to adjust namespace to match for Sample App to find) --- .../{ => InPlaceTextEditor}/InPlaceTextEditorAdorner.xaml | 5 +++-- .../{ => InPlaceTextEditor}/InPlaceTextEditorAdorner.xaml.cs | 2 +- .../InPlaceTextEditorAdornerSample.xaml | 4 ++-- .../InPlaceTextEditorAdornerSample.xaml.cs | 3 +-- 4 files changed, 7 insertions(+), 7 deletions(-) rename components/Adorners/samples/{ => InPlaceTextEditor}/InPlaceTextEditorAdorner.xaml (93%) rename components/Adorners/samples/{ => InPlaceTextEditor}/InPlaceTextEditorAdorner.xaml.cs (90%) rename components/Adorners/samples/{ => InPlaceTextEditor}/InPlaceTextEditorAdornerSample.xaml (96%) rename components/Adorners/samples/{ => InPlaceTextEditor}/InPlaceTextEditorAdornerSample.xaml.cs (96%) diff --git a/components/Adorners/samples/InPlaceTextEditorAdorner.xaml b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdorner.xaml similarity index 93% rename from components/Adorners/samples/InPlaceTextEditorAdorner.xaml rename to components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdorner.xaml index 34f4166d5..815b0739c 100644 --- a/components/Adorners/samples/InPlaceTextEditorAdorner.xaml +++ b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdorner.xaml @@ -1,10 +1,11 @@ - + + xmlns:local="using:AdornersExperiment.Samples.InPlaceTextEditor"> + diff --git a/components/Adorners/samples/InputValidation/InputValidationAdorner.xaml.cs b/components/Adorners/samples/InputValidation/InputValidationAdorner.xaml.cs new file mode 100644 index 000000000..fc6a0a51b --- /dev/null +++ b/components/Adorners/samples/InputValidation/InputValidationAdorner.xaml.cs @@ -0,0 +1,17 @@ +// 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 AdornersExperiment.Samples.InputValidation; + +public sealed partial class InputValidationAdornerResources : 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 InputValidationAdornerResources() + { + this.InitializeComponent(); + } +} diff --git a/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml b/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml new file mode 100644 index 000000000..2bd0859fe --- /dev/null +++ b/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /// The object type to attach to -public abstract class Adorner : Adorner where T : UIElement +public abstract partial class Adorner : Adorner where T : UIElement { /// public new T? AdornedElement From a8448750594555b88f3502ae56eafdbe160718bb Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 2 Dec 2025 22:12:18 -0800 Subject: [PATCH 22/34] Simplify the InPlaceTextEditorAdorner sample Note: This sample doesn't work on UWP for some weird issue with Popup.PlacementTarget at runtime, even though it should be supported according to the docs. --- .../InPlaceTextEditorAdornerSample.xaml | 10 +++---- .../InPlaceTextEditorAdornerSample.xaml.cs | 30 +++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml index fd336138f..1eb5c98e3 100644 --- a/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml +++ b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml @@ -1,4 +1,4 @@ - + - - + + - + diff --git a/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml.cs b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml.cs index e50298789..28171f7f0 100644 --- a/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml.cs +++ b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml.cs @@ -64,6 +64,21 @@ public void EndEdit() /// public sealed partial class InPlaceTextEditorAdorner : Adorner { + /// + /// Gets or sets the object being edited. + /// + public IEditableObject EditableObject + { + get { return (IEditableObject)GetValue(EditableObjectProperty); } + set { SetValue(EditableObjectProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty EditableObjectProperty = + DependencyProperty.Register(nameof(EditableObject), typeof(IEditableObject), typeof(InPlaceTextEditorAdorner), new PropertyMetadata(null)); + /// /// Gets or sets whether the popup is open. /// @@ -108,28 +123,19 @@ protected override void OnDetaching() private void AdornedElement_Tapped(object sender, TappedRoutedEventArgs e) { - if (AdornedElement?.DataContext is IEditableObject editableObject) - { - editableObject.BeginEdit(); - } + EditableObject?.BeginEdit(); IsPopupOpen = true; } public void ConfirmButton_Click(object sender, RoutedEventArgs e) { - if (AdornedElement?.DataContext is IEditableObject editableObject) - { - editableObject.EndEdit(); - } + EditableObject?.EndEdit(); IsPopupOpen = false; } public void CloseButton_Click(object sender, RoutedEventArgs e) { - if (AdornedElement?.DataContext is IEditableObject editableObject) - { - editableObject.CancelEdit(); - } + EditableObject?.CancelEdit(); IsPopupOpen = false; } } From b24245d09b5aeb6ef59b0e2b7f3aad6de3175e49 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:01:19 -0800 Subject: [PATCH 23/34] Move InputValidationAdorner from sample to Custom Adorner in NuGet Package Makes more type-safe and easier to use based on community feedback by placing context on Adorner itself (NotifyDataErrorInfo property) - akin to EditableObject sample change as well. Allows for string collection of validation messages as well as existing ValidationResult behavior. Separate out InputValidationAdorner into its own doc TODO: Issue with form clearing newly corrected fields... --- components/Adorners/samples/Adorners.md | 10 +- .../InputValidationAdornerSample.xaml | 59 ++----- .../InputValidationAdornerSample.xaml.cs | 116 ------------- .../samples/InputValidationAdorner.md | 45 +++++ .../InputValidation/InputValidationAdorner.cs | 155 ++++++++++++++++++ .../InputValidationAdorner.xaml | 12 +- .../InputValidationAdorner.xaml.cs | 9 +- components/Adorners/src/Themes/Generic.xaml | 9 +- 8 files changed, 237 insertions(+), 178 deletions(-) create mode 100644 components/Adorners/samples/InputValidationAdorner.md create mode 100644 components/Adorners/src/InputValidation/InputValidationAdorner.cs rename components/Adorners/{samples => src}/InputValidation/InputValidationAdorner.xaml (87%) rename components/Adorners/{samples => src}/InputValidation/InputValidationAdorner.xaml.cs (66%) diff --git a/components/Adorners/samples/Adorners.md b/components/Adorners/samples/Adorners.md index 5553b4e0e..4efe9d026 100644 --- a/components/Adorners/samples/Adorners.md +++ b/components/Adorners/samples/Adorners.md @@ -2,7 +2,7 @@ title: Adorners author: michael-hawker description: Adorners let you overlay content on top of your XAML components in a separate layer on top of everything else. -keywords: Adorners, Control, Layout +keywords: Adorners, Control, Layout, InfoBadge, AdornerLayer, AdornerDecorator, Adorner, Input Validation, Highlighting dev_langs: - csharp category: Controls @@ -60,13 +60,7 @@ The following example uses `IEditableObject` to control the editing lifecycle co Adorners are template-based controls, but you can use a class-backed resource dictionary to better enable usage of x:Bind for easier creation and binding to the `AdornedElement`, as seen here. -## Input Validation Example - -The custom adorner example above can be further extended to provide input validation feedback to the user using the standard `INotifyDataErrorInfo` interface. -We use the `ObservableValidator` class from the `CommunityToolkit.Mvvm` package to provide validation rules for our view model properties. -When the user submits invalid input, the adorner displays a red border around the text box and shows a tooltip with the validation error message: - -> [!SAMPLE InputValidationAdornerSample] +You can see other example of custom adorners with the other Adorner help topics for the built-in adorners provided in this package, such as the `InputValidationAdorner`. ## TODO: Resize Example diff --git a/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml b/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml index 2bd0859fe..a5606e46b 100644 --- a/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml +++ b/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml @@ -1,7 +1,8 @@ - + - - - - - - + - - + @@ -61,8 +30,8 @@ PlaceholderText="Last name" Text="{x:Bind ViewModel.LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> - + @@ -71,8 +40,8 @@ PlaceholderText="Email" Text="{x:Bind ViewModel.Email, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> - + @@ -81,8 +50,8 @@ PlaceholderText="Phone number" Text="{x:Bind ViewModel.PhoneNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> - + @@ -94,8 +63,8 @@ SpinButtonPlacementMode="Inline" Value="{x:Bind ViewModel.Age, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> - + diff --git a/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml.cs b/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml.cs index 23813af5d..828f0a3c2 100644 --- a/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml.cs +++ b/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml.cs @@ -76,119 +76,3 @@ private void Submit() } } } - -/// -/// An Adorner that shows an error message if Data Validation fails. -/// The adorned control's must implement . It assumes that the return of is a collection. -/// -public sealed partial class InputValidationAdorner : Adorner -{ - /// - /// Gets or sets the name of the property this adorner should look for errors on. - /// - public string PropertyName - { - get { return (string)GetValue(PropertyNameProperty); } - set { SetValue(PropertyNameProperty, value); } - } - - /// - /// Identifies the dependency property. - /// - public static readonly DependencyProperty PropertyNameProperty = - DependencyProperty.Register(nameof(PropertyName), typeof(string), typeof(InputValidationAdorner), new PropertyMetadata(null)); - - /// - /// Gets or sets whether the popup is open. - /// - public bool HasValidationFailed - { - get { return (bool)GetValue(HasValidationFailedProperty); } - set { SetValue(HasValidationFailedProperty, value); } - } - - /// - /// Identifies the dependency property. - /// - public static readonly DependencyProperty HasValidationFailedProperty = - DependencyProperty.Register(nameof(HasValidationFailed), typeof(bool), typeof(InputValidationAdorner), new PropertyMetadata(false)); - - /// - /// Gets or sets the validation message for this failed property. - /// - public string ValidationMessage - { - get { return (string)GetValue(ValidationMessageProperty); } - set { SetValue(ValidationMessageProperty, value); } - } - - /// - /// Identifies the dependency property. - /// - public static readonly DependencyProperty ValidationMessageProperty = - DependencyProperty.Register(nameof(ValidationMessage), typeof(string), typeof(InputValidationAdorner), new PropertyMetadata(null)); - - private INotifyDataErrorInfo? _dataErrorInfo; - - public InputValidationAdorner() - { - this.DefaultStyleKey = typeof(InputValidationAdorner); - - // Uno workaround - DataContext = this; - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - } - - protected override void OnAttached() - { - base.OnAttached(); - - if (AdornedElement?.DataContext is INotifyDataErrorInfo iError) - { - _dataErrorInfo = iError; - _dataErrorInfo.ErrorsChanged += this.INotifyDataErrorInfo_ErrorsChanged; - } - } - - private void INotifyDataErrorInfo_ErrorsChanged(object? sender, DataErrorsChangedEventArgs e) - { - if (_dataErrorInfo is not null) - { - // Reset state - if (!_dataErrorInfo.HasErrors) - { - HasValidationFailed = false; - ValidationMessage = string.Empty; - return; - } - - if (e.PropertyName == PropertyName) - { - HasValidationFailed = true; - - StringBuilder message = new(); - foreach (ValidationResult result in _dataErrorInfo.GetErrors(e.PropertyName)) - { - message.AppendLine(result.ErrorMessage); - } - - ValidationMessage = message.ToString().Trim(); - } - } - } - - protected override void OnDetaching() - { - if (_dataErrorInfo is not null) - { - _dataErrorInfo.ErrorsChanged -= this.INotifyDataErrorInfo_ErrorsChanged; - _dataErrorInfo = null; - } - - base.OnDetaching(); - } -} diff --git a/components/Adorners/samples/InputValidationAdorner.md b/components/Adorners/samples/InputValidationAdorner.md new file mode 100644 index 000000000..4fa8682db --- /dev/null +++ b/components/Adorners/samples/InputValidationAdorner.md @@ -0,0 +1,45 @@ +--- +title: InputValidationAdorner +author: michael-hawker +description: An InputValidationAdorner provides input validation to any element implementing INotifyDataErrorInfo to provide feedback to a user. +keywords: Adorners, Input Validation, INotifyDataErrorInfo, MVVM, CommunityToolkit.Mvvm +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 278 +issue-id: 0 +icon: assets/icon.png +--- + +# InputValidationAdorner + +The `InputValidationAdorner` provides input validation to any element implementing `INotifyDataErrorInfo` to provide feedback to a user. + +## Background + +Input Validation existed in WPF and was available in a couple of ways. See the Migrating from WPF section below for more details on the differences between WPF and WinUI Input Validation. + +## Input Validation Example + +The `InputValidationAdorner` can be attached to any element and triggered to be shown automatically based on validation provided by the `INotifyDataErrorInfo` interface set on the `NotifyDataErrorInfo` property of the adorner. + +The custom adorner will automatically display the validation message for the specified `PropertyName` is marked as invalid by the `INotifyDataErrorInfo` implementation. + +For the example below, we use the `ObservableValidator` class from the `CommunityToolkit.Mvvm` package to provide automatic validation of the rules within our view model properties. +When the user submits invalid input, the adorner displays a red border around the text box and shows a tooltip with the validation error message: + +> [!SAMPLE InputValidationAdornerSample] + +## Migrating from WPF + +Input Validation within WinUI is handled as a mix of both of WPF's [Binding Validation](https://learn.microsoft.com/dotnet/desktop/wpf/data/how-to-implement-binding-validation) and [Custom Object Validation](https://learn.microsoft.com/dotnet/desktop/wpf/data/how-to-implement-validation-logic-on-custom-objects). + +> [!WARNING] +> That the WinUI Adorner uses the `INotifyDataErrorInfo` interface for validation feedback, whereas WPF's Custom Object Validation uses the `IDataErrorInfo` interface. You will need to adapt your validation logic accordingly when migrating from WPF to WinUI. + +> [!NOTE] +> The `ValidationRule` Binding concept from WPF is not supported in WinUI. You will need to implement validation logic within your view model or data model using the `INotifyDataErrorInfo` interface instead. +> You can still specify a custom error template by styling the `InputValidationAdorner` control. + +When paired with the validation provided by the `CommunityToolkit.Mvvm` package, you can achieve similar functionality to WPF's Input Validation with less boilerplate code. diff --git a/components/Adorners/src/InputValidation/InputValidationAdorner.cs b/components/Adorners/src/InputValidation/InputValidationAdorner.cs new file mode 100644 index 000000000..769bae219 --- /dev/null +++ b/components/Adorners/src/InputValidation/InputValidationAdorner.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. + +using System.ComponentModel.DataAnnotations; + +using INotifyDataErrorInfo = System.ComponentModel.INotifyDataErrorInfo; +using DataErrorsChangedEventArgs = System.ComponentModel.DataErrorsChangedEventArgs; + +namespace CommunityToolkit.WinUI.Adorners; + +/// +/// An Adorner that shows an error message if Data Validation fails. +/// Set the with the object that must implement . It assumes that the return of is a or string collection. +/// Adorner is shown automatically when the event is raised and the matches the invalid property of the event arguments. +/// +public sealed partial class InputValidationAdorner : Adorner +{ + /// + /// Gets or sets the name of the property this adorner should look for errors on. + /// + public string PropertyName + { + get { return (string)GetValue(PropertyNameProperty); } + set { SetValue(PropertyNameProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty PropertyNameProperty = + DependencyProperty.Register(nameof(PropertyName), typeof(string), typeof(InputValidationAdorner), new PropertyMetadata(null)); + + /// + /// Gets or sets the context object to use for validation. + /// + public INotifyDataErrorInfo NotifyDataErrorInfo + { + get { return (INotifyDataErrorInfo)GetValue(NotifyDataErrorInfoProperty); } + set { SetValue(NotifyDataErrorInfoProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty NotifyDataErrorInfoProperty = + DependencyProperty.Register(nameof(NotifyDataErrorInfo), typeof(INotifyDataErrorInfo), typeof(InputValidationAdorner), new PropertyMetadata(null)); + + /// + /// Gets or sets whether the validation adorners is displayed (handled automatically). + /// + public bool HasValidationFailed + { + get { return (bool)GetValue(HasValidationFailedProperty); } + set { SetValue(HasValidationFailedProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HasValidationFailedProperty = + DependencyProperty.Register(nameof(HasValidationFailed), typeof(bool), typeof(InputValidationAdorner), new PropertyMetadata(false)); + + /// + /// Gets or sets the validation message for this failed property, set automatically by the adorner. + /// + public string ValidationMessage + { + get { return (string)GetValue(ValidationMessageProperty); } + set { SetValue(ValidationMessageProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ValidationMessageProperty = + DependencyProperty.Register(nameof(ValidationMessage), typeof(string), typeof(InputValidationAdorner), new PropertyMetadata(null)); + + /// + /// Initializes a new instance of the class. + /// + public InputValidationAdorner() + { + this.DefaultStyleKey = typeof(InputValidationAdorner); + + // Uno workaround + DataContext = this; + } + + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + } + + /// + protected override void OnAttached() + { + base.OnAttached(); + + NotifyDataErrorInfo?.ErrorsChanged += this.INotifyDataErrorInfo_ErrorsChanged; + } + + private void INotifyDataErrorInfo_ErrorsChanged(object? sender, DataErrorsChangedEventArgs e) + { + if (NotifyDataErrorInfo is not null) + { + // Reset state + if (!NotifyDataErrorInfo.HasErrors) + { + HasValidationFailed = false; + ValidationMessage = string.Empty; + return; + } + + if (e.PropertyName == PropertyName) + { + HasValidationFailed = true; + + var errors = NotifyDataErrorInfo.GetErrors(e.PropertyName); + + StringBuilder message = new(); + if (errors is IEnumerable validationResults) + { + foreach (ValidationResult result in validationResults) + { + message.AppendLine(result.ErrorMessage); + } + } + else if (errors is IEnumerable stringErrors) + { + foreach (string result in stringErrors) + { + message.AppendLine(result); + } + } + else + { + // TODO: Not sure if should handle more types of collections here? + throw new ArgumentException("The errors returned by INotifyDataErrorInfo.GetErrors must be of type IEnumerable or IEnumerable."); + } + + ValidationMessage = message.ToString().Trim(); + } + } + } + + /// + protected override void OnDetaching() + { + NotifyDataErrorInfo?.ErrorsChanged -= this.INotifyDataErrorInfo_ErrorsChanged; + + base.OnDetaching(); + } +} diff --git a/components/Adorners/samples/InputValidation/InputValidationAdorner.xaml b/components/Adorners/src/InputValidation/InputValidationAdorner.xaml similarity index 87% rename from components/Adorners/samples/InputValidation/InputValidationAdorner.xaml rename to components/Adorners/src/InputValidation/InputValidationAdorner.xaml index e23bf4c23..82a89d450 100644 --- a/components/Adorners/samples/InputValidation/InputValidationAdorner.xaml +++ b/components/Adorners/src/InputValidation/InputValidationAdorner.xaml @@ -1,28 +1,28 @@  - + diff --git a/components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml.cs b/components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml.cs new file mode 100644 index 000000000..266963e5b --- /dev/null +++ b/components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml.cs @@ -0,0 +1,24 @@ +// 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.Adorners.Resources; + +/// +/// XAML resource dictionary for . +/// +public sealed partial class ResizeElementAdornerResources : 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. + + /// + /// Initializes a new instance of the class. + /// + public ResizeElementAdornerResources() + { + this.InitializeComponent(); + } +} diff --git a/components/Adorners/src/ResizeElement/ResizeThumb.cs b/components/Adorners/src/ResizeElement/ResizeThumb.cs new file mode 100644 index 000000000..1ea36a003 --- /dev/null +++ b/components/Adorners/src/ResizeElement/ResizeThumb.cs @@ -0,0 +1,72 @@ +// 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. + +#if !WINAPPSDK +using CursorEnum = Windows.UI.Core.CoreCursorType; +#else +using Microsoft.UI.Input; +using CursorEnum = Microsoft.UI.Input.InputSystemCursorShape; +#endif + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// A simple Thumb control which can be manipulated in multiple directions to assist with resize scenarios. +/// +public partial class ResizeThumb : Control +{ + /// + /// Gets or sets the cursor which should be displayed when the mouse is over this thumb. + /// + public CursorEnum Cursor + { + get { return (CursorEnum)GetValue(CursorProperty); } + set { SetValue(CursorProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty CursorProperty = + DependencyProperty.Register(nameof(Cursor), typeof(CursorEnum), typeof(ResizeThumb), new PropertyMetadata(null, OnCursorChanged)); + + private static void OnCursorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ResizeThumb resizeThumb) + { +#if !WINAPPSDK + // On UWP, we use our XAML extension to control this behavior, + // so we'll update it here (and maintain any cursor override). + if (cursor is CursorEnum cursorValue) + { + FrameworkElementExtensions.SetCursor(gripper, cursorValue); + } + + return; +#endif + +#if WINUI3 + // On WinUI 3, we can set the ProtectedCursor directly. + if (e.NewValue is CursorEnum cursorValue && + (resizeThumb.ProtectedCursor == null || + (resizeThumb.ProtectedCursor is InputSystemCursor current && + current.CursorShape != cursorValue))) + { + resizeThumb.ProtectedCursor = InputSystemCursor.Create(cursorValue); + } +#endif + } + } + + /// + public ResizeThumb() + { + this.DefaultStyleKey = typeof(ResizeThumb); + } + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + } +} diff --git a/components/Adorners/src/ResizeElement/ResizeThumb.xaml b/components/Adorners/src/ResizeElement/ResizeThumb.xaml new file mode 100644 index 000000000..09df8ce61 --- /dev/null +++ b/components/Adorners/src/ResizeElement/ResizeThumb.xaml @@ -0,0 +1,29 @@ + + + + + diff --git a/components/Adorners/src/Themes/Generic.xaml b/components/Adorners/src/Themes/Generic.xaml index f5ed39a91..a974468bc 100644 --- a/components/Adorners/src/Themes/Generic.xaml +++ b/components/Adorners/src/Themes/Generic.xaml @@ -1,4 +1,4 @@ - @@ -7,6 +7,8 @@ - + + + From 808dd5e110d8b273788dd151e046c28358796a24 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Thu, 4 Dec 2025 00:49:55 -0800 Subject: [PATCH 27/34] Setup ResizeThumb control for ResizeElementAdorner, almost works... Having separate ResizeThumb which handles manipulation is basically a generalized ContentSizer, this helps encapsulate that logic as well nicely. Then the ResizeElementAdorner is just a coordinator/initializer to provide the complete package of resizing. TODO: Moving the element probably can just be handled by the underlying element with standared manipulation (do we have a Behavior for that?) and doesn't need to involve the Adorner layer (outside of syncing movement/position, which needs to be tested...) --- components/Adorners/samples/Adorners.md | 10 +- .../src/ResizeElement/ResizeElementAdorner.cs | 51 ++++++ .../ResizeElement/ResizeElementAdorner.xaml | 97 +++++------ .../ResizeElementAdorner.xaml.cs | 24 --- .../Adorners/src/ResizeElement/ResizeThumb.cs | 72 -------- .../ResizeElement/Thumb/ResizeDirection.cs | 56 ++++++ .../ResizeElement/Thumb/ResizePositionMode.cs | 21 +++ .../Thumb/ResizeThumb.Helpers.cs | 69 ++++++++ .../Thumb/ResizeThumb.Properties.cs | 163 ++++++++++++++++++ .../src/ResizeElement/Thumb/ResizeThumb.cs | 138 +++++++++++++++ .../{ => Thumb}/ResizeThumb.xaml | 0 components/Adorners/src/Themes/Generic.xaml | 4 +- 12 files changed, 546 insertions(+), 159 deletions(-) delete mode 100644 components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml.cs delete mode 100644 components/Adorners/src/ResizeElement/ResizeThumb.cs create mode 100644 components/Adorners/src/ResizeElement/Thumb/ResizeDirection.cs create mode 100644 components/Adorners/src/ResizeElement/Thumb/ResizePositionMode.cs create mode 100644 components/Adorners/src/ResizeElement/Thumb/ResizeThumb.Helpers.cs create mode 100644 components/Adorners/src/ResizeElement/Thumb/ResizeThumb.Properties.cs create mode 100644 components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs rename components/Adorners/src/ResizeElement/{ => Thumb}/ResizeThumb.xaml (100%) diff --git a/components/Adorners/samples/Adorners.md b/components/Adorners/samples/Adorners.md index 4efe9d026..eeacef555 100644 --- a/components/Adorners/samples/Adorners.md +++ b/components/Adorners/samples/Adorners.md @@ -2,7 +2,7 @@ title: Adorners author: michael-hawker description: Adorners let you overlay content on top of your XAML components in a separate layer on top of everything else. -keywords: Adorners, Control, Layout, InfoBadge, AdornerLayer, AdornerDecorator, Adorner, Input Validation, Highlighting +keywords: Adorners, Control, Layout, InfoBadge, AdornerLayer, AdornerDecorator, Adorner, Input Validation, Resize, Highlighting dev_langs: - csharp category: Controls @@ -60,13 +60,7 @@ The following example uses `IEditableObject` to control the editing lifecycle co Adorners are template-based controls, but you can use a class-backed resource dictionary to better enable usage of x:Bind for easier creation and binding to the `AdornedElement`, as seen here. -You can see other example of custom adorners with the other Adorner help topics for the built-in adorners provided in this package, such as the `InputValidationAdorner`. - -## TODO: Resize Example - -Another common use case for adorners is to allow a user to resize a visual element. - -// TODO: Make an example here for this w/ custom Adorner class... +You can see other example of custom adorners with the other Adorner help topics for the built-in adorners provided in this package, such as the `InputValidationAdorner` and `ResizeElementAdorner`. ## Migrating from WPF diff --git a/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs b/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs index 4c061628c..381f3e3a4 100644 --- a/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs +++ b/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs @@ -2,13 +2,32 @@ // 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 CommunityToolkit.WinUI.Adorners; /// /// An that will allow a user to resize the adorned element. /// +[TemplatePart(Name = nameof(TopThumbPart), Type = typeof(ResizeThumb))] +[TemplatePart(Name = nameof(BottomThumbPart), Type = typeof(ResizeThumb))] +[TemplatePart(Name = nameof(LeftThumbPart), Type = typeof(ResizeThumb))] +[TemplatePart(Name = nameof(RightThumbPart), Type = typeof(ResizeThumb))] +[TemplatePart(Name = nameof(TopLeftThumbPart), Type = typeof(ResizeThumb))] +[TemplatePart(Name = nameof(TopRightThumbPart), Type = typeof(ResizeThumb))] +[TemplatePart(Name = nameof(BottomLeftThumbPart), Type = typeof(ResizeThumb))] +[TemplatePart(Name = nameof(BottomRightThumbPart), Type = typeof(ResizeThumb))] public sealed partial class ResizeElementAdorner : Adorner { + private ResizeThumb? TopThumbPart; + private ResizeThumb? BottomThumbPart; + private ResizeThumb? LeftThumbPart; + private ResizeThumb? RightThumbPart; + private ResizeThumb? TopLeftThumbPart; + private ResizeThumb? TopRightThumbPart; + private ResizeThumb? BottomLeftThumbPart; + private ResizeThumb? BottomRightThumbPart; + /// /// Initializes a new instance of the class. /// @@ -23,18 +42,50 @@ public ResizeElementAdorner() /// protected override void OnApplyTemplate() { + OnDetaching(); + base.OnApplyTemplate(); + + TopThumbPart = GetTemplateChild(nameof(TopThumbPart)) as ResizeThumb; + BottomThumbPart = GetTemplateChild(nameof(BottomThumbPart)) as ResizeThumb; + LeftThumbPart = GetTemplateChild(nameof(LeftThumbPart)) as ResizeThumb; + RightThumbPart = GetTemplateChild(nameof(RightThumbPart)) as ResizeThumb; + TopLeftThumbPart = GetTemplateChild(nameof(TopLeftThumbPart)) as ResizeThumb; + TopRightThumbPart = GetTemplateChild(nameof(TopRightThumbPart)) as ResizeThumb; + BottomLeftThumbPart = GetTemplateChild(nameof(BottomLeftThumbPart)) as ResizeThumb; + BottomRightThumbPart = GetTemplateChild(nameof(BottomRightThumbPart)) as ResizeThumb; + + // OnApplyTemplate can be called after OnAttached, especially if the Adorner isn't initially visible, so we need to re-apply the TargetControl here. + OnAttached(); } /// protected override void OnAttached() { base.OnAttached(); + + TopThumbPart?.TargetControl = AdornedElement; + BottomThumbPart?.TargetControl = AdornedElement; + LeftThumbPart?.TargetControl = AdornedElement; + RightThumbPart?.TargetControl = AdornedElement; + TopLeftThumbPart?.TargetControl = AdornedElement; + TopRightThumbPart?.TargetControl = AdornedElement; + BottomLeftThumbPart?.TargetControl = AdornedElement; + BottomRightThumbPart?.TargetControl = AdornedElement; } /// protected override void OnDetaching() { base.OnDetaching(); + + TopThumbPart?.TargetControl = null; + BottomThumbPart?.TargetControl = null; + LeftThumbPart?.TargetControl = null; + RightThumbPart?.TargetControl = null; + TopLeftThumbPart?.TargetControl = null; + TopRightThumbPart?.TargetControl = null; + BottomLeftThumbPart?.TargetControl = null; + BottomRightThumbPart?.TargetControl = null; } } diff --git a/components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml b/components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml index 9520526f6..882caaeac 100644 --- a/components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml +++ b/components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml @@ -1,6 +1,5 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + diff --git a/components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml.cs b/components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml.cs deleted file mode 100644 index 266963e5b..000000000 --- a/components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml.cs +++ /dev/null @@ -1,24 +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.Adorners.Resources; - -/// -/// XAML resource dictionary for . -/// -public sealed partial class ResizeElementAdornerResources : 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. - - /// - /// Initializes a new instance of the class. - /// - public ResizeElementAdornerResources() - { - this.InitializeComponent(); - } -} diff --git a/components/Adorners/src/ResizeElement/ResizeThumb.cs b/components/Adorners/src/ResizeElement/ResizeThumb.cs deleted file mode 100644 index 1ea36a003..000000000 --- a/components/Adorners/src/ResizeElement/ResizeThumb.cs +++ /dev/null @@ -1,72 +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. - -#if !WINAPPSDK -using CursorEnum = Windows.UI.Core.CoreCursorType; -#else -using Microsoft.UI.Input; -using CursorEnum = Microsoft.UI.Input.InputSystemCursorShape; -#endif - -namespace CommunityToolkit.WinUI.Controls; - -/// -/// A simple Thumb control which can be manipulated in multiple directions to assist with resize scenarios. -/// -public partial class ResizeThumb : Control -{ - /// - /// Gets or sets the cursor which should be displayed when the mouse is over this thumb. - /// - public CursorEnum Cursor - { - get { return (CursorEnum)GetValue(CursorProperty); } - set { SetValue(CursorProperty, value); } - } - - /// - /// Identifies the dependency property. - /// - public static readonly DependencyProperty CursorProperty = - DependencyProperty.Register(nameof(Cursor), typeof(CursorEnum), typeof(ResizeThumb), new PropertyMetadata(null, OnCursorChanged)); - - private static void OnCursorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is ResizeThumb resizeThumb) - { -#if !WINAPPSDK - // On UWP, we use our XAML extension to control this behavior, - // so we'll update it here (and maintain any cursor override). - if (cursor is CursorEnum cursorValue) - { - FrameworkElementExtensions.SetCursor(gripper, cursorValue); - } - - return; -#endif - -#if WINUI3 - // On WinUI 3, we can set the ProtectedCursor directly. - if (e.NewValue is CursorEnum cursorValue && - (resizeThumb.ProtectedCursor == null || - (resizeThumb.ProtectedCursor is InputSystemCursor current && - current.CursorShape != cursorValue))) - { - resizeThumb.ProtectedCursor = InputSystemCursor.Create(cursorValue); - } -#endif - } - } - - /// - public ResizeThumb() - { - this.DefaultStyleKey = typeof(ResizeThumb); - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - } -} diff --git a/components/Adorners/src/ResizeElement/Thumb/ResizeDirection.cs b/components/Adorners/src/ResizeElement/Thumb/ResizeDirection.cs new file mode 100644 index 000000000..c6b34f14e --- /dev/null +++ b/components/Adorners/src/ResizeElement/Thumb/ResizeDirection.cs @@ -0,0 +1,56 @@ +// 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; + +/// +/// Specifies the direction in which a will resize its target element. +/// +public enum ResizeDirection +{ + /// + /// No resize. + /// + None, + + /// + /// Resize from the top. Manipulates the Y position and Height. + /// + Top, + + /// + /// Resize from the bottom. Manipulates the Height. + /// + Bottom, + + /// + /// Resize from the left. Manipulates the X position and Width. + /// + Left, + + /// + /// Resize from the right. Manipulates the Width. + /// + Right, + + /// + /// Resize from the upper-left corner. Manipulates the X position, Y position, Width, and Height. + /// + TopLeft, + + /// + /// Resize from the upper-right corner. Manipulates the Y position, Width, and Height. + /// + TopRight, + + /// + /// Resize from the lower-left corner. Manipulates the X position, Width, and Height. + /// + BottomLeft, + + /// + /// Resize from the lower-right corner. Manipulates the Width and Height. + /// + BottomRight, +} diff --git a/components/Adorners/src/ResizeElement/Thumb/ResizePositionMode.cs b/components/Adorners/src/ResizeElement/Thumb/ResizePositionMode.cs new file mode 100644 index 000000000..74c3842df --- /dev/null +++ b/components/Adorners/src/ResizeElement/Thumb/ResizePositionMode.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; + +/// +/// Specifies how the will adjust the position of its target element. +/// +public enum ResizePositionMode +{ + /// + /// Resize using Canvas.Left and Canvas.Top properties. + /// + Canvas, + + /// + /// Resize using 's Top and Left values. + /// + MarginTopLeft, +} diff --git a/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.Helpers.cs b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.Helpers.cs new file mode 100644 index 000000000..cfa5940e9 --- /dev/null +++ b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.Helpers.cs @@ -0,0 +1,69 @@ +// 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; + +/// +/// Protected helper methods for and subclasses. +/// +public partial class ResizeThumb : Control +{ + /// + /// Check for new requested vertical size is valid or not + /// + /// Target control being resized + /// The requested new height + /// The parent control's ActualHeight + /// Bool result if requested vertical change is valid or not + protected static bool IsValidHeight(FrameworkElement target, double newHeight, double parentActualHeight) + { + var minHeight = target.MinHeight; + if (newHeight < 0 || (!double.IsNaN(minHeight) && newHeight < minHeight)) + { + return false; + } + + var maxHeight = target.MaxHeight; + if (!double.IsNaN(maxHeight) && newHeight > maxHeight) + { + return false; + } + + if (newHeight <= parentActualHeight) + { + return false; + } + + return true; + } + + /// + /// Check for new requested horizontal size is valid or not + /// + /// Target control being resized + /// The requested new width + /// The parent control's ActualWidth + /// Bool result if requested horizontal change is valid or not + protected static bool IsValidWidth(FrameworkElement target, double newWidth, double parentActualWidth) + { + var minWidth = target.MinWidth; + if (newWidth < 0 || (!double.IsNaN(minWidth) && newWidth < minWidth)) + { + return false; + } + + var maxWidth = target.MaxWidth; + if (!double.IsNaN(maxWidth) && newWidth > maxWidth) + { + return false; + } + + if (newWidth <= parentActualWidth) + { + return false; + } + + return true; + } +} diff --git a/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.Properties.cs b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.Properties.cs new file mode 100644 index 000000000..f0b1b9a0a --- /dev/null +++ b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.Properties.cs @@ -0,0 +1,163 @@ +// 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. + +#if !WINAPPSDK +using CursorEnum = Windows.UI.Core.CoreCursorType; +#else +using Microsoft.UI.Input; +using CursorEnum = Microsoft.UI.Input.InputSystemCursorShape; +#endif + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// A simple Thumb control which can be manipulated in multiple directions to assist with resize scenarios. +/// +public partial class ResizeThumb : Control +{ + /// + /// Gets or sets how the should behave and manipulate it target element. Will automatically set the required and values. + /// + public ResizeDirection Direction + { + get { return (ResizeDirection)GetValue(DirectionProperty); } + set { SetValue(DirectionProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DirectionProperty = + DependencyProperty.Register(nameof(Direction), typeof(ResizeDirection), typeof(ResizeThumb), new PropertyMetadata(ResizeDirection.None, OnDirectionPropertyChanged)); + + private static void OnDirectionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ResizeThumb resizeThumb) + { + resizeThumb.ManipulationMode = resizeThumb.Direction switch + { + ResizeDirection.Top => ManipulationModes.TranslateY, + ResizeDirection.Bottom => ManipulationModes.TranslateY, + ResizeDirection.Left => ManipulationModes.TranslateX, + ResizeDirection.Right => ManipulationModes.TranslateX, + ResizeDirection.TopLeft => ManipulationModes.TranslateX | ManipulationModes.TranslateY, + ResizeDirection.TopRight => ManipulationModes.TranslateX | ManipulationModes.TranslateY, + ResizeDirection.BottomLeft => ManipulationModes.TranslateX | ManipulationModes.TranslateY, + ResizeDirection.BottomRight => ManipulationModes.TranslateX | ManipulationModes.TranslateY, + _ => ManipulationModes.None, + }; + } + } + + /// + /// Gets or sets how the should resize its target element. + /// + public ResizePositionMode PositionMode + { + get { return (ResizePositionMode)GetValue(PositionModeProperty); } + set { SetValue(PositionModeProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty PositionModeProperty = + DependencyProperty.Register(nameof(PositionMode), typeof(ResizePositionMode), typeof(ResizeThumb), new PropertyMetadata(ResizePositionMode.Canvas)); + + /// + /// Gets or sets the cursor which should be displayed when the mouse is over this thumb. If unset, will automatically be set based on + /// + public CursorEnum Cursor + { + get { return (CursorEnum)GetValue(CursorProperty); } + set { SetValue(CursorProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty CursorProperty = + DependencyProperty.Register(nameof(Cursor), typeof(CursorEnum), typeof(ResizeThumb), new PropertyMetadata(null, OnCursorPropertyChanged)); + + private static void OnCursorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ResizeThumb resizeThumb) + { + // Set cursor based on resize direction, if not explicitly set. + var cursor = resizeThumb.ReadLocalValue(CursorProperty); + if (cursor == DependencyProperty.UnsetValue || cursor == null) + { + cursor = resizeThumb.Direction switch { + ResizeDirection.Top => CursorEnum.SizeNorthSouth, + ResizeDirection.Bottom => CursorEnum.SizeNorthSouth, + ResizeDirection.Left => CursorEnum.SizeWestEast, + ResizeDirection.Right => CursorEnum.SizeWestEast, + ResizeDirection.TopLeft => CursorEnum.SizeNorthwestSoutheast, + ResizeDirection.TopRight => CursorEnum.SizeNortheastSouthwest, + ResizeDirection.BottomLeft => CursorEnum.SizeNortheastSouthwest, + ResizeDirection.BottomRight => CursorEnum.SizeNorthwestSoutheast, + _ => CursorEnum.UniversalNo, + }; + } + +#if !WINAPPSDK + // On UWP, we use our XAML extension to control this behavior, + // so we'll update it here (and maintain any cursor override). + if (cursor is CursorEnum cursorValue) + { + FrameworkElementExtensions.SetCursor(gripper, cursorValue); + } + + return; +#endif + +#if WINUI3 + // On WinUI 3, we can set the ProtectedCursor directly. + if (cursor is CursorEnum cursorValue && + (resizeThumb.ProtectedCursor == null || + (resizeThumb.ProtectedCursor is InputSystemCursor current && + current.CursorShape != cursorValue))) + { + resizeThumb.ProtectedCursor = InputSystemCursor.Create(cursorValue); + } +#endif + } + } + + /// + /// Gets or sets the control that the is resizing. Be default, this will be the visual ancestor of the . + /// + public FrameworkElement? TargetControl + { + get { return (FrameworkElement?)GetValue(TargetControlProperty); } + set { SetValue(TargetControlProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TargetControlProperty = + DependencyProperty.Register(nameof(TargetControl), typeof(FrameworkElement), typeof(ResizeThumb), new PropertyMetadata(null)); + + /// + /// Gets or sets the incremental amount of change for dragging with the mouse or touch of a sizer control. Effectively a snapping increment for changes. The default is 1. + /// + /// + /// For instance, if the DragIncrement is set to 16. Then when a component is resized with the sizer, it will only increase or decrease in size in that increment. I.e. -16, 0, 16, 32, 48, etc... + /// + /// + /// TODO: (Need to figure out how keyboard input works and if handled here or by adorner, or both) This value is independent of the KeyboardIncrement property. If you need to provide consistent snapping when moving regardless of input device, set these properties to the same value. + /// + public double DragIncrement + { + get { return (double)GetValue(DragIncrementProperty); } + set { SetValue(DragIncrementProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DragIncrementProperty = + DependencyProperty.Register(nameof(DragIncrement), typeof(double), typeof(ResizeThumb), new PropertyMetadata(1d)); +} diff --git a/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs new file mode 100644 index 000000000..3cfe7cfea --- /dev/null +++ b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs @@ -0,0 +1,138 @@ +// 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; + +/// +/// A simple Thumb control which can be manipulated in multiple directions to assist with resize scenarios. +/// +public partial class ResizeThumb : Control +{ + private Thickness? _originalMargin; + private Point? _originalPosition; + private Size? _originalSize; + + /// + public ResizeThumb() + { + this.DefaultStyleKey = typeof(ResizeThumb); + + Loaded += this.ResizeThumb_Loaded; + } + + private void ResizeThumb_Loaded(object sender, RoutedEventArgs e) + { + if (TargetControl == null) + { + TargetControl = this.FindAscendant(); + } + } + + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + // Ensure we have the proper cursor value setup + OnDirectionPropertyChanged(this, null!); + OnCursorPropertyChanged(this, null!); + } + + /// + protected override void OnManipulationStarting(ManipulationStartingRoutedEventArgs e) + { + // Snap the original size and position when we start dragging. + _originalSize = new Size(TargetControl?.ActualWidth ?? 0, TargetControl?.ActualHeight ?? 0); + + if (PositionMode == ResizePositionMode.MarginTopLeft) + { + _originalMargin = TargetControl?.Margin; + } + else + { + _originalPosition = new Point(Canvas.GetLeft(TargetControl ?? this), Canvas.GetTop(TargetControl ?? this)); + } + } + + /// + protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e) + { + base.OnManipulationDelta(e); + + // We use Truncate here to provide 'snapping' points with the DragIncrement property + // It works for both our negative and positive values, as otherwise we'd need to use + // Ceiling when negative and Floor when positive to maintain the correct behavior. + var horizontalChange = + Math.Truncate(e.Cumulative.Translation.X / DragIncrement) * DragIncrement; + var verticalChange = + Math.Truncate(e.Cumulative.Translation.Y / DragIncrement) * DragIncrement; + + // Important: adjust for RTL language flow settings and invert horizontal axis +#if !HAS_UNO + if (this.FlowDirection == FlowDirection.RightToLeft) + { + horizontalChange *= -1; + } +#endif + + // Apply the changes to the target control + if (TargetControl != null) + { + // Keep track if we became constrained in a direction and don't adjust position if we didn't update the size. + bool adjustWidth = false; + bool adjustHeight = false; + + // Calculate the new size + var newWidth = _originalSize?.Width ?? 0 + horizontalChange; + var newHeight = _originalSize?.Height ?? 0 + verticalChange; + + if (Direction != ResizeDirection.Top && Direction != ResizeDirection.Bottom) + { + if (IsValidWidth(TargetControl, newWidth, ActualWidth)) + { + TargetControl.Width = newWidth; + adjustWidth = true; + } + } + + if (Direction != ResizeDirection.Left && Direction != ResizeDirection.Right) + { + if (IsValidHeight(TargetControl, newHeight, ActualHeight)) + { + TargetControl.Height = newHeight; + adjustHeight = true; + } + } + + // Adjust the position based on position mode first + if (PositionMode == ResizePositionMode.MarginTopLeft) + { + var newMargin = _originalMargin ?? new Thickness(); + + if ((Direction == ResizeDirection.Left || Direction == ResizeDirection.TopLeft || Direction == ResizeDirection.BottomLeft) + && adjustWidth) + newMargin.Left += horizontalChange; + + if ((Direction == ResizeDirection.Top || Direction == ResizeDirection.TopLeft || Direction == ResizeDirection.TopRight) + && adjustHeight) + newMargin.Top += verticalChange; + + TargetControl.Margin = newMargin; + } + else + { + var newX = (_originalPosition?.X ?? 0) + horizontalChange; + var newY = (_originalPosition?.Y ?? 0) + verticalChange; + + if ((Direction == ResizeDirection.Left || Direction == ResizeDirection.TopLeft || Direction == ResizeDirection.BottomLeft) + && adjustWidth) + Canvas.SetLeft(TargetControl, newX); + + if ((Direction == ResizeDirection.Top || Direction == ResizeDirection.TopLeft || Direction == ResizeDirection.TopRight) + && adjustHeight) + Canvas.SetTop(TargetControl, newY); + } + } + } +} diff --git a/components/Adorners/src/ResizeElement/ResizeThumb.xaml b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.xaml similarity index 100% rename from components/Adorners/src/ResizeElement/ResizeThumb.xaml rename to components/Adorners/src/ResizeElement/Thumb/ResizeThumb.xaml diff --git a/components/Adorners/src/Themes/Generic.xaml b/components/Adorners/src/Themes/Generic.xaml index a974468bc..c25dc2906 100644 --- a/components/Adorners/src/Themes/Generic.xaml +++ b/components/Adorners/src/Themes/Generic.xaml @@ -8,7 +8,7 @@ - - + + From 3db9b603b6619188b71f9bbf4339c48dadac1347 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Thu, 4 Dec 2025 01:18:16 -0800 Subject: [PATCH 28/34] Get ResizeElementAdorner sizing to work, though Adorner isn't updating alongside --- ...ml => ResizeElementAdornerCanvasSample.xaml} | 2 +- ...=> ResizeElementAdornerCanvasSample.xaml.cs} | 6 +++--- .../Adorners/samples/ResizeElementAdorner.md | 6 +++++- .../src/ResizeElement/ResizeElementAdorner.cs | 7 ++++++- .../ResizeElement/Thumb/ResizePositionMode.cs | 3 +++ .../src/ResizeElement/Thumb/ResizeThumb.cs | 17 ++++++++++++++--- 6 files changed, 32 insertions(+), 9 deletions(-) rename components/Adorners/samples/ResizeElement/{ResizeElementAdornerSample.xaml => ResizeElementAdornerCanvasSample.xaml} (98%) rename components/Adorners/samples/ResizeElement/{ResizeElementAdornerSample.xaml.cs => ResizeElementAdornerCanvasSample.xaml.cs} (55%) diff --git a/components/Adorners/samples/ResizeElement/ResizeElementAdornerSample.xaml b/components/Adorners/samples/ResizeElement/ResizeElementAdornerCanvasSample.xaml similarity index 98% rename from components/Adorners/samples/ResizeElement/ResizeElementAdornerSample.xaml rename to components/Adorners/samples/ResizeElement/ResizeElementAdornerCanvasSample.xaml index 4f303b437..a12a62538 100644 --- a/components/Adorners/samples/ResizeElement/ResizeElementAdornerSample.xaml +++ b/components/Adorners/samples/ResizeElement/ResizeElementAdornerCanvasSample.xaml @@ -1,5 +1,5 @@ - [!SAMPLE ResizeElementAdornerSample] +> [!SAMPLE ResizeElementAdornerCanvasSample] + +This can be done above within a `Canvas` layout control, or with general layout using Margins as well: + +// TODO: Add Margin example here diff --git a/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs b/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs index 381f3e3a4..821578814 100644 --- a/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs +++ b/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs @@ -56,7 +56,12 @@ protected override void OnApplyTemplate() BottomRightThumbPart = GetTemplateChild(nameof(BottomRightThumbPart)) as ResizeThumb; // OnApplyTemplate can be called after OnAttached, especially if the Adorner isn't initially visible, so we need to re-apply the TargetControl here. - OnAttached(); + if (AdornedElement is not null) + { + // Guard this incase we're getting removed from the visual tree... + // Not sure if this is a bug in the Adorner lifecycle or not or specific to how we've set this up here. + OnAttached(); + } } /// diff --git a/components/Adorners/src/ResizeElement/Thumb/ResizePositionMode.cs b/components/Adorners/src/ResizeElement/Thumb/ResizePositionMode.cs index 74c3842df..04ea410d1 100644 --- a/components/Adorners/src/ResizeElement/Thumb/ResizePositionMode.cs +++ b/components/Adorners/src/ResizeElement/Thumb/ResizePositionMode.cs @@ -18,4 +18,7 @@ public enum ResizePositionMode /// Resize using 's Top and Left values. /// MarginTopLeft, + + // TODO: MarginBottomRight could be added in the future. Not sure of alternate anchor points are useful? e.g. TopRight, BottomLeft + // Alternate anchor points would require more complex calculations during resize to determine when to change the Width/Height of the element. } diff --git a/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs index 3cfe7cfea..4f8ab81fe 100644 --- a/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs +++ b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs @@ -76,6 +76,17 @@ protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e) } #endif + // If we're adjusting the opposite boundary then we need to invert the change values. + if (Direction == ResizeDirection.Right || Direction == ResizeDirection.TopRight || Direction == ResizeDirection.BottomRight) + { + horizontalChange *= -1; + } + + if (Direction == ResizeDirection.Bottom || Direction == ResizeDirection.BottomLeft || Direction == ResizeDirection.BottomRight) + { + verticalChange *= -1; + } + // Apply the changes to the target control if (TargetControl != null) { @@ -83,9 +94,9 @@ protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e) bool adjustWidth = false; bool adjustHeight = false; - // Calculate the new size - var newWidth = _originalSize?.Width ?? 0 + horizontalChange; - var newHeight = _originalSize?.Height ?? 0 + verticalChange; + // Calculate the new size (Note: This is the opposite direction to expand the opposing boundary of the thumb) + var newWidth = (_originalSize?.Width ?? 0) - horizontalChange; + var newHeight = (_originalSize?.Height ?? 0) - verticalChange; if (Direction != ResizeDirection.Top && Direction != ResizeDirection.Bottom) { From f42aa69b6c6993c62483a94e0a4cff0861915640 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Thu, 4 Dec 2025 02:19:38 -0800 Subject: [PATCH 29/34] Hook event into ResizeThumb to know when element has been changed so we can update Adorner I'm not sure why the underlying element changed is not triggering the existing Adorner layout hooks, so this'll have to do for now... It at least works as expected now. --- components/Adorners/src/Adorner.cs | 11 ++++++++ .../src/ResizeElement/ResizeElementAdorner.cs | 26 +++++++++++++++++++ .../src/ResizeElement/Thumb/ResizeThumb.cs | 22 ++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/components/Adorners/src/Adorner.cs b/components/Adorners/src/Adorner.cs index fa94415cf..4808edae7 100644 --- a/components/Adorners/src/Adorner.cs +++ b/components/Adorners/src/Adorner.cs @@ -97,6 +97,9 @@ internal void OnLayoutUpdated(object? sender, object e) Canvas.SetLeft(this, coord.X); Canvas.SetTop(this, coord.Y); + + // Also update size + OnSizeChanged(this, null!); } } @@ -134,4 +137,12 @@ protected virtual void OnAttached() { } /// Override this method to unhook functionality from the . /// protected virtual void OnDetaching() { } + + /// + public new void UpdateLayout() + { + OnLayoutUpdated(this, null!); + + base.UpdateLayout(); + } } diff --git a/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs b/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs index 821578814..8fca70310 100644 --- a/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs +++ b/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs @@ -77,6 +77,15 @@ protected override void OnAttached() TopRightThumbPart?.TargetControl = AdornedElement; BottomLeftThumbPart?.TargetControl = AdornedElement; BottomRightThumbPart?.TargetControl = AdornedElement; + + TopThumbPart?.TargetControlResized += OnTargetControlResized; + BottomThumbPart?.TargetControlResized += OnTargetControlResized; + LeftThumbPart?.TargetControlResized += OnTargetControlResized; + RightThumbPart?.TargetControlResized += OnTargetControlResized; + TopLeftThumbPart?.TargetControlResized += OnTargetControlResized; + TopRightThumbPart?.TargetControlResized += OnTargetControlResized; + BottomLeftThumbPart?.TargetControlResized += OnTargetControlResized; + BottomRightThumbPart?.TargetControlResized += OnTargetControlResized; } /// @@ -84,6 +93,15 @@ protected override void OnDetaching() { base.OnDetaching(); + TopThumbPart?.TargetControlResized -= OnTargetControlResized; + BottomThumbPart?.TargetControlResized -= OnTargetControlResized; + LeftThumbPart?.TargetControlResized -= OnTargetControlResized; + RightThumbPart?.TargetControlResized -= OnTargetControlResized; + TopLeftThumbPart?.TargetControlResized -= OnTargetControlResized; + TopRightThumbPart?.TargetControlResized -= OnTargetControlResized; + BottomLeftThumbPart?.TargetControlResized -= OnTargetControlResized; + BottomRightThumbPart?.TargetControlResized -= OnTargetControlResized; + TopThumbPart?.TargetControl = null; BottomThumbPart?.TargetControl = null; LeftThumbPart?.TargetControl = null; @@ -93,4 +111,12 @@ protected override void OnDetaching() BottomLeftThumbPart?.TargetControl = null; BottomRightThumbPart?.TargetControl = null; } + + private void OnTargetControlResized(ResizeThumb sender, TargetControlResizedEventArgs args) + { + // TODO: Investigate more + // Note: I'm not sure why the AdornedElement's SizeChanged/LayoutUpdate isn't getting triggered by our changes... + // So for now, we'll just force a layout update here of the Adorner itself to realign to the new size of the AdornedElement. + this.UpdateLayout(); + } } diff --git a/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs index 4f8ab81fe..2bc5aeccc 100644 --- a/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs +++ b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs @@ -9,6 +9,8 @@ namespace CommunityToolkit.WinUI.Controls; /// public partial class ResizeThumb : Control { + public event TypedEventHandler? TargetControlResized; + private Thickness? _originalMargin; private Point? _originalPosition; private Size? _originalSize; @@ -130,6 +132,12 @@ protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e) newMargin.Top += verticalChange; TargetControl.Margin = newMargin; + + TargetControlResized?.Invoke(this, new TargetControlResizedEventArgs( + TargetControl.Margin.Left, + TargetControl.Margin.Top, + TargetControl.Width, + TargetControl.Height)); } else { @@ -143,7 +151,21 @@ protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e) if ((Direction == ResizeDirection.Top || Direction == ResizeDirection.TopLeft || Direction == ResizeDirection.TopRight) && adjustHeight) Canvas.SetTop(TargetControl, newY); + + TargetControlResized?.Invoke(this, new TargetControlResizedEventArgs( + Canvas.GetLeft(TargetControl), + Canvas.GetTop(TargetControl), + TargetControl.Width, + TargetControl.Height)); } } } } + +public class TargetControlResizedEventArgs(double newLeft, double newTop, double newWidth, double newHeight) : EventArgs +{ + public double NewLeft { get; } = newLeft; + public double NewTop { get; } = newTop; + public double NewWidth { get; } = newWidth; + public double NewHeight { get; } = newHeight; +} From 46707782ab0537766db2cdb6ae42a9f4b731f766 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Thu, 4 Dec 2025 09:15:13 -0800 Subject: [PATCH 30/34] Apply XAML Styler --- .../ResizeElementAdornerCanvasSample.xaml | 6 +- .../ResizeElement/ResizeElementAdorner.xaml | 68 +++++++++---------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/components/Adorners/samples/ResizeElement/ResizeElementAdornerCanvasSample.xaml b/components/Adorners/samples/ResizeElement/ResizeElementAdornerCanvasSample.xaml index a12a62538..ef1650a09 100644 --- a/components/Adorners/samples/ResizeElement/ResizeElementAdornerCanvasSample.xaml +++ b/components/Adorners/samples/ResizeElement/ResizeElementAdornerCanvasSample.xaml @@ -1,4 +1,4 @@ - + - - + + diff --git a/components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml b/components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml index 882caaeac..5bb5cc6bd 100644 --- a/components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml +++ b/components/Adorners/src/ResizeElement/ResizeElementAdorner.xaml @@ -1,4 +1,4 @@ - + + BorderThickness="1"> + Margin="0,-8,0,0" + HorizontalAlignment="Center" + VerticalAlignment="Top" + Direction="Top" /> + Margin="0,0,0,-8" + HorizontalAlignment="Center" + VerticalAlignment="Bottom" + Direction="Bottom" /> + Margin="-8,0,0,0" + HorizontalAlignment="Left" + VerticalAlignment="Center" + Direction="Left" /> + Margin="0,0,-8,0" + HorizontalAlignment="Right" + VerticalAlignment="Center" + Direction="Right" /> + Margin="-8,-8,0,0" + HorizontalAlignment="Left" + VerticalAlignment="Top" + Direction="TopLeft" /> + Margin="0,-8,-8,0" + HorizontalAlignment="Right" + VerticalAlignment="Top" + Direction="TopRight" /> + Margin="-8,0,0,-8" + HorizontalAlignment="Left" + VerticalAlignment="Bottom" + Direction="BottomLeft" /> + Margin="0,0,-8,-8" + HorizontalAlignment="Right" + VerticalAlignment="Bottom" + Direction="BottomRight" /> From 7f2c9ac4f466732f5a0b4464c50f0c4297c96450 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 9 Dec 2025 23:10:04 -0800 Subject: [PATCH 31/34] Fix issue with UWP ResizeThumb code and misnamed variable --- .../Adorners/src/ResizeElement/Thumb/ResizeThumb.Properties.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.Properties.cs b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.Properties.cs index f0b1b9a0a..60a0bf9df 100644 --- a/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.Properties.cs +++ b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.Properties.cs @@ -106,7 +106,7 @@ private static void OnCursorPropertyChanged(DependencyObject d, DependencyProper // so we'll update it here (and maintain any cursor override). if (cursor is CursorEnum cursorValue) { - FrameworkElementExtensions.SetCursor(gripper, cursorValue); + FrameworkElementExtensions.SetCursor(resizeThumb, cursorValue); } return; From 84f6c44909b31b8b8acf928b7099958bb56035c2 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Wed, 10 Dec 2025 01:03:36 -0800 Subject: [PATCH 32/34] Add ResizeElementAdorner With Drag sample leveraging CanvasView Labs component Shows more complete interaction in setting up manipulatable items with Drag Manipulation and Resize Fixes ResizeElementAdorner not reacting to AdornedElement that is being manipulated (other events may be useful here) Fixes issue with CanvasView and containerization when specifying ContentPresenter within Items property. (add XMLDoc comments) --- .../Adorners/samples/Adorners.Samples.csproj | 2 + .../davis-vargas-2vSNlKHn9h0-unsplash.jpg | Bin 0 -> 95586 bytes .../sergey-zolkin-m9qMoh-scfE-unsplash.jpg | Bin 0 -> 39537 bytes .../ResizeElementAdornerCanvasSample.xaml | 4 +- .../ResizeElementAdornerWithDragSample.xaml | 33 ++++++++ ...ResizeElementAdornerWithDragSample.xaml.cs | 52 ++++++++++++ .../Adorners/samples/ResizeElementAdorner.md | 8 +- .../src/ResizeElement/ResizeElementAdorner.cs | 78 ++++++++++-------- .../src/ResizeElement/Thumb/ResizeThumb.cs | 3 + components/CanvasView/src/CanvasView.cs | 23 +++++- 10 files changed, 164 insertions(+), 39 deletions(-) create mode 100644 components/Adorners/samples/Assets/davis-vargas-2vSNlKHn9h0-unsplash.jpg create mode 100644 components/Adorners/samples/Assets/sergey-zolkin-m9qMoh-scfE-unsplash.jpg create mode 100644 components/Adorners/samples/ResizeElement/ResizeElementAdornerWithDragSample.xaml create mode 100644 components/Adorners/samples/ResizeElement/ResizeElementAdornerWithDragSample.xaml.cs diff --git a/components/Adorners/samples/Adorners.Samples.csproj b/components/Adorners/samples/Adorners.Samples.csproj index b721e9f86..03000296f 100644 --- a/components/Adorners/samples/Adorners.Samples.csproj +++ b/components/Adorners/samples/Adorners.Samples.csproj @@ -8,6 +8,8 @@ + + diff --git a/components/Adorners/samples/Assets/davis-vargas-2vSNlKHn9h0-unsplash.jpg b/components/Adorners/samples/Assets/davis-vargas-2vSNlKHn9h0-unsplash.jpg new file mode 100644 index 0000000000000000000000000000000000000000..687f59249ea2702608eb5d529152a62cb2fb1348 GIT binary patch literal 95586 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∾5S$GI<0-$C$AgeGK}M%Ak1qw({8(G_ zIbs~W+SkjB-3|q!1m3X$uW3N|9Y5dL5$z<|6^n{Ei}U!((o@iq-l4StTi zn5UwaAGn$}W{z1DAc?Kqv%2 zI)jBPv^!OSc0K~RxUy*;QTv;0$r7~ z&v|oxLO%`#YfWfZ01jPzyDOy5jQSe@g5P{_sny+NjRDrN)z!af!F&mjdnwq^B-nyX zot5O($kQxnxVjUTNVPvs5k*i3mpvQ*0LK{ip&jK8M##8=DMh2f0NA1#9H=P7B76$u zv2fiZVy1lO<`lFWvNjwCL4or$0|~AzdvLju_U$M0*3w~w9VA#O_PBdz9YR&D7L3sA zV}n!?pznA%dc}-un$!N6L)d270-7jN{$eev1axf+;<;h{=*ovfbT~GS8sgkS_~rY! ztX<6`mszGz3Z$dhANm}cu*UWlwNRZN;0Eqzof`9J%+e3uBs6>)b5T$RTY~f|CC24l z8j^}?Pe0Q&7JvFFbT{sS-h1wIS0kBwIh+)&!5*7%1`g2{$x#2ClaazdR z-vLz_CdskES4f{lRCs!sEKL|u#fVI3n~1S>X(GYY%R~tRee*Z`@UKB2E^pdQBSXSJ z2XfPZL$)MnbhuXeXlP4O8k}%R5WAsnQ5~?sMuB3qbjJO6W>r}1ts;~FS%&U}V$x4v z7+BSTU=-=D-t!R_%MpyII!c&sr6jR24WnsUgycl}SVr%^Se53=qPq~gHR;?l=_)|h z@>7YjqawiHO>H1%lKL0(`}K~j|iQvukdN#Z>iPwrS`9FbK76~_AI zos>IO4*+tGCwkVx(gu)JU11*C)c{>lUKawL*>sfcpyGhK!?DmIpaDfIPGDdLBSP>H zKJDDaQ6ktHcKTxhkbZ9_)LSxOOZiC*7W#$>Dx@GpYM&OE^qNg&F`E5od6?u4yml6f`U_vm&;5GNn~R{ zFp#7ukh&03X!vAlZuCyuE(axN5!grtN%|ZBP-Cg6gr#FlRzpaNmfhEGgSjNfyGjAV zCH6UF3>}GtmX^5>_*-LR>qg(qNsK^%6$V6T!(m$r0Hn|q8?!Gv^`i0Do-Sx2A8xeexf71Wu*xKyDpu7+neiJv_^;G z5pnAp9m0^;p5tv)D%$LH{xE7@iIh1df9%PnQj(${BE{{y4<1EL{K0R+Q+Y%vb=h-n4F1NKtB?0qP?Hd;uZCoeCYdF1 zLVhl33ZhJ8D(qdk=gPfs?GRtA)7jOkQb=WXnzn3Fzge_vymUgFe{n4#2=z8L>9q3> z5r|m6J`;uoPeQHAeL=r*%t~Zc*aiF*$c?6|kd00pPL&dLO}g*0zZZJdbiCK)GuLR9w ztPm-8@N2t7!Dyj0{ZF2;>&_Gi`97wPNwc)WKmQ)qd-FwE&)~_ zf~Ah&$wuciFuVcLTz%#aszkLzxc=ssowYI}0i%yKlWL=?`EPci(-?>s1B;=%w8Nzg z5;TQ{Y?y1C)(C%LJX7u-IHD;nHV+GcJBUduBCsGha~j>?d?QJH9l+&EGjI^LbkpWT z{{YB}bpHSldfZ^HBNi3b5h`yAAq8qIO04UH<`@Tx%2$TWK_5k_ts-qM3_}G~BGP@> z=L$j^L~5P}v8-UAB+0s1>k-}A3=lAWHs{PuB^ap?Iv+z0kww^LlrMtg3T@g*i+|FX ze2rm|Mh}eMvdX=L5>)#<#PJjQk$CIrkg4tcF5%Qo;z1c`L8uOfaw9K5ZxPH=Ri?j zc}MnRE6NcA=scf2!Y0^IKKz{RTE^5p=Qqra>3Q0yfh*?nr4e@|;PpfrUtEH8Z&ga_P6#JK}g{`vDIAlo6S1`uY|h=I@3?h0B>1QsZ}$>IzJy})YK z#5zhq5Ih6Je8m=+v);NG)a8e_VGE8DDAAYWroa)e_A2%kHR=H5h0^dGSSm{0uYHc zD5vU~3;-Yn(5b@#m?`sxr;O(?#wg%jsNUUo7OYOG9|Z8kb{kX#EqAgLz(&iph0+zw~| z!~iG}0RaF40s;a80s;d80RR910TBQpF+ovbae)w#p|K#rFwx=g@&DQY2mt~C0Y4C* zi}<;}_%GuAC*VI3xV`4^c3?0LPFsdCiE$=h@Gs$hAI1DH;^oWuTr%MQ0F4-|KO>Gx zca-lbiN@vk__?+J01^KHmgcixh00~c&1Dbp7vMzB@ch4mOb$ZVE?@9Hw}%7SLyhk% zkl6hS1`<#Px_Iv$!dr@<1=krZrC`kuw*+_~$n9O^0)TP{UhbdytCtYu{4LHQ_*}Vi z;Qlp`{Ku!rc*iT0ix|n?YjG{<$=ZOCQ9OS{&B42K%@=P*op`+&O zDb%?L4;LzoAl-A|$T1UbdOom&Bd2Db$JvU=VZ3p0k2rI6#>4zCEBq!*m^rf##JGMb zk(4;Z*Ae)mtU?g^&Xq008k+F$;}gj?vkK4`T(c^6CvGTmJ}@B~8&OxCVJp=a0&K$N zO#9PV3u-h{Cu8iGMFsZn8l>Gcwz_$^D&ZkF4hx95I_Dx(E;!f!01jA%HL=D} z1E}u?SlPVYLF0^#(vC6eZ>I2G=vlPF?%_*}ehTrNGJdiep!vc8+m8T8onxFko5%`2 zgDI2`P{R=mfLGq;>6Q7$fZ$v@#TWK5r4{nx?u(Y;Vnsd!ffO{YisA6mHPE-{W0J7h ztNY7su0}D6+FvkD(m5C*-8FT=a!dicCArT@!lsH~NTIhNZs^#AAn*G@HH1KBR=pcsKpQuW;@&m34&Xz2;qcuZPob6Ch@0?)zrZ} zhXZ58 za_Ke2?>XlUUkQukEAKW*%CiVlU^dJEX&fPnisP@00)jsAVxDqDV;ak`fz~+pP4|~r zIy*MKU`1{s%|n+oLvowmIy4Dc%)x+iybdX<;P~9G!OZU<<;)c4UYx&pD^6PwYK<8a z1I!fGqlcdWH080&0NUeK+CTRXX!SF7+}kU75yLoO==sPAXIR2Jz<~{CAaZ%LfK)UcxI;(Y@WrV4@8>V6 zxpDxpS4xt8FHUK~-mygC{wylH)ZnUh7)#sS&)gUvUE0{k4fsHdT|%>reaF~|yKB;96+ zbJnpzS=)_PYZcVZYZZq$1~`*vN23fxI$E+DQ(3?fX@hi^Z&(0@o;tvqQ>>vb@vT;K z`pMhc>kkW1QubjDDEP>gHz&g*UXS&cRDd;yXZaQz!1I0ckA4?GIAwHU+K{jB9{~vK z4yy@|KGt?K4;^+{5}&NPOFEqx`%mY1Y1ch*h{u$|U8FnC1m0}{4?X26UHQ(b+I->B zGp;3;bBdhh-C`F59eiN6J2dfbS6WK^%&2Kvyc@?8y5+>jN4o&0Tf%dKvg~kzV}Uil zJI9wL>TN%htiA|ChHQDb^vQ%1f-hXAK?AEbiGD*@2#lmG@r(`s09Gm8rpjr^r4e?( zPK`VkXaO?NIW42i#a6OF)&Br^%tLrK{RRz!>g$XU?N<{6d#4VCj%kU8wgA7=3@!6_ zadTz;ziv(w&EjDqFDt*)#WKd6UUON>b%D15qUPzpSjy3M$Bq#xoJj3*97NjkKD2S= zGhpb{9(ebQ8XX+RU5*8h*l&KTnB8dB(F2xDm?*Q(jK2l|OA@KwIRmST zPQ$Zf;FlMjjlB+>OPkl61BfgQOgXYz!dzEnPcJyF+j4;w!ySCX7;`d{TH~!}6geT+ z>lO#bZ4xibi8iLi(q`Hhrw|6m0OZ8V$8gl$^#c^k2a&>1Lh=WkFoImF<*`S1^_4Nu zgUYt~6ihOP-Q0gXKRAk6mCrhw+bE3d$=t+892>- zZ+~VS5pP^=$fp>+HvBn79JL%uCy(zr$cfSV!_K_r$b9uNa$3)KyPpyYI=EruaBvrt zj23IrhnirC6Vl`Uh7C*T-WrHdG>;p&2{W-10To8_9>M!5*|OC zgjpDkEk_?}bPpd`v_%Jcq3oZ$0@NbrR|}l*CV?Z-!w`2`)i1L*{34*%_FPdxS=C#b z9=Y(DsG6f%cDcg22&uo4;gtCRJ%6ro=XC_XtYip=zx2PGh%0$S`nY~fo+gQ8agwK3 zkM0HuiV&&&E*%u5v`>kwu4Emb?9ng)lM>M#`*GY2(?@uaP*2mWKrNdYjRL177X4s> zS7#43%{6$Um=TX9%Y!O_AkT@7y(F>kJ2=6Y%f=wI3J2s21n^|sam2=AX}4Z6Nq|?m z@rMDz5F^GQ0EX<1_l&NALuYA$#$YV8c;mO$3o_-XeQJDRR)>JvJQ(OnK}c}*aG)Z< zprUiFIkR~D8{YF-HPqW&xodC+<;A@UqHp5|H!T1!BNjKHI|+c1?hb+R&QJ&qI(hwP z7$ZSLf||vxq*S)0!5moN^uejB1YQp#mx=EUA24y`W8wX#24P({e>m5K+R}Vmk_iB% z&51a!5~&T90CRBnO>L~5XDdZg8Z|duAg+#B`jYqvo3W_Hu+2lUb)&{G&B!`wHG%_+O=@+4f=Rs|Mg)YbL=#VCaoYf-wEiv- zqIxvMgwlNPA|Z4zyn2>onp73ihZ7qRfU(z7@rs0$t-8^2+LnmS9ND}??OY5jGB)8L z?n%zwdfqc#4Gd{C7kAzRiCR3b4&!jrjrtpY{bjKOtZngJF;!T+-s~9xPQp?-sV=B6 z{{R>SDAcMq+Yj3HQEDgn2@<0i>8Sfau7VO^{A zSChjqs7V3Ia8EKf;mIKT4$UU8^4kwMTcpG&ici9D4X)bB@~Vqjm0V(qwIH*bZ5;pb5`bjWY4X+f(DYh>s4Plf=hZgXnJWj9J4-uU{DP zLpC@)M~Bu$I8fFc*#oH;d~>|oEI=H!&OPWX7AxSxtVBV+P{TCP!o5k4Bn_{yd3nl( zw_8oRVKZ8w5sSy4ysA7LtFwbP_MtA9g7lvl6!{@VN}OWM==_NI`A?*g7h4As&6Ud}j1qt$25PGAc_3>X;L93Ob)oYYAq+$?z^T3qZ6N)8{XQ7MeAx!Ne6!gs;=3 z!x04sX0a^Hc%U17;A1Wp53?Q=sxFSK^Mv6p!Rsn(j)bb>t|Y_>6{`+zMGVpsqIi2u zD%b*xvv}a#t($99zHUkrHk}T*n7H+U3pw+ELWd)V>x>mth~(!Rh7k>>2S&hM1o^l# zUd~gV9bu*hP(%)5;gBTZ2us6=N8tvyxEV4CQ63q*9XSz0Y1m@W0S24KML_v$UU!kA zq3XZh6UfcalT1P_N096r_kp({ZVfo^5W${*j_^{5eqTAh2d2F2JM%6hqofbx(SX?q z-lx-bg=C2GmXls^s3mD+hxo#kpm;roYFJUR0KMb#$|1|Oa%uymD~tDlH`vnp08CYF zHZLt;K!_4(9anP{NhwFk75UA4C>t!D6E_)k%XgWGc$3f@ubz6#7c6tMz0NTdu{Pi| z7_to`O*MPNDpROzC5^f<`yQM!Jy+=MNu15!;mjnjF4<+zxdb zBe5qAY9oTDH@#v{Ak%jvq2S*K!}6y3e?WO1OoO@^oc z0ByL4pOS7IxNr<bgf2x|!yYl@e(GfWb}t=l->B#G2gx1;rvcj$-b0>r-ldc`KB3RODi z0$5W*bF3q~wE}5}BsxDPY<~NKr@R3lUR-4gD`6>)p{>5IU2)5W>rP4pK=~hL5No2E z$H$zgdUCqNxX?MofD*_501N;?=nWh|ff@l!iMoiq#W9q;z=?S0sfsV8?WXs)22GmD zt!&rEMIHi-)o=>+zkW=cf`t@Avm20I#SY?fBZ9aplY&krK~-HBNsX&s)}Zj5eb@(J(Q3AdL?>(&KSJS!>2=1oSAHp`C5oRUbzw7m-0I>)g$tTQt|@;}>lcrRd%< z>!OScbl^j#gyUR}0K7EpFxSDDh()csD>Z=5%t2z)m7_6LDSR?avW07X8MG~vSQiI; zPkFU7LGh0YBH1^WU`-<@ScP*(20uQ@TJ5@_kH*3wnd-2cP?B@qDt z0s;a80s;d80RR91000315g{=_QDJcqfsvsgvBBZd|Jncu0RsU6KM?q@iu3XatC!>W z-^E|eP#`*q4) z23d(fDxew+(O!LAp`#9_GR<9e9VS7UnRif1Zujm|QVqlq3>)}=iI*-9!Gx?+@br}` z55w@<1y2FCwVwpNrxetCMQ)3@vZ|i?h@>2*^N0W~WZI#nl}g^@5Go5*m;V3}#4CsX zQwQOKm7cJ-0zbiO48M?rF8=_5W7ZBXH<#)WUr&7pv0phOy@6=57VQtx7n zEL*FM#GXNyk?#g@MUZr}<%a`;^y{(*UT`^1>IRMQK+twHL5U*|!X^Y$_zVk{p9Pjf zaYV!cn+nnyj!+oHSQDIAQKMQLI*ZOM!r^lQaW9(U4pK&Ev`rf zU5p{WB`8YEIX3k%=8bZJL|w;m3|W_Otusl|4oHwNSdkK7wiM=m-jXncY#XR>fwm!| zrZ6q#(NUG-wwtB)#th&7BI3ot!M#;jMr&BJMZK_jnh~P==!a3COg2Do3ALRXlqGHj z$!$Xa04Qz}9EVCfgiT{9(5m_FD+=lo?z>ALa8BCMx1R3qQH_*Sc&5@qvX&{N%L`YC zrne&Mt=tW0!3cq1$Z!{kgaZQ5dxCcGK%^bMmFe)*WH>a5N$_elGmB7l_Ug(jnN%$0 zui_>H%96ul9KX8!2;qg>X>|yKTM*%+XGB1w`$$9n<9E!!*!YV zRJtHF$yMmyAjrVGg)10yE?WwUKA|0&vIB05?3rq-6V#vycI_CV_T{a7#Ct~~#hl8w zN3ydD@`EWkCsT&QbzM2cs74v{UB?Ma!%Psp!|9rdlK#jrsq8MQm|#{c8L&)i2i#Fs zjtv}R%te`5bsCvF?lnzM-Safc!Ns_G5)z#99JT;g(c2s3El~ zScb%<*~AgSVHinuD$u(sm^avd9;LD6ffS{_M*jd(+AAiWMk=VhE`l2cyrl^96<%n> zeF3jgFDiE24t8jGWS{f0i({LhQjU2MZuufF*ce`ccNNw z-lOOR;5hn?8^aR>LeXX_7y_LuFlyEdT})YZ8cVBJ6mFtIP&vXl$XmPcSaFwX@4-a7jycVd{I%U;r ztxp2UE`61cY%QqBhPlV6C=sBDpQHIxK zRZ!sSnmaU)WB}I?LZ@SUy2KlIRbH8Rn~`Dna-dkfP(UJLI1MS)7C`r3Y|BjW62xDr zMrog89MB*dXtu+Jyur=qFhYjx0Z)031``bwwN7iPPUbi^x532C5{1y=p^hr2xl@N? z!y*8~XG>yIrm}SMW*^!*_Jua7ccFX|g}4J{dx3F)5~`lJTyn}}PyA!4kMb>gw@zoS zcQH|%a?1mt!#|Q_sKP-5JUBN8WoQt!9fM6Izi_l6D{{5&3y*{q;!~=MZM*DhP&Iw- zbYbaZgobPS;je>JLvJjNlq#z}J&B;uy7i9jub zH-P^DE;0*6o0)<?TUo|c21Zt&Juku-3kqjjV9|phB{~-GnR#n%S#we4pZy?4l`SC{CM&vxwY@-7`obA) z^O<;=YOyK>c!7kl@WZ8`^$Uces&^{^n0=A6Oam88EhHL@V&>?U*0+Kda3T4XN>vc^ zjZ8B*=E|Y^p$UdnSctHtRv;ToV)T#q9^Tl}ju`CJG9^%;)U>5N(;t%iSNkf4swn=% z%pudz_Xo@RA#GOrS*n;7PEoR3+`9!DxnWmnb%|YCluE6cnbUO4(=ga&M690Tw_Z1L z@Q}?c;DrRcm6*HwnFL0xORj4xZV1c7!6O$%w0%TLJv2)o%TMMofS2mgdZacf-ACaf ze}uBt27JaXG{h>!52<=udSQ-VwkN_{w&S|t@8b5j^JHnSrI#eO^#>NFnjj2Nic3_)wt%P^CQVfH0~c9pP&m{}B8Qhs#Jt>Uqu2ic zD7AwFld=eI7t0sF#H&%J1u~}3=z(Cr8;1)I+|O$e*|H!Z>&q*Ru<^ja{|aNCeD3a%F}(uqV3$xqi)v0ZL3YXrkC z+~Jj%mFfyPBa+h++Ei3Z-v;pvAf-FINnXr7WsN|1m>gfVOaV)%0dGCDL!!c~9l-Oa zxQF5kWrQoPQ*!ukhk_-6@G#2-r9`)0UWiTSQK5M(f@p6)h(fXrtWH3rO%}PqDMUW2 za;gBVt@jnC>S4ly=HStP<4_tnR$IP8WKCR1qdeZB*2}E{EQX4!4AdsW!xKV!7{AO= z+4fD)9o2r}8mwyfI$0gO%8VrgaqiSJE4zwaeIn7r6|0CwDP09ZPz8)iQrfjF_bsWQ z7zhQI!z^sksglQ_?&Hjr-PXOvK80PySxsK$rKknjV(wo?gO`G3rTC-$jOBmn=s)sOdEVAY(V^oZVFnGK{L=}tb5mwsT!t`ZDQ#B#xu@4LE z+Y9n)?&BM7El-%6S$X#X6vKXFF#2u@BG$3o*jo69PhsT)fCc^BIP}cGDc3+kMyV|a zR_TgRqLnT+TG1;Ri^3+tJP;@0=wPbIUMkP z#4@>*0S&9gE;db4(nlN#hawohIfxz=5w+%7E$S?`sLEGts7ZKwj6e#=Om%rBGTN|s zsAGk{87@*qNNqq|5q4@V?mpdJW2Sy>!g>4Dfcq KqF8Whr~lb4vim*& literal 0 HcmV?d00001 diff --git a/components/Adorners/samples/Assets/sergey-zolkin-m9qMoh-scfE-unsplash.jpg b/components/Adorners/samples/Assets/sergey-zolkin-m9qMoh-scfE-unsplash.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c85852e8b16dbbab56b5946327107a86d9e13c33 GIT binary patch literal 39537 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 z zNAEQJ!eKdnsGfFB3O$dAwwg05xtyA@<%NKPdXw>2 z$)Fy5K1c?e{bqL8JhDxV%{_?kanu3PLJKUyMYTY)yZ{%_EuT`t*{PYI8(66IrJc92XKVb9MOfw#b&fCWnsN#9J**FFZhTMBCtMfSx!LE2%?oFShkYh- zYx&9bUqae5QnGU48w0H)+llwjyJbx!n%bK<`CoRQ%{tlKlmJG=+UvA%Ydh}b+}75u zwbiD=f-|*szlPIh-REzBXH4vkzgist7)3 zHvs(C?FCDt#T+#4BILQ(Bc+#zwc4-igaxtL_Oet<{{Yl;e;`-v<3I4`$<{(&`mLEj z2V=7$BcmIO{;CIN$lK_%c6Yx5G*O*c56M#bppWJP*vBa&rTO7o8~*?;3&-G#7w8tm zn@x`!$3DrRersb+K>q;Q`Wus;APv0+t6@$B=pu^w6AN(It%+}{mZ60Bez5-lbq$@_ z?TVP4?!O5OX;S!h>;!+sW*^S$Z^L68P*o0al63fh7S)NZe47@h3DM1Ib^R4QWC=u( z9#3tRBY?v*RrL0Dr3w0q`B~Ey$DvzR8w!Y`oPUwulGzm&6E<3N_=YC>g-2T4RP`~+ zKLf6ZnKX|GLy~CBl-FoNAs&0$I<1{Sl#>GQalPaDD#@7k3mMSo?Xbcx0jx_+1}n>jzxsIv`^1zTn%c!JBLeL_LjbNx8Rlcz!54 z-P3R~_@+zTHYV7tRV7nh+EceXaO9bpr=86_k9aMmO?-60HqA6{f*{s-dudJae6moG zA5&84x@gr;MBEhgY!)Lmv-V?f;_WIeZpu9E(Z_op`Y5Aw zTG-?+IsgE_$dx~7*?k?L+L>7cL>_Lm!ri#U#e@Il8LPCgkb zsGT7VW2`PYi}O`8Vq}$BSY4sE^m1+|86M|f%3W|iN{xo%Jn!+ zCZ>(!@iZ0tH&=7?0TDc6I6p+_?r1iwjr=bc{Vcz0s4jkv%7%|{-}SPnq;?v*86rJF zZ|bR~(oa=!alfjTC*xz>*A6WRU4r?7x;(dskVa|EL%`Z zhS|hz>J=1ulXGikZkdRdQ?z2#oUdf=VLJNcp{I<#X`2}4=U6HjNfgnU&%ArBTT^IU z8SpUqS=|WnT@Y6ArhB>DRb<&6cW@R=J|%f1bL zyt!zjpo(c`mCX%!Ciz%@byr$tijqSN2h1{ihxS;3u}7FeyNTd}9MyKwdqV($gj`K1 zA*bNBM$VOySJ4?NxJe_4jpTcPTFN&{9N1nTqb6epd_W=ICA8|;ZA~HXVIi`XfzJGv z&@l-aK~LiGJlijd+Yh!5$*$A;t)%>BX&l!AY#WDDmZO4}3Z~3z&P~$i#mp(AmEF$U z7&6*Su=lb7-}ORciH{DIfQ%mLI${~BnIexAAj~a@YSx~J)kf?jVGpz2jA}X+Q`q*@ zy_vZUvzklorNWk=HSyHBvEMUhlG}c(>t&VI^GL}DgDeB#Y}G$**`MEgMVWmW@KCgp z*jO%ZY&9x2lHn6U=WNt3d3%|eX=%1szf~n9k8^VZ=NKFXvLlYFTjxsdI=B*lHB~d( z_yZz)9pZ4k+r(%!QXOr&>f6J&Q&cpGU~7j_b^AM6bMzq_kD4y$=%t9c)H_wp{4$IF zi}sIaU$vkUw0WU8za<_l4-Sdm>&1#1-2^v2Cx^!OF~f^ZirTccZ1`$f$@x3{!Ba7< z?6!QR_R-E3A3WCDo9v;60r13sHMWcx46)+=W)AoIDw@aRo@YqzZa3d;Q&4VX<;WoK z18wnITSW0F7*3k-ULT4%n`EP7JKXFnj4XQ#ZHdJ9cwITnj|hFnVYmHIz9%~qwR@YJ zZr+RRl4XG zvfJ6>bLN@fXzzIGipW8RX(VB&a{wSbfbW1&NIOZUbE%l{{PJ_Z3wkQOt0P@I`>pUW zXx+tiSiH&J@*`I&y7_xLXdNTD1x#Sav?<^u$fQvETJM9VV?bEE?o{J9LSf!Z)rHSLX!ZZ~IaXG;TfexYL8ZiWZCC)g71$3u=A=34AsOk=M~4 zE>ZFbEuFMlSyMw4pVZr}jYu6OKGzgZ_a%69eGY0^<$E0XDS51w-**V17-Rt@xc_d~hc4UV)pH&ln zv=Cb$)NoNrQbbZS02g@{IJz&S;&JZN#CwB7cJt__u8vV7T?<3*(#LqfPnnpSaKaix z*jt(OPL@cE3)=*2y9;!GLv-nkm%`D;!#foWY-?IF83h0sdnTwcV*<{#t`k`B^WGe8 znD@loXR>3iUcqACBQlMAqYy2or*>}QUDu*JO0O6eje`tr>AL8Jbmo+KDvh&L*qGeB z&g2yq%BE~kvpz23O2d{0P(UV*tni4yjMiPG+k3#juNlA$IOXN=B@s#4y9cld+-diPI-TOK75fjZ~p+r z7D!};z2Gz+Z;I?~D>IMI&vdG5hkz}vT~-D-M)J^jUzY_el;YB4*JM>q*w+kTc&(NC zAo$x1Oc4X(7tNsYLuLr0Y_6PN1^B3oF&#OoJVB;mi-1%hNog;H*r_W<7?}JnY0M23 zeO)scSR=DQ#5E}Fs%B8fCHrpk`Q(ZL1ki=;xzatXXt^fIyDEk@{o{q+dhTh|B300B zvrRV%nY)_Iq5`nK;zuOy3hJ+9QMzwL75u5%-8hYXRzo$Gn~I6PC$xM@>$)0l^s+IH z_nhl3jZbjdOsD=BRSwFUjjSBraV@b}NRqYSa%XUUNVweR@!SHwVTrs@wn1}Tinkrl zJQcX2tcof6D}jlLi#s$S$fs!5sf)DBntyw_l8~HTorqFccQaLNaeQC^Mq`?o#_U-% z{z-th6Yt2XNU`KjEXd%g+2M>R9N=(;OMBqrsRK)Gk<=}o9f-gQ=2a|XxlZ3iOMB2^ zDq*M=POIA$B%cp-e79%5=%H1%=^91DPA^4dcVP%+-b3QJfVIs6+ZAA}fd~#eiB3-U zkVAmnIW9ZG3$de}qM@<8wBd0!7hRQi`$Y0Q(;no}W*uzwa;j@EzKM@_2kGh-*{!}B zp&L&K#gx*U%Ojq7xZP%$I4rWs6z6b2a9B>aP2;M+3m^(8@lD~r=q`wEE(-zD>+q)X zO*{}8Szms)L8qZs_$y>pk=@k*KtW>&wh2B6y0W03<`tUFH*n;$O^a?pb8CVSJa^*N z4a0D_Sh7wQxXYT1=bTb5|>->~OqvMhe=NAAGF+ucFq| z%fYV-fWs5Snzd34gIrW&HbP2{u$H;F9n09d1A9A8&}y$>6C!2ELUy+cQvu=CNE$FS zb-IEvcQ^ynH50Bw^6G51bP~%IK@G#DjnxB7g*uTgb17&>6 zy;E8@6UW$V^U*RZ*}3ky)ntH7XjVrxnkXW2vd>MxrVck*Ws-0?1VV7)oa~z0sHw%? z@aKD>J@*Xmx1uWJ+P3iJZYXn22R9AQhTm37nBnZ=*P5z-p2^hJ-KPmZUEvMfEx5P} zcO37MXvl?Y<&wde$gEgdDbJq!dm~hAsHquwWov$GX2$1Wb;#nKCHpK(XgmPdhN|k1 z6-0;ToON%P+_bs_g6Zs8 zVOuX=g6c^Q zs##7!OD&0Njw?8Y*yUu`Aa~PL#@ueRlP2)FIT5%yTQTe$mHm;b2&2g|qBk||x|Wgd zk`Z?DSIdvIQA3K1Xcr)f>0gK29W0kODyHkfxi2KvOdw>b!B}5k=zjCn8Bv-h}?aX?b~rqx^=mr2Y8^|nxvD8 zmSd1lkhHicVFS3B%>o0890ES#lai4WtB@JortW(y8->+nPi?^d&@M!McSaS`mmS)vsQ?U zAWCT9hV2$hC8Gbt045Lt00II60s{a80RaI40000101+WEK~WH4aeQ!)J~QJ!GvPiH;XV`N82HE1e1C+rw7kFZfuou4 zWp`)I0Of%4@`W6;*q-DImIcat5H47Epj@-uhiW~jcR?DsG?Y%I zI-eQvpBeFwtbJ$3d?&&_lHE^)S5bP4>Rv0TuA=&j>MN+OqPl{3je!Air3@qqUK7GW z;Kn{p_y{2WDKHX{GMzy60V&iY;RzHZQ_(Nf^-o0fPfR-D*AAFKN7oLy>4ln}iRzx2 z>y^XTJsuI@LPf%&(YJv5L_&Ir>LaMiI+f}Ns0<4LRw`maiBTd!{02S~!SG`k_*5XP zg6bD)9T1nI9aGT`iRp{t9dn@wj5zQw0zm;F;AkZj4VyD~yuNa$k5W=!R(PJ}_k_{Y{hk@cRK`o(oE)Xy2nq_rx>C?ja7k?_S5B&kRv;KsZu7{K)m>Y?cd z1E@Vmq64TsLF!kjb5o%Xm~=y?25NMtLUiX&6zMvXrwERyb@<0mgiK7ldZPjyN3K0F ziA}@R5NMR@Bd9%3gs)T4A6e<1iSUkkhpeNB!>QAln3!NCMhL}0;Knq9K1tw2@WREt zK*9nLkC~wH5McoI1|f9^ zsYE#NV-hhV;`wP9NS+Bh@TMduwRlM&eNRmELp?*%9msmY^%4@XdV}f;qd|BU4iZTE zf{QONE!1IML3plS3MNhlqIoh%Yenk4Sn(J~8x41R3PMx($nZnd zq<@Ah5gB0dK0GCJDoRHDN76FJ@bQwKW?oE4JQ( zQ20oZl_I=OYJ+h)kzl7%KTv@2E!T-6;w&6Vcqk}82&KGAm#8Q<4kWa+av_8mpA6_r z^$RQ zLGsig;xNF5R3JQ&l&WSFh&`SW)xYDM%*-G~q2LOLtRn&32q^^`mzaz&SPU?bPZUIg zBoILdhVWwd7lnBYN@ipa5QII#5QM=LJRfwM$4focSlAs@-St^j$RB{#qfL&g^XBuPXPLZ6eb>04+Mp% z5R{sHL=ZxXyaNb$FAI$({{SQfM}da!h%Aa@4Z-<`c3UEV*>Q+BQv-wF z15=V(f>R>EN{M7g!vqJok&JwgixM!w5+n-n7%zbEgcxoNGKYb1XUb+Mt{9C5Ux{IO zGUfjO67z*0fYpGlpkke|K4G|m5V2M~J{edMHsWAd7$m}Q&5H-z7jlAJ!Hyy}If@dP z!Qt$W%&5U|ME)gXQ7(HhQ66WuEzBE-gm)?*-~;1$F+li!7rDm^*+!j82I)2ERHC-?}W@fbc2B7+KpU&5r`aF&Y8XDIIsF{cc-LaZ^;eo>VU z;m3N57J~H?8SIGa4ZI$_C!VnKi8;;|xpg?mumQtf`3^xNQ0v3eZ4jw543NDd% zMstkPkL4N4au`{>BY2LK(zrAlRkO`0fT$Bf64;brECrK0#2!-&h>*Zy0mdQ5P@mci zAMcxA(irBNqYyQO(*w;c8gbej5f*N$g-e5F>ck1QH01;T0Iaa8%BE05ETc>dM{+c^HEs+ zpVUt7R>cM)kz`UW&rAl%)FC8){KFC!IF#X&nE)Dsm52c?i3xCy(SJ}AYvK(k1Fb3E} zikFWG+Q+}kPAPUU6A$la3SkvCE4n)!mmSzO%&4_4uOQQbER0?sn8nx&$a@hROA zO%Zb^rGOpUu^9KDs#+!4LV`D>{+J+G)EO@5=Hd;&OeiJ$^ca?Z+#=cgV9eT8%ps^3 zG=o|oV+24fjzwTj60V#BlvFBsGZu9S!pzI5LsTMu1Q;^*RKBdbNlm8aIE449PaVkrb0vZK|%Ov!N0 zHv&DfCvZhFFDdgYEJh)OHP751L-!0(taDhSwkL->LSIG4%eTr=D3^53^k%a)VINC6 zB?!5P^uaU>@`zy{QxyJ8TMzJ{e-V_ju4UB?_aDfB;I}D|FQJt7{SzG-mR0SHpY{-D z?S3XAyiGE<;tB-F6fdkm*<)nGfd)J|2=icgL^{FQ8;UlUk`WA~4Yomd3}}G@)y9Dd z1?u7&CJTa0IEuQAxuPSv&}IevVkvL-O7me0WHf?j<`b*KBAafuKLG^`W{M%2^Hd3I zbb^4Mq5vo27OD6mLlfyXX+SHFaC#)}3n3uob5Z+LcbKV{`yO~{+|jwmv`juV!e!5M zbhBWDU!Fmz8OlS#yVRwErQnj4Zdjo9L*MC5y!Mf~gSY)>fQIAqgnx=_zXAteoLqH! zj}V_y!8VLr5V$3Zsym>KOCz@nHAvm66oX6M7u3?=^%9@q6|#pDfijI@4!jrvT-2xr z3gu+ZT%j9^#4D}2nqfKZk(5(i>2ECFD4_Gc-1%(N?THkryW2Ethgopfl$ z0y*jv$1!8PRj4?QctW!%EDLiAw+URsCc+A8Qr$AzDt$))x*>p|nqzzskZ;3;Gu^p} zfy)6By|J^qRExQM2qH;vde&F~a)Sd79*8&r=p_eH9g!Rm-J?W+=3BIDc#k4o&xn25 zL4_Ap!>R;glIm5Qh$0pRsSrak7^7Azne`0gIwYWRgVXiQMy0q#!DR$I4e1Di>u|$m zQU$<(1nMud%xuUPGU8Eodx3()v*rOaVQPqdK;(dNQ(Z+%xMn>OCKQaqE-KQMF9d8{ zBS(z1bo#d$G5Zxi+Ay^l^0DHxvLOM#0y-@@gGj@?$|3#?X!w*Ojw9k!RDqNeghhlo zWr$g6U>`-Kp)GSLtP7tv~^x$Xo_A^hBXgRVdaT;me0GL0aZ|Y#N@J0U-AR#I-Qu zOU&YWAS*24aCLBJaR3mni)JBi0+3)DjanmS=_|X0qYw@y=}_!OiU_O)g2hm~_W-yG zaT#cbq9ch%1$Y+?6G^FO{o_$Valjw}S2f`-=3H)ItC+b(=0c}}g0UMQu&ZMSltmS! zg8?l|C=je6?le0ysYXqu7Rm~8tPHpuYI8xt5cH2!SWB6(SQ4&#f$#_gP@ParqV|_0 z0$Ot&V&jk*LqU#n8fJ`9Z9)(hmNwQSBs*^sOs;3B=#gR3SUWVV z;#K=bP1Fs;EvT0ST;Zj}7YK9VODnDj<*%CdL-VX9=hP ztp_Xz=VhQ4`X&5lt;?bV+}A2UX#i7iwglKRQQ_?vy4UQ3wdSTL~vBgmMzR6j#yU`#DJWz z3|U-bM}QPH^*9pSq9ulR0$ra887sAn2SIe$5#yk1DJ(4!b}+pxW(*jjkg5=Ns8lmW zdlN-9-#Nrpm9qYs2=M4UT_vmQ!Q8X`)oi5JN|d9%Zyzz`2{y@2rR1BSMTr7tpyUXc z;zS`$?k8QW4Uh($UC2s z`6RD!0W6uAgDPBswMhG!$|^lk1{Yv%3LmP(11lyW5zD`DY6NA5>Lx1$D-V^l*92np zA~$$p?Lf~_s*~d>Ev$+ZM>W3jaMWzfdy1p*GKb+ji*ypv{SnJd zb{Za{_byn8QoiNs9nk&_$I;>A_BN@GAZP`Sq^U(y*oY?u%YW>2JT?pdB)%Q2ihtXO zSsu$8A54%PMeP}RdbtTN&QMtOyQUlqbN&SMh`1tgj;u^P%uwq{%{FC9%&x64c#GQ9 za9pdoG>cuNeT2ebBTO%6{ejeQhK<=**5eQ|DYQCWVilJq(VzPe_eeEVYQ=s`P@1Bl z`mjXafB_oxZX_6}TPCk&pb^8Mqvg1hEXXYUOks(eaQp-t*-*6y3w+A@1r1@#bm9QZ zLznoVv8Ch7k&OO=frA*c0-&Mzh6Uz4~pdtAgss+Qtv6{89?_bL4a1zd2cZYEPIianXr zFtlw3e$=0l)2o`_i{I2#FT_B0#;OS}(wUqDP>vnzfjfH$iyp+4aPf8j03{Yc@T?4N z7sVNY*sFyiVSW&80@#Mjd;b9a$cVCHfZ&ZvPcji$sBF)852!i%NvFvQ6DB{<0Sh6N zLa+8Rz`BjmZaCJTqY$;nQDPKzYmu&|VkNl-8lkwNI5|9Tn5N1FEIIZ;!LCogCZ>3L zrVn7NFV9wpz8i*TQ2zi*<*S#9{{Y2}xxfDaBE>e#+i?P!2&nU``+%BL*)wM`P}2v) zEmlIuCM5?W9@Fx1Qtb@9{2D7W6C6>LwLPmJs6C+K zSr=)cg1y9T!qV!}Cb(tRVCLiCL4+z^d6-lQwAjpf&}ccfXso$h%$W_{VRIgC4ZqrR zVXS&W+muj7>W}>9HOylo^yw~>Zh!4C&OjDQcqv3==<=BCnTPEE03n(q$85=uvkr^; zfz004IRH0iI+c2W)ED3NxKbq@`j%CF43!Xt**B&M(}rH90-e3LG|#|GhDyV2xqwPj z9M>Z(=|gK%zpTIQRAi2cz6Gd;6K1l9B!*nHYTr{W(ory{#R5?1fdF5yFpyr=PnRfB zwT*Se0Zo1;H893@%!)GFWl;1$^7tY!DxWx?qJsYbvuJ|wzvMda{S=iy1({swHR`k88!5#h`t$Kn>0_?W=M+`FP%5kLHcEaQp|{@6v# z#;^M`p5Ke@K!c7A=(ssOQ|LO5SCC*7J$r?z+NHYx03q?6^2eA9+X0Y?9tw?@R+&>| z3lm7E5DfnSXg1jp>4_}4wY9cX=HPPFcFrJD<)HAd^!F7g8tzH~T7#mR#<<5WU>$iu zD7GE^!sUvDV6|CpAz0S%k9nG}L!!9FOL%W!b>K6Lmy43BqPn4|@klAIgOx|h?}BRh zVkL(pwhlKG481z2g8GE7nk1ZyIxcvLi|ngsUr3>5s1a&`aiez<=Ygf_s~sCo0wb(9 zk{C1{;w6=ursM9gean+gz8^ynYRpC^`7sfpey9<^G4^Ho7L3{Vlj)i9cbNYGL+L0h zaVCgZQNZDVDh6Z@e?H|b{Sz}8?Wt+4WvTBa5x^=URCM1MP~!BfM=ZHo3U~MhiBs~` z1Fi&Udq?_H0reGdtt(dsP=i(v`4mcxL0<`9ZSi0#x4Ee=hQMCT#oSj87fV-3F}Gx} z$?gTW0?SQ}P+qZY51QDaOO(#NZbTt~s#nWI2xyv09_*q%olv%Dwg7V6Dn!Ab04y}A zt-r=PJ$q~Yh_jKxHbQtKyhm&>yBxv^X9fMCJj600h4arv37T|g6&o)VJe9Lw?pDQu!3Q0 zCH!6paNn3;0HUX&+y3ma7HZ0VsDo?=bMl1*taQQXsYo!Wq-D{}skDq1(I^fIR(GU+ zU?>nWw5B@%8G9zNckcujgat?gzf43))RQN_;^baF%H>7a81f zrSY1ATS2u|6L-Ik$sxkD`gvRaNUArwZgI%wtJHH%sp+WaMOwEs%SjU0bS;Id9hWxU ziz+WxF^f&?27R#sm;%e=Ik;!>&q;uTRtd7&0Mo_BD~5*j(w7R@kik5P5IC^3^VGs9 zg6J2lpkMK**@X&eTrqoPAw}TU%ap>jXqV^?W7|e+`^;qpUQuHkkPU6wL@8&p%qP`S6I?}Fpvu(E=23tzo+Qo2eRrGZjirE6DnRI^f|tGdJ@vXq6e!Ex?#^_aD zXhu7tfHZQT$zCBTRxHiU98AzmiINamp*g_riCw1xrj-2DW`<5 zaFS8mD+;(CM1G@?-BqD&Ehi$5BD6KOlv6ebUBCb-FKjC4Ec0wh_S{37HP`aDQjj z9oH-(&Du;Gh!ojz;$o1)EdAjX9MHmp_K^;A0as|J`i3|=aeXq;K%6F}M0k8231Nly zOFVL^$LK%Nj#MR{#{jZSBS9|YJwnG5l0bgNw$5@sFH;(|W-}9O3n+u}GL8dk$8k|n zL%73BRw|9)hkO%GY7S z%5Sb5iDm{Z1auvza4e2(70ZT5YsHW$Tpx}nLYZXgNYb~NQTU=TOltUMU^u>1F4M7-+t0lzzYfp$|lhK%Sa&3cC?bb*E1;ea)v#Jx8=dV^-6 zKv8B*MScUHu$LX0V9St&h6}V5_fZu0CMJvuthBK@7_79BkCh1bfW{_+6n5RkYCHt~ zQ+VGnG9OEz{NgU99%2W%^u-)QAj?vhzBLJ{)TTdW`tY9_6?!4I(LzP@3h@*@gNgtr$v( zlr1>1)GaEm>O^2B+zsU7ss`-9DqAqzB8jr>;kQG{4+gt#6@ta2cNB~R1Or6i3(FWt zG+v2x5IU$&i|MYTyir?lbMpZO2nW@U!PG&@2m%gi4Z0ZsyZK?lu_@t1byO@u4cQV1 zBiaf)QXwTp{_?^+wpzI9&A&uR!K>y}k@ehc7T%l)sc??qtAa(SL74CjcUK-mF1I_3 zCLmDI&m=fR-&&NRcV-UBLx3Yd*Ba9ZwRtI5;#Y8DH?}aMh!{>Ogk^KX0aQxb8Jb>M z`<8DaDN8?L8ZAY;ZG?_Lg+Qjo>|gwfLbZ_p0I;;-oea6Bd_Rcx+FLs;n7#qK)x za(`t>e+>R5C3@%o0Fsj=UPWboR%vbMxG0vdaJ6OI6U=ZhfkilMANC?n`i`hzApBL! z41UYGg+nx+;dRcg4!@%TX++q~DISw$(GnJuCtidGhFVnI6V8MKk_e4eqc3bllq~4H zA>A8avM(?#nM%~ROs)|+OYzxbpnHRVgzEnQJ=6@L?3D~zMwwbVM}10N5oByjw*#p@ zd6#BN+fYXbz(7i>^OZ#b zvsqw?ERZ^HGbUlWZa|C9@n}^y0Ku6RpN28e`$z4fU2$dS;Y4gzbL{%Xilw#5e%QI3 z%*DC~heR1xt!Rp%lf1!JO7|69t(aU=$_S%5$7;kHxF;i0-GLB!MF1f$6f59?dkcmh2bw5wW~0|s>mcUQkbHMfWms81p`Pmi3KTer3K$Ry8#ravBJ78n(Z=S9n_G1& zhYg*>bc$I_eAjCd$}AUI#JH=anL5~3q(N@2S|+S`q5VKLYwc_^md3nPFszZxRX{x4 z6yho5^)7{N$M+BiSS)UwOh6`4%uxaZ6h=5M;iwf5WfWqW@=}u0jb%l|R!ozVRHsT| zDr0TbC_{0xI`AxyxKPV1cMJl2;Elr3Nkhc1bXXQBfGkkVGM0La9U)#3bU~?etFelr zjJufdLFxr97wS=0W+-OVqKhaFzibiENCYAkxqYLuV{>a*VW{M_Gc132W&?eIwpUb! zLuYG5EQ!=jZ+2WF+o^kPoVKCTn3a|!M2$tA$wgA;+C9QFf(99MwqQv_+@XSzx|In} z1WUsdma%U-m|)`^w9N5Q29D*T+Ja+x;Ti!r93i83<%Gw}A7FB^!l?(HR3DKz&B27A zX=>Pms2C1)!z+PBzy=tiKNkxn>1;5w4`I~&2zz*!-L{~oG#E%wR%r;^I-5pf(p`)( z1!6qI37QmZQ5T4GiqNpaSr}rmGr&g8XSnt*p^5w&%42JY`=d=kwg_$(4r&0yGv*~A zl*I^%iNWFste{#jnrbz|jRn+1tP+rch=9ZZ;#tj880BKE6E`j4g}Oz;8m%VDge)kz zV%jb@FscFu*P?1tFH``JnAV~39Yy#`*#oFarlm)jM@l7bkiUhN^}N2ZX++i-HZP)I z>kQ~)dYdBs3M$&D7z36h3Wx;5 z@I9eXoWt0G@NKv=nCQlgl-^speNqERx{gf7#AU+1P;w?9dV#`KuiVh2u)rjVO~$mV z2M}*lOv20va4^`B$-+a{6I)!|r{1HWCHsq;zj&EpKe<+>88+1k%6QPl&W{GtKZx2W zFvQJGToRWJm|2J!VK9)Gjl)!KQ8Kp%s3Uca8={V4<{=JcCWNx)EWoy4w)F|Z`+11xxPl`EgnR%%iIO?gRqF%_C~RZ+0z~!T6GUt_@KZ(1Y&P-3G{VXc3YD?; zi_}oZaPuq-q$q;uM6!4UMw~%v-AB$~5JEn#QZP)TD+Qi4DXD5|wgf^{YcpVCU9`c_ zje%zOBm69$S4^I$UBMvjH5~nO^sYjX&Z=)62mehp>3pP?m9>{9PEGk4rFifdxd1 z+*T$kGl;E5#Re{6>QYcF(hFFY7{D2I0qqqM>fCLy;t``VuAD%H)x|k7mK?z>>p+As zgjJ{p%Z3zXrJ@E%Ht>*@AOFMvA`t)r0R;g80RaI40RR91000330{{dC5fT#+6d(o_ zF#p;B2mt~C0RjN$j(OvbIOC2y_~VW_=Z-x5{QUg9d^}8g(f+`#XaF^>YwoqJYvBGx zYo0lhfd2r%f8)LHuDUJPUH9L8_urs#C#Rt_f06TaD|-j7y6e7+_19f*b-nLmw_D!m zd_UI5-TWtH?{%sl#;ah~z5)CiCx3VFAD+Ix-lktqPW?SQ^b&h|`ugQEpZEU&?;P*_ zzx!u0a{mDJaru9LZ&4?wr>A00`|q$m?t%Ib@+OQ=J9miqHUZ6OK9HqL!B>iWpIc{{Wl#MYos~4|eCU+pQYV62@)91m=k}9URa4 zO~<*L)V|7y{JaDP-}8tJD@cnFiD3)QzHzGF{WjAz(7Bp%Znn!0B1KVe`NTW~3rxHi zbPEs5`9Bxq{ASd|_aC*gkDF}!{{W(Y^beUrTV+})Y|4J5j&fo+-@yWPE!oV+b|v%@ z3C+Xg8!-7+%eHPD-|f1)ISDTPHffHu7tE)SHQ8xxp@rvst7lt2*@wK^s(~lClf9QR zvsR6Ceg&XQ8rG?&Y>Q@}KK}seAN@3+=$kVo_JB35s1Y*aPvoUibz^b*HT>sjvp|lx7oP;M(yVG{U&YOp*4QOg>>|^lKOOnw_T;2 z7}T)kovxZ;N|B`(_3uOxM3yh1NgbrL!IAOJY``;o?Gij(ScJx^?6cg-a`KZ5ai@@H zl_VD-3~ZbT);}Wkf7c4_fp>af~X-D{Cf66q*jJItkcUP znnqGtNjqjTWAa-ho10{^EI>ds^1b^^O2QyW79Tc{yL^V-s_QoM6;;Yx^HH&_lecB7 zd@N{9HEfD%n&y0hl~e%{6o6!Mpvh+n+uh~NtOIr%|HJ?@5dZ=K0s{sG0|NyC0RR91 z000330}&xHQ3MbYK~iBLGD2~Y6C!~yKtnTPp|R0Wg8$k82mt~C13v)pz06}6#z>Zg zi4Q}u*#6OUi=>Z94w4-tI!I_oL2~_)-Hzlf$U@w=p$l>li1PiT-24p;dW>Tj#xaaz z{nH=*k{5ygn*EIEPBBX|v0atTqf=UOx1$NJOjZz+%ANGj2)xKYQAcU0r+0FfQz#>6UXy2Dlu=%kP>b@S zlJufIB3VS6B&N4^+=L~*`7zrvS|hp_QX|$!Rwc=i=@q=Fw?U>}No973vK-`!Hq3z(Eq6WY{ua_)1iDGbC_jRIEOKdG+?sa8 zsW`h`K3J6(nUPBrCCsIl)kQb_h>uYcE0G;HEy;+`sFrA#NSd=nH|a$#lAeS+icsM*$2Z`l+$Q~HGz-f|x(v|r51wWPkCNTh9Ca#5;nDZA8U=CLHly$C`R z%#E@dR=(+K#`A2YSIq>-(p)jUic#}UE8UwU9lo@G7@IQ7iT(!H*+=D@RI+hi$gMSL zR*@H+h}r$Zj#9aAQzz9gk@LCO>|1l(^$?a<+`4Xd-24x9DCbfu@@&!0v`vyew*8So zWeKxN5Aw?EDp5A6WaA%XZBx#(Ue-lM;X=0Nh9ce;GFBb~8Fj6P&3=pC zw_ke0a~QQAsDjOrin2#yZzzI?w++V?0qF)TFqxdJOE}~qR$8qVp!aIyiM`AK5qAOCYUE@w` z#e`B-ix_tm-nHGjsVV!ry9dz9dz?$QE|t8|Pt3S>3MWE}UXhOBvbe z%yl2yr6Q}%sQZxhRBzc>qOJBf?#fZhlzt0xNJ3~s(T}8D(M_yjAA*CKE@qb`u(P>YWl^vGnY73G+xN%) zm7|@d_)Gm4&WFUTZ1T=hi|)VS8Tl$kUe4r^MyH;Xf72<;$x+9%#TDt~xV?)upT&e+ zt2|oaUGOMV#jZEsAtsW;cX7!)MDuKhmmwS3V2%dt+;C0Uo(qLH@K$&y!+fpGomnf% z+gFJQT2&I1zrx=b;@y!-EygX~bG6Y;;`pU6d<3$jk}5nDBEJPz?<{Z=vtI(j%J^KA zRu)ODt%%-joy7B36Sf`)nY7cjt4i8Gxqp$XeM(E-F2yS9@Ne?1x{TGPlQw9ps#osa z%5Zm$B)iy@komG3ub8=_D&{S%ehGAyl(4@ES>&clF^in6K|xJrlz;Dm?GtHv6TxfW zcVfuBnUQ9RYAwvUW?P=>lXi?;y^&s4y`%2RO5Ag8_Q0V}6-|4*L^iF&IV{Bz>0J#H zOv?f~7WALmi8X5#IY^p!<7G~^RC&%%@^MPm6e&}Vkz1@av({^+?q<8JNpmE|kgS9u zyb?Bv{{S7m@FN($IbFRu8#>CG{NgzBu@##kjjSh|9I_S{d#pVMvrKOhJYR*0rGhRw zJ0v*Swveh(rqkuUN-|HHu^LiwZItZzZ*iv6-mX}j+u?&{c_oE}llqlD4W2%pF6@jI zqZJ=zxh0E2SreB$2h3)7cNK`1_9JgGE8!w5%!gXs5W`o3C{S0cJ}0^s%!AZla^Y-T zskTY_B8fcb@KaAV%23JzR)ezB!S$nqw3bY%N=`CXXyU<@B_pv!VSroFdR;E$1)tffVlc_7dKTx$rVi9F=a+nER}$(Qn> z$=$KYy|22$L}FleIENQxR;bn-D_5DbY|8SL*(qI9R_;n}@{?oaGBs#R1$B+fZpYui z$ANcZ_LQo891)Ce-M>LoSzMf!7!Fyz$XDHvt>D7;9e16IbR&KnvTun&aHL9d#fl>7 z9gy`u|HJ?z5dZ-M0s;d80RaI300000009630}%ug5EBIyAQc7{F#p;B2mu2D0RjNK z+ikYm?(c1`)!xDT_I8QAk89lbMs(RW(mo0(qOBeYy|>?e_b(6Z-+lW#J3D)OM7^Dc zRz2hq#iQ~|O@gUbpacM5W}1fS)??~02uwYFT|xlY7LbR);*|lYXl8;gu@Q_$8W1}d znK!#9iU0*}aX|zS`=9^-Gzu!qi;Ii)+ii=t?CkCBGWKuS919v+5J3)Vkj3>zH$Z^G zVlj0XOePZv$MwB$TL;DdPvrhz=6-kQi~)s}f$I9Lf$;S7bO?GndU`qu99ZItD2D}H zw0P(7d|wV3EKT4bhuQePDu^LhEm(36LLWKttK=Yu&Fl`qt0wm(qT=G<=e{Sx`uePh z`V0ppnL<{)Ins=&GaEZuwe8p*5iW6Yd36kbSJP$@@$q=fc5SdAs;)Jau5D1_D1n+x zIoagBBY-eQ3vh-nsBry1SYtK;4@iu$ZNG1KcW&{oQfzr43$jdiVjN=?42#pNt3nu= zj_h_tCy+UAoJpxJpV~PiDbuYS$0+iQ(ntL(Jkta4$Bu7{9x=EL z#1T4KvL42|De?2npP2mYJfCEF5uY@=GZ*s?AjWP#tu83T7%lPfVm}@{X*_#zV}+=^ zxm9v=8<+YAA%slz23*-A40x9KSo}jUSsvn7KV^Ge*e8(dR);skFY39~_B zC76wkP;DxuRq*^%hDEU6lx6LuN~(yg`I^AcVvSe9As>JdMnK|=84+{j4kqbRC@pBB zr6H>ra(+i3DDoX9;h}&OUh{NYpx~NBaYRnbra7+qr$4u~Y$OS>2(m>UDY8N1lMSLvw%DR?Cl;FBB7O9= zgtewH8o&rIGQM*mdslv zKtf^#BSWz>a-q@x+5iXv0|5g+07BS85fVEQB1DN2BnT1Qj^gtl$o^yVlKhOH#2)pqX2u*iib&R(>UgH?| z7{)Q-a^43_9_L&vbuHZLJ;%7mxc46>GMu4#6g;I5A}DzgN63mkL{ai0lKqIKXwFN$*Bhg=H{zjTmvMMjsTaHMbnkOTyiET9L1?q-i|Tfv z7vT2d(C#=c(~d+)^C`WEK4{NUjADrPzSc*SSt(|qNu=Caov=^Amn6x{J2J17t>h}~ zRg$wM6=v=_aqQc?`-yTpP?ECHjoNP3B1dy(O2p)dq^^YWAq%M#Q+=q9&VM#9Ga^KZ z5ik z$VyG!i zA7xg$6*z{SMH{V^ijsL#m$EpCE4V*)R{W8VxodW~i9bY8k10P^Q;n?zbu5P73v3w1 zb?&kFRpq5AKW1?+{7?NHL)IyKl{wUkAwH_aQ8sOoGyEaLK{GsEX1fdPI}hljc3%4HDdZ$H^8-_9C|)!*|TjIlqw`M9)G? zq9f*y$sasNxi2$b`FX@Q!Mh5)a}W~n&o?v zYg-V8uI!Y#q*_9HoRYgLzuRTij``2wSdTlua-ZXm!EF{OgVvm1u}hQ^^vI2}b5H)) z{{VL;w5xynM*jfn#bx^=FGv3X(lPtvDEgu;WYZ+HW9J)?eC_MpiYD|{E}PdJ?js?Q zmZ(?CL{+bu7%Y-QleBab)XPJXS7UDSe#nefl1mM{mwpW?YXyDQPvsI{ZBJ=tPAzt! z?z2Vt5?Wmq9!=ZOZ&?;P62Xqxyjhy2y~fLG$D(IqHu-)lc*PC8qDy-m?sKrCYby># zo3jvyxci-Ow=wQ?u;o&3sm}!JN5$%jl`TQo{HqTO!yl z;&>;ZhZ>UgzBZg%{NKke0GW&}@4ocALG(rJ5b= zkrl{q1dCW!YUGj`CS0|yCCf5R>Qkqzv)L8v-BXWdEQE8GY)LCC3QFnU)484&?q2eO z^;j$?9IbRs>11$l@#Hmu4k6 z+0W`^Vyu;xGN)Na?$4Gw;hnHoEp}&O5p8x`XKGGXG-d2FBwV + - + + + + + + + + + + + + + + + + diff --git a/components/Adorners/samples/ResizeElement/ResizeElementAdornerWithDragSample.xaml.cs b/components/Adorners/samples/ResizeElement/ResizeElementAdornerWithDragSample.xaml.cs new file mode 100644 index 000000000..643b50123 --- /dev/null +++ b/components/Adorners/samples/ResizeElement/ResizeElementAdornerWithDragSample.xaml.cs @@ -0,0 +1,52 @@ +// 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.Mvvm.ComponentModel; + +namespace AdornersExperiment.Samples.ResizeElement; + +[ToolkitSample(id: nameof(ResizeElementAdornerWithDragSample), "ResizeElementAdorner combined with Drag", description: "A sample for showing how to use an Adorner for resizing an element in combination with drag functionality.")] +public sealed partial class ResizeElementAdornerWithDragSample : Page +{ + public readonly List Images = new() + { + new CanvasImage("ms-appx:///AdornersExperiment.Samples/Assets/davis-vargas-2vSNlKHn9h0-unsplash.jpg", 100, 50), + new CanvasImage("ms-appx:///AdornersExperiment.Samples/Assets/sergey-zolkin-m9qMoh-scfE-unsplash.jpg", 300, 150), + }; + + public ResizeElementAdornerWithDragSample() + { + this.InitializeComponent(); + } + + private void Image_PointerPressed(object sender, PointerRoutedEventArgs e) + { + // Select the tapped image and place in front of the other. + if (e.OriginalSource is FrameworkElement fe && fe.DataContext is CanvasImage ci) + { + foreach (var img in Images) + { + img.IsSelected = img == ci; + img.ZIndex = img == ci ? 1 : -1; + } + } + } +} + +public partial class CanvasImage(string _imageSource, double _left, double _top) : ObservableObject +{ + public string ImageSource => _imageSource; + + [ObservableProperty] + public partial double Left { get; set; } = _left; + + [ObservableProperty] + public partial double Top { get; set; } = _top; + + [ObservableProperty] + public partial int ZIndex { get; set; } = -1; + + [ObservableProperty] + public partial bool IsSelected { get; set; } +} diff --git a/components/Adorners/samples/ResizeElementAdorner.md b/components/Adorners/samples/ResizeElementAdorner.md index 66030ec62..0aa3485f2 100644 --- a/components/Adorners/samples/ResizeElementAdorner.md +++ b/components/Adorners/samples/ResizeElementAdorner.md @@ -24,4 +24,10 @@ The `ResizeElementAdorner` can be attached to any element and displayed to allow This can be done above within a `Canvas` layout control, or with general layout using Margins as well: -// TODO: Add Margin example here +// TODO: Add Margin-based example here + +## Complete Example + +Using the `CanvasView` control allows for advanced manipulation with dragging elements as well, this can be combined to create a design-type surface: + +> [!SAMPLE ResizeElementAdornerWithDragSample] diff --git a/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs b/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs index 8fca70310..df268ab1d 100644 --- a/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs +++ b/components/Adorners/src/ResizeElement/ResizeElementAdorner.cs @@ -69,23 +69,26 @@ protected override void OnAttached() { base.OnAttached(); - TopThumbPart?.TargetControl = AdornedElement; - BottomThumbPart?.TargetControl = AdornedElement; - LeftThumbPart?.TargetControl = AdornedElement; - RightThumbPart?.TargetControl = AdornedElement; - TopLeftThumbPart?.TargetControl = AdornedElement; - TopRightThumbPart?.TargetControl = AdornedElement; - BottomLeftThumbPart?.TargetControl = AdornedElement; - BottomRightThumbPart?.TargetControl = AdornedElement; - - TopThumbPart?.TargetControlResized += OnTargetControlResized; - BottomThumbPart?.TargetControlResized += OnTargetControlResized; - LeftThumbPart?.TargetControlResized += OnTargetControlResized; - RightThumbPart?.TargetControlResized += OnTargetControlResized; - TopLeftThumbPart?.TargetControlResized += OnTargetControlResized; - TopRightThumbPart?.TargetControlResized += OnTargetControlResized; - BottomLeftThumbPart?.TargetControlResized += OnTargetControlResized; - BottomRightThumbPart?.TargetControlResized += OnTargetControlResized; + var parts = new ResizeThumb?[] + { + TopThumbPart, + BottomThumbPart, + LeftThumbPart, + RightThumbPart, + TopLeftThumbPart, + TopRightThumbPart, + BottomLeftThumbPart, + BottomRightThumbPart + }; + + foreach (var part in parts) + { + part?.TargetControl = AdornedElement; + part?.TargetControlResized += OnTargetControlResized; + } + + // If the adorned element moves than we need to update our layout. + AdornedElement?.ManipulationDelta += OnTargetManipulated; } /// @@ -93,23 +96,30 @@ protected override void OnDetaching() { base.OnDetaching(); - TopThumbPart?.TargetControlResized -= OnTargetControlResized; - BottomThumbPart?.TargetControlResized -= OnTargetControlResized; - LeftThumbPart?.TargetControlResized -= OnTargetControlResized; - RightThumbPart?.TargetControlResized -= OnTargetControlResized; - TopLeftThumbPart?.TargetControlResized -= OnTargetControlResized; - TopRightThumbPart?.TargetControlResized -= OnTargetControlResized; - BottomLeftThumbPart?.TargetControlResized -= OnTargetControlResized; - BottomRightThumbPart?.TargetControlResized -= OnTargetControlResized; - - TopThumbPart?.TargetControl = null; - BottomThumbPart?.TargetControl = null; - LeftThumbPart?.TargetControl = null; - RightThumbPart?.TargetControl = null; - TopLeftThumbPart?.TargetControl = null; - TopRightThumbPart?.TargetControl = null; - BottomLeftThumbPart?.TargetControl = null; - BottomRightThumbPart?.TargetControl = null; + var parts = new ResizeThumb?[] + { + TopThumbPart, + BottomThumbPart, + LeftThumbPart, + RightThumbPart, + TopLeftThumbPart, + TopRightThumbPart, + BottomLeftThumbPart, + BottomRightThumbPart + }; + + foreach (var part in parts) + { + part?.TargetControlResized -= OnTargetControlResized; + part?.TargetControl = null; + } + + AdornedElement?.ManipulationDelta -= OnTargetManipulated; + } + private void OnTargetManipulated(object sender, ManipulationDeltaRoutedEventArgs e) + { + // If the underlying adorned element moves than we need to update our layout. + this.UpdateLayout(); } private void OnTargetControlResized(ResizeThumb sender, TargetControlResizedEventArgs args) diff --git a/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs index 2bc5aeccc..41ef35aa1 100644 --- a/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs +++ b/components/Adorners/src/ResizeElement/Thumb/ResizeThumb.cs @@ -100,6 +100,9 @@ protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e) var newWidth = (_originalSize?.Width ?? 0) - horizontalChange; var newHeight = (_originalSize?.Height ?? 0) - verticalChange; + // TODO: There may be other constraints on elements (aspect ratio of constrained box, image set to uniform, etc...) + // that may need to be considered here as well in which case we should restrict our reactions to those as well to stay synced + // and not move the element unexpectedly. if (Direction != ResizeDirection.Top && Direction != ResizeDirection.Bottom) { if (IsValidWidth(TargetControl, newWidth, ActualWidth)) diff --git a/components/CanvasView/src/CanvasView.cs b/components/CanvasView/src/CanvasView.cs index 84ea30a72..6f7e2b472 100644 --- a/components/CanvasView/src/CanvasView.cs +++ b/components/CanvasView/src/CanvasView.cs @@ -12,10 +12,13 @@ namespace CommunityToolkit.WinUI.Controls; /// /// is an which uses a for the layout of its items. /// It which provides built-in support for presenting a collection of items bound to specific coordinates -/// and drag-and-drop support of those items. +/// and support of those items. /// public partial class CanvasView : ItemsControl { + /// + /// Gets the set of properties that will be automatically bound to the root element within the template to the containing itself within the . This allows for data binding these properties to a templated object for tracking position based properties. + /// private (DependencyProperty, string)[] LiftedProperties = new (DependencyProperty, string)[] { (Canvas.LeftProperty, "(Canvas.Left)"), (Canvas.TopProperty, "(Canvas.Top)"), @@ -23,12 +26,27 @@ public partial class CanvasView : ItemsControl (ManipulationModeProperty, "ManipulationMode") }; + /// + /// Initializes a new instance of the class. + /// public CanvasView() { // TODO: Need to use XamlReader because of https://github.com/microsoft/microsoft-ui-xaml/issues/2898 - ItemsPanel = XamlReader.Load("") as ItemsPanelTemplate; + ItemsPanel = XamlReader.Load( + """ + + + + """) as ItemsPanelTemplate; } + /// + protected override DependencyObject GetContainerForItemOverride() => new ContentPresenter(); + + /// + protected override bool IsItemItsOwnContainerOverride(object item) => item is ContentPresenter; + + /// protected override void PrepareContainerForItemOverride(DependencyObject element, object item) { base.PrepareContainerForItemOverride(element, item); @@ -50,6 +68,7 @@ protected override void PrepareContainerForItemOverride(DependencyObject element // TODO: Do we want to support something else in a custom template?? else if (item is FrameworkElement fe && fe.FindDescendant/GetContentControl?) } + /// protected override void ClearContainerForItemOverride(DependencyObject element, object item) { base.ClearContainerForItemOverride(element, item); From c21abdf9b59675a3c5119bf90417252c9dc18211 Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Wed, 10 Dec 2025 01:04:23 -0800 Subject: [PATCH 33/34] Some initial fixing of the unloaded Adorner issue Properly handles unloading and loading to mirror with detaching and attaching, but still some other underlying lifecycle work to probably investigate/complete. --- components/Adorners/src/Adorner.cs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/components/Adorners/src/Adorner.cs b/components/Adorners/src/Adorner.cs index 4808edae7..4a077cc2c 100644 --- a/components/Adorners/src/Adorner.cs +++ b/components/Adorners/src/Adorner.cs @@ -67,10 +67,18 @@ private void OnAdornedElementChanged(UIElement? oldvalue, UIElement? newvalue) OnSizeChanged(null, null!); OnLayoutUpdated(null, null!); + // Track if AdornedElement is loaded + var weakPropertyChangedListenerLoaded = new WeakEventListener(this) + { + OnEventAction = static (instance, source, eventArgs) => instance.OnAdornedElementLoaded(source, eventArgs), + OnDetachAction = (weakEventListener) => newfe.Loaded -= weakEventListener.OnEvent // Use Local References Only + }; + newfe.Loaded += weakPropertyChangedListenerLoaded.OnEvent; + // Track if AdornedElement is unloaded var weakPropertyChangedListenerUnloaded = new WeakEventListener(this) { - OnEventAction = static (instance, source, eventArgs) => instance.OnUnloaded(source, eventArgs), + OnEventAction = static (instance, source, eventArgs) => instance.OnAdornedElementUnloaded(source, eventArgs), OnDetachAction = (weakEventListener) => newfe.Unloaded -= weakEventListener.OnEvent // Use Local References Only }; newfe.Unloaded += weakPropertyChangedListenerUnloaded.OnEvent; @@ -103,13 +111,23 @@ internal void OnLayoutUpdated(object? sender, object e) } } - private void OnUnloaded(object source, RoutedEventArgs eventArgs) + private void OnAdornedElementLoaded(object source, RoutedEventArgs eventArgs) + { + if (AdornerLayer is null) return; + + OnAttached(); + } + + private void OnAdornedElementUnloaded(object source, RoutedEventArgs eventArgs) { if (AdornerLayer is null) return; OnDetaching(); - AdornerLayer.RemoveAdorner(AdornerLayer, this); + // TODO: Need to evaluate lifecycle a bit more, right now AdornerLayer (via attached property) mostly constrols the lifecycle + // We could use private WeakReference to AdornedElement to re-listen for Loaded event and still remove/re-add via those + // We just like to have the harder reference while we're active to make binding/interaction for Adorner implementer easier in XAML... + //// AdornerLayer.RemoveAdorner(AdornerLayer, this); } internal AdornerLayer? AdornerLayer { get; set; } From 2a81bc4a03b3ac01f431cf37a193abe65c288d0d Mon Sep 17 00:00:00 2001 From: Michael Hawker <24302614+michael-hawker@users.noreply.github.com> Date: Wed, 10 Dec 2025 01:27:16 -0800 Subject: [PATCH 34/34] =?UTF-8?q?Apply=20XAML=20Styling...=20=F0=9F=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I swore I formatted this file multiple times... --- .../ResizeElement/ResizeElementAdornerWithDragSample.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/Adorners/samples/ResizeElement/ResizeElementAdornerWithDragSample.xaml b/components/Adorners/samples/ResizeElement/ResizeElementAdornerWithDragSample.xaml index 9111342ff..3a9ca153d 100644 --- a/components/Adorners/samples/ResizeElement/ResizeElementAdornerWithDragSample.xaml +++ b/components/Adorners/samples/ResizeElement/ResizeElementAdornerWithDragSample.xaml @@ -1,4 +1,4 @@ -