Skip to content

Commit e72e4d8

Browse files
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

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 565 additions & 0 deletions
Large diffs are not rendered by default.

Reactor.slnx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,12 @@
221221
<Platform Solution="*|x64" Project="x64" />
222222
</Project>
223223
</Folder>
224+
<Folder Name="/samples/apps/dock-showcase/">
225+
<Project Path="samples/apps/dock-showcase/DockShowcase.csproj">
226+
<Platform Solution="*|ARM64" Project="ARM64" />
227+
<Platform Solution="*|x64" Project="x64" />
228+
</Project>
229+
</Folder>
224230
<Folder Name="/samples/apps/headtrax/">
225231
<Project Path="samples/apps/headtrax/HeadTrax/HeadTrax.csproj">
226232
<Platform Solution="*|ARM64" Project="ARM64" />

ThirdPartyNoticeText.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ THIS SOFTWARE.
7373

7474

7575
------------------- WinUI.Dock ----------------------
76+
The Reactor docking subsystem (Microsoft.UI.Reactor.Docking, spec 045)
77+
was developed using WinUI.Dock as the architectural reference and
78+
Phase-1 implementation basis. The vendored copy was removed at Phase-2
79+
exit once the native rewrite replaced it; this attribution remains in
80+
recognition of the upstream project that informed the design.
81+
7682
MIT License
7783

7884
Copyright (c) 2025 qian-o
@@ -97,3 +103,4 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
97103
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
98104
SOFTWARE.
99105
------------------------------------------------------
106+

docs/_pipeline/apps/docking/App.cs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
using Microsoft.UI.Reactor;
2+
using Microsoft.UI.Reactor.Core;
3+
using Microsoft.UI.Reactor.Docking;
4+
using Microsoft.UI.Reactor.Docking.Native;
5+
using Microsoft.UI.Xaml.Controls;
6+
using static Microsoft.UI.Reactor.Factories;
7+
8+
// <snippet:register>
9+
ReactorApp.Run<DockingApp>(
10+
title: "Docking",
11+
width: 900,
12+
height: 600,
13+
devtools: true,
14+
configure: host => DockingNativeInterop.Register(host.Reconciler));
15+
// </snippet:register>
16+
17+
class DockingApp : Component
18+
{
19+
public override Element Render() => Component<TwoPaneDemo>();
20+
}
21+
22+
// <snippet:two-pane>
23+
class TwoPaneDemo : Component
24+
{
25+
public override Element Render() => new DockManager
26+
{
27+
Layout = new DockSplit(
28+
Orientation.Horizontal,
29+
new DockNode[]
30+
{
31+
new DockableContent(
32+
Title: "Solution",
33+
Key: "tool:solution",
34+
Content: VStack(4,
35+
TextBlock("📁 MyApp.sln").SemiBold(),
36+
TextBlock(" 📄 App.cs"),
37+
TextBlock(" 📄 MainView.cs")
38+
).Padding(12),
39+
Width: 240),
40+
41+
new DockableContent(
42+
Title: "App.cs",
43+
Key: "doc:app-cs",
44+
Content: TextBlock("// editor body").Padding(12)),
45+
}),
46+
};
47+
}
48+
// </snippet:two-pane>
49+
50+
// <snippet:tab-group>
51+
class TabGroupDemo : Component
52+
{
53+
public override Element Render() => new DockManager
54+
{
55+
Layout = new DockTabGroup(
56+
Documents: new[]
57+
{
58+
new DockableContent("App.cs",
59+
VStack(4,
60+
TextBlock("// App.cs"),
61+
TextBlock("public sealed class App : Component"),
62+
TextBlock("{"),
63+
TextBlock(" public override Element Render() =>"),
64+
TextBlock(" Text(\"hello, world\");"),
65+
TextBlock("}")
66+
).Padding(16),
67+
Key: "doc:app", CanClose: true),
68+
new DockableContent("MainView.cs",
69+
TextBlock("// MainView.cs body").Padding(16),
70+
Key: "doc:main", CanClose: true),
71+
new DockableContent("Readme.md",
72+
TextBlock("# Readme").Padding(16),
73+
Key: "doc:readme", CanClose: true),
74+
},
75+
SelectedIndex: 0),
76+
};
77+
}
78+
// </snippet:tab-group>
79+
80+
// <snippet:side-pin>
81+
class SidePinDemo : Component
82+
{
83+
public override Element Render() => new DockManager
84+
{
85+
Layout = new DockableContent(
86+
Title: "Document",
87+
Key: "doc:main",
88+
Content: VStack(8,
89+
TextBlock("Document area").SemiBold(),
90+
TextBlock("Click the pinned tab on the right to expand it."),
91+
TextBlock("Pin / unpin from inside the popup to toggle.")
92+
).Padding(16)),
93+
94+
RightSide = new[]
95+
{
96+
new DockableContent(
97+
Title: "Properties",
98+
Key: "tool:properties",
99+
Content: VStack(4,
100+
TextBlock("Name").SemiBold(),
101+
TextBlock("Width: 240"),
102+
TextBlock("Height: 120")
103+
).Padding(12),
104+
CanPin: true),
105+
},
106+
};
107+
}
108+
// </snippet:side-pin>
109+
110+
// <snippet:persistence>
111+
class PersistenceDemo : Component
112+
{
113+
public override Element Render() => new DockManager
114+
{
115+
// Layout JSON is auto-saved to WindowPersistedScope["docking:my-shell"]
116+
// on unmount and restored on next mount.
117+
PersistenceId = "my-shell",
118+
Layout = new DockSplit(
119+
Orientation.Horizontal,
120+
new DockNode[]
121+
{
122+
new DockableContent("Pane 1",
123+
TextBlock("Rearrange me, then relaunch.").Padding(12),
124+
Key: "p1", Width: 220),
125+
new DockableContent("Pane 2",
126+
TextBlock("Layout restores from PersistenceId.").Padding(12),
127+
Key: "p2"),
128+
}),
129+
};
130+
}
131+
// </snippet:persistence>
132+
133+
// <snippet:floating-adapter>
134+
class FloatingChromeAdapter : IDockAdapter
135+
{
136+
public Element? OnContentCreated(DockableContent content) => null;
137+
public void OnGroupCreated(DockTabGroupContext group) { }
138+
139+
// Custom title bar painted on torn-out floating windows.
140+
public Element? GetFloatingWindowTitleBar(DockableContent? source) =>
141+
HStack(8,
142+
TextBlock("📌").Opacity(0.7),
143+
TextBlock(source?.Title ?? "Floating").SemiBold(),
144+
TextBlock(" — My App").Opacity(0.5)
145+
).Padding(12, 6, 12, 6);
146+
}
147+
// </snippet:floating-adapter>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
app:
2+
title: "Docking"
3+
width: 900
4+
height: 600
5+
startup-delay: 1500
6+
7+
screenshots:
8+
- id: two-pane
9+
description: "Minimal horizontal split with a tool pane and an editor pane"
10+
component: TwoPaneDemo
11+
region: client
12+
format: png
13+
- id: tab-group
14+
description: "Single tab group with three closeable document tabs"
15+
component: TabGroupDemo
16+
region: client
17+
format: png
18+
- id: side-pin
19+
description: "Document area with a pinned tool collapsed to the right edge"
20+
component: SidePinDemo
21+
region: client
22+
format: png
23+
- id: persistence
24+
description: "Two-pane horizontal split with PersistenceId set"
25+
component: PersistenceDemo
26+
region: client
27+
format: png
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>WinExe</OutputType>
4+
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
5+
<Platforms>x64;ARM64</Platforms>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<UseWinUI>true</UseWinUI>
9+
<WindowsPackageType>None</WindowsPackageType>
10+
</PropertyGroup>
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.WindowsAppSDK" Version="$(WindowsAppSDKVersion)" />
13+
</ItemGroup>
14+
<ItemGroup>
15+
<ProjectReference Include="..\..\..\..\src\Reactor\Reactor.csproj" />
16+
</ItemGroup>
17+
</Project>
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
---
2+
title: "Docking Windows"
3+
app: docking
4+
order: 21.5
5+
audience: intermediate
6+
goal: |
7+
Introduce DockManager: the minimal two-pane setup, tab groups, side
8+
pins, persistence, and floating tear-outs. Concise — not a full tour
9+
of every DockNode field, every adapter callback, or the four-phase
10+
rollout plan. Solid-tier: code first, then short prose.
11+
tier: solid
12+
---
13+
14+
# Docking Windows
15+
16+
Microsoft.UI.Reactor's docking system lets a single shell host multiple
17+
user-rearrangeable surfaces — the Visual Studio / VS Code / Photoshop /
18+
Figma layout idiom. Users drag tabs between groups, split panes, pin
19+
tool windows to a side, and tear panes out into floating sub-windows;
20+
layouts persist across sessions.
21+
22+
The element is `DockManager`. Its `Layout` is an immutable `DockNode`
23+
tree describing the desired arrangement; the reconciler turns that
24+
tree into native WinUI controls and applies the minimum mutations on
25+
every re-render.
26+
27+
## Minimal Setup
28+
29+
Docking is an opt-in element type — register it at host construction
30+
time, then use `DockManager` like any other Reactor element:
31+
32+
```csharp snippet="docking/register"
33+
```
34+
35+
`DockingNativeInterop.Register` wires the `DockManager`, splitter, and
36+
drop-target elements into the reconciler. Without it, a `DockManager`
37+
in your tree will not be recognized.
38+
39+
A two-pane horizontal split:
40+
41+
```csharp snippet="docking/two-pane"
42+
```
43+
44+
![Two-pane docking layout with a Solution tool on the left and an App.cs editor on the right](screenshot://docking/two-pane)
45+
46+
The leaves of the tree are `DockableContent` records. Each carries a
47+
`Title` (shown on the tab / floating window), an optional `Content`
48+
element subtree, and — importantly — a stable `Key`.
49+
50+
**`Key` is required for any pane whose state should survive
51+
reorderings, tab moves, and tear-outs.** Reactor's keyed reconciler
52+
matches panes by `Key` and preserves the element subtree (and its
53+
`UseState` slots) across tree rebuilds. There is no implicit
54+
`Title`-as-key fallback; always supply one.
55+
56+
The `DockNode` algebra has three node kinds (all immutable records):
57+
58+
| Type | Purpose |
59+
|------|---------|
60+
| `DockSplit(Orientation, Children, …)` | Splits children along one axis, with drag-resize splitters between them |
61+
| `DockTabGroup(Documents, TabPosition, CompactTabs, …)` | Presents children as tabs |
62+
| `DockableContent(Title, Content, Key, CanClose, CanPin, CanFloat, CanMove, …)` | Leaf pane |
63+
64+
`DockManager` itself accepts these props:
65+
66+
| Prop | Purpose |
67+
|------|---------|
68+
| `Layout` | Root of the `DockNode` tree |
69+
| `LeftSide` / `TopSide` / `RightSide` / `BottomSide` | Pinned tool windows along an edge |
70+
| `ActiveDocument` | Resolves by `Key` against `Layout`; mismatched keys leave activation alone |
71+
| `Adapter` | `IDockAdapter` for rehydration and floating chrome |
72+
| `PersistenceId` | Routes layout JSON through `WindowPersistedScope` |
73+
74+
## Tab Groups
75+
76+
`DockTabGroup` holds N `DockableContent` leaves and presents them as
77+
tabs. Users reorder by drag; `SelectedIndex` reports the active tab:
78+
79+
```csharp snippet="docking/tab-group"
80+
```
81+
82+
![Three editor tabs in a single dock tab group](screenshot://docking/tab-group)
83+
84+
`TabPosition.Bottom` combined with `CompactTabs: true` produces
85+
Office's tool-pane shape. `CanClose: true` shows an X on each tab; the
86+
reconciler removes the leaf from the tree when the user clicks it.
87+
88+
## Side Pins (Auto-Hide)
89+
90+
`LeftSide`, `TopSide`, `RightSide`, and `BottomSide` on `DockManager`
91+
carry pinned tool windows. Each collapses to an edge icon; clicking
92+
the icon expands a popup, clicking out collapses it back:
93+
94+
```csharp snippet="docking/side-pin"
95+
```
96+
97+
![Editor on the left with a Properties tool pinned to the right edge](screenshot://docking/side-pin)
98+
99+
Set `CanPin: true` on a `DockableContent` to enable the pin
100+
affordance on its tab — users can pin and unpin at runtime, and the
101+
moved-to-side state round-trips through persistence.
102+
103+
## Persistence
104+
105+
Set `PersistenceId` to enable automatic save/restore. Reactor routes
106+
the layout JSON through `WindowPersistedScope["docking:<id>"]` so the
107+
arrangement survives app restarts:
108+
109+
```csharp snippet="docking/persistence"
110+
```
111+
112+
![Persisted two-pane layout that restores across launches](screenshot://docking/persistence)
113+
114+
The persisted layout takes precedence over the declarative `Layout`
115+
on remount when the IDs match. Re-render with a different
116+
`PersistenceId` to start fresh.
117+
118+
## Floating Tear-Outs
119+
120+
When a user drags a tab title into open space, a floating window
121+
appears at the pointer with a custom title bar supplied by an
122+
`IDockAdapter`:
123+
124+
```csharp snippet="docking/floating-adapter"
125+
```
126+
127+
Pass the adapter on `DockManager.Adapter`. `OnContentCreated` is also
128+
called when a pane is rehydrated from persisted JSON — return the
129+
Reactor subtree to mount inside it, keyed off `content.Key`.
130+
131+
## Tips
132+
133+
**Always set `Key` on panes with stateful content.** A controlled
134+
`TextField` inside a pane without a `Key` will lose its draft text the
135+
moment a user drags the tab — the reconciler can't tell it's the
136+
"same" pane, so it remounts the subtree. Keys can be strings, GUIDs,
137+
enums, or any equatable domain identifier.
138+
139+
**Build the tree from data, not branches.** Mapping a
140+
`List<DocumentVm>` through `.Select(d => new DockableContent(…))` is
141+
the idiomatic way to drive open documents. There is no
142+
`DocumentsSource` binding API; the closure does the work.
143+
144+
**Register once per host.** `DockingNativeInterop.Register` is
145+
idempotent, but the natural place to call it is the `configure:`
146+
callback on `ReactorApp.Run`. Apps that open secondary windows via
147+
`ReactorApp.OpenWindow` should register on each new `ReactorHost`.
148+
149+
**Layouts are immutable records — produce a new tree.** Like any
150+
Reactor element, mutate by re-rendering with a new `DockManager`.
151+
Keyed reconciliation handles the diff; you don't manage the underlying
152+
control yourself.
153+
154+
## Next Steps
155+
156+
- **[Windows](windows.md)** — top-level window lifecycle, the host
157+
surface that docking lives inside.
158+
- **[Persistence](persistence.md)** — `UsePersisted`, scopes, and the
159+
`WindowPersistedScope` that docking layouts route through.
160+
- **[Components](components.md)** — `Key` rules and the reconciler
161+
identity model that `DockableContent.Key` plugs into.
162+
- **[Reactor](readme.md)** — back to the docset index.

docs/_pipeline/templates/readme.md.dt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ first; everyone else can follow the order.
121121

122122
- **[Navigation](navigation.md)** — NavigationView, TabView, multi-page apps, routing
123123
- **[Windows](windows.md)** — Top-level windows, tray icons, OpenWindow, shutdown policy
124+
- **[Docking Windows](docking.md)** — Rearrangeable dock panes: splits, tabs, side pins, floating tear-outs, persistence
124125
- **[Async Resources](async-resources.md)** — `UseResource`, `UseInfiniteResource`, `UseMutation`, `Pending`
125126
- **[Persistence](persistence.md)** — UsePersisted, scopes, migration
126127
- **[Localization](localization.md)** — Multi-language support, resource strings, RTL layouts

0 commit comments

Comments
 (0)