Skip to content

Commit 28630aa

Browse files
feat(045): cross-window tab dock-in (Center) + floating tab chrome (#388)
* feat(045): cross-window tab dock-in (Center) + floating tab chrome Adds support for dragging a tab from any DockHost (main window or floating) into another floating window's tab group, materializing it as an appended tab. Also lands the §4.2 `TitleBar`-zone tab chrome for floating windows (Edge / Files / modern-VS pattern). ## Cross-window dock-in (§4.6 / §2.4) WinUI's `TabView.CanDragTabs` drag pipeline is **window-local**: drag events never reach AllowDrop elements in another XAML island, so the P3 live-overlay path cannot deliver Center-only dock-in across HWNDs. Workaround: drop-completion cursor hit-test. - `src/Reactor/Docking/Native/DockFloatingPaneRouter.cs` (new): global `ReactorWindow -> append-as-tab` registry guarded by a lock. `Register` / `Unregister` / `TryAppendUnderCursor` / `HasRegisteredWindows`. Uses Win32 `GetCursorPos` + `GetWindowRect` + `SetForegroundWindow` (P/Invoke) for the HWND-boundary hit-test. - `DockHostNativeComponent.HandleTabDragCompleted`: now consults the router *before* falling back to tear-out, so a tab dropped over a floating window's chrome appends as a new tab in that window. - `DockFloatingWindowComponent`: self-registers with the router on mount via `UseEffect` deferred through `DispatcherQueue.TryEnqueue` (so the post-Open `windowHolder[0]` assignment lands first), and unregisters on close. Floating-to-floating redirect lives in the component's `onTabDragCompleted` closure. - `DockDropTargetOverlayControl`: reserved `DockDropOverlayMode.CenterOnly` enum value + `ApplyModeVisibility` branch for the future live-overlay UX. - `DockHostModel.OnExternalCrossWindowDrop`: internal hook for adapters. ## Floating tab chrome (§4.2 / §4.3) - `DockFloatingWindow`: `Open()` now mounts `DockFloatingWindowComponent` with `WindowSpec.ExtendsContentIntoTitleBar = true`. `BuildChrome` renders `DockTabGroup` at the root + a transparent `BorderElement` in `TabStripFooter` whose `OnMount` calls `window.SetTitleBar(self)`. Brush construction is deferred to `OnMount` to keep `BuildFloatingRoot` headless-safe for unit tests. - Component owns panes via `UseReducer<IReadOnlyList<DockableContent>>` (functional updater avoids stale-closure bugs); subscribes to `DockDragSession.SessionChanged`. ## Spec / tests - Spec 045: tick §4.2 / §4.3 / §2.4 cross-window dock-in bullets; document the drop-completion hit-test and the WinUI window-local limitation. - `tests/Reactor.Tests/Docking/Native/DockFloatingWindowTests.cs` (new): 5 unit tests pinning the `BuildFloatingRoot` element-tree shape. ## Verification - `dotnet build Reactor.slnx -c Debug -p:Platform=x64`: 0 errors. - Full unit suite (`tests/Reactor.Tests`): 8773 passed, 0 failed, 46 skipped. - Selftests (`Reactor.AppTests.Host --self-test`): 826/827 pass (1 flake in `DynDock_OpenWelcomeButton_Mounted` is a pre-existing render-timing flake — passes in isolation). - E2E (`tests/Reactor.AppTests`): 64/64 pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * review: address Copilot CR feedback on #388 Six inline comments from the automated reviewer: 1. `DockFloatingWindow.cs` (~L80) — Comment near `ExtendsContentIntoTitleBar = true` referenced the old `TitleBarElement` / `Microsoft.UI.Xaml.Controls.TitleBar` path that the PR explicitly moved away from. Rewrote to describe the actual implementation (root TabView + transparent `BorderElement` in `TabStripFooter` wired via `Window.SetTitleBar`) and call out why `Microsoft.UI.Xaml.Controls.TitleBar` is intentionally NOT used here. 2. `DockFloatingWindow.cs` (~L270) — `fe.Loaded += (_, _) => Apply();` never unsubscribed; WinUI `Loaded` re-fires on reparent/reload which would re-call `SetTitleBar` and leak the delegate. Replaced with a one-shot `RoutedEventHandler` that detaches itself before calling `Apply()` (mirrors the pattern in `DockSideStripRenderer.cs:260`). 3. `DockFloatingWindow.cs` (~L470) — The temporary `reAppend` callback used during the unregister/append/re-register gap (the floating→floating drag-completed path) did not update the `AppWindow.Title` on first-tab append, unlike the main router `append` closure built in `UseEffect`. A pane appended during that small gap would leave the title stale until the next render re-registered the canonical `append`. Refactored by hoisting `append` to a local function shared by both registration sites. 4. `DockFloatingWindow.cs` (~L501) — Dead-code `ContainsByRef` helper removed. 5. `DockHostNativeComponent.cs` (~L387) — Cross-window dock-in into an existing floating window was being logged as `DragTearOut` even though it appends as a tab (no new floating window is opened). That makes the operation log / replay analysis misleading. Switched to `DragConfirm` to match the semantics used by the `OnExternalCrossWindowDrop` adapter path. 6. `samples/apps/dock-showcase/App.cs` (~L1144) — Typo in persistence-note string: literal TAB + `"abChrome"` (missing the leading `t`). Fixed to `"tabChrome"` so the showcase UI reads correctly. Verification: - `dotnet build Reactor.slnx -c Debug -p:Platform=x64` — 0 errors. - `dotnet test tests/Reactor.Tests --filter Docking` — 350/350 pass. - `NativeDocking_FloatingWindow_*` selftest fixtures — 4/4 pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * review: address CodeQL feedback on #388 Second-round review from `github-code-quality[bot]` flagged 10 issues across 3 files (with several near-duplicate comments on the same lines): 1. **Floating-point equality (CornerRadius == 0)** — `NativeDockingSmokeFixture.cs:1507-1509`. Production sets `new CornerRadius(0)` which yields exact 0.0, but CodeQL flags any `== 0.0` on a `double`. Switched to `Math.Abs(x) < 1e-9` with a documented epsilon constant. Same intent, no CodeQL noise. 2. **Foreach-with-type-guard → `OfType<T>()`** — `NativeDockingSmokeFixture.cs:1562-1573` (HasHeader helper). Replaced two nested `foreach (var x in seq) if (x is T t)` with `foreach (var t in seq.OfType<T>())` for `TabItems` and `StackPanel.Children`. Implicit usings already give us LINQ. 3. **Same-variable branch → ternary** — `DockDropTargetOverlayControl.cs:289-294`. Collapsed the `if/else` in the `centerOnly` mode (both branches assigning `entry.Button.Visibility`) into one ternary assignment. 4. **Generic catch clauses (3 sites)** — `DockFloatingPaneRouter.cs:87, 91, 101`. Replaced bare `catch { … }` with typed catches for the expected recoverable exceptions when probing a floating window during shutdown: `COMException` / `ObjectDisposedException` / `InvalidOperationException`. Unknown exceptions now surface instead of being silently swallowed. Verification: - `dotnet build Reactor.slnx -c Debug -p:Platform=x64` — 0 errors. - `dotnet test tests/Reactor.Tests --filter Docking` — 350/350 pass. - `NativeDocking_TabChromePresetsApplyAndClear` selftest — 14/14 assertions pass (including the epsilon CornerRadius check). - `NativeDocking_FloatingWindow_*` selftests — 0 failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e72e4d8 commit 28630aa

19 files changed

Lines changed: 1626 additions & 107 deletions

File tree

docs/specs/tasks/045-docking-windows-implementation.md

Lines changed: 89 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -596,9 +596,22 @@ WinUI.Dock wrapper for side-by-side review.
596596
WinUI.Dock string-keyed GUID table — spec §8.9 security).
597597
`DockDragSession` carries object refs to the dragged pane + source
598598
manager; there is no GUID table, no static dict keyed by string,
599-
no serializable payload. Cross-window HWND-boundary drag itself
600-
remains deferred to a follow-up (TabView's native cross-HWND drag
601-
is the WinUI primitive there).
599+
no serializable payload. **Cross-window HWND-boundary dock-in
600+
(Center only) lands via a drop-completion cursor hit-test:**
601+
`DockFloatingPaneRouter` (`src/Reactor/Docking/Native/`) maintains a
602+
global `ReactorWindow → append-as-tab` registry; floating windows
603+
self-register on mount (via dispatcher-deferred `UseEffect`) and
604+
unregister on close. `HandleTabDragCompleted` (and the equivalent
605+
hook on floating windows for floating→floating) Win32-hit-tests
606+
`GetCursorPos` against each registered window's
607+
`GetWindowRect` *before* falling back to tear-out, so a tab dropped
608+
over another window's chrome appends as a new tab in that window's
609+
tab group. The WinUI cross-window drag pipeline itself is
610+
window-local (drag events do not cross XAML islands), so this
611+
drop-completion hit-test is the canonical workaround; live-overlay
612+
N/E/S/W targeting across windows remains deferred (the
613+
`DockDropOverlayMode.CenterOnly` enum value is reserved for the
614+
future overlay UX).
602615
- [x] Keyboard-initiated move: `Ctrl+Shift+M` enters drop-target focus
603616
mode (spec §5.3.3 / §8.7). Landed in §2.10 via the chord-bridge
604617
wiring on `DockHostNativeComponent.EnterKeyboardDropMode`; flips
@@ -2061,44 +2074,73 @@ re-run with new chrome plus eight visual items.
20612074

20622075
### 4.2 `TitleBar` control adoption (spec §7.1.1)
20632076

2064-
- [ ] Floating dockable windows use
2065-
`Microsoft.UI.Xaml.Controls.TitleBar` as root chrome.
2066-
- [ ] `ReactorWindow.NativeWindow.ExtendsContentIntoTitleBar = true`.
2067-
- [ ] Root content gets `TitleBar` slot at top; docking tab strip lives
2068-
inside its content area.
2069-
- [ ] Reactor's existing `TitleBar(...)` element (`src/Reactor/Elements/
2070-
Dsl.cs:610`, `TitleBarElement`) wires through `DockHost` for the
2071-
floating-window path.
2077+
- [x] Floating dockable windows extend their content into the title-bar
2078+
zone so the tab strip becomes the visible chrome
2079+
(`WindowSpec.ExtendsContentIntoTitleBar = true`). *(The WinUI 3
2080+
`Microsoft.UI.Xaml.Controls.TitleBar` control is intentionally NOT
2081+
used here — its `Content` slot is a small in-row chrome slot
2082+
designed for things like Edge's address bar and cannot host a full
2083+
TabView with bodies. The Edge / Files pattern uses
2084+
`Window.SetTitleBar(...)` directly on a strip-footer drag region
2085+
instead. Reactor's `TitleBarElement` remains available for app
2086+
shells that want the control's distinct row layout.)*
2087+
- [x] `ReactorWindow.NativeWindow.ExtendsContentIntoTitleBar = true`.
2088+
- [x] Root content puts the docking tab strip in the title-bar zone
2089+
(Edge / Files / VS Code "tabs in title bar" layout).
2090+
- [x] `Window.SetTitleBar(dragRegion)` is wired from a `TabStripFooter`
2091+
element so the OS reserves caption-button inset and treats the
2092+
footer area as window drag-move surface.
2093+
2094+
*(Landed: `DockFloatingWindow.BuildFloatingRoot`
2095+
(`src/Reactor/Docking/Native/DockFloatingWindow.cs`) renders the
2096+
`DockTabGroup` as the floating window's root and adds a transparent
2097+
`BorderElement` to `TabStripFooter` whose `OnMount` calls
2098+
`floatingWindow.NativeWindow.SetTitleBar(self)`. `Open()` sets
2099+
`WindowSpec.ExtendsContentIntoTitleBar = true`. Unit tests in
2100+
`tests/Reactor.Tests/Docking/Native/DockFloatingWindowTests.cs`;
2101+
visual coverage in selftest fixture
2102+
`NativeDocking_FloatingWindow_TitleBarChromeAndTabsInTitleBar`.)*
20722103

20732104
### 4.3 Tabs in the title bar (spec §7.1.2) — headline feature
20742105

2075-
- [ ] **Single-group float:** tab strip renders inside `TitleBar`
2076-
content slot, flush with caption buttons, active tab styles as
2077-
window's identity (Edge / Files / modern-VS pattern).
2106+
- [x] **Single-group float:** tab strip occupies the title-bar zone at
2107+
y=0, flush with OS caption buttons, active tab styles as the
2108+
window's identity (Edge / Files / modern-VS pattern). *(Implemented
2109+
via `ExtendsContentIntoTitleBar=true` + TabView at root + drag
2110+
region in `TabStripFooter` registered via `Window.SetTitleBar`.)*
20782111
- [ ] **Multi-group float:** revert to standard floating title (via
20792112
`IDockAdapter.GetFloatingWindowTitleBar`); tabs render in normal pane
2080-
position. The same `GetFloatingWindowTitleBar` surface continues to
2081-
exist — it supplies the non-tab portion (text/branding) in both
2082-
modes.
2113+
position. *(Deferred: floating windows are single-pane today; the
2114+
multi-group float infrastructure itself is a separate slice.)*
20832115
- [ ] Drag semantics:
2084-
- [ ] Drag a **tab** → tear it out (most-tested case; tab now lives
2085-
inside OS-managed title-bar hit-test surface).
2086-
- [ ] Drag **title-bar background** (non-tab, non-caption) → move
2087-
floating window. OS-handled.
2116+
- [x] Drag a **tab** → tear it out. *(Existing `onTabDragStarting`
2117+
path continues to work in the new layout.)*
2118+
- [x] Drag **title-bar background** (non-tab, non-caption) → move
2119+
floating window. *(OS-handled via the `TabStripFooter` drag region
2120+
registered with `Window.SetTitleBar``MinWidth(180)` ensures a
2121+
grabbable region even with many tabs.)*
20882122
- [ ] Drag **active tab when it is the only tab** → moves whole
2089-
window (single-tab floats behave like normal windows).
2123+
window. *(Deferred: `onTabDragStarting` currently fires tear-out
2124+
unconditionally.)*
20902125
- [ ] `AppWindow.TitleBar.SetDragRectangles` integration: hit-test
20912126
regions computed per frame from tab-strip measured geometry; pushed
20922127
to OS so it knows interactive vs drag-region sub-rects.
2093-
- [ ] **Debounce `SetDragRectangles` to layout-measure-change events**
2094-
(not every-frame). Perf budget per spec §7.3 + §8.5.
2128+
*(N/A — we hand the OS a single `TabStripFooter` element via
2129+
`Window.SetTitleBar`, which is the modern WinUI 3 replacement for
2130+
explicit `SetDragRectangles` plumbing.)*
2131+
- [ ] **Debounce `SetDragRectangles` to layout-measure-change events.**
2132+
*(N/A — see above.)*
20952133

20962134
### 4.4 Caption-button area awareness (spec §7.1.5)
20972135

2098-
- [ ] Tab strip reserves `AppWindow.TitleBar.RightInset` as
2099-
right-padding (LeftInset in RTL).
2100-
- [ ] Subscribe to `AppWindowTitleBar.LayoutMetricsChanged`; re-measure
2101-
on next layout pass (handles theme switch flipping RTL mid-flight).
2136+
- [x] Tab strip reserves caption-button inset automatically — the
2137+
`Window.SetTitleBar(footer)` registration tells the OS where
2138+
caption buttons should sit (top-right) and the buttons render over
2139+
the right edge of the footer drag region.
2140+
- [x] OS handles RTL / theme-switch caption-button placement via the
2141+
same `SetTitleBar` registration; no manual
2142+
`LayoutMetricsChanged` subscription required for the current
2143+
Window-level inset behavior.
21022144

21032145
### 4.5 Snap Layouts (spec §7.1.3)
21042146

@@ -2113,8 +2155,16 @@ re-run with new chrome plus eight visual items.
21132155
- [ ] Floating dockable windows inherit `WindowSpec.Backdrop`
21142156
(spec 036 §4.1, §5).
21152157
- [ ] Splitter gutters semi-transparent under Mica / Acrylic.
2116-
- [ ] Tab-strip background uses `TitleBarBackgroundFillBrush`, not a
2117-
hard color.
2158+
- [x] Tab-strip background uses `TitleBarBackgroundFillBrush`, not a
2159+
hard color. *(Phase 4 slice landed: `TabChrome` enum on `DockTabGroup`
2160+
with `Win11` / `Flat` / `TitleBar` presets, scoped to each TabView's
2161+
`Resources` via `DockTabGroupRenderer.BuildSetters`. `TitleBar` preset
2162+
resolves `TitleBarBackgroundFillBrush` from `Application.Current.Resources`
2163+
and writes to `TabViewBackground`; pool-safe via a "blanker" setter
2164+
that strips all managed keys before applying a new preset. JSON
2165+
persistence: optional `tabChrome` field, omitted on `Win11` for
2166+
legacy-file back-compat. Dock-showcase **Scene I — Tab Styles** is
2167+
the visual gate input for §4.11 Item 23.)*
21182168

21192169
### 4.7 Dark mode / accent color (spec §7.1.6)
21202170

@@ -2124,9 +2174,15 @@ re-run with new chrome plus eight visual items.
21242174

21252175
### 4.8 Floating-window persona (spec §7.1.7)
21262176

2127-
- [ ] Single-tab float: tab `Title` and `Icon``AppWindow.Title` and
2128-
`AppWindow.SetIcon`. Alt-tab shows the tab title.
2129-
- [ ] Multi-tab float: active tab's title reflected.
2177+
- [x] Single-tab float: tab `Title``AppWindow.Title`. Alt-tab shows
2178+
the tab title. *(Title landed: `DockFloatingWindow.Open` passes
2179+
`pane.Title` as `WindowSpec.Title`. Icon deferred: `DockableContent`
2180+
has no `Icon` field yet — adding one is a separate slice.
2181+
Note: in the Edge tabs-in-titlebar layout the tab itself displays
2182+
the title text; there is no separate WinUI 3 `TitleBar` widget to
2183+
also bind to. `AppWindow.Title` still controls taskbar / alt-tab.)*
2184+
- [ ] Multi-tab float: active tab's title reflected. *(Deferred with
2185+
multi-tab float — see §4.3 above.)*
21302186
- [ ] Composition with spec 036 §8 persistence: persisted Window
21312187
identity is the dockable-window key, not the transient tab content.
21322188

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ InfoBadge() → InfoBadgeElement
8989
InfoBadge(int value) → InfoBadgeElement
9090
InfoBar(string title = null, string message = null) → InfoBarElement
9191
InterspersedGrid(Orientation orientation, Element[] items, double[] proportions, double separatorSize, Func<int, Element> separatorFactory) → GridElement
92+
ItemContainer(Element child) → ItemContainerElement
9293
ItemsView<T>(IReadOnlyList<T> items, Func<T, string> keySelector, Func<T, int, Element> viewBuilder) → ItemsViewElement<T>
9394
LazyHStack<T>(IReadOnlyList<T> items, Func<T, int, Element> viewBuilder) → LazyHStackElement<T>
9495
LazyHStack<T>(IReadOnlyList<T> items, Func<T, string> keySelector, Func<T, int, Element> viewBuilder) → LazyHStackElement<T>
@@ -346,6 +347,7 @@ InfoBarElement.Set(Action<InfoBar> configure) → InfoBarElement
346347
InfoBarElement.Severity(InfoBarSeverity severity) → InfoBarElement
347348
InfoBarElement.Success() → InfoBarElement
348349
InfoBarElement.Warning() → InfoBarElement
350+
ItemContainerElement.Set(Action<ItemContainer> configure) → ItemContainerElement
349351
ItemsViewElement<T>.ItemInvoked<T>(Action<T> handler) → ItemsViewElement<T>
350352
ItemsViewElement<T>.SelectionChanged<T>(Action<IReadOnlyList<T>> handler) → ItemsViewElement<T>
351353
ItemsViewElement<T>.Set<T>(Action<ItemsView> configure) → ItemsViewElement<T>
@@ -918,6 +920,7 @@ DockOperationKind { Mount, DragStart, DragHover, DragConfirm, DragCancel, DragTe
918920
DockPaneState { Docked, Floating, AutoHidden, AutoHiddenExpanded, Hidden }
919921
DockSide { Left, Top, Right, Bottom }
920922
DockTarget { Center, SplitLeft, SplitTop, SplitRight, SplitBottom, DockLeft, DockTop, DockRight, DockBottom }
923+
TabChrome { Win11, Flat, TitleBar }
921924
TabPosition { Top, Bottom }
922925
DragOperations { None, Copy, Move, Link, All }
923926
GesturePhase { Began, Changed, Ended, Cancelled }

samples/apps/dock-showcase/App.cs

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ public override Element Render()
6363
SceneButton("persist", "Scene E — Persistence", scene, setScene),
6464
SceneButton("programmatic", "Scene F — Programmatic Dock", scene, setScene),
6565
SceneButton("sliders", "Scene G — Slider Resize", scene, setScene),
66-
SceneButton("droptargets", "Scene H — Drop Targets", scene, setScene)
66+
SceneButton("droptargets", "Scene H — Drop Targets", scene, setScene),
67+
SceneButton("tabstyles", "Scene I — Tab Styles", scene, setScene)
6768
).Width(240).Padding(8);
6869

6970
Element body = scene switch
@@ -76,6 +77,7 @@ public override Element Render()
7677
"programmatic" => Component<SceneFProgrammatic>(),
7778
"sliders" => Component<SceneGSliders>(),
7879
"droptargets" => Component<SceneHDropTargets>(),
80+
"tabstyles" => Component<SceneITabStyles>(),
7981
_ => TextBlock("Unknown scene"),
8082
};
8183

@@ -1040,3 +1042,125 @@ Element MakeSlider(string label, double value, Action<double> setter) =>
10401042
).Padding(16);
10411043
}
10421044
}
1045+
1046+
1047+
// ════════════════════════════════════════════════════════════════════════
1048+
// Scene I — Tab Styles (spec 045 §4.6)
1049+
// ════════════════════════════════════════════════════════════════════════
1050+
//
1051+
// Three tab-chrome presets side-by-side, plus the spec-documented
1052+
// fallback for TabPosition.Bottom (renders as Top in WinUI 3 — see
1053+
// microsoft-ui-xaml#7395; the upstream workaround requires subclassing
1054+
// TabViewItem to counter-scale the template parts).
1055+
1056+
class SceneITabStyles : Component
1057+
{
1058+
public override Element Render() => Grid(
1059+
new[] { GridSize.Star(1), GridSize.Star(1) },
1060+
new[] { GridSize.Auto, GridSize.Auto, GridSize.Star(1), GridSize.Star(1), GridSize.Auto, GridSize.Auto },
1061+
1062+
TextBlock("Scene I — Tab Styles").FontSize(20).SemiBold()
1063+
.Grid(row: 0, column: 0, columnSpan: 2),
1064+
1065+
TextBlock(
1066+
"Three preset chromes for DockTabGroup.TabChrome. Same underlying " +
1067+
"WinUI TabView (full accessibility/keyboard parity) — only theme " +
1068+
"resources differ. Pick whichever matches your app's identity."
1069+
).Opacity(0.8).Margin(0, 0, 0, 12)
1070+
.Grid(row: 1, column: 0, columnSpan: 2),
1071+
1072+
// Top-left — Win11 default
1073+
StyleCard(
1074+
"Win11 (default)",
1075+
"Rounded corners, theme background. The shipping Win11 TabView look.",
1076+
new DockTabGroup(
1077+
Documents: new[]
1078+
{
1079+
new DockableContent("Welcome.md", ChromeBody("# Welcome", "Win11"), Key: "i:w11:welcome"),
1080+
new DockableContent("App.cs", ChromeBody("class App {…}", "Win11"), Key: "i:w11:appcs"),
1081+
new DockableContent("README", ChromeBody("Project readme.", "Win11"), Key: "i:w11:readme"),
1082+
},
1083+
TabChrome: TabChrome.Win11))
1084+
.Grid(row: 2, column: 0),
1085+
1086+
// Top-right — Flat (VS Code-style)
1087+
StyleCard(
1088+
"Flat (VS Code style)",
1089+
"Zero corner radius, tighter header padding. Modeled on the modern IDE document strip.",
1090+
new DockTabGroup(
1091+
Documents: new[]
1092+
{
1093+
new DockableContent("Welcome.md", ChromeBody("# Welcome", "Flat"), Key: "i:flat:welcome"),
1094+
new DockableContent("App.cs", ChromeBody("class App {…}", "Flat"), Key: "i:flat:appcs"),
1095+
new DockableContent("README", ChromeBody("Project readme.", "Flat"), Key: "i:flat:readme"),
1096+
},
1097+
TabChrome: TabChrome.Flat))
1098+
.Grid(row: 2, column: 1),
1099+
1100+
// Bottom-left — TitleBar (mica-ish; tab strip uses the title-bar brush)
1101+
StyleCard(
1102+
"TitleBar (chromeless feel)",
1103+
"Tab strip uses the system TitleBarBackgroundFillBrush so the strip " +
1104+
"blends with the window chrome (effective when ExtendsContentIntoTitleBar is on).",
1105+
new DockTabGroup(
1106+
Documents: new[]
1107+
{
1108+
new DockableContent("Welcome.md", ChromeBody("# Welcome", "TitleBar"), Key: "i:tb:welcome"),
1109+
new DockableContent("App.cs", ChromeBody("class App {…}", "TitleBar"), Key: "i:tb:appcs"),
1110+
new DockableContent("README", ChromeBody("Project readme.", "TitleBar"), Key: "i:tb:readme"),
1111+
},
1112+
TabChrome: TabChrome.TitleBar))
1113+
.Grid(row: 3, column: 0),
1114+
1115+
// Bottom-right — Flat + CompactTabs (full classic VS feel)
1116+
StyleCard(
1117+
"Flat + CompactTabs",
1118+
"Same flat chrome paired with tightly-packed tabs — the closest match " +
1119+
"to the dense tool-window strip in classic Visual Studio.",
1120+
new DockTabGroup(
1121+
Documents: new[]
1122+
{
1123+
new DockableContent("Errors", ChromeBody("0 errors.", "Flat compact"), Key: "i:flatc:err"),
1124+
new DockableContent("Warnings", ChromeBody("12 warnings.", "Flat compact"), Key: "i:flatc:warn"),
1125+
new DockableContent("Output", ChromeBody("Build complete.", "Flat compact"), Key: "i:flatc:out"),
1126+
new DockableContent("Terminal", ChromeBody("PS> _", "Flat compact"), Key: "i:flatc:term"),
1127+
},
1128+
TabChrome: TabChrome.Flat,
1129+
CompactTabs: true))
1130+
.Grid(row: 3, column: 1),
1131+
1132+
// Position note
1133+
TextBlock(
1134+
"Note — TabPosition.Bottom currently renders as Top under WinUI 3. " +
1135+
"Microsoft.UI.Xaml.Controls.TabView has no TabStripPlacement property " +
1136+
"(microsoft-ui-xaml#7395). Spec 045 §4.6 / DockTabGroupRenderer line 196 " +
1137+
"documents the deferral; a real bottom strip lands when a custom " +
1138+
"TabViewItem subclass replaces the shared WinUI template."
1139+
).Opacity(0.65).FontSize(11).Margin(0, 12, 0, 4)
1140+
.Grid(row: 4, column: 0, columnSpan: 2),
1141+
1142+
// Persistence note
1143+
TextBlock(
1144+
"Persistence — TabChrome serializes via the DockLayoutSerializer JSON; " +
1145+
"legacy layouts without the tabChrome field default to Win11."
1146+
).Opacity(0.55).FontSize(11)
1147+
.Grid(row: 5, column: 0, columnSpan: 2)
1148+
).Padding(16);
1149+
1150+
// ── helpers ──────────────────────────────────────────────────────────
1151+
1152+
static Element StyleCard(string heading, string body, DockTabGroup group)
1153+
=> VStack(6,
1154+
TextBlock(heading).SemiBold(),
1155+
TextBlock(body).Opacity(0.7).FontSize(11),
1156+
new DockManager { Layout = group }
1157+
.Height(180)
1158+
.Margin(0, 4, 0, 0)
1159+
).Margin(0, 0, 8, 16);
1160+
1161+
static Element ChromeBody(string title, string chromeLabel)
1162+
=> VStack(4,
1163+
TextBlock(title).SemiBold(),
1164+
TextBlock($"(rendered under TabChrome.{chromeLabel})").Opacity(0.55).FontSize(11)
1165+
).Padding(12);
1166+
}

0 commit comments

Comments
 (0)