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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ Conventions for contributors:
that cache only on a theme or palette change, so such an edit was invisible to `ThemeRef`
modifiers, overrides and `ThemeRef.Resolve`. The call clears the cache and re-renders
every live host past memoization and the skip of unchanged elements. Callable from
any thread (issue #1327).
any thread (issue #1327). A brush resolved eagerly and kept (`ThemeRef.Resolve` inside a
memo factory, or stored in state) is a fixed value and is not refreshed; bind it with
`Theme.Ref` instead.

- **Getting Started documents the single-file path.** A Reactor app does not need
a `.csproj`: .NET 10 runs a lone `.cs` file whose `#:package` / `#:property`
Expand Down
6 changes: 6 additions & 0 deletions docs/_pipeline/templates/styling.md.dt
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,12 @@ new value, memoized subtrees included. It is safe to call from any
thread; await a host's `WaitForIdleAsync()` to observe the result.
Mutating an existing brush's `Color` in place does not need it.

A brush you resolve *eagerly* and keep is not refreshed: a
`ThemeRef.Resolve(...)` inside a `UseMemo` or `Memo(key, ...)` factory, or a
resolved brush stored in state, is a fixed value Reactor can't trace back
to its key. Bind it instead (`.Foreground(Theme.Ref(key))`), or resolve it
in `Render` outside the memo.

### Per-element theme override scope

Pin one panel to a different scheme while the rest of the app follows
Expand Down
6 changes: 6 additions & 0 deletions docs/guide/styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,12 @@ new value, memoized subtrees included. It is safe to call from any
thread; await a host's `WaitForIdleAsync()` to observe the result.
Mutating an existing brush's `Color` in place does not need it.

A brush you resolve *eagerly* and keep is not refreshed: a
`ThemeRef.Resolve(...)` inside a `UseMemo` or `Memo(key, ...)` factory, or a
resolved brush stored in state, is a fixed value Reactor can't trace back
to its key. Bind it instead (`.Foreground(Theme.Ref(key))`), or resolve it
in `Render` outside the memo.

### Per-element theme override scope

Pin one panel to a different scheme while the rest of the app follows
Expand Down
82 changes: 1 addition & 81 deletions src/Reactor/Core/Reconciler.Mount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public sealed partial class Reconciler
// factory output) when it changes, so the mounted inner is never re-derived from
// the old factory. Any modifiers on the wrapper itself are applied by the
// post-dispatch ApplyModifiers below, exactly like any other element.
KeyedMemoElement km => MountKeyedMemo(km, requestRerender),
KeyedMemoElement km => Mount(WithWrapperKey(km.Factory() ?? EmptyElement.Instance, km.Key), requestRerender),
// EmptyElement is a no-op sentinel — callers (Reconcile, panel
// children loops, ChildReconciler) already filter it before
// reaching Mount, but MountContext.MountChild does not, so a V1
Expand Down Expand Up @@ -277,86 +277,6 @@ private static Element WithWrapperKey(Element inner, string? wrapperKey) =>
? inner
: inner with { Key = wrapperKey };

private UIElement? MountKeyedMemo(KeyedMemoElement km, Action requestRerender)
{
var inner = WithWrapperKey(km.Factory() ?? EmptyElement.Instance, km.Key);
var control = Mount(inner, requestRerender);
// Nested memos share the realized control, so each layer records its own output. On a
// fresh control the layers inside this one have already recorded; this one goes on top.
if (control is FrameworkElement fe)
{
var state = GetOrCreateReactorState(fe);
SetKeyedMemoOutput(state, km.MemoKey, CountKeyedMemoOutputs(state, km.MemoKey), inner);
}
return control;
}

// The outputs recorded on a control are kept in the order their layers mounted, innermost
// first; layers with an equal MemoKey are told apart by that order (nth among the key's
// entries). RefreshKeyedMemo walks them outermost first.

internal static int CountKeyedMemoOutputs(ReactorState state, object memoKey)
{
int count = state.KeyedMemoKey is { } first && first.Equals(memoKey) ? 1 : 0;
if (state.MoreKeyedMemoOutputs is { } more)
count += more.Count(entry => entry.Key.Equals(memoKey));
return count;
}

/// <summary>The <paramref name="nth"/> output recorded for <paramref name="memoKey"/>, or null.</summary>
internal static Element? GetKeyedMemoOutput(ReactorState state, object memoKey, int nth)
{
if (state.KeyedMemoKey is { } first && first.Equals(memoKey))
{
if (nth == 0) return state.KeyedMemoOutput;
nth--;
}
if (state.MoreKeyedMemoOutputs is { } more)
{
foreach (var entry in more.Where(entry => entry.Key.Equals(memoKey)))
{
if (nth == 0) return entry.Value;
nth--;
}
}
return null;
}

/// <summary>
/// Replaces the <paramref name="nth"/> output recorded for <paramref name="memoKey"/>, or
/// appends one when fewer are recorded.
/// </summary>
internal static void SetKeyedMemoOutput(ReactorState state, object memoKey, int nth, Element output)
{
if (state.KeyedMemoKey is null)
{
state.KeyedMemoKey = memoKey;
state.KeyedMemoOutput = output;
return;
}
if (state.KeyedMemoKey.Equals(memoKey))
{
if (nth == 0)
{
state.KeyedMemoOutput = output;
return;
}
nth--;
}
var more = state.MoreKeyedMemoOutputs ??= new();
for (int i = 0; i < more.Count; i++)
{
if (!more[i].Key.Equals(memoKey)) continue;
if (nth == 0)
{
more[i] = new(memoKey, output);
return;
}
nth--;
}
more.Add(new(memoKey, output));
}

/// <summary>
/// Final dispatch arm: the four resolution arms in <see cref="Mount"/>
/// (per-host <c>_v1Handlers</c>, per-host <c>_typeRegistry</c>, global
Expand Down
Loading
Loading