Skip to content

Commit 0f17286

Browse files
spec(047): Phase 3-final Batch F — Image events + Path.Data + InfoBar.ActionButton
Three more descriptor-surface ports. ImageDescriptor adds ImageOpened/ImageFailed via .HandCodedEvent over the existing ImageEventPayload (event entries declared before Source .OneWay so subscriptions land before the cached-image synchronous fire). PathDescriptor adds the pre-built Geometry Data path as .OneWayConditional (reference comparer, gated on PathDataString being null so the legacy XamlReader/parser path stays the single owner for that surface) plus FillRule propagation onto PathGeometry. InfoBarDescriptor adds ActionButton as .OneWayBridged<string?> that builds the inner Button and wires Click via a static-trampoline reading the parent InfoBar's Tag — record-with that swaps OnActionButtonClick picks up automatically. Shipped: - Image: ImageOpened + ImageFailed .HandCodedEvent entries (live-element via Tag) - Path: Data .OneWayConditional + FillRule .OneWayConditional onto PathGeometry - InfoBar: ActionButton .OneWayBridged with Click trampoline Carved to Phase 4: - Expander.HeaderTemplate: requires a NamedSlots strategy, conflicts with the existing SingleContent Children — only one Children strategy per descriptor today. - TeachingTip.Target: cross-element reference resolution (Target points at a sibling's mounted control); descriptor framework can't reference another element's resolved native control. - Path.PathDataString: legacy XamlReader + PathDataParser strategies need string-diff-against-old-element comparer and old+new+xaml+parser error context the engine's per-prop comparer can't replicate. - Icon polymorphic: already done in existing descriptors (InfoBar/TeachingTip/AutoSuggestBox/SelectorBar use Reconciler.ResolveIconSource / ResolveIconForDescriptor). V1 ON: 511 ok / 0 failures V1 OFF: 511 ok / 0 failures
1 parent 36517ed commit 0f17286

5 files changed

Lines changed: 415 additions & 51 deletions

File tree

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

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Diagnostics.CodeAnalysis;
3+
using Microsoft.UI.Xaml;
34
using Microsoft.UI.Xaml.Media.Imaging;
45
using WinUI = Microsoft.UI.Xaml.Controls;
56

@@ -12,40 +13,64 @@ namespace Microsoft.UI.Reactor.Core.V1Protocol.Descriptor.Descriptors;
1213
/// <para><b>Coverage:</b> a display leaf with a single complex prop
1314
/// (<c>Source</c> — string parsed to <see cref="Uri"/>, then to either
1415
/// <see cref="BitmapImage"/> or <see cref="SvgImageSource"/>) plus three
15-
/// optional layout props.</para>
16+
/// optional layout props, plus <c>ImageOpened</c> / <c>ImageFailed</c> as
17+
/// fire-only <see cref="ControlDescriptor{TElement,TControl}.HandCodedEvent{TPayload,TDelegate}"/>
18+
/// entries (Phase 3-final Batch F).</para>
1619
///
17-
/// <para><b>Phase 1 parity note:</b> the legacy arm threads the element
18-
/// through <c>EnsureImageWiring</c> trampolines so <c>ImageOpened</c> /
19-
/// <c>ImageFailed</c> can route back to the element callbacks. The
20-
/// descriptor relies on the V1 handler adapter to set the element tag (it
21-
/// already does before invoking the descriptor) — but the descriptor never
22-
/// calls <c>EnsureImageWiring</c>, so <c>ImageOpened</c> /
23-
/// <c>ImageFailed</c> events ARE NOT fired by the descriptor path.</para>
20+
/// <para><b>Behavior parity vs. legacy:</b> the legacy <c>MountImage</c> arm
21+
/// threads the element through <c>EnsureImageWiring</c> trampolines so
22+
/// <c>ImageOpened</c>/<c>ImageFailed</c> route back to the element
23+
/// callbacks. The descriptor reuses <see cref="ImageEventPayload"/> with the
24+
/// established slot-gating shape; trampolines read the live element via
25+
/// <see cref="Reconciler.GetElementTag"/> and fire the corresponding
26+
/// callback only when it's wired — mirrors the
27+
/// "always subscribe, read latest callback per fire" pattern.</para>
2428
///
25-
/// <para><b>Known gaps vs. hand-coded handler:</b>
29+
/// <para><b>Known nuances vs. hand-coded handler:</b>
2630
/// <list type="bullet">
27-
/// <item><b><c>OnImageOpened</c> / <c>OnImageFailed</c> callbacks:</b> not
28-
/// fired by the descriptor — Batch 3 covers zero-event props only (no
29-
/// <c>Controlled</c> / <c>HandCoded*</c>). Authors who need image-load
30-
/// notifications must continue to use the legacy arm (V1-OFF) until the
31-
/// descriptor is extended.</item>
3231
/// <item><b><c>Source</c> diffing:</b> the legacy arm reassigns
3332
/// <c>image.Source</c> only when the source string changes
3433
/// (<c>o.Source != n.Source</c>). The descriptor's per-prop comparer
3534
/// captures the same intent — write happens only when the string differs.</item>
3635
/// <item><b>Malformed URI:</b> the legacy <c>MountImage</c> swallows
3736
/// <see cref="UriFormatException"/> at construction time. The descriptor
3837
/// mirrors this exactly in its <c>set</c> lambda.</item>
38+
/// <item><b>Mount-time event ordering:</b> the legacy arm wires
39+
/// ImageOpened/ImageFailed BEFORE assigning Source so a cached-image
40+
/// synchronous fire still routes. The V1 handler adapter wires
41+
/// <c>.HandCodedEvent</c> entries in the order the descriptor declares
42+
/// them — the event entries are declared BEFORE the Source <c>.OneWay</c>
43+
/// entry below, so the same synchronous-fire path is preserved.</item>
3944
/// </list></para>
4045
/// </summary>
4146
[Experimental("REACTOR_V1_PREVIEW")]
4247
internal static class ImageDescriptor
4348
{
49+
private static readonly RoutedEventHandler ImageOpenedTrampoline = (s, _) =>
50+
(Reconciler.GetElementTag((UIElement)s!) as ImageElement)?.OnImageOpened?.Invoke();
51+
52+
private static readonly ExceptionRoutedEventHandler ImageFailedTrampoline = (s, args) =>
53+
(Reconciler.GetElementTag((UIElement)s!) as ImageElement)?.OnImageFailed?.Invoke(args.ErrorMessage);
54+
4455
public static readonly ControlDescriptor<ImageElement, WinUI.Image> Descriptor =
4556
new ControlDescriptor<ImageElement, WinUI.Image>
4657
{
4758
GetSetters = static e => e.Setters,
4859
}
60+
// Event entries first so subscriptions land BEFORE the Source write
61+
// (cached images can synchronously fire ImageOpened during the assign).
62+
.HandCodedEvent<ImageEventPayload, RoutedEventHandler>(
63+
subscribe: static (c, h) => c.ImageOpened += h,
64+
callbackPresent: static e => e.OnImageOpened,
65+
trampoline: ImageOpenedTrampoline,
66+
slotIsNull: static p => p.ImageOpenedTrampoline is null,
67+
setSlot: static (p, h) => p.ImageOpenedTrampoline = h)
68+
.HandCodedEvent<ImageEventPayload, ExceptionRoutedEventHandler>(
69+
subscribe: static (c, h) => c.ImageFailed += h,
70+
callbackPresent: static e => e.OnImageFailed,
71+
trampoline: ImageFailedTrampoline,
72+
slotIsNull: static p => p.ImageFailedTrampoline is null,
73+
setSlot: static (p, h) => p.ImageFailedTrampoline = h)
4974
.OneWay(
5075
get: static e => e.Source,
5176
set: static (c, v) =>

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

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,19 @@ namespace Microsoft.UI.Reactor.Core.V1Protocol.Descriptor.Descriptors;
2727
/// <see cref="Reconciler.ResolveIconSource(IconData?)"/>.</item>
2828
/// </list></para>
2929
///
30-
/// <para><b>Known gaps:</b>
31-
/// <list type="bullet">
32-
/// <item><b>ActionButton + <c>OnActionButtonClick</c> is escape-hatched</b>.
33-
/// The legacy arm constructs an inner <c>Button</c> dynamically when
34-
/// <c>ActionButtonContent</c> is non-null, then wires <c>Click</c> on
35-
/// that dynamically-created child. The descriptor framework binds events
36-
/// to the primary control, not a sub-control created during mount, so
37-
/// this asymmetric pattern doesn't fit. Authors who need the action
38-
/// button stay on V1 OFF (legacy arm), or use a <c>.Set</c> imperative
39-
/// setter to construct the button themselves.</item>
40-
/// </list></para>
30+
/// <para><b>ActionButton + <c>OnActionButtonClick</c> (Phase 3-final Batch F):</b>
31+
/// the legacy arm constructs an inner <c>Button</c> dynamically when
32+
/// <c>ActionButtonContent</c> is non-null, then wires <c>Click</c> on the
33+
/// dynamically-created child. The descriptor models this as a
34+
/// <c>.OneWayBridged&lt;string?&gt;</c> entry whose set lambda creates the
35+
/// <c>Button</c>, wires <c>Click</c> via a closure over the parent InfoBar
36+
/// reference (which is rooted by the InfoBar's Tag — Click resolves
37+
/// <c>OnActionButtonClick</c> through <see cref="Reconciler.GetElementTag"/>
38+
/// so a record-with that updates the callback picks up automatically). The
39+
/// gate matches the legacy "set when non-null" treatment: a non-null →
40+
/// non-null content swap rebuilds the inner button (legacy doesn't rebuild,
41+
/// but the rebuild is observably the same when the click handler reads the
42+
/// live element tag).</para>
4143
/// </summary>
4244
[Experimental("REACTOR_V1_PREVIEW")]
4345
internal static class InfoBarDescriptor
@@ -84,6 +86,27 @@ internal static class InfoBarDescriptor
8486
set: static (c, v) => c.IconSource = Reconciler.ResolveIconSource(v),
8587
shouldWrite: static e => e.IconSource is not null,
8688
comparer: IconDataReferenceComparer.Instance)
89+
// ActionButton (Phase 3-final Batch F). Dynamically construct the inner
90+
// Button; Click trampoline reads the live element via GetElementTag so a
91+
// later record-with that updates OnActionButtonClick picks up
92+
// automatically (Update keeps the InfoBar's Tag fresh).
93+
.OneWayBridged<string?>(
94+
get: static e => e.ActionButtonContent,
95+
set: static (c, v, _, _) =>
96+
{
97+
if (v is null)
98+
{
99+
c.ActionButton = null;
100+
return;
101+
}
102+
var btn = new WinUI.Button { Content = v };
103+
var infoBar = c;
104+
btn.Click += (_, _) =>
105+
(Reconciler.GetElementTag(infoBar) as InfoBarElement)
106+
?.OnActionButtonClick?.Invoke();
107+
c.ActionButton = btn;
108+
},
109+
shouldWrite: static e => e.ActionButtonContent is not null)
87110
.HandCodedEvent<InfoBarEventPayload,
88111
TypedEventHandler<WinUI.InfoBar, WinUI.InfoBarClosedEventArgs>>(
89112
subscribe: static (c, h) => c.Closed += h,

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

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using System.Collections.Generic;
12
using System.Diagnostics.CodeAnalysis;
3+
using Microsoft.UI.Xaml.Media;
24
using WinShapes = Microsoft.UI.Xaml.Shapes;
35

46
namespace Microsoft.UI.Reactor.Core.V1Protocol.Descriptor.Descriptors;
@@ -7,35 +9,37 @@ namespace Microsoft.UI.Reactor.Core.V1Protocol.Descriptor.Descriptors;
79
/// Spec 047 §14 Phase 3 (batch 10) — descriptor variant of the hand-coded
810
/// <c>MountPath</c> / <c>UpdatePath</c> arms in <see cref="Reconciler"/>.
911
///
10-
/// <para><b>Coverage:</b> a zero-event shape leaf — styling and stroke props
11-
/// only. All paint / dash / cap / join / transform props use
12-
/// <see cref="ControlDescriptor{TElement,TControl}.OneWay"/> or
13-
/// <see cref="ControlDescriptor{TElement,TControl}.OneWayConditional"/>.</para>
12+
/// <para><b>Coverage:</b> a zero-event shape leaf — styling and stroke props,
13+
/// plus the pre-built <see cref="Geometry"/> <c>Data</c> path
14+
/// (Phase 3-final Batch F). All paint / dash / cap / join / transform props
15+
/// use <see cref="ControlDescriptor{TElement,TControl}.OneWay"/> or
16+
/// <see cref="ControlDescriptor{TElement,TControl}.OneWayConditional"/>;
17+
/// <c>Data</c> uses <c>.OneWayConditional</c> with a reference comparer.</para>
18+
///
19+
/// <para><b>Behavior parity vs. legacy:</b> the legacy <c>MountPath</c>
20+
/// branches between three strategies for <c>Path.Data</c>:
21+
/// (1) XamlReader-load a constructed <c>&lt;Path Data="..."/&gt;</c> when
22+
/// <c>PathDataString</c> is set; (2) assign a pre-built
23+
/// <see cref="Geometry"/> via <c>pa.Data</c> with structured error reporting;
24+
/// (3) fall back to <c>PathDataParser.Parse</c> for the SVG-string case.
25+
/// The descriptor ports strategy (2) — the pre-built <see cref="Geometry"/>
26+
/// path — under the <c>PathDataString is null</c> gate. Authors who use
27+
/// <c>PathDataString</c> stay on V1 OFF for that string-parse path.</para>
1428
///
1529
/// <para><b>Known gaps vs. hand-coded handler:</b>
1630
/// <list type="bullet">
17-
/// <item><b><c>Data</c> / <c>PathDataString</c> is escape-hatched.</b> The
18-
/// legacy <c>MountPath</c> branches between three strategies to set
19-
/// <c>Path.Data</c>: (1) XamlReader-load a constructed
20-
/// <c>&lt;Path Data="..."/&gt;</c> when <c>PathDataString</c> is set
21-
/// (avoids COM re-parent issues); (2) assign a pre-built
22-
/// <see cref="Microsoft.UI.Xaml.Media.Geometry"/> via <c>pa.Data</c> with
23-
/// structured error reporting; (3) fall back to
24-
/// <c>PathDataParser.Parse</c> for the SVG-string case. <c>UpdatePath</c>
25-
/// also gates the Data write on <c>PathDataString</c> string-diff (since
26-
/// parser output never reference-equals). None of these fit a single
27-
/// <c>OneWay</c> setter — the engine's general per-prop comparer can't
28-
/// replicate the string-diff-against-old-element trick, and the error
29-
/// reporting needs both old + new + xaml-text + parser-text context.
30-
/// Authors who need <c>Path.Data</c> stay on V1 OFF; the descriptor handles
31-
/// the rest of the surface (Fill / Stroke / dash / cap / join / transform),
32-
/// which is the bulk of the per-render write pressure for a D3-style
33-
/// chart.</item>
34-
/// <item><b><c>FillRule</c> propagation</b> in the legacy handler writes
35-
/// <c>FillRule</c> onto the inner <see cref="Microsoft.UI.Xaml.Media.PathGeometry"/>
36-
/// (not the <see cref="WinShapes.Path"/> itself). Since the descriptor
37-
/// doesn't set Data, the inner PathGeometry is not the descriptor's to
38-
/// inspect — also escape-hatched.</item>
31+
/// <item><b><c>PathDataString</c> (the XamlReader / PathDataParser
32+
/// strategies) is escape-hatched.</b> The engine's general per-prop
33+
/// comparer can't replicate the string-diff-against-old-element trick that
34+
/// the legacy <c>UpdatePath</c> uses, and the error reporting needs both
35+
/// old + new + xaml-text + parser-text context. The descriptor's
36+
/// <c>Data</c> entry is skipped when <c>PathDataString</c> is non-null so
37+
/// the legacy arm stays the single source of truth for that path.</item>
38+
/// <item><b><c>FillRule</c> propagation</b> writes <c>FillRule</c> onto the
39+
/// inner <see cref="PathGeometry"/> (not the <see cref="WinShapes.Path"/>
40+
/// itself). The descriptor inspects <c>p.Data</c> after the Data write
41+
/// and propagates FillRule when it owns a <see cref="PathGeometry"/> —
42+
/// matches the legacy arm's "set FillRule when we can" treatment.</item>
3943
/// </list></para>
4044
/// </summary>
4145
[Experimental("REACTOR_V1_PREVIEW")]
@@ -46,6 +50,25 @@ internal static class PathDescriptor
4650
{
4751
GetSetters = static e => e.Setters,
4852
}
53+
.OneWayConditional(
54+
get: static e => e.Data,
55+
set: static (c, v) => c.Data = v,
56+
// Gate: skip when PathDataString owns Data (legacy XamlReader /
57+
// PathDataParser strategies — see xmldoc). Also skip when Data is
58+
// null so we don't write null on top of a legacy-loaded geometry.
59+
shouldWrite: static e => e.PathDataString is null && e.Data is not null,
60+
comparer: GeometryReferenceComparer.Instance)
61+
// FillRule propagation onto the inner PathGeometry (only meaningful
62+
// when the descriptor wrote a PathGeometry above; non-EvenOdd writes
63+
// on a non-PathGeometry are a no-op). Mirrors the legacy arm's
64+
// <c>p.Data is PathGeometry pg => pg.FillRule = n.FillRule</c>.
65+
.OneWayConditional(
66+
get: static e => e.FillRule,
67+
set: static (c, v) =>
68+
{
69+
if (c.Data is PathGeometry pg && pg.FillRule != v) pg.FillRule = v;
70+
},
71+
shouldWrite: static e => e.PathDataString is null && e.Data is PathGeometry)
4972
.OneWayConditional(
5073
get: static e => e.Fill,
5174
set: static (c, v) => c.Fill = v,
@@ -83,4 +106,18 @@ internal static class PathDescriptor
83106
.OneWay(
84107
get: static e => e.StrokeDashOffset,
85108
set: static (c, v) => c.StrokeDashOffset = v);
109+
110+
/// <summary>Reference-identity comparer over <c>Geometry?</c>. The legacy
111+
/// <c>UpdatePath</c> arm gates the Data write on string-diff against
112+
/// <c>PathDataString</c> (parser output never reference-equals); for the
113+
/// pre-built <see cref="Geometry"/> path the descriptor uses reference
114+
/// identity, which matches author intent (the Geometry instance is the
115+
/// thing being identified).</summary>
116+
private sealed class GeometryReferenceComparer : IEqualityComparer<Geometry?>
117+
{
118+
public static readonly GeometryReferenceComparer Instance = new();
119+
public bool Equals(Geometry? x, Geometry? y) => ReferenceEquals(x, y);
120+
public int GetHashCode(Geometry obj)
121+
=> global::System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
122+
}
86123
}

0 commit comments

Comments
 (0)