Skip to content

Commit 36517ed

Browse files
spec(047): Phase 3-final Batch E — Panel per-child attached props + WrapGrid
Wires Grid/Canvas/FlexPanel descriptors to use Batch A's additive Panel<>.PerChildAttached callback. Each panel descriptor declares a captured-free set lambda that reads the child element's attached-prop hint (GridAttached / CanvasAttached / FlexAttached) and writes the corresponding WinUI attached DP. Mirrors what the legacy MountXxx arms do after each child mounts; closes the "descriptor-mounted children stack at row 0 / column 0" known gap from PR #435 batch 8. WrapGridDescriptor (new) — closes the Phase 3 batch 8 escape-hatch ("WrapGrid escape-hatched (needs per-child attached-prop hook)"). Mirrors MountWrapGrid: Orientation always written; MaximumRowsOrColumns / ItemWidth / ItemHeight via .OneWayConditional; per-child WrapGridAttached (RowSpan / ColumnSpan) via PerChildAttached. Registered in the perf DescriptorVariantFactory alongside the other panel descriptors. FlexPanelDescriptor delegates to Reconciler.ApplyFlexAttached (promoted from private to internal) so descriptor and legacy paths share the "always apply — reset to defaults when no hint" semantics that protect against stale Yoga config on pool-rented controls. CanvasDescriptor delegates to Reconciler.ApplyCanvasPosition (already internal) so the anchor-state ConditionalWeakTable + SizeChanged wiring is shared. Each descriptor's PerChildAttached callback resets the relevant WinUI attached DPs via ClearValue when the child has no hint, so a reordered child whose attached prop drops between renders is not stuck with the prior placement. Documented gaps: - RelativePanelDescriptor carved to Phase 4: the per-child callback fires sequentially during the mount loop, so name references like RightOf="foo" can't resolve against siblings that haven't been mounted yet. Needs either a post-loop second-pass shape on Panel<> or a dedicated NamedRelativePanel strategy. Doc updated in-place. Self-test V1 ON/OFF Desc_ filter: 494/494 pass on both paths (includes 4 new fixtures: Desc_Grid_AttachedRowColumn, Desc_Canvas_AttachedLeftTop, Desc_FlexPanel_AttachedFlexProps, Desc_WrapGrid_MountUpdate). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 34955db commit 36517ed

9 files changed

Lines changed: 515 additions & 33 deletions

File tree

src/Reactor/Core/Reconciler.Update.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3775,7 +3775,7 @@ private static void UpdateAppBarItems(
37753775
return null;
37763776
}
37773777

3778-
private static void ApplyFlexAttached(Element child, Microsoft.UI.Xaml.UIElement ctrl)
3778+
internal static void ApplyFlexAttached(Element child, Microsoft.UI.Xaml.UIElement ctrl)
37793779
{
37803780
var fa = child.GetAttached<FlexAttached>();
37813781
// Always apply — reset to defaults when no FlexAttached, so stale values

src/Reactor/Core/V1Protocol/Descriptor/Descriptors/CanvasDescriptor.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics.CodeAnalysis;
2+
using Microsoft.UI.Xaml;
23
using WinUI = Microsoft.UI.Xaml.Controls;
34

45
namespace Microsoft.UI.Reactor.Core.V1Protocol.Descriptor.Descriptors;
@@ -12,21 +13,35 @@ namespace Microsoft.UI.Reactor.Core.V1Protocol.Descriptor.Descriptors;
1213
/// Children are dispatched through the
1314
/// <see cref="Panel{TElement,TControl}"/> strategy.</para>
1415
///
15-
/// <para><b>Known gaps:</b> the legacy hand-coded path applies
16-
/// <see cref="CanvasAttached"/> (Canvas.Left / Canvas.Top) per child after
17-
/// children mount. The Panel strategy in V1HandlerAdapter doesn't surface a
18-
/// per-child post-mount hook yet, so descriptor-mounted children stay at
19-
/// the panel origin. Authors who need <c>Canvas.SetLeft</c> /
20-
/// <c>Canvas.SetTop</c> stay on V1 OFF (legacy arm). Pure-children scenarios
21-
/// have parity.</para>
16+
/// <para><b>§14 Phase 3-final Batch E:</b> per-child
17+
/// <see cref="CanvasAttached"/> (Canvas.Left / Canvas.Top, plus the
18+
/// AnchorX / AnchorY post-layout offset) is now applied via
19+
/// <see cref="Panel{TElement,TControl}.PerChildAttached"/>. The callback
20+
/// delegates to <see cref="Reconciler.ApplyCanvasPosition"/> so descriptor
21+
/// children share the same anchor-state ConditionalWeakTable + size-change
22+
/// subscription used by the legacy <c>MountCanvas</c> arm.</para>
2223
/// </summary>
2324
[Experimental("REACTOR_V1_PREVIEW")]
2425
internal static class CanvasDescriptor
2526
{
2627
private static readonly Panel<CanvasElement, WinUI.Canvas> ChildrenStrategy =
2728
new Panel<CanvasElement, WinUI.Canvas>(
2829
GetChildren: static e => e.Children,
29-
GetCollection: static c => c.Children);
30+
GetCollection: static c => c.Children)
31+
{
32+
PerChildAttached = static (canvas, ui, childEl) =>
33+
{
34+
if (ui is not FrameworkElement fe) return;
35+
var ca = childEl.GetAttached<CanvasAttached>();
36+
if (ca is null)
37+
{
38+
fe.ClearValue(WinUI.Canvas.LeftProperty);
39+
fe.ClearValue(WinUI.Canvas.TopProperty);
40+
return;
41+
}
42+
Reconciler.ApplyCanvasPosition(fe, ca);
43+
},
44+
};
3045

3146
public static readonly ControlDescriptor<CanvasElement, WinUI.Canvas> Descriptor =
3247
new ControlDescriptor<CanvasElement, WinUI.Canvas>

src/Reactor/Core/V1Protocol/Descriptor/Descriptors/FlexPanelDescriptor.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,26 @@ namespace Microsoft.UI.Reactor.Core.V1Protocol.Descriptor.Descriptors;
1515
/// (<c>FlexPanel : Panel</c>, so <c>Children</c> is the standard
1616
/// <c>UIElementCollection</c>).</para>
1717
///
18-
/// <para><b>Known gaps:</b> the legacy hand-coded path applies
19-
/// <see cref="FlexAttached"/> per child after children mount (Grow / Shrink /
20-
/// Basis / Order / AlignSelf / margins). The Panel strategy in
21-
/// V1HandlerAdapter doesn't surface a per-child post-mount hook, so flex
22-
/// children stay at default Yoga node config. Authors who need per-child
23-
/// flex tuning stay on V1 OFF (legacy arm). Container-level layout has
24-
/// parity.</para>
18+
/// <para><b>§14 Phase 3-final Batch E:</b> per-child
19+
/// <see cref="FlexAttached"/> (Grow / Shrink / Basis / MinWidth / MinHeight
20+
/// / AlignSelf / Position / Left / Top / Right / Bottom) is now applied via
21+
/// <see cref="Panel{TElement,TControl}.PerChildAttached"/>. The callback
22+
/// delegates to <see cref="Reconciler.ApplyFlexAttached"/> so descriptor
23+
/// children share the same "always apply — reset to defaults when no hint"
24+
/// semantics as the legacy <c>MountFlex</c> arm (the reset is required for
25+
/// pool-rented controls that could otherwise inherit stale Yoga config).</para>
2526
/// </summary>
2627
[Experimental("REACTOR_V1_PREVIEW")]
2728
internal static class FlexPanelDescriptor
2829
{
2930
private static readonly Panel<FlexElement, FlexPanel> ChildrenStrategy =
3031
new Panel<FlexElement, FlexPanel>(
3132
GetChildren: static e => e.Children,
32-
GetCollection: static c => c.Children);
33+
GetCollection: static c => c.Children)
34+
{
35+
PerChildAttached = static (panel, ui, childEl) =>
36+
Microsoft.UI.Reactor.Core.Reconciler.ApplyFlexAttached(childEl, ui),
37+
};
3338

3439
public static readonly ControlDescriptor<FlexElement, FlexPanel> Descriptor =
3540
new ControlDescriptor<FlexElement, FlexPanel>

src/Reactor/Core/V1Protocol/Descriptor/Descriptors/GridDescriptor.cs

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Generic;
22
using System.Diagnostics.CodeAnalysis;
3+
using Microsoft.UI.Xaml;
34
using WinUI = Microsoft.UI.Xaml.Controls;
45

56
namespace Microsoft.UI.Reactor.Core.V1Protocol.Descriptor.Descriptors;
@@ -20,21 +21,44 @@ namespace Microsoft.UI.Reactor.Core.V1Protocol.Descriptor.Descriptors;
2021
/// gate). Children are dispatched through the
2122
/// <see cref="Panel{TElement,TControl}"/> strategy.</para>
2223
///
23-
/// <para><b>Known gaps:</b> the legacy hand-coded path applies
24-
/// <see cref="GridAttached"/> (Row / Column / RowSpan / ColumnSpan) per
25-
/// child after children mount. The Panel strategy in V1HandlerAdapter
26-
/// doesn't surface a per-child post-mount hook yet, so descriptor-mounted
27-
/// children stack at row 0 / column 0. Authors who need
28-
/// <c>Grid.SetRow</c> / <c>Grid.SetColumn</c> stay on V1 OFF (legacy arm).
29-
/// Container-level spacing and definitions have parity.</para>
24+
/// <para><b>§14 Phase 3-final Batch E:</b> per-child
25+
/// <see cref="GridAttached"/> (Row / Column / RowSpan / ColumnSpan) is now
26+
/// applied via <see cref="Panel{TElement,TControl}.PerChildAttached"/>,
27+
/// which the V1HandlerAdapter fires after each child Mount and after each
28+
/// child Update. Mirrors the legacy <c>MountGrid</c> arm —
29+
/// <c>Grid.SetRow</c> / <c>Grid.SetColumn</c> always set; <c>SetRowSpan</c>
30+
/// / <c>SetColumnSpan</c> only when the hint is &gt; 1 (default).</para>
3031
/// </summary>
3132
[Experimental("REACTOR_V1_PREVIEW")]
3233
internal static class GridDescriptor
3334
{
3435
private static readonly Panel<GridElement, WinUI.Grid> ChildrenStrategy =
3536
new Panel<GridElement, WinUI.Grid>(
3637
GetChildren: static e => e.Children,
37-
GetCollection: static c => c.Children);
38+
GetCollection: static c => c.Children)
39+
{
40+
PerChildAttached = static (grid, ui, childEl) =>
41+
{
42+
if (ui is not FrameworkElement fe) return;
43+
var ga = childEl.GetAttached<GridAttached>();
44+
if (ga is null)
45+
{
46+
// Reset to defaults so pool-rented / reused controls don't
47+
// inherit stale positioning from a prior parent.
48+
fe.ClearValue(WinUI.Grid.RowProperty);
49+
fe.ClearValue(WinUI.Grid.ColumnProperty);
50+
fe.ClearValue(WinUI.Grid.RowSpanProperty);
51+
fe.ClearValue(WinUI.Grid.ColumnSpanProperty);
52+
return;
53+
}
54+
WinUI.Grid.SetRow(fe, ga.Row);
55+
WinUI.Grid.SetColumn(fe, ga.Column);
56+
if (ga.RowSpan > 1) WinUI.Grid.SetRowSpan(fe, ga.RowSpan);
57+
else fe.ClearValue(WinUI.Grid.RowSpanProperty);
58+
if (ga.ColumnSpan > 1) WinUI.Grid.SetColumnSpan(fe, ga.ColumnSpan);
59+
else fe.ClearValue(WinUI.Grid.ColumnSpanProperty);
60+
},
61+
};
3862

3963
public static readonly ControlDescriptor<GridElement, WinUI.Grid> Descriptor =
4064
new ControlDescriptor<GridElement, WinUI.Grid>

src/Reactor/Core/V1Protocol/Descriptor/Descriptors/RelativePanelDescriptor.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,20 @@ namespace Microsoft.UI.Reactor.Core.V1Protocol.Descriptor.Descriptors;
1313
/// <c>Setters</c>. Children are dispatched through the
1414
/// <see cref="Panel{TElement,TControl}"/> strategy.</para>
1515
///
16-
/// <para><b>Known gaps:</b> the legacy hand-coded path executes a two-pass
17-
/// resolution to apply <see cref="RelativePanelAttached"/> attached
18-
/// properties (RightOf / Below / AlignLeftWith / AlignWithPanel etc.) using
19-
/// a name → control map. The Panel strategy in V1HandlerAdapter doesn't
20-
/// surface a per-child post-mount hook, so descriptor-mounted children
21-
/// stack at the panel origin without relative wiring. Authors who depend on
22-
/// <c>RelativePanelAttached</c> stay on V1 OFF (legacy arm). Pure-children
23-
/// scenarios have parity.</para>
16+
/// <para><b>Known gap — carved to Phase 4:</b> the legacy hand-coded path
17+
/// executes a two-pass resolution to apply
18+
/// <see cref="RelativePanelAttached"/> attached properties (RightOf / Below
19+
/// / AlignLeftWith / AlignWithPanel etc.) using a name → control map built
20+
/// up across all children. Phase 3-final Batch A's
21+
/// <see cref="Panel{TElement,TControl}.PerChildAttached"/> hook fires
22+
/// per-child sequentially — at the moment any given child's callback
23+
/// fires, later siblings haven't been mounted yet, so name references
24+
/// like <c>RightOf="foo"</c> can't resolve. A correct port requires
25+
/// either a post-loop second-pass shape on <see cref="Panel{TElement,TControl}"/>
26+
/// or a dedicated <c>NamedRelativePanel&lt;…&gt;</c> strategy; both are
27+
/// out of Batch E's scope. Pure-children scenarios remain at parity;
28+
/// authors who depend on <c>RelativePanelAttached</c> stay on V1 OFF
29+
/// (legacy arm) until the Phase 4 follow-up.</para>
2430
/// </summary>
2531
[Experimental("REACTOR_V1_PREVIEW")]
2632
internal static class RelativePanelDescriptor
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using Microsoft.UI.Xaml;
3+
using WinUI = Microsoft.UI.Xaml.Controls;
4+
5+
namespace Microsoft.UI.Reactor.Core.V1Protocol.Descriptor.Descriptors;
6+
7+
/// <summary>
8+
/// Spec 047 §14 Phase 3-final Batch E — descriptor variant of the
9+
/// hand-coded <c>MountWrapGrid</c> / <c>UpdateWrapGrid</c> arms in
10+
/// <see cref="Reconciler"/>. Closes the Phase 3 batch-8 escape-hatch
11+
/// ("WrapGrid escape-hatched (needs per-child attached-prop hook)") — the
12+
/// Batch A additive <see cref="Panel{TElement,TControl}.PerChildAttached"/>
13+
/// surface makes the per-child <see cref="WrapGridAttached"/> writes
14+
/// expressible declaratively.
15+
///
16+
/// <para><b>Coverage:</b> a zero-event panel container backed by
17+
/// <see cref="WinUI.VariableSizedWrapGrid"/>. Four conditional one-way
18+
/// props (<c>MaximumRowsOrColumns</c> ≥ 0, <c>ItemWidth</c> /
19+
/// <c>ItemHeight</c> non-NaN, <c>Orientation</c> always written). Children
20+
/// are dispatched through the <see cref="Panel{TElement,TControl}"/>
21+
/// strategy with a <see cref="Panel{TElement,TControl}.PerChildAttached"/>
22+
/// callback that mirrors the legacy <c>MountWrapGrid</c> arm —
23+
/// <c>VariableSizedWrapGrid.SetRowSpan</c> /
24+
/// <c>VariableSizedWrapGrid.SetColumnSpan</c> only when the hint is
25+
/// &gt; 1 (default).</para>
26+
/// </summary>
27+
[Experimental("REACTOR_V1_PREVIEW")]
28+
internal static class WrapGridDescriptor
29+
{
30+
private static readonly Panel<WrapGridElement, WinUI.VariableSizedWrapGrid> ChildrenStrategy =
31+
new Panel<WrapGridElement, WinUI.VariableSizedWrapGrid>(
32+
GetChildren: static e => e.Children,
33+
GetCollection: static c => c.Children)
34+
{
35+
PerChildAttached = static (grid, ui, childEl) =>
36+
{
37+
if (ui is not FrameworkElement fe) return;
38+
var wga = childEl.GetAttached<WrapGridAttached>();
39+
if (wga is null)
40+
{
41+
fe.ClearValue(WinUI.VariableSizedWrapGrid.RowSpanProperty);
42+
fe.ClearValue(WinUI.VariableSizedWrapGrid.ColumnSpanProperty);
43+
return;
44+
}
45+
if (wga.RowSpan > 1) WinUI.VariableSizedWrapGrid.SetRowSpan(fe, wga.RowSpan);
46+
else fe.ClearValue(WinUI.VariableSizedWrapGrid.RowSpanProperty);
47+
if (wga.ColumnSpan > 1) WinUI.VariableSizedWrapGrid.SetColumnSpan(fe, wga.ColumnSpan);
48+
else fe.ClearValue(WinUI.VariableSizedWrapGrid.ColumnSpanProperty);
49+
},
50+
};
51+
52+
public static readonly ControlDescriptor<WrapGridElement, WinUI.VariableSizedWrapGrid> Descriptor =
53+
new ControlDescriptor<WrapGridElement, WinUI.VariableSizedWrapGrid>
54+
{
55+
Children = ChildrenStrategy,
56+
GetSetters = static e => e.Setters,
57+
}
58+
.OneWay(
59+
get: static e => e.Orientation,
60+
set: static (c, v) => c.Orientation = v)
61+
.OneWayConditional(
62+
get: static e => e.MaximumRowsOrColumns,
63+
set: static (c, v) => c.MaximumRowsOrColumns = v,
64+
shouldWrite: static e => e.MaximumRowsOrColumns >= 0)
65+
.OneWayConditional(
66+
get: static e => e.ItemWidth,
67+
set: static (c, v) => c.ItemWidth = v,
68+
shouldWrite: static e => !double.IsNaN(e.ItemWidth))
69+
.OneWayConditional(
70+
get: static e => e.ItemHeight,
71+
set: static (c, v) => c.ItemHeight = v,
72+
shouldWrite: static e => !double.IsNaN(e.ItemHeight));
73+
}

0 commit comments

Comments
 (0)