Skip to content
Merged
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
357 changes: 272 additions & 85 deletions src/Reactor/Core/Element.cs

Large diffs are not rendered by default.

36 changes: 24 additions & 12 deletions src/Reactor/Elements/BrushHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,36 @@ namespace Microsoft.UI.Reactor;
/// <summary>
/// Color and brush parsing utilities.
/// Supports named colors, hex (#RRGGBB, #AARRGGBB), and direct Color values.
/// Colors are cached by string; a fresh SolidColorBrush is created per call
/// because DependencyObjects have thread affinity and cannot be safely shared.
/// Parsed colors are cached by string (an immutable value type, safe to share),
/// but a fresh <see cref="SolidColorBrush"/> is created per call: a brush is a
/// DependencyObject with thread affinity and mutable Color/Opacity, so a single
/// instance cannot be safely shared across controls.
/// </summary>
public static class BrushHelper
{
private static readonly ConcurrentDictionary<string, global::Windows.UI.Color> _colorCache = new();
// OrdinalIgnoreCase so equivalent colors that differ only in casing
// ("Red"/"red", "#FF0000"/"#ff0000") share one cache entry instead of
// creating duplicates. Safe because ParseColor lowercases named colors
// before matching and ParseHex is case-insensitive, so case-folded keying
// yields identical results to the prior case-sensitive keying.
private static readonly ConcurrentDictionary<string, global::Windows.UI.Color> _colorCache =
new(global::System.StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Parses a color string into a SolidColorBrush.
/// Supports named colors (red, green, blue, white, black, gray, lightgray, transparent)
/// and hex codes (#RRGGBB or #AARRGGBB).
/// Color parsing is cached; a new brush is created each call (thread-safe).
/// Parses a color string into a fresh <see cref="SolidColorBrush"/> owned by the
/// caller. Supports named colors (red, green, blue, white, black, gray, lightgray,
/// transparent) and hex codes (#RRGGBB or #AARRGGBB). The parsed color is cached,
/// but a new brush instance is returned on every call, so callers may safely mutate
/// it and a brush is never shared between controls.
/// </summary>
public static SolidColorBrush Parse(string color)
{
var parsed = _colorCache.GetOrAdd(color, static c =>
public static SolidColorBrush Parse(string color) => new(ParseColor(color));

Comment thread
azchohfi marked this conversation as resolved.
/// <summary>
/// Parses a color string into a <see cref="global::Windows.UI.Color"/>.
/// The result is cached by string so repeated parses are allocation-free.
/// </summary>
internal static global::Windows.UI.Color ParseColor(string color) =>
_colorCache.GetOrAdd(color, static c =>
c.ToLowerInvariant() switch
{
"red" => global::Windows.UI.Color.FromArgb(255, 255, 0, 0),
Expand All @@ -35,8 +49,6 @@ public static SolidColorBrush Parse(string color)
_ when c.StartsWith('#') => ParseHex(c),
_ => global::Windows.UI.Color.FromArgb(255, 128, 128, 128),
});
return new SolidColorBrush(parsed);
}

internal static global::Windows.UI.Color ParseHex(string hex)
{
Expand Down
117 changes: 92 additions & 25 deletions src/Reactor/Elements/Dsl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ namespace Microsoft.UI.Reactor;
/// </summary>
public static partial class Factories
{
// Shared single-Star track array for the cross-axis of UniformGrid /
// InterspersedGrid. Safe to share: GridDefinition immediately converts
// GridSize[] tracks to string[] (read-only consumption), so the array is
// never retained or mutated by callers, and GridSize is a value struct.
private static readonly GridSize[] s_oneStar = { GridSize.Star() };

private static Optional<int> ToOptionalSelectedIndex(int? selectedIndex) =>
selectedIndex.HasValue ? Optional<int>.Of(selectedIndex.Value) : Optional<int>.Unset;

Expand Down Expand Up @@ -796,33 +802,38 @@ public static GridElement InterspersedGrid(
throw new ArgumentOutOfRangeException(nameof(proportions), $"proportions[{i}] must be a non-negative number, got {proportions[i]}");
}

var sizes = new List<GridSize>();
var children = new List<Element>();
var oneStar = s_oneStar;
bool isHorizontal = orientation == Orientation.Horizontal;

// #172 — exact track/child count is known up front (one track per item
// plus a separator between each pair), so fill pre-sized arrays directly
// instead of growing two Lists and copying them with ToArray().
int trackCount = items.Length * 2 - 1;
var sizes = new GridSize[trackCount];
var children = new Element[trackCount];

for (int i = 0; i < items.Length; i++)
{
var starValue = proportions[i];
sizes.Add(GridSize.Star(starValue));
sizes[i * 2] = GridSize.Star(starValue);

children.Add(isHorizontal
children[i * 2] = isHorizontal
? items[i].Grid(row: 0, column: i * 2)
: items[i].Grid(row: i * 2, column: 0));
: items[i].Grid(row: i * 2, column: 0);

if (i < items.Length - 1)
{
sizes.Add(GridSize.Px(separatorSize));
sizes[i * 2 + 1] = GridSize.Px(separatorSize);
var sep = separatorFactory(i);
children.Add(isHorizontal
children[i * 2 + 1] = isHorizontal
? sep.Grid(row: 0, column: i * 2 + 1)
: sep.Grid(row: i * 2 + 1, column: 0));
: sep.Grid(row: i * 2 + 1, column: 0);
}
}

var oneStar = new[] { GridSize.Star() };
return isHorizontal
? Grid(sizes.ToArray(), oneStar, children.ToArray())
: Grid(oneStar, sizes.ToArray(), children.ToArray());
? Grid(sizes, oneStar, children)
: Grid(oneStar, sizes, children);
}

/// <summary>
Expand All @@ -834,20 +845,34 @@ public static GridElement UniformGrid(Orientation orientation, params Element?[]
var filtered = FilterChildren(items);
if (filtered.Length == 0) return Grid(global::System.Array.Empty<GridSize>(), global::System.Array.Empty<GridSize>());

var sizes = Enumerable.Repeat(GridSize.Star(), filtered.Length).ToArray();
var oneStar = new[] { GridSize.Star() };
// #171 — fill the equal-Star track array with a pre-sized loop instead
// of Enumerable.Repeat(...).ToArray() (which allocates a LINQ iterator on
// top of the array). GridSize is a value struct, so every slot is the
// same Star value.
var sizes = new GridSize[filtered.Length];
var star = GridSize.Star();
for (int i = 0; i < sizes.Length; i++)
sizes[i] = star;
var oneStar = s_oneStar;
bool isHorizontal = orientation == Orientation.Horizontal;

// Position each cell into its OWN array — never write back into
// `filtered`. FilterChildren's fast path returns the caller's array
// aliased (no copy), so mutating it in place would corrupt a
// caller-supplied `items` array with .Grid(...) wrappers. This matches
// the historical behavior, where FilterChildren always returned a fresh
// owned array that was safe to fill.
var positioned = new Element[filtered.Length];
for (int i = 0; i < filtered.Length; i++)
{
filtered[i] = isHorizontal
positioned[i] = isHorizontal
? filtered[i].Grid(row: 0, column: i)
: filtered[i].Grid(row: i, column: 0);
}

return isHorizontal
? Grid(sizes, oneStar, filtered)
: Grid(oneStar, sizes, filtered);
? Grid(sizes, oneStar, positioned)
: Grid(oneStar, sizes, positioned);
}

// ── Navigation ──────────────────────────────────────────────────
Expand Down Expand Up @@ -1317,15 +1342,36 @@ public static Element Expr(Func<Element?> render)
/// Map a list to elements (like .map() in React JSX):
/// ForEach(items, item =&gt; TextBlock(item.Name))
/// </summary>
public static Element ForEach<T>(IEnumerable<T> items, Func<T, Element> render) =>
new GroupElement(items.Select(render).ToArray());
public static Element ForEach<T>(IEnumerable<T> items, Func<T, Element> render)
{
// #170 — IReadOnlyList fast-path: pre-size the Element[] and index
// directly, skipping the Select iterator + closure allocations that
// dominate when re-rendering large data-bound collections every frame.
if (items is IReadOnlyList<T> list)
{
var arr = new Element[list.Count];
for (int i = 0; i < arr.Length; i++)
arr[i] = render(list[i]);
return new GroupElement(arr);
}
return new GroupElement(items.Select(render).ToArray());
}

/// <summary>
/// Map with index:
/// ForEach(items, (item, i) =&gt; TextBlock($"{i}: {item}"))
/// </summary>
public static Element ForEach<T>(IEnumerable<T> items, Func<T, int, Element> render) =>
new GroupElement(items.Select((item, i) => render(item, i)).ToArray());
public static Element ForEach<T>(IEnumerable<T> items, Func<T, int, Element> render)
{
if (items is IReadOnlyList<T> list)
{
var arr = new Element[list.Count];
for (int i = 0; i < arr.Length; i++)
arr[i] = render(list[i], i);
return new GroupElement(arr);
}
return new GroupElement(items.Select((item, i) => render(item, i)).ToArray());
}

/// <summary>
/// Groups elements without introducing a layout container (like React's Fragment).
Expand Down Expand Up @@ -1900,23 +1946,44 @@ private static Element[] FilterChildren(Element?[] children)
}
if (!needsExpansion) return (Element[])(object)children;

Comment thread
azchohfi marked this conversation as resolved.
// Flatten GroupElements and remove nulls
var result = new List<Element>();
// #173 — slow path: two passes (count, then fill an exactly-sized array)
// instead of growing a List and copying it with ToArray(). Flattens one
// level of GroupElement and drops null / EmptyElement, matching the prior
// behavior exactly.
int count = 0;
for (int i = 0; i < children.Length; i++)
{
if (children[i] is GroupElement group)
{
foreach (var gc in group.Children)
{
if (gc is not null and not EmptyElement)
count++;
}
Comment thread
azchohfi marked this conversation as resolved.
}
else if (children[i] is not null and not EmptyElement)
{
count++;
}
}

var result = new Element[count];
int idx = 0;
for (int i = 0; i < children.Length; i++)
{
if (children[i] is GroupElement group)
{
foreach (var gc in group.Children)
{
if (gc is not null and not EmptyElement)
result.Add(gc);
result[idx++] = gc;
}
}
else if (children[i] is not null and not EmptyElement)
{
result.Add(children[i]!);
result[idx++] = children[i]!;
}
}
return result.ToArray();
return result;
}
}
Loading
Loading