Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/winui-port/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ Apply ALL of the following porting rules when converting C++ to C#.
#### 2.1. General Rules

- **Never remove or simplify code.** Anything you cannot convert must be preserved as a comment with a clear `TODO Uno:` explanation.
- **Preserve all `//` code comments exactly.** Every comment in the C++ source β€” explanatory notes, rationale, section dividers, `#pragma region` labels (converted to `// #pragma region`), TODOs, and inline remarks β€” must appear in the C# output at the same relative position. Do not paraphrase, summarize, or omit comments. The only acceptable changes are adapting C++ syntax references inside a comment to their C# equivalents (e.g., `winrt::hstring` β†’ `string`).
- **Maintain method order and structure** exactly as in the original C++ files.
- **Preserve all behavior and intent**, even if the resulting C# does not compile yet.
- Any Uno-specific code must be wrapped in `#if HAS_UNO` / `#endif`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// MUX Reference Repeater/APITests/ItemCollectionTransitionProviderTests.cs, tag winui3/release/1.6-stable

#nullable enable

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Media;
using Private.Infrastructure;
using Windows.Foundation;

namespace Uno.UI.RuntimeTests.Tests.Microsoft_UI_Xaml_Controls;

[TestClass]
public partial class Given_ItemCollectionTransitionProvider
{
/// <summary>
/// Validates that the transition provider receives the expected add/remove/move calls
/// when items are inserted and removed from the repeater's data source.
/// Port of ValidateItemCollectionTransitionProvider from WinUI.
/// </summary>
[TestMethod]
[RunsOnUIThread]
public async Task When_Items_Changed_TransitionProvider_Receives_Correct_Calls()
{
var data = new ObservableCollection<string>(Enumerable.Range(0, 10).Select(i => $"Item #{i}"));

var elementFactory = new RecyclingElementFactory();
elementFactory.RecyclePool = new RecyclePool();
elementFactory.Templates["Item"] = (DataTemplate)XamlReader.Load(
@"<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>
<TextBlock Text='{Binding}' Height='50' />
</DataTemplate>");

var repeater = new ItemsRepeater()
{
ItemsSource = data,
ItemTemplate = elementFactory,
};

TestServices.WindowHelper.WindowContent = new ItemsRepeaterScrollHost()
{
Width = 400,
Height = 800,
ScrollViewer = new ScrollViewer
{
Content = repeater
}
};

await TestServices.WindowHelper.WaitForLoaded(repeater);
await TestServices.WindowHelper.WaitForIdle();

var addCalls = new List<CallInfo>();
var removeCalls = new List<CallInfo>();
var moveCalls = new List<CallInfo>();

var transitionProvider = new ItemCollectionTransitionProviderDerived
{
ShouldAnimateFunc = _ => true,
StartTransitionsFunc = transitions =>
{
foreach (var transition in transitions)
{
var progress = transition.Start();

switch (transition.Operation)
{
case ItemCollectionTransitionOperation.Add:
addCalls.Add(new CallInfo(repeater.GetElementIndex(progress.Element), transition));
break;
case ItemCollectionTransitionOperation.Remove:
removeCalls.Add(new CallInfo(repeater.GetElementIndex(progress.Element), transition));
break;
case ItemCollectionTransitionOperation.Move:
moveCalls.Add(new CallInfo(repeater.GetElementIndex(progress.Element), transition));
break;
}

progress.Complete();
}
}
};

repeater.ItemTransitionProvider = transitionProvider;

data.Insert(0, "new item");
data.RemoveAt(2);

await TestServices.WindowHelper.WaitForIdle();

Assert.AreEqual(1, addCalls.Count, "Expected 1 add call");
var call = addCalls[0];
Assert.AreEqual(0, call.Index, "Add call should be at index 0");
Assert.AreEqual(ItemCollectionTransitionTriggers.CollectionChangeAdd, call.Transition.Triggers);

Assert.AreEqual(1, removeCalls.Count, "Expected 1 remove call");
call = removeCalls[0];
Assert.AreEqual(-1, call.Index, "Removed item should no longer be in the repeater");
Assert.AreEqual(ItemCollectionTransitionTriggers.CollectionChangeRemove, call.Transition.Triggers);

Assert.AreEqual(1, moveCalls.Count, "Expected 1 move call");
call = moveCalls[0];
Assert.AreEqual(1, call.Index, "Moved item should be at index 1");
Assert.AreEqual(ItemCollectionTransitionTriggers.CollectionChangeAdd | ItemCollectionTransitionTriggers.CollectionChangeRemove, call.Transition.Triggers);
Assert.AreEqual(0, call.Transition.OldBounds.Y, "Old Y bound should be 0");
Assert.AreEqual(50, call.Transition.NewBounds.Y, "New Y bound should be 50 (one item height)");

addCalls.Clear();
removeCalls.Clear();
moveCalls.Clear();

// Validate filtering: only animate Add operations.
transitionProvider.ShouldAnimateFunc = t => t.Operation == ItemCollectionTransitionOperation.Add;

data.Insert(0, "new item");
data.RemoveAt(2);

await TestServices.WindowHelper.WaitForIdle();

Assert.AreEqual(1, addCalls.Count, "Expected 1 add call in filtered scenario");
call = addCalls[0];
Assert.AreEqual(0, call.Index);
Assert.AreEqual(ItemCollectionTransitionTriggers.CollectionChangeAdd, call.Transition.Triggers);

Assert.AreEqual(0, removeCalls.Count, "Remove should not be animated when filtered");
Assert.AreEqual(0, moveCalls.Count, "Move should not be animated when filtered");
}

/// <summary>
/// Validates HasStarted transitions through Start() and that TransitionCompleted fires on Complete().
/// </summary>
[TestMethod]
[RunsOnUIThread]
public async Task When_Progress_Complete_TransitionCompleted_Is_Raised()
{
var data = new ObservableCollection<string>(Enumerable.Range(0, 3).Select(i => $"Item #{i}"));

var elementFactory = new RecyclingElementFactory();
elementFactory.RecyclePool = new RecyclePool();
elementFactory.Templates["Item"] = (DataTemplate)XamlReader.Load(
@"<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>
<TextBlock Text='{Binding}' Height='50' />
</DataTemplate>");

var repeater = new ItemsRepeater()
{
ItemsSource = data,
ItemTemplate = elementFactory,
};

TestServices.WindowHelper.WindowContent = new ItemsRepeaterScrollHost()
{
Width = 400,
Height = 800,
ScrollViewer = new ScrollViewer { Content = repeater }
};

await TestServices.WindowHelper.WaitForLoaded(repeater);
await TestServices.WindowHelper.WaitForIdle();

ItemCollectionTransition? capturedTransition = null;
var completedArgs = new List<ItemCollectionTransitionCompletedEventArgs>();

var transitionProvider = new ItemCollectionTransitionProviderDerived
{
ShouldAnimateFunc = _ => true,
StartTransitionsFunc = transitions =>
{
foreach (var transition in transitions)
{
capturedTransition = transition;

Assert.IsFalse(transition.HasStarted, "HasStarted should be false before Start()");
var progress = transition.Start();
Assert.IsTrue(transition.HasStarted, "HasStarted should be true after Start()");

// Repeated Start() should return the same object.
var progress2 = transition.Start();
Assert.AreSame(progress, progress2, "Repeated Start() calls should return the same progress object");

Assert.IsNotNull(progress.Element, "Progress.Element should not be null");
Assert.AreSame(transition, progress.Transition, "Progress.Transition should match");

progress.Complete();
}
}
};

transitionProvider.TransitionCompleted += (s, e) => completedArgs.Add(e);
repeater.ItemTransitionProvider = transitionProvider;

data.Insert(0, "new item");

await TestServices.WindowHelper.WaitForIdle();

Assert.IsNotNull(capturedTransition, "At least one transition should have been started");
Assert.IsTrue(completedArgs.Count > 0, "TransitionCompleted should have been raised");
Assert.AreSame(capturedTransition, completedArgs[0].Transition, "CompletedEventArgs.Transition should match");
Assert.IsNotNull(completedArgs[0].Element, "CompletedEventArgs.Element should not be null");
}

private struct CallInfo
{
public CallInfo(int index, ItemCollectionTransition transition)
{
Index = index;
Transition = transition;
}

public int Index { get; set; }
public ItemCollectionTransition Transition { get; set; }

public override string ToString() =>
$"Index: {Index} Operation: {Transition.Operation} Triggers: {Transition.Triggers} " +
$"OldBounds: {Transition.OldBounds} NewBounds: {Transition.NewBounds}";
}

private class ItemCollectionTransitionProviderDerived : ItemCollectionTransitionProvider
{
public Func<ItemCollectionTransition, bool>? ShouldAnimateFunc { get; set; }
public Action<IList<ItemCollectionTransition>>? StartTransitionsFunc { get; set; }

protected override bool ShouldAnimateCore(ItemCollectionTransition transition) =>
ShouldAnimateFunc?.Invoke(transition) ?? false;

protected override void StartTransitions(IList<ItemCollectionTransition> transitions) =>
StartTransitionsFunc?.Invoke(transitions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,77 +3,21 @@
#pragma warning disable 114 // new keyword hiding
namespace Microsoft.UI.Xaml.Controls
{
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
#if false || false || false || false || false || false || false
[global::Uno.NotImplemented]
#endif
public partial class ItemCollectionTransition
{
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
internal ItemCollectionTransition()
{
}
#endif
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
public bool HasStarted
{
get
{
throw new global::System.NotImplementedException("The member bool ItemCollectionTransition.HasStarted is not implemented. For more information, visit https://aka.platform.uno/notimplemented#m=bool%20ItemCollectionTransition.HasStarted");
}
}
#endif
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
public global::Windows.Foundation.Rect NewBounds
{
get
{
throw new global::System.NotImplementedException("The member Rect ItemCollectionTransition.NewBounds is not implemented. For more information, visit https://aka.platform.uno/notimplemented#m=Rect%20ItemCollectionTransition.NewBounds");
}
}
#endif
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
public global::Windows.Foundation.Rect OldBounds
{
get
{
throw new global::System.NotImplementedException("The member Rect ItemCollectionTransition.OldBounds is not implemented. For more information, visit https://aka.platform.uno/notimplemented#m=Rect%20ItemCollectionTransition.OldBounds");
}
}
#endif
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
public global::Microsoft.UI.Xaml.Controls.ItemCollectionTransitionOperation Operation
{
get
{
throw new global::System.NotImplementedException("The member ItemCollectionTransitionOperation ItemCollectionTransition.Operation is not implemented. For more information, visit https://aka.platform.uno/notimplemented#m=ItemCollectionTransitionOperation%20ItemCollectionTransition.Operation");
}
}
#endif
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
public global::Microsoft.UI.Xaml.Controls.ItemCollectionTransitionTriggers Triggers
{
get
{
throw new global::System.NotImplementedException("The member ItemCollectionTransitionTriggers ItemCollectionTransition.Triggers is not implemented. For more information, visit https://aka.platform.uno/notimplemented#m=ItemCollectionTransitionTriggers%20ItemCollectionTransition.Triggers");
}
}
#endif
// Skipping already declared property HasStarted
// Skipping already declared property NewBounds
// Skipping already declared property OldBounds
// Skipping already declared property Operation
// Skipping already declared property Triggers
// Forced skipping of method Microsoft.UI.Xaml.Controls.ItemCollectionTransition.Operation.get
// Forced skipping of method Microsoft.UI.Xaml.Controls.ItemCollectionTransition.Triggers.get
// Forced skipping of method Microsoft.UI.Xaml.Controls.ItemCollectionTransition.OldBounds.get
// Forced skipping of method Microsoft.UI.Xaml.Controls.ItemCollectionTransition.NewBounds.get
// Forced skipping of method Microsoft.UI.Xaml.Controls.ItemCollectionTransition.HasStarted.get
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
public global::Microsoft.UI.Xaml.Controls.ItemCollectionTransitionProgress Start()
{
throw new global::System.NotImplementedException("The member ItemCollectionTransitionProgress ItemCollectionTransition.Start() is not implemented. For more information, visit https://aka.platform.uno/notimplemented#m=ItemCollectionTransitionProgress%20ItemCollectionTransition.Start%28%29");
}
#endif
// Skipping already declared method Microsoft.UI.Xaml.Controls.ItemCollectionTransition.Start()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,13 @@
#pragma warning disable 114 // new keyword hiding
namespace Microsoft.UI.Xaml.Controls
{
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
#if false || false || false || false || false || false || false
[global::Uno.NotImplemented]
#endif
public partial class ItemCollectionTransitionCompletedEventArgs
{
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
internal ItemCollectionTransitionCompletedEventArgs()
{
}
#endif
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
public global::Microsoft.UI.Xaml.UIElement Element
{
get
{
throw new global::System.NotImplementedException("The member UIElement ItemCollectionTransitionCompletedEventArgs.Element is not implemented. For more information, visit https://aka.platform.uno/notimplemented#m=UIElement%20ItemCollectionTransitionCompletedEventArgs.Element");
}
}
#endif
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
public global::Microsoft.UI.Xaml.Controls.ItemCollectionTransition Transition
{
get
{
throw new global::System.NotImplementedException("The member ItemCollectionTransition ItemCollectionTransitionCompletedEventArgs.Transition is not implemented. For more information, visit https://aka.platform.uno/notimplemented#m=ItemCollectionTransition%20ItemCollectionTransitionCompletedEventArgs.Transition");
}
}
#endif
// Skipping already declared property Element
// Skipping already declared property Transition
// Forced skipping of method Microsoft.UI.Xaml.Controls.ItemCollectionTransitionCompletedEventArgs.Transition.get
// Forced skipping of method Microsoft.UI.Xaml.Controls.ItemCollectionTransitionCompletedEventArgs.Element.get
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,12 @@
#pragma warning disable 114 // new keyword hiding
namespace Microsoft.UI.Xaml.Controls
{
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
#if false || false || false || false || false || false || false
public enum ItemCollectionTransitionOperation
{
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
Add = 0,
#endif
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
Remove = 1,
#endif
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
Move = 2,
#endif
// Skipping already declared field Microsoft.UI.Xaml.Controls.ItemCollectionTransitionOperation.Add
// Skipping already declared field Microsoft.UI.Xaml.Controls.ItemCollectionTransitionOperation.Remove
// Skipping already declared field Microsoft.UI.Xaml.Controls.ItemCollectionTransitionOperation.Move
}
#endif
}
Loading
Loading