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; } } }