Skip to content

Commit 2f7c9ba

Browse files
Add standalone IconElement for the element tree
Wrap the existing IconData hierarchy (SymbolIconData, FontIconData, BitmapIconData, PathIconData, ImageIconData) in a new IconElement record that can be placed as a child of any layout container. - Element record: IconElement(IconData Data) - Factory methods: Icon(IconData), Icon(string) - Reconciler mount/update handlers - Fluent .Set() extension - Unit tests for record, factories, and with-expressions Closes #257 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0dba55c commit 2f7c9ba

8 files changed

Lines changed: 118 additions & 0 deletions

File tree

plugins/reactor/skills/reactor-dsl/references/reactor.api.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ HyperlinkButton(Command command) → HyperlinkButtonElement
8181
HyperlinkButton(string content, Uri navigateUri = null, Action onClick = null) → HyperlinkButtonElement
8282
If(bool condition, Func<Element> then, Func<Element> otherwise = null) → Element
8383
Image(string source) → ImageElement
84+
Icon(IconData data) → IconElement
85+
Icon(Symbol symbol) → IconElement
86+
Icon(string symbol) → IconElement
8487
ImageIcon(Uri source) → ImageIconData
8588
InfoBadge() → InfoBadgeElement
8689
InfoBadge(int value) → InfoBadgeElement
@@ -308,6 +311,7 @@ HyperlinkButtonElement.TextLink() → HyperlinkButtonElement
308311
ImageElement.ImageFailed(Action<string> handler) → ImageElement
309312
ImageElement.ImageOpened(Action handler) → ImageElement
310313
ImageElement.NineGrid(Thickness nineGrid) → ImageElement
314+
IconElement.Set(Action<IconElement> configure) → IconElement
311315
ImageElement.Set(Action<Image> configure) → ImageElement
312316
InfoBadgeElement.Set(Action<InfoBadge> configure) → InfoBadgeElement
313317
InfoBarElement.ActionButtonClick(Action handler) → InfoBarElement

skills/reactor.api.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ HyperlinkButton(Command command) → HyperlinkButtonElement
8181
HyperlinkButton(string content, Uri navigateUri = null, Action onClick = null) → HyperlinkButtonElement
8282
If(bool condition, Func<Element> then, Func<Element> otherwise = null) → Element
8383
Image(string source) → ImageElement
84+
Icon(IconData data) → IconElement
85+
Icon(Symbol symbol) → IconElement
86+
Icon(string symbol) → IconElement
8487
ImageIcon(Uri source) → ImageIconData
8588
InfoBadge() → InfoBadgeElement
8689
InfoBadge(int value) → InfoBadgeElement
@@ -308,6 +311,7 @@ HyperlinkButtonElement.TextLink() → HyperlinkButtonElement
308311
ImageElement.ImageFailed(Action<string> handler) → ImageElement
309312
ImageElement.ImageOpened(Action handler) → ImageElement
310313
ImageElement.NineGrid(Thickness nineGrid) → ImageElement
314+
IconElement.Set(Action<IconElement> configure) → IconElement
311315
ImageElement.Set(Action<Image> configure) → ImageElement
312316
InfoBadgeElement.Set(Action<InfoBadge> configure) → InfoBadgeElement
313317
InfoBarElement.ActionButtonClick(Action handler) → InfoBarElement

src/Reactor/Core/Element.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,6 +1650,16 @@ public record BitmapIconData(global::System.Uri Source, bool ShowAsMonochrome =
16501650
public record PathIconData(string Data) : IconData;
16511651
public record ImageIconData(global::System.Uri Source) : IconData;
16521652

1653+
/// <summary>
1654+
/// Standalone icon element that can be placed in the element tree.
1655+
/// Wraps an <see cref="IconData"/> and mounts to the corresponding
1656+
/// native <see cref="WinUI.IconElement"/> subtype.
1657+
/// </summary>
1658+
public record IconElement(IconData Data) : Element
1659+
{
1660+
internal Action<WinUI.IconElement>[] Setters { get; init; } = [];
1661+
}
1662+
16531663
public abstract record AppBarItemBase;
16541664
public record AppBarButtonData(string Label, Action? OnClick = null, string? Icon = null) : AppBarItemBase
16551665
{

src/Reactor/Core/Reconciler.Mount.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ public sealed partial class Reconciler
145145
CalendarViewElement cv => MountCalendarView(cv),
146146
SwipeControlElement swipe => MountSwipeControl(swipe, requestRerender),
147147
AnimatedIconElement ai => MountAnimatedIcon(ai),
148+
IconElement ie => MountIcon(ie),
148149
ParallaxViewElement pv => MountParallaxView(pv, requestRerender),
149150
MapControlElement mc => MountMapControl(mc),
150151
FrameElement frame => MountFrame(frame),
@@ -3455,6 +3456,15 @@ private WinUI.AnimatedIcon MountAnimatedIcon(AnimatedIconElement ai)
34553456
return icon;
34563457
}
34573458

3459+
// ── Icon ─────────────────────────────────────────────────────────
3460+
3461+
private WinUI.IconElement? MountIcon(IconElement ie)
3462+
{
3463+
var icon = ResolveIcon(ie.Data, null);
3464+
if (icon is not null) ApplySetters(ie.Setters, icon);
3465+
return icon;
3466+
}
3467+
34583468
// ── ParallaxView ──────────────────────────────────────────────────
34593469

34603470
private WinUI.ParallaxView MountParallaxView(ParallaxViewElement pv, Action requestRerender)

src/Reactor/Core/Reconciler.Update.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,8 @@ public sealed partial class Reconciler
274274
=> UpdateSwipeControl(o, n, swipe, requestRerender),
275275
(AnimatedIconElement, AnimatedIconElement n, WinUI.AnimatedIcon ai)
276276
=> UpdateAnimatedIcon(n, ai),
277+
(IconElement, IconElement n, WinUI.IconElement icon)
278+
=> UpdateIcon(n, icon),
277279
(ParallaxViewElement o, ParallaxViewElement n, WinUI.ParallaxView pv)
278280
=> UpdateParallaxView(o, n, pv, requestRerender),
279281
(MapControlElement, MapControlElement n, WinUI.MapControl mc)
@@ -3832,6 +3834,49 @@ private static void SyncSelectedDates(WinUI.CalendarView cv, IReadOnlyList<DateT
38323834
return null;
38333835
}
38343836

3837+
private UIElement? UpdateIcon(IconElement n, WinUI.IconElement icon)
3838+
{
3839+
// If the IconData subtype changed, replace the entire native control.
3840+
var fresh = Reconciler.ResolveIcon(n.Data, null);
3841+
if (fresh is null) return null;
3842+
3843+
if (fresh.GetType() != icon.GetType())
3844+
{
3845+
ApplySetters(n.Setters, fresh);
3846+
return fresh; // signals reconciler to swap the control
3847+
}
3848+
3849+
// Same native type — patch in place.
3850+
switch (n.Data)
3851+
{
3852+
case SymbolIconData sym when icon is WinUI.SymbolIcon si:
3853+
if (Enum.TryParse<Symbol>(sym.Symbol, ignoreCase: true, out var s)) si.Symbol = s;
3854+
break;
3855+
case FontIconData fi when icon is WinUI.FontIcon fontIcon:
3856+
fontIcon.Glyph = fi.Glyph;
3857+
if (fi.FontFamily is not null)
3858+
fontIcon.FontFamily = new Microsoft.UI.Xaml.Media.FontFamily(fi.FontFamily);
3859+
if (fi.FontSize is not null) fontIcon.FontSize = fi.FontSize.Value;
3860+
break;
3861+
case BitmapIconData bi when icon is WinUI.BitmapIcon bitmapIcon:
3862+
bitmapIcon.UriSource = bi.Source;
3863+
bitmapIcon.ShowAsMonochrome = bi.ShowAsMonochrome;
3864+
break;
3865+
case PathIconData pi when icon is WinUI.PathIcon pathIcon:
3866+
if (Microsoft.UI.Xaml.Markup.XamlReader.Load(
3867+
$"<Geometry xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>{pi.Data}</Geometry>")
3868+
is Microsoft.UI.Xaml.Media.Geometry geo)
3869+
pathIcon.Data = geo;
3870+
break;
3871+
case ImageIconData ii when icon is WinUI.ImageIcon imageIcon:
3872+
imageIcon.Source = new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(ii.Source);
3873+
break;
3874+
}
3875+
3876+
ApplySetters(n.Setters, icon);
3877+
return null;
3878+
}
3879+
38353880
private UIElement? UpdateMapControl(MapControlElement n, WinUI.MapControl mc)
38363881
{
38373882
mc.ZoomLevel = n.ZoomLevel;

src/Reactor/Elements/Dsl.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,12 @@ public static BitmapIconData BitmapIcon(global::System.Uri source, bool showAsMo
11921192

11931193
public static ImageIconData ImageIcon(global::System.Uri source) => new(source);
11941194

1195+
/// <summary>Creates a standalone icon element from an <see cref="IconData"/> instance.</summary>
1196+
public static Core.IconElement Icon(IconData data) => new(data);
1197+
1198+
/// <summary>Creates a standalone symbol icon element (e.g. <c>Icon("Home")</c>).</summary>
1199+
public static Core.IconElement Icon(string symbol) => new(new SymbolIconData(symbol));
1200+
11951201
// ── Keyboard Accelerators ───────────────────────────────────────
11961202

11971203
public static KeyboardAcceleratorData Accelerator(global::Windows.System.VirtualKey key, global::Windows.System.VirtualKeyModifiers modifiers = global::Windows.System.VirtualKeyModifiers.None) =>

src/Reactor/Elements/ElementExtensions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1730,6 +1730,10 @@ public static SwipeControlElement Set(this SwipeControlElement el, Action<WinUI.
17301730
public static AnimatedIconElement Set(this AnimatedIconElement el, Action<WinUI.AnimatedIcon> configure) =>
17311731
el with { Setters = [.. el.Setters, configure] };
17321732

1733+
// Icon
1734+
public static Core.IconElement Set(this Core.IconElement el, Action<WinUI.IconElement> configure) =>
1735+
el with { Setters = [.. el.Setters, configure] };
1736+
17331737
// ParallaxView
17341738
public static ParallaxViewElement Set(this ParallaxViewElement el, Action<WinUI.ParallaxView> configure) =>
17351739
el with { Setters = [.. el.Setters, configure] };

tests/Reactor.Tests/ElementRecordCoverageTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,41 @@ public void Icon_Data_Variants_Construct()
2727
Assert.NotNull(img.Source);
2828
}
2929

30+
[Fact]
31+
public void IconElement_Wraps_IconData()
32+
{
33+
var sym = new SymbolIconData("Home");
34+
var el = new IconElement(sym);
35+
Assert.Same(sym, el.Data);
36+
Assert.Empty(el.Setters);
37+
}
38+
39+
[Fact]
40+
public void IconElement_With_Expression_Preserves_Data()
41+
{
42+
var el = new IconElement(new FontIconData("\uE700", "Segoe Fluent Icons", 16));
43+
var copy = el with { Setters = [_ => { }] };
44+
Assert.Same(el.Data, copy.Data);
45+
Assert.Single(copy.Setters);
46+
}
47+
48+
[Fact]
49+
public void Icon_Factory_From_IconData()
50+
{
51+
var data = new PathIconData("M 0,0 L 10,10");
52+
var el = Factories.Icon(data);
53+
Assert.IsType<PathIconData>(el.Data);
54+
Assert.Equal("M 0,0 L 10,10", ((PathIconData)el.Data).Data);
55+
}
56+
57+
[Fact]
58+
public void Icon_Factory_From_String_Creates_SymbolIconData()
59+
{
60+
var el = Factories.Icon("Home");
61+
Assert.IsType<SymbolIconData>(el.Data);
62+
Assert.Equal("Home", ((SymbolIconData)el.Data).Symbol);
63+
}
64+
3065
[Fact]
3166
public void AppBar_Data_Variants_Construct()
3267
{

0 commit comments

Comments
 (0)