From 2e704d1fd411b8ee74eb2465f1e0a0b55689e366 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/21] 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 c32b1eb3956bc6b3a74c44a40d60379b9287bb5c 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/21] [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 8bff99eed4e21a2f177b3eb1f1491123562bac67 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/21] 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 72b2c6ecb8e787a0a6933ebb0799e4bdf427ae66 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/21] 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 18395e46a378f460f64521be3e2bd6abef04d7b5 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/21] 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 a8bacd0d2a56c81122f6895019306414304df605 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/21] 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 bcdc3f3a2d3c9fb3f4884313f1c29dad0953f03e 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/21] 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/21] 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 bb120de80506bc44738745a3f36f740fd7f65c39 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/21] 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 2f8d387b3882a92264022e89bc824546e03e7ff6 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/21] 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 558d40c9b1ce74b5da38a0a4a7ca8b3845150177 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/21] 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 1f7152720b20dc7e8c7f107abed0e73f485818e3 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/21] 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 e2844ec06f42f009a4ff61688cb0d91bb2dc3834 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/21] 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/21] 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 460c3f3bfe98287f92c5d79a810798748c1a9988 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/21] 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 f37a46b8ee3ecb3ddb385da7faeac79cbeaba427 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/21] 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 9eb63f530338016ed38557b1b6de282cc96cde5a 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/21] 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