diff --git a/CommunityToolkit.Labs.Uwp/CommunityToolkit.Labs.Uwp.csproj b/CommunityToolkit.Labs.Uwp/CommunityToolkit.Labs.Uwp.csproj
index fc4e9fff7..344a4e4eb 100644
--- a/CommunityToolkit.Labs.Uwp/CommunityToolkit.Labs.Uwp.csproj
+++ b/CommunityToolkit.Labs.Uwp/CommunityToolkit.Labs.Uwp.csproj
@@ -25,6 +25,10 @@
{a14189c0-39a8-4fbe-bf86-a78a94654c48}
CanvasLayout.Sample
+
+ {fe19fff0-6ab6-4fc7-bfdf-b6499153dcd5}
+ CommunityToolkit.Labs.Uwp.UI.CanvasLayout
+
\ No newline at end of file
diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj b/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj
index 935bb389d..e8f62ae6e 100644
--- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj
+++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj
@@ -19,11 +19,13 @@
+
+
diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs
index b9e80ba2b..faaaa4411 100644
--- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs
+++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs
@@ -51,7 +51,7 @@ namespace CanvasLayout.Sample.SampleOne
[ToolkitSampleMultiChoiceOption("TextFontFamily", label: "Arial", value: "Arial")]
[ToolkitSampleMultiChoiceOption("TextFontFamily", label: "Consolas", value: "Consolas")]
- [ToolkitSample(id: nameof(SamplePage), "Canvas Layout", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: "A canvas-like VirtualizingPanel for use in an ItemsControl")]
+ [ToolkitSample(id: nameof(SamplePage), "Simple Options", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: "A sample page for showing how to do simple options.")]
public sealed partial class SamplePage : Page
{
public SamplePage()
diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleThree/SamplePage3.xaml b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleThree/SamplePage3.xaml
new file mode 100644
index 000000000..19dc5ee21
--- /dev/null
+++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleThree/SamplePage3.xaml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleThree/SamplePage3.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleThree/SamplePage3.xaml.cs
new file mode 100644
index 000000000..784149e81
--- /dev/null
+++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleThree/SamplePage3.xaml.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices.WindowsRuntime;
+using CommunityToolkit.Labs.Core.SourceGenerators;
+using CommunityToolkit.Labs.Core.SourceGenerators.Attributes;
+using CommunityToolkit.Labs.Uwp;
+
+#if !WINAPPSDK
+using Windows.Foundation;
+using Windows.Foundation.Collections;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Controls.Primitives;
+using Windows.UI.Xaml.Data;
+using Windows.UI.Xaml.Input;
+using Windows.UI.Xaml.Media;
+using Windows.UI.Xaml.Navigation;
+#else
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Controls.Primitives;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Navigation;
+#endif
+
+// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238
+
+namespace CanvasLayout.Sample.SampleThree
+{
+ ///
+ /// An empty page that can be used on its own or navigated to within a Frame.
+ ///
+ [ToolkitSample(id: nameof(SamplePage3), "Canvas Layout", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: "A canvas-like VirtualizingLayout for use in an ItemsRepeater")]
+ public sealed partial class SamplePage3 : Page
+ {
+ public ObservableCollection Items = new()
+ {
+ new() { Left = 100, Top = 50, Width = 100, Height = 100, Text = "Item 1" },
+ new() { Left = 400, Top = 250, Width = 200, Height = 200, Text = "Item 2" },
+ new() { Left = 200, Top = 500, Width = 100, Height = 100, Text = "Item 3" },
+ new() { Left = 1200, Top = 2500, Width = 100, Height = 100, Text = "Item 4" },
+ new() { Left = 2200, Top = 1500, Width = 100, Height = 100, Text = "Item 5" },
+ new() { Left = 1200, Top = 3500, Width = 100, Height = 100, Text = "Item 6" },
+ };
+
+ public SamplePage3()
+ {
+ this.InitializeComponent();
+ }
+ }
+
+ public class CanvasItem : CanvasLayoutItem
+ {
+ public string Text { get; set; }
+ }
+}
diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml.cs
index c6ff1026d..b84d8295a 100644
--- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml.cs
+++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml.cs
@@ -13,7 +13,7 @@
namespace CanvasLayout.Sample.SampleTwo
{
- [ToolkitSample(id: nameof(SamplePage2), "Example sample", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: "An empty sample used to demonstrate the sample system.")]
+ [ToolkitSample(id: nameof(SamplePage2), "Custom options", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: "An empty sample used to demonstrate the sample system.")]
public sealed partial class SamplePage2 : UserControl
{
public SamplePage2()
diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Wasm/CanvasLayout.Wasm.csproj b/Labs/CanvasLayout/samples/CanvasLayout.Wasm/CanvasLayout.Wasm.csproj
index 9613ce94f..3588925e7 100644
--- a/Labs/CanvasLayout/samples/CanvasLayout.Wasm/CanvasLayout.Wasm.csproj
+++ b/Labs/CanvasLayout/samples/CanvasLayout.Wasm/CanvasLayout.Wasm.csproj
@@ -7,6 +7,7 @@
+
\ No newline at end of file
diff --git a/Labs/CanvasLayout/src/CanvasLayout.cs b/Labs/CanvasLayout/src/CanvasLayout.cs
index 8f3c2edba..c0b9f221d 100644
--- a/Labs/CanvasLayout/src/CanvasLayout.cs
+++ b/Labs/CanvasLayout/src/CanvasLayout.cs
@@ -2,11 +2,135 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System;
+using System.Collections.Generic;
using Microsoft.UI.Xaml.Controls;
+using Windows.Foundation;
namespace CommunityToolkit.Labs.Uwp
{
public class CanvasLayout : VirtualizingLayout
{
+ #region Setup / teardown
+ protected override void InitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ base.InitializeForContextCore(context);
+
+ if (!(context.LayoutState is CanvasLayoutState state))
+ {
+ // Store any state we might need since (in theory) the layout could be in use by multiple
+ // elements simultaneously
+ context.LayoutState = new CanvasLayoutState();
+ }
+ }
+
+ protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ base.UninitializeForContextCore(context);
+
+ // clear any state
+ context.LayoutState = null;
+ }
+
+ #endregion
+
+ #region Layout
+
+ protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
+ {
+ int maxWidth = 0;
+ int maxHeight = 0;
+
+ // Get underlying data about positioning of items and determine if in viewport.
+ for (int i = 0; i < context.ItemCount; i++)
+ {
+ if (context.GetItemAt(i) is CanvasLayoutItem item)
+ {
+ // See if this item pushes our maximum boundary
+ maxWidth = Math.Max(item.Left + item.Width, maxWidth);
+ maxHeight = Math.Max(item.Top + item.Height, maxHeight);
+
+ // Calculate if this item is in our current viewport
+ Rect rect = new(item.Left, item.Top, item.Width, item.Height);
+ rect.Intersect(context.RealizationRect);
+
+ // Check if we're in view now so we can compare to if we were last time.
+ bool nowInView = rect.Width > 0 || rect.Height > 0;
+
+ // If it wasn't visible and now is, realize the container
+ if (nowInView && !item.IsInView)
+ {
+ var element = context.GetOrCreateElementAt(i);
+ element.Measure(new Size(item.Width, item.Height));
+ }
+ // If it was visible, but now isn't recycle the container
+ else if (!nowInView && item.IsInView)
+ {
+ var element = context.GetOrCreateElementAt(i);
+ context.RecycleElement(element);
+ }
+
+ // Update our current visibility
+ item.IsInView = rect.Width > 0 || rect.Height > 0;
+ }
+ }
+
+ return new Size(maxWidth, maxHeight);
+ }
+
+ protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
+ {
+ for (int i = 0; i < context.ItemCount; i++)
+ {
+ if (context.GetItemAt(i) is CanvasLayoutItem item && item.IsInView)
+ {
+ var container = context.GetOrCreateElementAt(i);
+ // Is it better to have cached this from above?
+ container.Arrange(new Rect(item.Left, item.Top, item.Width, item.Height));
+ }
+ }
+
+ return finalSize;
+ }
+
+ #endregion
+ }
+
+ internal class CanvasLayoutState
+ {
+ public int FirstRealizedIndex { get; set; }
+
+ ///
+ /// List of layout bounds for items starting with the
+ /// FirstRealizedIndex.
+ ///
+ public List LayoutRects
+ {
+ get
+ {
+ if (_layoutRects == null)
+ {
+ _layoutRects = new List();
+ }
+
+ return _layoutRects;
+ }
+ }
+
+ private List _layoutRects;
+ }
+
+ // TODO: Make DP? Can we do this with property mapping instead?
+ public class CanvasLayoutItem
+ {
+ public int Left { get; set; }
+
+ public int Top { get; set; }
+
+ public int Width { get; set; }
+
+ public int Height { get; set; }
+
+ public bool IsInView { get; internal set; }
}
}