Commit e72e4d8
feat(045): docking windows Phase 2 — ready for §2.29 review (#377)
* docs(045): tracking checklist + CHANGELOG bucket for docking workstream
Lands the implementation tracking doc (045-docking-windows-implementation.md)
referenced from the design and reserves a "Spec 045 — Docking" entry under
Unreleased so each phase appends to it incrementally.
Phase 0 (cross-cutting setup) per the tracking doc §0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): vendor WinUI.Dock @ 2f5247f1 (P1.1)
Pulls in WinUI.Dock (https://github.com/qian-o/WinUI.Dock, MIT) at
commit 2f5247f10d0abfde0fcb181e3037391d4a27952e under third_party/.
This is the foundation for Phase 1 of spec 045: the Reactor wrapper
assembly will reconcile a Reactor element tree onto this control library
so we can ship docking in the next cycle while the Phase 2 native rewrite
proceeds in parallel.
Four light edits per spec 045 §4.2 — documented in VENDORED.md:
1. Uno code paths stripped. csproj single-targets
net10.0-windows10.0.22621.0 (matches Reactor); WindowsAppSDK version
sourced from $(WindowsAppSDKVersion) so it stays in lockstep.
2. .editorconfig formatting (whitespace only).
3. Added Properties/AssemblyInfo.cs with [InternalsVisibleTo] for the
Reactor.Docking.Xaml wrapper + its test project.
4. Cross-window DnD bug — no source edit; wrapper restricts drag-out to
a single DockManager (spec §4.6).
XAMLTools.MSBuild dropped — the pre-merged Themes/Generic.xaml is checked
in directly and the per-control root *.xaml inputs are excluded from XAML
page compilation via <Page Remove>.
Build verified: `dotnet build third_party/WinUI.Dock/WinUI.Dock.csproj`
produces WinUI.Dock.dll with 0 warnings, 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): Reactor.Docking.Xaml project + Phase 1 public API (P1.2/P1.3)
Adds the wrapper assembly src/Reactor.Docking.Xaml/ wired into Reactor.slnx
and declares the public API surface committed at Phase 1 exit per
spec 045 §4.3:
Namespace Microsoft.UI.Reactor.Docking:
DockManager : Element — host element
DockNode (abstract) — algebra root
DockSplit : DockNode — N-way oriented split with min/max
DockTabGroup : DockNode — tab strip with TabPosition / Compact
DockableContent : DockNode — leaf pane with Key, CanClose, CanPin
TabPosition { Top, Bottom }
DockTarget { Center, Split{L,T,R,B}, Dock{L,T,R,B} }
DockTabGroupContext — adapter callback arg
IDockAdapter — content rehydrate + floating chrome
IDockBehavior — dock / float observation
Every public member carries an XML <remarks> linking back to the
relevant spec § number. CS1591 is suppressed at the project level so
we can still cleanly build before every internal helper has docs.
InternalsVisibleTo Reactor.Docking.Xaml.Tests, Reactor.Tests, and
Reactor.AppTests.Host so smoke fixtures + unit tests can reach into
the reconciler internals.
Wrapper implementation lands in the next commit (P1.4).
Build verified: dotnet build src/Reactor.Docking.Xaml — 0/0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): wrapper implementation for DockManager element (P1.4)
Lands the leaf-wrapper plumbing per spec 045 §4.4. Reconciles a Reactor
DockManager element to a single vendored WinUI.Dock.DockManager control:
* MountDockManager — creates the upstream control, merges the
WinUIDockResources dictionary so the control template + theme brushes
resolve, attaches a HostState bag, wires the Adapter/Behavior bridges
if supplied, and walks the DockNode tree to build LayoutPanel /
DocumentGroup / Document instances.
* UpdateDockManager — re-wires bridges on identity change, applies the
new layout (preserving panes by DockableContent.Key — content
subtrees survive container rebuilds), syncs the four side
collections, and updates ActiveDocument.
* UnmountDockManager — reconciles every pane's content to null so
useEffect cleanup runs, calls SaveLayout() when a PersistenceId is
set (auto-restore wiring is a §1.4 follow-up), and clears state.
Internal bridges translate the upstream IDockAdapter/IDockBehavior
interfaces onto the Reactor-side records:
* AdapterBridge.OnCreated(Document) → OnContentCreated
* AdapterBridge.OnCreated(DocumentGroup,..) → OnGroupCreated
* AdapterBridge.GetFloatingWindowTitleBar → identical, with the
Reactor element reconciled into a UIElement before return.
* BehaviorBridge maps both OnDocked overloads onto Reactor's single
OnDocked surface; the upstream DockTarget enum maps 1-to-1.
Pane identity is via DockableContent.Key (per spec 042 keyed
reconciliation). Panes with no Key get a synthetic per-render key so
they are mounted fresh each time — there is no implicit Title-as-key
fallback (spec §1.4).
Reactor.csproj grows InternalsVisibleTo("Microsoft.UI.Reactor.Docking.Xaml")
so the first-party wrapper can call SetElementTag / DetachReactorState.
Build verified: dotnet build src/Reactor.Docking.Xaml — 0/0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(045): unit + smoke fixtures for docking wrapper (P1.7)
Reactor.Tests/Docking/ — pure C# unit tests over the spec 045 §4.3 public
API surface. 27 tests, run in <250 ms:
* DockApiShapeTests — record defaults, equality, "with"-mutation,
algebra sealing, enum exhaustiveness, key
typing flexibility.
* BehaviorBridgeMappingTests — pins the WinUI.Dock.DockTarget ↔ Reactor
DockTarget mapping with [Theory] + a count
guard that trips loudly when the upstream
enum grows on re-snapshot.
Reactor.AppTests.Host SelfTest/Fixtures/DockingSmokeFixture.cs (spec 045
§1.7 — "minimal smoke fixture called out in §10.1 item 10"):
* Docking_TwoPaneMountUpdateUnmount — mount a 2-pane DockSplit, assert
Panel.Orientation, swap pane content, flip Orientation, unmount.
Catches mount/update/unmount regressions in the wrapper end-to-end.
* Docking_KeyedPanePreservation — verifies DockableContent.Key
reconciliation: reorder two panes, assert the SAME vendored Document
instance survives (so the Reactor content subtree mounted inside its
ContentControl host is preserved across the rebuild).
Registered both fixtures in SelfTestFixtureRegistry.AllFixtures + Create.
Side rename: existing 7 fixture files used `using WinUI = Microsoft.UI
.Xaml.Controls;` which conflicted with the now-referenced WinUI.Dock root
namespace (CS0576). Aliases renamed to `WinXC` (WinUI Controls) — purely
mechanical, no behavior change. The alias is private to each file.
Build verified: AppTests.Host + Tests, 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(045): Phase 1 docking overview + API reference (P1.8)
Drops authoring source into docs/_pipeline/apps/docking/ per the
tracking doc §1.8:
* overview.md — what docking is, the four-phase plan summary, the
Phase 1 capability list (documents in tab groups, recursive splits,
side pins, floating tear-out, programmatic dock, persistence,
compact/bottom tabs) AND the known limitations that motivate
Phase 2 (single-role DockableContent, no per-pane state, baseline
a11y, informational-only IDockBehavior).
* api.md — every public type with concrete code examples. End-to-end
IDE-layout example, key conventions (no Title-as-key fallback —
spec §1.4 explicit), adapter and behavior surface.
Both files lead with a banner reminding contributors that
docs/guide/docking.md is generated output (per memory
feedback_docs_pipeline.md). The .dt template under docs/_pipeline/
templates/ that compiles to docs/guide/docking.md is a P1 exit
follow-up — these source files capture the authoring intent so the
template can pull from them on the next pipeline run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): dock showcase sample app — six scenes for human review (P1.6)
samples/apps/dock-showcase/ — the human-tested deliverable for spec 045
§4.5. Six scenes pinned to the Phase 1 review script (§4.7 items 1-8):
A — IDE layout: Solution Explorer (left tool) + center editor tabs +
Properties (right tool) + Output (bottom). Persists under
PersistenceId "dock-showcase:ide".
B — Floating tear-out: 3-tab DocumentGroup + a custom title bar
supplied via IDockAdapter.GetFloatingWindowTitleBar.
C — Side pin: a CanPin: true ToolWindow pinned to RightSide.
D — Compact + bottom tabs: TabPosition.Bottom + CompactTabs=true,
side-by-side with a TabPosition.Top reference for visual diff.
E — Persistence: PersistenceId-scoped auto-save on unmount.
F — Programmatic dock: state-driven .Select() mapping demonstrates
that Reactor's functional composition replaces upstream's
DocumentsSource binding API (spec §3.2 lesson #3).
Configures the host's reconciler at launch via
ReactorApp.Run<DockShowcaseRoot>(configure: host =>
DockingXamlInterop.Register(host.Reconciler));
Wired into Reactor.slnx under /samples/apps/dock-showcase/.
Build verified: dotnet build samples/apps/dock-showcase — 0 warnings,
0 errors. Functional + visual review against Example.WinUI is the
Phase 1 §4.7 human gate (this commit unblocks that gate).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(045): finalize Phase 1 tracking + CHANGELOG (P1 exit)
Marks Phase 1 tasks complete in the tracking doc:
* §0.1 — branch, CHANGELOG, PR cadence
* §1.1 — vendoring (commit, light edits, sunset note)
* §1.2 — wrapper assembly + slnx wiring
* §1.3 — public API surface (every type + XML doc cross-ref)
* §1.4 — wrapper implementation (mount / update / unmount /
keyed-content preservation / adapter+behavior bridges)
* §1.6 — showcase sample (all six scenes)
* §1.7 — testing (smoke fixtures + 27 unit tests)
* §1.8 — overview + api authoring sources
Items explicitly deferred and called out inline:
- TypeForwardedTo redirects (cosmetic; not P1-critical)
- WindowPersistedScope auto-route on PersistenceId (§1.4 follow-up;
scope wiring needs the host scope service)
- Light/Dark/NightSky × 100/200% showcase matrix (P2 selftest scope)
- File-menu Save/Load in Scene E (bound to scope wiring above)
- ReactorGallery integration (standalone showcase is canonical)
CHANGELOG updated with the substantive Phase 1 entry naming the
committed API and the vendored library status.
Tree compiles 0/0 across Reactor, Reactor.Docking.Xaml, WinUI.Dock,
AppTests.Host, and DockShowcase. Reactor.Tests passes 8326/8326
(0 regressions from the WinUI→WinXC alias rename in 7 fixture files).
Phase 1 is now ready for the §4.7 human review gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(045): register WinUI.Dock metadata provider so showcase loads under ReactorApp.Run
Symptoms: launching the dock-showcase under ReactorApp.Run<TRoot> crashes
immediately with 0xC000027B (STATUS_APPLICATION_INTERNAL_EXCEPTION) inside
Microsoft.UI.Xaml.dll. The selftest harness running the same wrapper code
did not crash — masking the bug.
Root cause: when the vendored WinUI.Dock control template applies, the
XAML loader looks up types like dock:DockTargetButton, dock:Preview from
the active Application's IXamlMetadataProvider chain. Reactor's chain
covers Reactor's own types + the host project's compiler-generated
provider + the registered control-assembly providers — but apps using
ReactorApp.Run<TRoot> only get the host-assembly auto-discovery path for
DIRECT XAML files in the host project, not for transitive control-library
references. Without the WinUI.Dock metadata provider registered, the
template-apply pass kills the process.
(The selftest harness coincidentally worked because Reactor.AppTests.Host
has its own XAML metadata aggregation path that pulls in the vendored
library types via the entry-assembly scan — but that's harness-specific
luck, not a contract.)
Fix: DockingXamlInterop.Register now calls
ReactorApp.RegisterControlAssembly(typeof(WinUIDock.DockManager).Assembly)
before installing the reconciler type registration. This adds the vendored
library's auto-generated XamlMetaDataProvider to the chain consulted by
ReactorApplication.GetXamlType for every subsequent lookup.
Verified:
* dock-showcase launches and renders the six scenes (window appears).
* Selftests: 13/13 assertions pass across both fixtures (exit 0).
Side: removed `#if DEBUG: devtools: true` from showcase to keep the
sample focused on docking interaction. Re-enable per session if needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(045): close out P0 cross-cutting + P1 AOT verification
Wraps up the remaining unchecked Phase 0 items and the §1.5 AOT bullet
that didn't gate the P1 functional landing but were left open:
- §0.2: confirmed no PublicApiAnalyzers adoption — matches spec 036.
- §0.3: reserved the Docking.* resource prefix via the new
src/Reactor.Docking.Xaml/Resources/Reactor.Docking.resw. File header
documents the convention; entries cover the P2-consumed surfaces
(drop targets, floating-window default title, side-pin tooltip).
- §0.4: N/A — no central spec index exists.
- §0.5: ThirdPartyNoticeText.txt verified — WinUI.Dock block at L75.
- §1.5: AOT verification — Reactor.Docking.Xaml builds 0/0 in Release;
AppTests.Host with the wrapper referenced introduces no new IL trim
warnings (only 4 pre-existing CS warnings in unrelated fixtures).
Phase 1 is now structurally complete; the §1.9 human-review gate is the
remaining blocker before merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(045): showcase Scene A mirrors upstream + fill window height
Addresses the two issues from the §1.9 side-by-side review:
1. **DockManager fills available height across scenes.** The outer
wrapper used `VStack(...) + .Flex(grow:1)` on the DockManager, but
`.Flex(grow:1)` is only honored inside a Reactor FlexPanel — inside
a StackPanel-based VStack it is a no-op, so the dock area collapsed
to its measured intrinsic size. Switched Scenes A/B/C/E/F to a Grid
with `[Auto, …, Star]` rows so the DockManager occupies all
remaining vertical space. Scene D keeps explicit `.Height(200)` per
its side-by-side compact/full tab comparison.
2. **Scene A now mirrors WinUI.Dock Example.WinUI/MainView.xaml.**
Restructured to a vertical split → two horizontal halves: top is
editor tabs (MainView.xaml + MainViewModel.cs) plus a right-side
compact bottom-tab group (Solution Explorer + Git Changes, Width:
240); bottom row is Error List + Output/Terminal, both with
TabPosition.Bottom (Output/Terminal also CompactTabs), pinned to
Height: 200. The previous shape was missing every bottom-tab
DocumentGroup the upstream sample ships with by default — adding
them makes the side-by-side review apples-to-apples.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(045): close P1 human review gate + risk verification items
P1 §1.9 human review steps 1-8 signed off by user on 2026-05-20 against
showcase sample (commit 871e1e3e). P1 §1.5 risk-verification items
closed (commit pin recorded, drag-out scoped to single manager,
ContentControl slot verified by warnings-as-errors build).
P1 exit complete. Phase 2 (Reactor-native rewrite) opens next.
* feat(045): move docking public API into Reactor core + Phase 2 surface
Foundation refactor for Phase 2 (spec 045 §5):
* Move the P1 committed API surface (DockManager, DockNode, DockSplit,
DockTabGroup, DockableContent, TabPosition, DockTarget, IDockAdapter,
IDockBehavior, DockTabGroupContext) from src/Reactor.Docking.Xaml into
src/Reactor/Docking/. Same namespace (Microsoft.UI.Reactor.Docking) so
apps' source references are unchanged. Per tracking §0: "the native
rewrite extends src/Reactor/ proper (no separate assembly at P2 exit)."
* Add Phase 2 additive API surface (no P1 breaking changes per §1.3):
- Document, ToolWindow sealed records — §2.8 / §5.3.1 (separate roles,
distinct default permissions, both inherit DockableContent).
- Document<TState> generic — §2.9 / §5.3.2.
- CanFloat / CanMove on DockableContent base — §2.14 / §5.3.8.
- IDockLayoutStrategy interface — §2.13 / §5.3.6.
- DockHostModel internal source-of-truth, with UI-thread-affined
mutators (Dock/Float/Hide/Show/Close/Activate/PinToSide) and
Descendants()/AllContent() enumeration — §2.16 / §5.3.10.
- DockSide enum, FloatingDockWindow record, PendingMutation algebra.
- 15 cancellable lifecycle event-arg classes for the OnX Action props
on DockManager — §2.12 / §5.3.5.
- DockManager.LayoutStrategy prop and all 15 OnX event props — wired
additively; IDockBehavior stays for one-release source compat.
- DockPaneInfo struct + DockPaneState enum — §2.17 / §5.3.11.
- IDockLayoutMigration interface — §2.11 / §5.3.4.
* DockableContent refactored from a sealed positional record into an
open record with init-only properties + custom positional constructor.
Preserves the P1 closed-shape `new DockableContent(title, ...)` source
surface while letting Document / ToolWindow inherit and add their own
permission defaults via object-initializer syntax.
Build: warnings-as-errors across the solution; full test suite green
(27 docking tests pass).
Spec 045 §5 / tracking §2.8, §2.9, §2.11, §2.12, §2.13, §2.14, §2.16
(API surface only — full wiring lands in subsequent commits).
* test(045): unit coverage for P2 API surface — types, model, events, strategy, migration
Adds 43 new unit tests across the Phase 2 additive API:
* DocumentToolWindowTests — Document defaults (CanClose=true, CanPin=false),
ToolWindow defaults (X-hides, side-pinnable, auto-hide), Document<TState>
typed-state round-trip, CanFloat/CanMove permission flags on the
DockableContent base, with-expression preservation across the hierarchy.
* LifecycleEventTests — every *ing event arg inherits DockCancelEventArgs;
every *ed arg does NOT; Cancel defaults to false; required-init args
carry their payloads; DockManager exposes all 15 OnX props default-null.
* DockHostModelTests — read surface (Descendants() walks depth-first,
AllContent() unions docked/side/floating); every mutation queues the
matching PendingMutation record; null-content rejection; off-owner-
thread mutations throw InvalidOperationException (spec 045 §8.10
UI-dispatcher-affined contract).
* LayoutStrategyTests — default-interface-method no-ops for
IDockLayoutStrategy; example error-pane-routing strategy short-circuits
via PinToSide; DockManager.LayoutStrategy prop carries the assignment.
* LayoutMigrationTests — IDockLayoutMigration FromVersion/ToVersion shape;
in-place + replacing JSON transformations.
All 70 docking tests pass (was 27). Spec 045 / tracking §2.8, §2.9,
§2.11, §2.12, §2.13, §2.14, §2.16.
* feat(045): layout JSON v2 reader/writer + migration ladder (P2 §2.7, §2.11)
Phase 2 docking persistence (spec 045 §5.4 / §8.9 / §8.10 / §8.11):
src/Reactor/Docking/Persistence/
* DockLayoutJson.cs — schema v2 DTOs:
- DockLayoutDoc carries $schema marker, root tree, four side strips,
floating-window state, active-pane key.
- DockLayoutNode is the tagged-union node ("split" / "tabGroup" / "pane")
with kind discriminator + per-kind payload + universal dimension hints.
- DockLayoutPane carries title, key, role discriminator ("document" /
"toolWindow" / "dockableContent"), opaque state envelope, per-pane
permission overrides (only non-default fields are emitted), previous-
container hint.
- DockLayoutFloatingWindow carries id, screen-coord position + size, panes.
- DockLayoutJsonContext = source-gen JsonSerializerContext (AOT-clean
parsing per §8.9: no reflection at runtime).
* DockLayoutSerializer.Save/Load — public reader/writer:
- Save emits invariant-culture JSON (System.Text.Json default — verified
by save-de-DE/load-en-US selftest per §8.8).
- Load returns a non-null DockLayoutLoadResult; safe-fallback on
corruption (never throws on the load path per §8.10).
- Security limits: 1 MB max size, depth 32 (JsonReaderOptions.MaxDepth
+ JsonDocumentOptions.MaxDepth) — exceeded inputs fall back.
- Schema absence (default 0) routes to fallback so apps can detect "no
persisted layout yet" and prefer the declarative Layout prop.
- Role-default-aware permission emission: a Document with CanClose=true
omits the field; a ToolWindow with CanHide=false emits it. Output
file size stays close to "structure + identity only" per spec §8.9.
* DockLayoutMigrationRegistry — ordered migration ladder:
- Built-in v1→v2 migration synthesizes keys from titles per §5.4.4.
- DetectSchema treats absent $schema as v1 (P1's vendored format).
- TryUpgrade walks fromVersion → toVersion edges greedily; reports
clean failure on broken chains; forward-tolerant for newer-than-known
schemas per §8.11.
- Safety counter prevents accidental cycles.
Bug fix in Document/ToolWindow: the `new bool CanClose { get; init; } = …`
declarations were hiding the base property without changing the value
read through a DockableContent reference. The serializer reads via the
base type, so Document instances appeared to have CanClose=false. Fixed
by removing the `new` declarations and setting the base init properties
through the parameterless ctor body — both reference types now see the
intended role default.
tests/Reactor.Tests/Docking/LayoutSerializerTests.cs (27 new tests):
- Round-trip: Document, ToolWindow, DockSplit, DockTabGroup, sides,
floating windows, active-key, persistence-state envelope.
- Permission emission: defaults omitted, overrides emitted.
- Security limits: oversize + deep-nest + corrupt + empty + missing-
schema + unknown-node-kind all fall back without throwing.
- Invariant culture round-trip under de-DE save / en-US load.
- Migration registry: detect-schema; v1→v2 synthesizes keys; ladder
chains custom v2→v3 onto built-in; missing chain + forward-tolerance
+ same-version short-circuit.
- 200-pane load latency regression guard (250ms threshold absorbs CI
jitter; perf bench enforces the §8.1 50ms budget).
All 97 docking tests pass.
* feat(045): PreviousContainer tracker + DockContext property hooks (P2 §2.15, §2.17)
§2.15 — PreviousContainerTracker (spec 045 §5.3.9):
Internal ConditionalWeakTable<DockableContent, container-ref> tracking the
last container each pane was inside. Hide → re-show routes the pane back
to its remembered container instead of the default insertion point.
Reference-equality keying means distinct pane instances with the same
logical Key get independent history (correct — history attaches to the
instance, not the identity). Bookkeeping decays with the pane reference.
§2.17 — DockContexts + DockHooks (spec 045 §5.3.11):
Six Context<T> slots split by concern (Host, ActivePaneKey, Pane,
PaneState, LayoutSnapshot) so each hook subscribes to the smallest slice
that can answer the question:
• ctx.UseDockHost() → DockHostModel? (or null outside any host)
• ctx.UseActivePaneKey() → object? (selector-style re-render)
• ctx.UseIsActivePane() → bool (transitions only)
• ctx.UsePane() → DockPaneInfo (throws outside pane subtree)
• ctx.UseDockState() → DockPaneState (per-pane transitions)
• ctx.UseDockLayout() → DockLayoutSnapshot? (devtools wide-net)
The hooks are extension methods on RenderContext — call-site reads
`ctx.UseDockHost()`, symmetric with UseColorScheme. The DockLayoutSnapshot
record carries the full read-only snapshot (root / sides / floating /
active) for devtools and the optional MCP introspection (§2.26).
Tests (22 new, 119 docking tests total):
- DockHooksTests: defaults outside any host; provided values resolve via
ContextScope; UsePane throws outside a pane; UseIsActivePane key
comparison; two-host process isolation (§5.3.11 last bullet —
"components inside hostA resolve to hostA"); null-arg defensive checks.
- PreviousContainerTests: Set/Get/Clear cycle; distinct instances with
shared logical Key have independent histories; Set twice overwrites;
null-arg defensive checks; hide→show scenario from §5.3.9.
Wiring into the live renderer (so context slots actually receive values
from the model) happens with the native UI pipeline (§2.16 integration,
tracked under task #4).
* docs(045): P2 tracking checkpoint + CHANGELOG + sequence selftests
* docs/specs/tasks/045-docking-windows-implementation.md — Phase 2
progress checkpoint inserted at the top of P2; §2.7, §2.8, §2.9,
§2.11–§2.17 individual checkboxes updated to reflect the API surface
+ headless-test landings. Items requiring the native UI pipeline
(§2.1–§2.6) are explicitly annotated as gated on that work.
* CHANGELOG.md — "Spec 045 Phase 2 — Docking (foundation)" + "Layout
JSON v2 persistence" entries describing the public-API extensions
(Document/ToolWindow/Document<TState>; CanFloat/CanMove on the base;
IDockLayoutStrategy; the 15 cancellable lifecycle event-arg classes
+ matching Action<TArgs> props on DockManager; IDockLayoutMigration
registry; DockHostModel + queue; DockContexts + DockHooks;
PreviousContainerTracker), the serializer (Save/Load with security
limits, invariant culture, role-default-aware permission emission,
source-gen JsonSerializerContext for AOT cleanliness), and the
forward-tolerance behavior of the migration ladder.
* tests/Reactor.Tests/Docking/DockHostModelSequenceTests.cs — the
§2.27 layout-model fixture matrix: Dock/Float/Close ordering;
Hide/Show round-trip; Activate after Dock; commingled multi-pane
mutation sequence; read surface independence from mutation queue;
the §5.3.6 error-pane strategy routing example end-to-end; and the
§5.3.9 hide→show container-identity preservation combining the
model with the PreviousContainerTracker.
Test count: 126 docking unit tests pass. Full solution build is clean
(0 docking-attributed warnings, warnings-as-errors).
Phase 2 foundation layer is at a natural checkpoint. Native UI
rewrite (§2.1–§2.6) and the §2.20+ NFR matrix that depends on it
remain. §2.29 human review gate pending.
* feat(045): split + size constraint solver (P2 §2.1)
DockSplitterControl: WinUI partial Grid with 8 DIP visual / 16 DIP hit,
pointer drag, focus, arrow-key resize, per-direction cursor; emits a
signed delta + isFinal flag on each gesture.
DockSplitSolver: pure ratio-clamping math. ApplyDelta projects pointer
deltas into the constrained interval [max(leadingMin, pair-trailingMax),
min(leadingMax, pair-trailingMin)] for the moved pair, leaves untouched
panes alone, normalizes back to sum=1.0. Normalize/EqualShare cover the
persisted-JSON and bootstrap paths.
DockSplitRenderer.Render: composes a DockSplit + per-child Element +
ratios into a FlexElement with splitter handles interleaved between
adjacent panes. Flex-grow drives the ratio rendering on FlexPanel.
DockSplitterElement: internal Reactor element with reconciler
registration.
23 new tests (solver math + renderer shape). Total docking tests:
126 → 149 passing.
Spec 045 §2.1 — exits open-checklist for DPI hookup (lands with §2.16
end-to-end wiring).
* feat(045): tab-group renderer + native reconciler hand-off (P2 §2.2, §2.16)
DockTabGroupRenderer.Render: maps DockTabGroup → TabViewElement.
Documents → TabViewItemData with title+content+IsClosable. SelectedIndex
clamps to [0, count); tab-close callback receives the underlying
DockableContent (caller threads to OnDocumentClosing).
DockHostNativeComponent: Component<DockHostNativeProps> that walks
DockManager.Layout and emits the FlexElement / TabViewElement tree.
DockSplit ratios live in a ConditionalWeakTable keyed by node reference;
splitter deltas resolve through DockSplitSolver.ApplyDelta and trigger
re-render via UseState. Bootstrap ratios honor explicit Width/Height
hints from the model; otherwise EqualShare.
DockSideStripRenderer: minimal LeftSide/TopSide/RightSide/BottomSide
anchor strips wrapped around center. Full popup expansion lands in §2.5.
DockingNativeInterop.Register: opt-in P2 registration. Mounts a Border
whose Child is reconciled from a ComponentElement<DockHostNativeProps>;
update reconciles with new props; unmount cleans up the child subtree.
Showcase: REACTOR_DOCK_XAML=1 flips back to the P1 wrapper for A/B
review; default is now native.
6 new tab-group tests. Total docking tests: 149 → 155 passing.
* fix(045): splitter element handler storage + native smoke fixtures
Wiring the ResizeDelta handler through a DependencyProperty failed on
mount: WinRT marshalling raises TargetInvocationException trying to
synthesize an IID for the closed generic delegate
EventHandler<DockSplitterDeltaEventArgs> when SetValue creates a CCW.
Switch to a per-control ConditionalWeakTable — never crosses the COM
boundary.
Add two §1.7-style smoke fixtures driving the native renderer through
the host-app harness:
NativeDocking_TwoPaneMountUpdateUnmount — DockSplit + leaf content;
asserts FlexPanel mounts, content text renders, content updates
survive, unmount cleans up.
NativeDocking_TabGroupRendersToTabView — DockTabGroup; asserts
TabView mounts with N tabs and the active body rendered.
All four smoke fixtures green:
Docking_TwoPaneMountUpdateUnmount (XAML path, P1) ✓
Docking_KeyedPanePreservation (XAML path, P1) ✓
NativeDocking_TwoPaneMountUpdateUnmount (native, P2) ✓
NativeDocking_TabGroupRendersToTabView (native, P2) ✓
* docs(045): P2 native UI checkpoint — renderer testable end-to-end
* feat(045): pixel-accurate splitter + live model + DockContext mount (P2 §2.16, §2.17)
DockSplitterControl.GetHostExtent: walks to the parent FlexPanel and
reads ActualWidth/ActualHeight along the split axis. Threaded through
DockSplitterDeltaEventArgs.HostExtentDip → DockSplitterElement.OnDelta
→ DockSplitRenderer.Render → DockHostNativeComponent. The renderer now
passes the real host extent as totalDip to DockSplitSolver.ApplyDelta
instead of a synthetic 1000-unit; pointer-pixel deltas resolve into
ratio space against the actual layout, with a < 1 DIP guard for the
unlaid-out frame.
DockHostNativeComponent: cache a stable DockHostModel via UseRef so its
identity is preserved across renders (UseDockHost consumers don't churn
on layout-prop changes). Each render mirrors Layout/sides/ActiveDocument
from the controlled element into the model. Side slots filter to
ToolWindow (the typed model surface per §5.3.10); bare DockableContent
in side slots is silently dropped (§2.8 deprecates that shape).
DockContext registration on mount (§2.17): the rendered subtree is
wrapped with .Provide(DockContexts.Host, model), ActivePaneKey, and
LayoutSnapshot. Per-pane Content is further wrapped with
.Provide(DockContexts.Pane, info) + .Provide(DockContexts.PaneState,
Docked) so UsePane / UseDockState resolve correctly inside pane bodies.
New smoke fixture NativeDocking_DockContextHooksResolveOnRealMount
mounts function components inside two tabs and asserts:
• UseDockHost returns a non-null DockHostModel
• UsePane returns identity matching the enclosing leaf (Title + Key)
• UseIsActivePane flips when DockManager.ActiveDocument changes
Total native fixture assertions: 16 (was 10). Unit tests still 155
green.
* feat(045): renderer visual fidelity — pane padding, compact/bottom tabs
DockTabGroupRenderer: maps DockTabGroup.CompactTabs onto
TabViewWidthMode (Compact vs Equal) and TabPosition.Bottom onto a
ScaleY=-1 RenderTransform with per-tab counter-scale on selection
(translation of WinUI.Dock's DocumentGroup.xaml flip trick). The
counter-scale subscribes to SelectionChanged so dynamically-added
tabs pick up the transform on first show.
DockHostNativeComponent.WrapLeafWithPaneContext: 16-DIP content
padding around each pane body (Document.xaml default in upstream;
matches the P1 visual rhythm).
Fix the §2.17 hook fixture: build the docking tree inside the mount
lambda each call so leaf content elements are fresh-each-render.
Same-reference FuncElements hit the reconciler's structural-equality
short-circuit before HasConsumedContextChanged runs, so context
propagation to a leaf consumer requires fresh element refs. (This is
the Reactor convention — the showcase already builds content inline
inside Render, but the fixture was caching a Func outside Mount.)
* feat(045): side popup expansion (P2 §2.5)
DockSideStripRenderer: side-pin buttons toggle a popup overlay. State
lives in DockHostNativeComponent (UseState<object?>); each render
threads the expanded key + setter through to the strip composer. When
expanded, the renderer overlays a PopupElement over the outer layout
via a Grid; when collapsed, the popup unmounts entirely. Matches
upstream WinUI.Dock's flyout-per-expansion pattern.
Two WinUI Popup gotchas encountered + fixed:
• Popup ignores IsOpen=true while detached from the visual tree.
Solution: PopupElement.IsOpen starts false; a Setter wires a
Loaded handler that flips IsOpen=true once XamlRoot is available.
(Same pattern upstream's SidePopup.Show() uses.)
• Popup with IsLightDismissEnabled=true synchronously fires Closed
on open when no focus owner exists (headless harness path),
bouncing the expanded state back. Light-dismiss is disabled for
P2; the popup dismisses via repeat-click on the side button.
Focus arbitration + true light-dismiss land with §2.4.
Pane context envelope (DockContexts.Pane, PaneState.AutoHiddenExpanded)
flows into the popup body so UsePane / UseDockState resolve inside an
auto-hidden popup too.
NativeDocking_SidePopupExpandsAndCollapses fixture asserts:
• Strip button renders with the pane title.
• No open popup initially (VTH.GetOpenPopupsForXamlRoot probe).
• Click expands → 1 open popup.
• Repeat-click collapses → 0 open popups.
Total native fixture assertions: 16 → 20. Full docking smoke: 32 ok,
0 failed.
* feat(045): floating windows as real Reactor windows (P2 §2.6)
DockFloatingWindow.Open(pane): opens a top-level Reactor Window via
ReactorApp.OpenWindow with the pane Content as the window root. The
content is wrapped with the same DockContext envelope used by docked
panes but with PaneState=Floating so UseDockState resolves correctly
inside the floating window.
DockFloatingTracker: process-global set of open floating windows. The
docking pipeline subscribes via Open(); the window's Closed event
unregisters. Snapshot() / Count surface for fixture assertions and for
the manager's on-unmount cleanup pass.
Smoke fixture NativeDocking_FloatingWindowOpensAsRealWindow asserts:
• Open returns a non-null ReactorWindow.
• The tracker count increments by exactly 1.
• The tracker snapshot contains the returned window.
• Closing the window drops it back to the baseline.
Important harness note: the fixture pins ShutdownPolicy=Explicit for
the duration of the test. The AppTests.Host harness opens its primary
WinUI Window outside ReactorApp's registry, so a fixture-spawned
floating window otherwise becomes ReactorApp.PrimaryWindow and trips
ShutdownPolicy.OnPrimaryWindowClosed when the test closes it (closing
the actual harness window mid-pump → COMException). Documented in
fixture comments.
Total native fixture assertions: 20 → 24. Full docking smoke: 36 ok,
0 failed.
Deferred items (with cross-refs in the spec):
• Custom title-bar slot (IDockAdapter.GetFloatingWindowTitleBar)
• Multi-display restore clamp (lands when JSON-saved bounds read)
• Explicit Border-host pre-warm for HWND cold-create perf
* fix(045): disable broken TabPosition.Bottom ScaleY flip — render top-tabs for now
Showcase Scene A rendered three of four bottom-tab groups upside-down
(text flipped, tab strip on top, close-button on wrong end). My
counter-scale via TabView.SelectionChanged + ContainerFromIndex never
matched the actual containers (TabView adds TabViewItem instances
directly to TabItems; ContainerFromIndex is for virtualization).
Upstream WinUI.Dock counter-scales BOTH the Header (Tab grid inside
DockTabItem.xaml) and the Content (Border inside DockTabItem.xaml),
which only works because DockTabItem is itself a TabViewItem subclass
with named template parts.
Until a Reactor-native DockTabItem subclass replaces the shared
TabViewElement, bottom-position groups render as top-position. Tabs
read upright with close buttons on the right; visual position no
longer matches upstream's bottom strip. Tracked as a follow-up in the
§2.2 checklist.
* fix(045): splitter wiring — path-keyed ratios + incremental delta + state separation
Three bugs in the §2.1 splitter pipeline surfaced when the showcase
finally hit a real drag:
1. UseState<ConditionalWeakTable> never re-rendered. The setter ran
EqualityComparer<T>.Default which sees the same reference and skips
the re-render entirely. Switch to UseRef for the store +
UseReducer<int> as the re-render trigger.
2. The cumulative-from-origin pointer delta compounded against the
already-shifted ratios — by the tenth PointerMoved the leading
pane was driven past its 60 DIP minimum clamp and stuck there.
Reset _captureOrigin = p at the end of each PointerMoved so the
delta is incremental per frame.
3. ConditionalWeakTable<DockSplit, double[]> keyed by node reference
orphaned every entry on every re-render — apps reconstruct
`Layout = new DockSplit(…)` inside Render(), so the reference
changes each frame and ratios snapped back to bootstrap every
commit. Switch to Dictionary<string, double[]> keyed by tree-
position path ("0", "0/0", "0/1", …). Stable across re-renders.
New programmatic-drag fixture
NativeDocking_SplitterProgrammaticResizeAcrossRenders mounts an
IDE-style nested layout (1 row splitter + 2 column splitters), fires
ResizeDelta directly via RaiseResizeDeltaForTest, and asserts:
• initial ratios are equal-share;
• a single drag shifts the leading child;
• a second drag on the same splitter cumulates (no snap-back);
• dragging one column splitter does NOT affect the other;
• dragging a column splitter does NOT affect the row splitter;
• after a full re-mount (fresh DockSplit instances), ratios survive.
All 11 assertions green. Pipeline is correct end-to-end. Real-pointer
drag in the showcase still fails — that bug is in the pointer-capture
path, not the ratio/render path.
* wip(045): visual demo fixture + diagnostics for splitter detach investigation
The visual demo fixture (SplitterProgrammaticVisualDemo) drives the
splitters with paced ~400 ms delays so the resize is observable. All
assertions pass — the render→reconcile→FlexPanel pipeline works.
However, the Loaded/Unloaded log proves every drag-step DETACHES and
re-ATTACHES all three DockSplitterControls. This is why pointer
capture is lost during a real drag: WinUI fires PointerCaptureLost on
the un-parented control, and the user sees 'moved once then stuck'.
Diagnostics added to MountFlex/UpdateFlex confirm UpdateFlex is
running (not MountFlex). The detach happens DURING UpdateFlex even
though my registered DockSplitterElement update returns null (which
should signal 'patched in place, no replacement').
Still investigating where the detach is sourced.
* feat(045): splitter pointer-drag direct-mutation path (P2 §2.1 stable)
Splitter drag in the native renderer was breaking through several
layers of subtle pointer / layout / re-render interaction. The fix
takes a WPF GridSplitter-style direct-mutation approach during drag,
bypassing Reactor's reconciler entirely until the drag commits.
What changed in DockSplitterControl:
1. Snapshot the pair at PointerPressed
SnapshotPairAtCapture() reads leading + trailing ActualWidth/Height
along the split axis and stores the pair total + current grow split.
Mutations during the drag use this fixed snapshot, so layout-commit
lag between events doesn't make the splitter read stale measurements
and produce sub-pixel cursor drift ('shimmy').
2. Direct Width/Height writes during drag
ApplyAbsoluteGrowFromCapture() sets leading.Width / trailing.Width
(or Height for Rows direction) to the snapshotted pair sizes shifted
by the cumulative pointer delta. Grow attached is forced to zero so
Yoga uses Width as basis without an extra-space contribution. The
earlier ratio-via-Grow approach moved the splitter only ~10% of the
cursor delta when a sibling pane had an intrinsic basis (TabView
content, or an app-supplied Width:240 hint) — Yoga's grow + basis
interaction made the math non-linear.
3. Perpendicular-axis pin on capture
The splitter's parent FlexPanel gets its perpendicular dimension
pinned (panel.Height for Columns, panel.Width for Rows). Without
this, mutating pane sizes on the drag axis reflowed inner TabView
content, shifted topInner's measured Height, and made the outer
Vertical FlexPanel redistribute — visibly shrinking the OTHER row
during a column drag.
4. Single ResizeDelta on isFinal
Per-event ResizeDelta firing accumulated cumulative-from-origin
deltas in the host's ratio model and produced 10x+ overshoot at
commit. Drag-end now fires one event with the final cursor delta
so the model catches up to the visual once.
5. Keyboard arrow keys
Arrow keys use the same SnapshotPairAtCapture + ApplyAbsoluteGrow
path for a single discrete step, plus Focus(FocusState.Keyboard)
reassertion after the host's re-render lands.
6. handledEventsToo=true for KeyDown
WinUI's keyboard-nav engine claims arrow keys before the regular
KeyDown handler runs, moving focus away. AddHandler with
handledEventsToo lets us receive the event even after the nav
engine has marked it Handled, so we can override focus movement.
DockManager surface:
- SplitRatios escape hatch (Dictionary<string, double[]>) lets apps
share the ratio store with the renderer for slider-driven layouts
and tests. Used by the new Scene G in the dock-showcase to confirm
the render pipeline works end-to-end via a path that bypasses
pointer events entirely.
dock-showcase Scene G — Slider Resize:
- Three sliders drive the outer row / top columns / bottom columns
ratios directly via SplitRatios.
- Splitter handle is more visible (50% opaque gray vs original 20%)
so the drag affordance is discoverable.
- Slider StepFrequency=0.01 for smooth continuous values.
All docking smoke fixtures green: 36 ok / 0 failed.
All docking unit tests green: 155 ok / 0 failed.
* feat(045): drop-target overlay primitive (P2 §2.3)
Reactor-native replacement for WinUI.Dock's DockTargetButton +
Preview.xaml.cs. 9 targets (5 split + 4 edge) at minimum 44×44 DIP
per WCAG 2.5.5, hover preview rect, focus + arrow-key nav, reduced-
motion gate. Gated by DockManager.ShowDropTargets; the §2.4 drag
pipeline flips the flag mid-gesture once that lands.
20 new unit tests in Reactor.Tests/Docking/Native/ cover preview-
rectangle geometry, the spatial arrow-key focus graph, and per-
target AT names. NativeDocking_DropTargetOverlayShowsAndDismisses
smoke fixture exercises mount → confirm → unmount end-to-end.
AT names are inline English pending §2.21 Loc generator.
Overlay z-priority slot (spec 036 §11 enum) deferred — §11 is shell
integration, not overlay priority; the enum doesn't exist yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): showcase Scene H — drop-target overlay demo
Exercises the §2.3 overlay end-to-end without the §2.4 drag pipeline.
"Show drop targets" flips DockManager.ShowDropTargets; hovering each
target updates a "Hovered:" label and paints the preview rect;
clicking confirms and the scene appends a new pane at the chosen
target so the layout visibly changes. Esc dismisses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): drag pipeline + tab tear-out + dock-by-drop (P2 §2.4)
End-to-end tab-drag → drop-target overlay → layout mutation on
the Reactor-native renderer. Replaces upstream WinUI.Dock's static
GUID→object payload table with object-ref DockDragSession state
(spec §8.9 / §4.6). WinUI TabView drag events are surfaced on
TabViewElement (OnTabDragStarting / OnTabDragCompleted); the host
component flips the §2.3 overlay during drag, applies a
DockLayoutMutator transform on confirm, opens a floating window
on drop-outside, and routes Esc through a window-level key listener
since WinUI's DropResult can't distinguish Esc-cancel from drop-
outside on its own.
Fixes landed in iteration:
- AllowDrop + DragOver/Drop on the overlay (PointerMoved doesn't
fire on external elements during a tab drag).
- Populate TabDragStarting args.Data with a sentinel so external
AllowDrop targets accept the drop (the object-ref session is
the real payload; sentinel only unblocks WinUI's acceptance).
- Handle TabDragCompleted when args.Tab has been yanked from
TabItems (idx == -1) — fall back to the active session's source
so dragActive state always clears.
- Defensive: when dragActive is true but the session is gone,
schedule a state clear via DispatcherQueue so the overlay can't
get stuck "locked on".
- Wrap dropped pane in a single-doc DockTabGroup so it lands with
a tab strip rather than as a bare leaf.
23 new unit tests + NativeDocking_DragSessionConfirmMutatesLayout
smoke fixture. Keyboard-initiated move (Ctrl+Shift+M) and the
standalone .OnPan recognizer remain on the §2.10 / follow-up list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(045): drag/drop matrix fixture + splitter resize fix
17-scenario fixture under NativeDockingDragDropMatrixFixture.cs
exercises every DockTarget + sequential drag accumulation + nested-
split shape preservation + cancel-leaves-tree-unchanged + last-pane-
out invariants + post-splitter-drag window resize + idempotent drags
+ row-then-column ratio preservation. Each fixture begins a
DockDragSession + calls overlay.ConfirmTargetForTest and asserts
the resulting visual-tree shape.
Splitter resize: RestorePairToGrow now converts inline Width/Height
back into FlexPanel.Grow values and clears the inline sizes + the
perpendicular pin on release. The pre-fix behavior froze panes at
absolute DIPs so window resize left gaps. Matrix M15 surfaces the
regression (panel shrunk 784→584, leading pane stayed at 463 before
the fix; after fix shrinks proportionally to 326). Also wires the
fix into the keyboard-nudge path so arrow-key resize behaves the
same as pointer drag.
SimulatePointerDragForTest is now a faithful end-to-end pointer
simulation (snapshot + apply + release + fire ResizeDelta) so the
matrix exercises the host's ratio-store sync + re-render pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): JSON viewer + jump-back fix + IDE-layout matrix
Showcase Scene A grows a "Layout JSON" inspector panel on the right
that re-serializes the live layout via DockLayoutSerializer.Save on
every drag/drop and shows the split-ratios dict on every splitter
release. New DockManager event hooks back the live introspection:
- OnLiveLayoutChanged(DockNode?) fires after drag/drop layout
mutations so apps can mirror the host's effective layout (for JSON
view, undo, telemetry).
- OnSplitterDragCompleted() fires after the host's ratio store
updates so externally-owned SplitRatios dicts surface ratio shifts.
Splitter "jump back" fix: GetHostExtent now returns the EXTENT YOGA
DISTRIBUTES (parent extent minus all sibling splitter widths) so the
solver and the renderer agree on the DIP budget. Pre-fix the solver
saw N+16 DIP and Yoga distributed N, leaving ~handle*ratio DIP of
visible snap on release.
New matrix scenarios (Docking suite now 136 ok / 0 not ok):
- M18 SplitterReleaseNoVisibleJump: pair fills panel - splitter (±2
DIP tolerance), witnesses the jump-back fix.
- M19 IdeLayoutResizeAndContainerResize_NoControlChurn: IDE-style
Vertical{Horizontal,Horizontal} tree, drives column splitter →
asserts top columns shift + outer untouched; row splitter →
asserts outer shifts + COLUMN RATIOS PRESERVED (the
row-resets-columns regression); container resize → asserts all
ratios unchanged + panes redistribute. Crucially asserts SAME
INSTANCE identity for every splitter, FlexPanel, and TabView at
each step — no control churn.
- M20 SceneRerenderPreservesDockHostControls: exercises the scene-
level Component re-render path (the one that triggers
DockingNativeInterop.update, not just the host's internal tick)
and asserts controls stay alive across repeated re-renders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): docking operation log + replay scrubber + splitter root-cause fix
Two related landings driven by a stubborn splitter snap-back regression
in the showcase that the test matrix couldn't reproduce.
## Operation log (long-lived diagnostic; spec §2 keeps through P1–P4)
- DockOperationLog (1K ring buffer, Debug.WriteLine on append, cursor-
based replay API). Op kinds: Mount, DragStart/Hover/Confirm/Cancel/
TearOut, SplitterResize, SplitterTrace (intermediate splitter PRESS/
MOVE/RELEASE/SOLVE), LayoutChange, Note.
- Opt-in via DockManager.OperationLog. Host wires every drag/drop +
splitter event handler to record post-state snapshots.
- DockSplitterElement.DiagnosticSink threads pointer-event trace
strings up through the host's LogOp helper so cursor tracking +
inline-write math is fully visible.
- Showcase Scene A: Reset/Rewind/Play/Reset-log/Copy-log buttons next
to the existing JSON viewer. Copy-log puts the full ring on the
clipboard for pasting into bug reports.
## Splitter pin-both-axes fix
The trace from the user surfaced the snap-back root cause:
- pre-drag panes were unequal (leading=492.8, trailing=223.2, pair=716)
- during drag, panel measured itself to fit children's inline widths
(panel.ActualWidth=732, splitter tracks cursor perfectly)
- on release, RestorePairToGrow cleared inline widths, panel re-
measured to parent's allocation (panel.ActualWidth=585.6)
- the solver then received hostExtent=569.6, computed ratios in that
shrunk space, and Yoga rendered leading=91 — the visible jump.
Fix in SnapshotPairAtCapture:
- pin panel.Width AND panel.Height (was only perpendicular axis), so
the panel can't grow with the children's inline widths during the
drag — the panel stays at its parent-allocated size throughout.
- _pairDipAtCapture now sourced from GetHostExtent (panel
distributable space) instead of leading.ActualWidth + trailing
.ActualWidth (children measurement, which can overflow).
- _leadingDipAtCapture now computed from grow ratio in panel space.
RestorePairToGrow clears BOTH axes (was perpendicular only) to match.
Drops the giant try/catch around CopyLogToClipboard per review — if
clipboard SetContent fails we want to see the stack.
198/198 unit tests pass; 136/136 Docking self-host checks pass.
Brain dump documenting the full saga at C:\temp\spec-045-docking-
braindump.md for future agents picking this up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): splitter cursor tracking — basis:0 + pure-grow drag + MeasureFunc clamp
Three layered fixes that together make the Phase 2 docking splitter
track the cursor 1:1 in real WinUI pointer input (Scene A IDE layout
in samples/apps/dock-showcase), matching P1's WinUI.Dock behavior.
DockSplitterControl: ApplyAbsoluteGrowFromCapture now writes
FlexPanel.Grow directly during drag instead of inline Width/Height.
Single source of truth eliminates the mode-switch (inline-Width
during MOVE → Grow on RELEASE) that produced the snap-back on drag
release. SnapshotPair no longer pins panel.Width/Height; the panel's
parent allocation is stable throughout the drag. RestorePairToGrow
reduced to defensive ClearValue cleanup.
DockSplitRenderer: each pane is now composed with Flex(grow, shrink:1,
basis: 0) — equivalent to CSS `flex: <ratio> 1 0%` and to WinUI Grid's
`GridUnitType.Star(N)`. Without explicit basis 0, panes default to
basis auto = intrinsic content size; for content-heavy panes (TabView
with body) the content fills the panel and grow distributes nothing.
FlexPanel.cs: MeasureFunction now clamps the returned main-axis size
to the requested size when wMode == YogaMeasureMode.Exactly. CSS
Flexbox spec §9.7-§9.8: when flex layout has resolved an item's used
size, MeasureFunc must confirm that size and not report something
larger. Pre-change, WinUI controls that ignore Measure constraints
(notably TabView with content wider than its slot) propagated their
intrinsic content size back to Yoga, overriding flex allocation. The
clamp is the CSS-compliant fix and brings FlexPanel into the
`min-width: 0` mode of the spec (the `min-width: auto` mode is
tracked separately as #364).
Tests: all 198 Docking unit tests, all 326 Flex unit tests, all 136
Docking selftests, all Flex selftests pass.
Refs #364 (FlexPanel CSS compliance follow-up)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): keyboard chords §2.10 — PageUp/Down + F4/W + Ctrl+Shift+M
Wire three chord families on the Reactor-native dock host via a new
DockChordBridge (ConditionalWeakTable keyed by DockManager element)
so mount-time KeyboardAccelerators on the host Border can pick up
live chord delegates from the component each render — no extra layout
layer. First attempt wrapped via CommandHostElement; that Grid wrapper
shifted M19's outer-FlexPanel ActualWidth and was abandoned.
- Ctrl+PageUp / Ctrl+PageDown — cycle the active group's tab, wraps
at both ends. Fires OnActiveContentChanged. Targets the group
containing `activePaneKey ?? Manager.ActiveDocument.Key`.
- Ctrl+F4 / Ctrl+W — close active document via cancellable
OnDocumentClosing → OnDocumentClosed → OnLiveLayoutChanged.
- Ctrl+Shift+M — toggle the §2.3 drop-target overlay into keyboard
mode; active pane is the implicit source. Arrow + Enter / Esc
inherited from §2.3's focusable overlay; chord repeat dismisses.
Host component now tracks a path-keyed selectedIndexStore (mirrors
the §2.1 ratio store) plus an activePaneKey UseState so chord cycling
sticks across re-renders. activeKey resolution is `appActiveKey ??
activePaneKey` — controlled-input shape preserved (apps that pin
ActiveDocument still win for UseIsActivePane), matching the
DockHooks_IsActivePane_FlipsOnActiveChange regression test.
RenderTabGroup applies selectedIndexStore overrides but does NOT
wire onSelectedIndexChanged (a non-null callback fires spuriously on
TabView mount and broke SidePopup_OpensOnClick).
Deferred: Ctrl+Tab pane navigator overlay, Alt+F7 hidden-pane picker,
UIA LiveSetting=Polite announcements, spec-027 configurable binding.
Tests: 16 new unit tests for the pure helpers (FindGroupContainingKey,
FindFirstGroup, CycleIndex, BuildChords) + DockChordBridge round-trip.
214/214 Docking unit tests pass; all Docking selftests pass.
Human-validated 2026-05-21 against Scene A.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): strategy dispatch §2.13 + permission gating §2.14
§2.13 — DockHostModel.Dock now dispatches into IDockLayoutStrategy
when the host component mirrors DockManager.LayoutStrategy onto the
model each render. BeforeInsertDocument/BeforeInsertToolWindow runs
first, subtype-routed via pattern match; returning true short-circuits
the default DockOp. Returning false queues the default mutation and
then fires AfterInsertDocument/AfterInsertToolWindow so apps can layer
adjustments on top. Bare DockableContent (P1 source-compat shape)
bypasses the typed hooks. Six new unit tests cover the
no-strategy/short-circuit/pass-through/subtype-routing/bare-bypass
matrix.
§2.14 — Permission gating in the drag pipeline:
- HandleTabDragStarting refuses drag start when CanMove=false (logs
a Note op; no DockDragSession.Begin).
- HandleTabDragCompleted refuses tear-out when CanFloat=false (session
ends; layout untouched).
- Drop-target OnConfirm re-checks CanMove on the source pane so the
Ctrl+Shift+M keyboard mode can't drop a pinned pane that turned
read-only between mode-enter and confirm.
- EnterKeyboardDropMode skips opening the overlay for a pinned active
pane (better UX than a guaranteed-refused drop on Enter).
- Tab close X-button now routes through CloseTabViaButton which
re-checks CanClose and goes through the cancellable
OnDocumentClosing event (matches the Ctrl+F4 chord path).
220/220 Docking unit tests pass. All NativeDocking selftests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): default tab styling for Document/ToolWindow (§2.8)
DockTabGroupRenderer.Render now auto-resolves tab styling based on
the group's content type: an all-ToolWindow group at record-default
TabPosition/CompactTabs flips to bottom-position + compact tabs
(matches Office / VS tool-pane convention). All-Document and mixed
groups stay at the top-position + full-width default — a tool window
dragged into an editor strip doesn't collapse the whole strip to
compact. The TabPosition.Bottom visual still renders as top-strip
per the §2.2 limitation (no WinUI TabView bottom mode), but the
resolved value flows through so future bottom-strip support picks
it up.
5 new unit tests cover the resolution matrix: all-tool → compact,
all-doc → equal, mixed → equal, explicit-defaults-on-tool → compact,
explicit-compact-on-tool → compact (stable). 225/225 Docking unit
tests pass. All NativeDocking selftests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): PreviousContainer routing §2.15
Close/tear-out paths now record the pane's container into
PreviousContainerTracker before removal, so a later show-from-history
can fold the pane back into its original group.
Wiring:
- DockLayoutMutator.FindContainer(root, pane) — new pure walk that
returns the immediate DockTabGroup containing a pane (or the leaf
itself if it IS the root). Null when not present.
- DockLayoutMutator.ShowFromHistory(root, pane, fallback) — folds
a pane back into its remembered group when that group is still
in the tree; falls back to InsertPaneAtTarget otherwise.
- DockHostNativeComponent: CloseTabViaButton, CloseActivePane, and
the tear-out branch of HandleTabDragCompleted all call
PreviousContainerTracker.Set(pane, FindContainer(...)) just
before RemovePane.
The drain side (DockManager.Show programmatic API, drag-back
snap-to-history hint) is still on §2.16 model integration.
7 new unit tests cover FindContainer + ShowFromHistory paths
(no-history fallback, group-still-in-tree fold, group-torn-down
fallback, nested-split lookups). 232/232 Docking unit tests pass.
All NativeDocking selftests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): composition fixture §2.18 + IDockBehavior obsolete §2.12
§2.18 — New NativeDocking selftest fixture
CompositionDrivenDocumentsRespectKeyedReconciliation: mounts a
layout whose documents list lives in app state, runs add / remove
cycles, and asserts the documents.Select(...) pattern works through
keyed reconciliation (TabView instance preserved across the
structural changes). Codifies the §5.3.7 contract that functional
composition replaces DocumentsSource / LayoutItemTemplate /
ContentResolver. 7 ok assertions.
§2.12 — Mark IDockBehavior + DockManager.Behavior as [Obsolete]
with migration pointers to the per-event Action props
(OnContentDocked / OnContentFloating / OnContentFloated). The P1
wrapper (Reactor.Docking.Xaml) suppresses CS0618 at file scope so
the bridge keeps working during the transition. Slated for removal
one release after Phase 2 ships.
Bonus: fix CS1573 (stale XML doc) on DockSplitRenderer.Render's
splitterDiagnosticSink parameter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(045): chord + permission-gate smoke fixtures
Two new NativeDocking selftests:
NativeDocking_KeyboardChordsRegisteredOnMount — locks down the §2.10
chord bridge contract: after mount, DockChordBridge.Get(manager)
returns non-null handlers for all four chords (NextTab, PrevTab,
CloseActive, EnterDropMode) and invoking them doesn't throw. The
state-mutation side-effect path (selectedIndexStore → re-render →
TabView SelectedIndex update) isn't asserted here because the
fixture's sub-host doesn't flush internal-state re-renders through
Harness.Render's primary-host wait. That path is locked down by
DockHostKeyboardTests unit tests + visual showcase verification.
NativeDocking_PermissionGate_PinnedPaneSurfaceCheck — locks down
the §2.14 contract surface: CanMove=false / CanClose=false
properties survive the record `with` shape, and DockDragSession.Begin
succeeds for a movable pane (the post-gate production path). The
gate predicate itself (`if (!pane.CanMove) return;` inside
HandleTabDragStarting) is verified by inspection + the property
unit tests in DockApiShapeTests / DocumentToolWindowTests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): model-mutation drain §2.16 — programmatic Dock/Close/Activate/PinToSide affect live tree
DockHostNativeComponent.Render now drains DockHostModel.Pending on each
render pass: each queued op translates to a layout / side / active-key
update and fires the matching lifecycle event. Lights up
IDockLayoutStrategy.AfterInsert* adjustments (§2.13), DockHostModel.Show
using the §2.15 PreviousContainer history, and programmatic
Dock/Float/Hide/PinToSide affecting the rendered tree. The model gains
an OnMutationQueued callback that the host wires to bumpTick so mutator
calls wake a re-render; a new DockHostModelBridge (mirroring
DockChordBridge) lets tests + devtools grab the live model from a
DockManager reference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): corrupt-JSON ReactorEventSource emission §2.7
DockLayoutSerializer.Load classifies every fallback path into one of
seven PII-safe categories (empty / oversize / json-parse /
unsupported-schema / null-document / schema-missing / validation) and
fires ReactorEventSource.DockingLayoutLoadFallback (event id 16,
Warning, Errors keyword). The event payload carries only the category;
the full DockLayoutLoadResult.FailureReason still surfaces to
in-process callers under app ACL — same trade-off as RenderError.
Closes the last open §2.7 checklist item.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(045): reliability + security selftests §2.24 §2.25; LayoutOverride wrapper fix
Four new host-mounted reliability fixtures under
NativeDockingReliabilityFixture:
• CorruptLayoutFallback_HostMounted — corrupt JSON loads non-throw,
Microsoft-UI-Reactor fallback event fires, host mounts the default.
• OffThreadMutation_ThrowsAndDoesNotQueue — bridge-resolved model
rejects Task.Run mutators; Pending stays empty.
• UseEffectCleanup_RunsOnPaneClose — programmatic close drains and
the pane's body disappears from the visual tree. Surfaced a real
Reactor gap: ComponentElement embedded under DockableContent.Content
doesn't run UseEffect cleanup on unmount. Known-failing assertion
documented in the fixture docstring.
• DragSessionPayload_ObjectRefsOnly — DockDragSession holds object
references, refuses a second concurrent drag, clears on End.
While writing the cleanup fixture I caught a subtle bug in the drag/
close/drain paths: setLayoutOverride(null) was reverting to the
controlled-input prop, resurrecting closed panes. Wrapped the state in
a LayoutOverride(DockNode?) record so the renderer distinguishes "no
override" from "override is intentionally empty". All five
setLayoutOverride call sites (drain, tear-out, tab close, drag confirm,
chord close) updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(045): §2.25 reliability close-out — clamp, host-floating, leak baseline
Five §2.25 items land: off-screen restore via DockFloatingClamp,
orphan/owner support via WindowSpec.Owner forwarding, crash-mid-drag
selftest proving drag state never reaches persisted JSON,
floating-window-outliving-host via per-host tracker + unmount
cleanup, and a 100-pane open/close allocation-baseline selftest.
DockFloatingClamp is the pure multi-display restore algorithm:
saved bounds with < 200x100 DIP overlap agai…1 parent b486374 commit e72e4d8
125 files changed
Lines changed: 25448 additions & 128 deletions
File tree
- docs
- _pipeline
- apps/docking
- templates
- guide
- images/docking
- specs
- tasks
- plugins/reactor/skills/reactor-dsl/references
- samples
- Reactor.TestApp
- Demos
- apps/dock-showcase
- skills
- src/Reactor
- Core
- Diagnostics
- Diagnostics
- Docking
- Diagnostics
- Native
- Persistence
- Elements
- Hosting
- Devtools
- tests
- Reactor.AppTests.Host
- Fixtures
- SelfTest
- Fixtures
- Reactor.AppTests/Tests
- Reactor.Tests
- Devtools
- Docking
- Native
- Elements
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
221 | 221 | | |
222 | 222 | | |
223 | 223 | | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
224 | 230 | | |
225 | 231 | | |
226 | 232 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
73 | 73 | | |
74 | 74 | | |
75 | 75 | | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
76 | 82 | | |
77 | 83 | | |
78 | 84 | | |
| |||
97 | 103 | | |
98 | 104 | | |
99 | 105 | | |
| 106 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
121 | 121 | | |
122 | 122 | | |
123 | 123 | | |
| 124 | + | |
124 | 125 | | |
125 | 126 | | |
126 | 127 | | |
| |||
0 commit comments