diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dc523fe3..c3e375f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,12 @@ Conventions for contributors: ### Added +- **TitleBar drag regions — `.AutoRefreshDragRegions()` and `.IsDragRegion()` + (spec 059).** Windows App SDK bumped 2.0.1 → 2.1.3; custom `TitleBar` content + now auto-excludes interactive controls from the window drag region by default. + Override per element with `.IsDragRegion(false)` (force clickable) / + `.IsDragRegion(true)` (force draggable), and set `.AutoRefreshDragRegions()` to + re-derive regions when content changes across renders. - **`DockFloatingWindowClosedEventArgs.Reason` — close-reason discriminator for floating-window closes (spec 045 §5.3.5, issue #417).** A new `required DockFloatingCloseReason Reason { get; init; }` on diff --git a/Directory.Build.props b/Directory.Build.props index 749bc182c..2c1ebf317 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -17,7 +17,7 @@ before any bootstrapper has run. --> - 2.0.1 + 2.1.3 1.4.0 false diff --git a/SKILL.md b/SKILL.md index e876bd92b..2e181c34b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -208,7 +208,7 @@ Workflow modes (Phase-2 ranker): - + ``` diff --git a/bootstrap.ps1 b/bootstrap.ps1 index fc131b4af..5a85195be 100644 --- a/bootstrap.ps1 +++ b/bootstrap.ps1 @@ -220,7 +220,7 @@ function Get-VsExtensionSkipReason { # output stays a standalone deployable. # # Net effect: the user needs the WindowsAppRuntime 2.0 install matching -# our WindowsAppSDKVersion=2.0.1 to run most things in this repo. +# our WindowsAppSDKVersion=2.1.3 to run most things in this repo. # # So we prompt by default. `-InstallWinAppSdk` to force-install, # `-InstallWinAppSdk:$false` to skip the prompt non-interactively. diff --git a/docs/_pipeline/templates/index.md.dt b/docs/_pipeline/templates/index.md.dt index 740e390f2..6cf1cf1ed 100644 --- a/docs/_pipeline/templates/index.md.dt +++ b/docs/_pipeline/templates/index.md.dt @@ -198,7 +198,7 @@ Create a console project, then edit the `.csproj`: None - + diff --git a/docs/_pipeline/templates/windows.md.dt b/docs/_pipeline/templates/windows.md.dt index dc1f30138..00152e461 100644 --- a/docs/_pipeline/templates/windows.md.dt +++ b/docs/_pipeline/templates/windows.md.dt @@ -216,6 +216,21 @@ VStack( TextBlock("Body")); ``` +`TitleBar(...)` accepts custom `Content` (and a trailing `RightHeader`). Interactive +controls inside the content are excluded from the window drag region automatically +(WinApp SDK ≥ 2.1.3). Override per element with `.IsDragRegion(false)` to force a +visual clickable or `.IsDragRegion(true)` to force it draggable, and set +`.AutoRefreshDragRegions()` on the title bar when the content changes across renders: + +```csharp +(TitleBar("Gallery") with +{ + Content = HStack(8, + AutoSuggestBox("", _ => {}).Width(200), + Button("\uE713", OnSettings).IsDragRegion(false)), +}).AutoRefreshDragRegions(); +``` + Caveats: - Setting `ExtendsContentIntoTitleBar = false` while still rendering a `TitleBar(...)` diff --git a/docs/guide/README.md b/docs/guide/README.md index 322751233..17442e141 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -224,7 +224,7 @@ Create a console project, then edit the `.csproj`: None - + diff --git a/docs/guide/index.md b/docs/guide/index.md index 322751233..17442e141 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -224,7 +224,7 @@ Create a console project, then edit the `.csproj`: None - + diff --git a/docs/guide/windows.md b/docs/guide/windows.md index b9bda96d9..ba6b834fa 100644 --- a/docs/guide/windows.md +++ b/docs/guide/windows.md @@ -208,6 +208,21 @@ VStack( TextBlock("Body")); ``` +`TitleBar(...)` accepts custom `Content` (and a trailing `RightHeader`). Interactive +controls inside the content are excluded from the window drag region automatically +(WinApp SDK ≥ 2.1.3). Override per element with `.IsDragRegion(false)` to force a +visual clickable or `.IsDragRegion(true)` to force it draggable, and set +`.AutoRefreshDragRegions()` on the title bar when the content changes across renders: + +```csharp +(TitleBar("Gallery") with +{ + Content = HStack(8, + AutoSuggestBox("", _ => {}).Width(200), + Button("\uE713", OnSettings).IsDragRegion(false)), +}).AutoRefreshDragRegions(); +``` + Caveats: - Setting `ExtendsContentIntoTitleBar = false` while still rendering a `TitleBar(...)` diff --git a/docs/specs/059-titlebar-drag-regions.md b/docs/specs/059-titlebar-drag-regions.md new file mode 100644 index 000000000..8baec2cb9 --- /dev/null +++ b/docs/specs/059-titlebar-drag-regions.md @@ -0,0 +1,199 @@ +# TitleBar Drag Regions — Windows App SDK 2.1 Uptake + +## Status + +**Implemented — 2026-06-26.** Builds on [spec 036 — Window Model](036-window-design.md) +and [spec 054 — Windowing Evolution](054-windowing-evolution.md) (modern title bar), and consumes +the auto-mapping path from [spec 058 — Control Wrapper Generator](058-control-wrapper-generator.md). + +The triggering question was open-ended: \*"survey the Windows App SDK 2.x release notes and find new +APIs Reactor could leverage."\* This spec captures that survey, adopts the one high-value uptake it +surfaced (the `Microsoft.UI.Xaml.Controls.TitleBar` drag-region APIs added in 2.1.3), and records the +rest of the survey as considered-but-deferred so the decision trail is durable. + +**Status of work:** the 2.0.1 → 2.1.3 bump, `AutoRefreshDragRegions`, and the `.IsDragRegion()` modifier +are implemented and verified — the full `Reactor.Tests` suite is green (9711 passed / 0 failed), plus a +real-WinUI selftest (`TitleBar_DragRegions`) confirming `AutoRefreshDragRegions` round-trips onto the +control and `.IsDragRegion(false)` writes the attached property on a child. The §6 decisions were +resolved per the recommendations. + +--- + +## Table of contents + +- [§1 Motivation — the 2.x survey](#1-motivation--the-2x-survey) +- [§2 Goals / non-goals](#2-goals--non-goals) +- [§3 The SDK bump — 2.0.1 → 2.1.3](#3-the-sdk-bump--201--213) +- [§4 API surface](#4-api-surface) +- [§5 Considered but deferred](#5-considered-but-deferred) +- [§6 Decisions (resolved)](#6-decisions-resolved-2026-06-26) +- [§7 Implementation phases](#7-implementation-phases) +- [§8 Testing](#8-testing) + +--- + +## §1 Motivation — the 2.x survey + +Reactor pins **Windows App SDK 2.0.1** (`Directory.Build.props`, centralized via CPM; Win2D 1.4.0). +The 2.x line has since shipped 2.1.3 (2026-05-21) and 2.2.0 (2026-06-09). Filtering the release notes +to *XAML / UI* surface (the AI / ML / Video / Storage additions are out of scope for a UI framework), +the new APIs vs. 2.0.1 are: + +| API (version) | Reactor relevance | Verdict | +| --- | --- | --- | +| **`TitleBar` drag regions** — `SetIsDragRegion` / `GetIsDragRegion`, `AutoRefreshDragRegions`, `RecomputeDragRegions()` (2.1.3) | `TitleBarElement` maps **directly** to `Microsoft.UI.Xaml.Controls.TitleBar` and exposes a user `Content` slot. The gallery sample already places an `AutoSuggestBox` + `Button` there — the exact mixed interactive / non-interactive case these APIs fix. | **Adopt** (this spec) | +| `XamlBindingHelper.SetPropertyFromThickness` / `…CornerRadius` / `…Color` (2.2.0) | Boxing-free value-type DP sets. Reactor sets value props through source-generated strongly-typed CLR setters; no boxing-pool infra exists today. | Defer (§5) | +| `Setter.ValueProperty` (2.2.0) | Exposes the `Setter.Value` DP; relevant only to programmatic `Style`/`Setter` construction. | Defer (§5) | +| `ApplicationData.GetForUnpackaged()` (2.2.0) | First-class per-user app data for unpackaged apps — could simplify dock-layout / window-placement persistence. | Defer (§5) | + +The **only** addition that lands squarely on an existing Reactor surface is the title-bar drag-region +family. It is also self-justifying: 2.1.3 changed the *default* drag-region behavior so the framework +now recursively walks `TitleBar.Content` and excludes only **interactive** controls from the drag +region (RuntimeCompatibilityChange `TitleBar_ContentDragRegions`). Today, Reactor hands the whole +`TitleBar` to `Window.SetTitleBar`, so custom `Content` controls fight the drag region or need manual +hole-punching. Just bumping the SDK inherits the better default; surfacing the two override APIs +completes the story declaratively. + +## §2 Goals / non-goals + +**Goals** + +1. Adopt Windows App SDK **2.1.3** repo-wide, with the runtime / bootstrap / CI implications documented. +2. Surface `TitleBar.AutoRefreshDragRegions` as a declarative `TitleBarElement` property + fluent modifier. +3. Surface the `TitleBar.IsDragRegion` attached property as a cross-cutting `.IsDragRegion()` modifier + usable on any element placed in a title bar's content. +4. Keep the API DIP-only / declarative (spec 036 / 054 conventions) — no imperative escape hatch unless + a declarative form can't express the scenario. + +**Non-goals** + +- Adopting 2.2.0 or any of its APIs (§5). +- Reworking the `Window.SetTitleBar` registration path (`RegisterWindowTitleBar` in `Element.cs`) — the + new drag-region behavior layers on top of it unchanged. +- A boxing-elimination pass over the reconciler's value-prop setters (§5). + +## §3 The SDK bump — 2.0.1 → 2.1.3 + +**Decision:** bump `WindowsAppSDKVersion` to **2.1.3** — the *minimum* version that exposes the +title-bar drag-region APIs. Rationale for "minimum, not latest": + +- 2.1.3 unlocks every API this spec adopts; 2.2.0 adds only the deferred surface (§5), so going further + widens the behavioral-change blast radius for no in-scope benefit. +- 2.1.3 is **already the pinned runtime** in `tests/stress_perf/ci/Run-PerfBenchmark.ps1` + (`Microsoft.WindowsAppSDK.Runtime` `2.1.3`), so the perf leg and the framework converge on one version. + +**Runtime / packaging implications** (`WindowsAppSDKSelfContained=false` by default — the framework and +its samples consume the *installed* runtime): + +| Concern | Finding | +| --- | --- | +| Side-by-side runtime family | All 2.x releases share **one** SbS framework keyed on the API-contract major (`Microsoft.WindowsAppRuntime.2.msix`), per the comment in `Run-PerfBenchmark.ps1`. So 2.0.1 → 2.1.3 is an **in-place servicing** bump, not a new SbS runtime. | +| `bootstrap.ps1` | Installs `Microsoft.WindowsAppRuntime.2.0` (the major-2 winget id), which services forward to ≥ 2.1.3. The "matching `WindowsAppSDKVersion=2.0.1`" comment should be refreshed to 2.1.3; the winget id itself does not change. | +| In-process test hosts | `Reactor.Tests`, `Reactor.AppTests.Host`, etc. set `WindowsAppSDKSelfContained=true`, so they **bundle** the matching 2.1.3 runtime and don't depend on a machine install — unit tests + selftests validate against the bumped version directly. | +| Win2D | 1.4.0 is already paired with 2.x in-repo; staying within major 2 keeps that pairing valid. | +| **Standalone scaffolding pins** | The `dotnet new` template (`tools/Templates/.../Company.ReactorApp1.csproj`), the CLI's local-scaffold csproj string (`Reactor.Cli/Program.cs`), and the `Microsoft.WindowsAppSDK` install snippets in `docs/guide/index.md`, `SKILL.md`, and `samples/WinFormsInterop/README.md` each **directly** pin the SDK and do **not** inherit the central `WindowsAppSDKVersion`. A stale `2.0.1` / `2.0.*` pin is an **NU1605 downgrade** against the framework's new `>= 2.1.3` dependency — the `Integration Tests` (template smoke) and `bootstrap.ps1` CI jobs scaffold + restore and fail on it. These must be bumped in lockstep (done: `2.1.3` / `2.1.*`). Future SDK bumps should grep `Microsoft.WindowsAppSDK" Version=` and update every literal pin. | +| Inherited default-behavior change | `TitleBar_ContentDragRegions` (auto-exclude interactive controls from the drag region) applies to every `TitleBarElement.Content` for free. This is the headline win even before the override APIs. | + +**Validated:** with `WindowsAppSDKVersion=2.1.3`, `dotnet restore` + `dotnet build src/Reactor/Reactor.csproj` +both succeed clean. The descriptor generator regenerates without new diagnostics. + +## §4 API surface + +### §4.1 `AutoRefreshDragRegions` (per-title-bar, declarative) + +Add a non-nullable `bool AutoRefreshDragRegions` to the `TitleBarElement` record. Because the title-bar +wrapper runs in **descriptor-only auto-discovery** mode (spec 058 §15), the generator discovers the new +`TitleBar.AutoRefreshDragRegions` dependency property and emits the \`.OneWay(e => e.AutoRefreshDragRegions, +(c, v) => c.AutoRefreshDragRegions = v)\` mapping automatically; a non-nullable value-typed record prop is +promoted to an unconditional write, so no `Customize` hand-wiring is needed. Default `false` matches the +WinUI control default. + +````csharp +// fluent modifier (ElementExtensions) +public static TitleBarElement AutoRefreshDragRegions(this TitleBarElement el, bool value = true) => + el with { AutoRefreshDragRegions = value }; +``` + +When `true`, the control re-derives drag regions on every layout pass — the right fit for a reactive +framework whose `Content` changes across renders. + +### §4.2 `.IsDragRegion(bool?)` (cross-cutting child modifier) + +`TitleBar.IsDragRegion` is a **static attached property** (`bool?` — `true` = draggable, `false` = +clickable, unset = framework-decided). It can sit on any descendant of the title-bar content, so it is +modeled as a cross-cutting modifier on `ElementModifiers`, exactly like `AutomationName` / `IsTabStop`: + +1. `ElementModifiers` gains `public bool? IsDragRegion { get; init; }`. +2. A generic modifier: + ```csharp + public static T IsDragRegion(this T el, bool isDragRegion = true) where T : Element => + Modify(el, new ElementModifiers { IsDragRegion = isDragRegion }); + ``` +3. `Reconciler.ApplyModifiers` applies it on the realized `FrameworkElement`, mirroring the + `AutomationName` set/clear arms: + ```csharp + if (m.IsDragRegion.HasValue && m.IsDragRegion != oldM?.IsDragRegion) + WinUI.TitleBar.SetIsDragRegion(fe, m.IsDragRegion.Value); + else if (!m.IsDragRegion.HasValue && oldM?.IsDragRegion.HasValue == true) + fe.ClearValue(WinUI.TitleBar.IsDragRegionProperty); + ``` + +Setting the attached DP on an element that is *not* inside a `TitleBar` is inert (an unread attached +value), so the modifier is safe on any element and needs no title-bar-subtree gating. The unset state +intentionally falls through to the new 2.1.3 default (auto-exclusion), so authors only reach for +`.IsDragRegion(false)` to make a non-interactive visual clickable, or `.IsDragRegion(true)` to force a +gap draggable. + +```csharp +(TitleBar("Gallery") with +{ + Content = HStack(8, + AutoSuggestBox("", _ => {}).Width(200), // interactive → excluded by default + Button("\uE713", () => {}).IsDragRegion(false)) // explicit: keep clickable +}).AutoRefreshDragRegions(); +``` + +### §4.3 `RecomputeDragRegions()` — intentionally not surfaced + +The imperative `RecomputeDragRegions()` method is **not** wrapped. `AutoRefreshDragRegions` provides the +same effect declaratively and continuously, which is the idiomatic Reactor fit; exposing an imperative +"recompute now" method would invite call sites that fight the render loop. Revisit only if a concrete +scenario needs a one-shot recompute that `AutoRefreshDragRegions` can't cover (§6, D3). + +## §5 Considered but deferred + +| Candidate | Why deferred | +|---|---| +| `XamlBindingHelper.SetPropertyFrom{Thickness,CornerRadius,Color}` (2.2.0) | Boxing-free value-type DP sets. Reactor writes value props via source-generated strongly-typed CLR setters (`c.Margin = …`), which box internally; capturing this win means threading explicit `DependencyProperty` references through the generator, fighting its strongly-typed model for a marginal GC improvement. No boxing-pool infra exists today to justify the inconsistency. Revisit only with reconcile-allocation evidence (the `perf-compare` harness). Requires 2.2.0. | +| `Setter.ValueProperty` (2.2.0) | Useful only for boxing-free programmatic `Style`/`Setter` construction; tangential to Reactor's theming-token path. Requires 2.2.0. | +| `ApplicationData.GetForUnpackaged()` (2.2.0) | Could replace bespoke file-path persistence for dock-layout / window-placement in unpackaged apps with a first-class `ApplicationData`. Worth a focused look, but orthogonal to this spec and gated on a 2.2.0 bump. | + +## §6 Decisions (resolved 2026-06-26) + +| # | Question | Resolution | +|---|---|---| +| D1 | Target version — **2.1.3** (minimal) vs. **2.2.0** (latest stable, picks up §5 candidates) | **2.1.3.** Minimal blast radius; aligns with the pinned perf runtime. | +| D2 | `AutoRefreshDragRegions` default | **`false`** (match WinUI). Opt-in via `.AutoRefreshDragRegions()`. Avoids a per-layout recompute cost no author asked for. | +| D3 | Expose `RecomputeDragRegions()`? | **No** (§4.3). | +| D4 | Spec home — standalone **059** vs. a new section in **054** (windowing) | **Standalone**, because the SDK bump is repo-wide, not windowing-local. | +| D5 | Forced consumer floor — the bump makes `Microsoft.UI.Reactor` advertise `Microsoft.WindowsAppSDK >= 2.1.3` (transitive, no `PrivateAssets`), so every consumer's floor rises. Mitigate (compile against 2.0.1 + reflection-guard the drag-region DPs so the feature lights up only on 2.1.3+, matching the `BackdropKind.Transparent` graceful-degradation precedent) vs. accept? | **Accept.** 2.0 → 2.1 is an **in-place servicing** bump within the *same* side-by-side 2.x runtime family (`Microsoft.WindowsAppRuntime.2.msix`), not a new SxS major — so the upgrade is low-friction for consumers. The reflection-guard alternative keeps the floor at 2.0.1 but adds string-based reflection + `[UnconditionalSuppressMessage]` to the AOT-strict core hot path (`ApplyModifiers`) and silently no-ops drag regions on 2.0 runtimes; the added complexity and the "silently does nothing" failure mode aren't worth avoiding a within-family servicing bump. Revisit only if a concrete consumer is pinned to 2.0.x and cannot service forward. | + +## §7 Implementation phases + +1. ✅ **SDK bump** — `Directory.Build.props` 2.0.1 → 2.1.3; refreshed the `bootstrap.ps1` version comment. +2. ✅ **`AutoRefreshDragRegions`** — added the record property to `TitleBarElement`; generator auto-maps it + (`.OneWay(e => e.AutoRefreshDragRegions, …)`, confirmed in the generated descriptor); added the + `.AutoRefreshDragRegions()` fluent modifier. +3. ✅ **`.IsDragRegion()`** — added `bool? IsDragRegion` to `ElementModifiers`, the generic modifier, and the + `ApplyModifiers` set/clear arm. +4. ✅ **Sample** — extended `samples/ReactorGallery/.../TitleBarPage.cs` "TitleBar with Content". +5. ✅ **Docs** — windowing skill + `windows.md.dt` guide template (recompiled `docs/guide/windows.md`). + +## §8 Testing + +| Tier | Coverage | +|---|---| +| Unit (`Reactor.Tests`) | `.IsDragRegion()` / `.AutoRefreshDragRegions()` produce the expected `ElementModifiers` / `TitleBarElement` records (modifier plumbing, default + override values). | +| Selftest (`Reactor.AppTests.Host/SelfTest/Fixtures`) | Mount a real `TitleBar` with content; assert `TitleBar.GetIsDragRegion(child)` reflects `.IsDragRegion(false/true)` and clears on unset; assert `AutoRefreshDragRegions` round-trips on the control. | +| Build / CI | Full `Reactor.slnx` build + unit tests + selftests on the bumped SDK (the standard PR gate). | +```` \ No newline at end of file diff --git a/plugins/reactor/skills/reactor-dsl/references/reactor.api.txt b/plugins/reactor/skills/reactor-dsl/references/reactor.api.txt index f557370dc..fb9911c71 100644 --- a/plugins/reactor/skills/reactor-dsl/references/reactor.api.txt +++ b/plugins/reactor/skills/reactor-dsl/references/reactor.api.txt @@ -605,6 +605,7 @@ T.HierarchyLevel(int level) → T T.HorizontalAlignment(HorizontalAlignment alignment) → T T.Immediate() → T T.InteractionStates(Func configure, Curve curve = null) → T +T.IsDragRegion(bool? isDragRegion = true) → T T.IsEnabled(bool enabled = true) → T T.IsTabStop(bool isTabStop = true) → T T.IsVisible(bool isVisible) → T @@ -797,6 +798,7 @@ TextBoxElement.TextWrapping(TextWrapping wrapping = TextWrapping.Wrap) → TextB TextBoxElement.UrlInput() → TextBoxElement TimePickerElement.Set(Action configure) → TimePickerElement TimePickerElement.TimeChanged(Action handler) → TimePickerElement +TitleBarElement.AutoRefreshDragRegions(bool value = true) → TitleBarElement TitleBarElement.BackButtonEnabled(bool enabled) → TitleBarElement TitleBarElement.BackButtonVisible(bool visible) → TitleBarElement TitleBarElement.BackRequested(Action handler) → TitleBarElement @@ -2963,6 +2965,7 @@ Vector3? Translation { get; init; } VerticalAlignment? VerticalAlignment { get; init; } VisualModifiers Visual { get; init; } XYFocusKeyboardNavigationMode? XYFocusKeyboardNavigation { get; init; } +bool? IsDragRegion { get; init; } bool? IsEnabled { get; init; } bool? IsTabStop { get; init; } bool? IsVisible { get; init; } @@ -6832,6 +6835,7 @@ Action OnPaneToggleRequested { get; init; } Element Content { get; init; } Element RightHeader { get; init; } IconData Icon { get; init; } +bool AutoRefreshDragRegions { get; init; } bool IsBackButtonEnabled { get; init; } bool IsBackButtonVisible { get; init; } bool IsPaneToggleButtonVisible { get; init; } diff --git a/plugins/reactor/skills/reactor-windowing/SKILL.md b/plugins/reactor/skills/reactor-windowing/SKILL.md index 5e4906493..35cf4436f 100644 --- a/plugins/reactor/skills/reactor-windowing/SKILL.md +++ b/plugins/reactor/skills/reactor-windowing/SKILL.md @@ -120,6 +120,22 @@ spec value is `null`. Explicit `true` or `false` wins. > path) so it is safe. Prefer omitting `TitleBar(...)` when you want the system > title bar. +### Custom title-bar content and drag regions + +`TitleBar(...)` accepts custom `Content`. Interactive controls are excluded from +the drag region automatically (WinApp SDK ≥ 2.1.3). Override per element with +`.IsDragRegion(false)` (force clickable) or `.IsDragRegion(true)` (force draggable), +and add `.AutoRefreshDragRegions()` when the content changes across renders: + +```csharp +(TitleBar("Gallery") with +{ + Content = HStack(8, + AutoSuggestBox("", _ => {}).Width(200), + Button("\uE713", OnSettings).IsDragRegion(false)), +}).AutoRefreshDragRegions(); +``` + ```csharp VStack(...).Backdrop(BackdropKind.Mica); new WindowSpec { Backdrop = BackdropChoice.Of(BackdropKind.DesktopAcrylic) }; diff --git a/samples/ReactorGallery/ControlPages/Navigation/TitleBarPage.cs b/samples/ReactorGallery/ControlPages/Navigation/TitleBarPage.cs index 10c55d17a..9295e08ed 100644 --- a/samples/ReactorGallery/ControlPages/Navigation/TitleBarPage.cs +++ b/samples/ReactorGallery/ControlPages/Navigation/TitleBarPage.cs @@ -39,19 +39,19 @@ public override Element Render() SampleCard("TitleBar with Content", Border( - TitleBar("Gallery") with + (TitleBar("Gallery") with { Content = HStack(8, AutoSuggestBox("", _ => { }).Width(200), - Button("\uE713", () => { }).Width(36).Height(36) + Button("\uE713", () => { }).Width(36).Height(36).IsDragRegion(false) ), - } + }).AutoRefreshDragRegions() ).Background(Theme.LayerFill).CornerRadius(4).Height(48), - @"TitleBar(""Gallery"") with { + @"(TitleBar(""Gallery"") with { Content = HStack(8, AutoSuggestBox("""", _ => {}).Width(200), - Button(""⚙"", () => {})) -}") + Button(""⚙"", () => {}).IsDragRegion(false)) +}).AutoRefreshDragRegions()") ).Margin(36, 24, 36, 36) ); } diff --git a/samples/WinFormsInterop/README.md b/samples/WinFormsInterop/README.md index 43cf5f130..8ebfc9512 100644 --- a/samples/WinFormsInterop/README.md +++ b/samples/WinFormsInterop/README.md @@ -40,7 +40,7 @@ static class Program true None - + ``` diff --git a/skills/reactor.api.txt b/skills/reactor.api.txt index f557370dc..fb9911c71 100644 --- a/skills/reactor.api.txt +++ b/skills/reactor.api.txt @@ -605,6 +605,7 @@ T.HierarchyLevel(int level) → T T.HorizontalAlignment(HorizontalAlignment alignment) → T T.Immediate() → T T.InteractionStates(Func configure, Curve curve = null) → T +T.IsDragRegion(bool? isDragRegion = true) → T T.IsEnabled(bool enabled = true) → T T.IsTabStop(bool isTabStop = true) → T T.IsVisible(bool isVisible) → T @@ -797,6 +798,7 @@ TextBoxElement.TextWrapping(TextWrapping wrapping = TextWrapping.Wrap) → TextB TextBoxElement.UrlInput() → TextBoxElement TimePickerElement.Set(Action configure) → TimePickerElement TimePickerElement.TimeChanged(Action handler) → TimePickerElement +TitleBarElement.AutoRefreshDragRegions(bool value = true) → TitleBarElement TitleBarElement.BackButtonEnabled(bool enabled) → TitleBarElement TitleBarElement.BackButtonVisible(bool visible) → TitleBarElement TitleBarElement.BackRequested(Action handler) → TitleBarElement @@ -2963,6 +2965,7 @@ Vector3? Translation { get; init; } VerticalAlignment? VerticalAlignment { get; init; } VisualModifiers Visual { get; init; } XYFocusKeyboardNavigationMode? XYFocusKeyboardNavigation { get; init; } +bool? IsDragRegion { get; init; } bool? IsEnabled { get; init; } bool? IsTabStop { get; init; } bool? IsVisible { get; init; } @@ -6832,6 +6835,7 @@ Action OnPaneToggleRequested { get; init; } Element Content { get; init; } Element RightHeader { get; init; } IconData Icon { get; init; } +bool AutoRefreshDragRegions { get; init; } bool IsBackButtonEnabled { get; init; } bool IsBackButtonVisible { get; init; } bool IsPaneToggleButtonVisible { get; init; } diff --git a/src/Reactor.Cli/Program.cs b/src/Reactor.Cli/Program.cs index 829353d8a..423b189c6 100644 --- a/src/Reactor.Cli/Program.cs +++ b/src/Reactor.Cli/Program.cs @@ -314,7 +314,7 @@ string GenerateCsproj() => Trim="true" /> - + diff --git a/src/Reactor/Core/Element.cs b/src/Reactor/Core/Element.cs index 92918ea50..c041d5307 100644 --- a/src/Reactor/Core/Element.cs +++ b/src/Reactor/Core/Element.cs @@ -1072,6 +1072,7 @@ internal static bool ModifiersEqual(ElementModifiers? a, ElementModifiers? b) && a.ContextFlyout is null && b.ContextFlyout is null // Accessibility Tier 1 && a.HeadingLevel == b.HeadingLevel + && a.IsDragRegion == b.IsDragRegion && a.IsTabStop == b.IsTabStop && a.TabIndex == b.TabIndex && a.AccessKey == b.AccessKey @@ -1679,6 +1680,15 @@ public ElementTheme? RequestedTheme init => Layout = Layout is null ? new LayoutModifiers { RequestedTheme = value } : Layout with { RequestedTheme = value }; } + /// + /// When set, writes the Microsoft.UI.Xaml.Controls.TitleBar.IsDragRegion + /// attached property on this element's control (WinApp SDK ≥ 2.1.3): + /// true = draggable, false = clickable. Unset leaves the title bar + /// to decide (interactive controls are auto-excluded from the drag region). + /// Inert on elements that are not inside a TitleBar. See spec 059. + /// + public bool? IsDragRegion { get; init; } + // ── Accessibility — Tier 1 (inline, commonly needed for WCAG AA) ─ public Microsoft.UI.Xaml.Automation.Peers.AutomationHeadingLevel? HeadingLevel { get; init; } public bool? IsTabStop { get; init; } @@ -1767,6 +1777,7 @@ public ElementModifiers Merge(ElementModifiers other) DragSource = other.DragSource ?? DragSource, DropTarget = other.DropTarget ?? DropTarget, HeadingLevel = other.HeadingLevel ?? HeadingLevel, + IsDragRegion = other.IsDragRegion ?? IsDragRegion, IsTabStop = other.IsTabStop ?? IsTabStop, TabIndex = other.TabIndex ?? TabIndex, AccessKey = other.AccessKey ?? AccessKey, @@ -4172,7 +4183,7 @@ public partial record NavigationViewElement( } // Spec 058 §15 (P5.23) — Title/Subtitle/IsBackButtonVisible/IsBackButtonEnabled/ -// IsPaneToggleButtonVisible auto-map. Content+RightHeader (NamedSlots → overwrite d.Children), +// IsPaneToggleButtonVisible/AutoRefreshDragRegions auto-map. Content+RightHeader (NamedSlots → overwrite d.Children), // Icon (Icon→IconSource via IconResolver transform), the window.SetTitleBar registration // (.Imperative) and the BackRequested/PaneToggleRequested events (Excluded) are bespoke. // Replaces TitleBarDescriptor. @@ -4190,6 +4201,13 @@ string Title public Action? OnBackRequested { get; init; } public bool IsPaneToggleButtonVisible { get; init; } public Action? OnPaneToggleRequested { get; init; } + /// + /// When true, the WinUI TitleBar re-derives its drag regions on + /// every layout pass (WinApp SDK ≥ 2.1.3). Useful when + /// changes across renders. Default false (matches the control default; + /// interactive controls are still auto-excluded from the drag region). See spec 059. + /// + public bool AutoRefreshDragRegions { get; init; } public Element? Content { get; init; } public Element? RightHeader { get; init; } /// diff --git a/src/Reactor/Core/ElementPool.cs b/src/Reactor/Core/ElementPool.cs index 51447d993..f358264dd 100644 --- a/src/Reactor/Core/ElementPool.cs +++ b/src/Reactor/Core/ElementPool.cs @@ -248,6 +248,9 @@ internal static void CleanElement(FrameworkElement fe) fe.ClearValue(UIElement.IsHitTestVisibleProperty); if (fe is Control tabStopControl) tabStopControl.ClearValue(Control.IsTabStopProperty); + // spec 059: clear the TitleBar.IsDragRegion attached prop so a pooled + // control marked .IsDragRegion(...) can't poison the next renter. + fe.ClearValue(WinUI.TitleBar.IsDragRegionProperty); fe.ClearValue(Microsoft.UI.Xaml.Automation.AutomationProperties.IsRequiredForFormProperty); fe.ClearValue(Microsoft.UI.Xaml.Automation.AutomationProperties.LiveSettingProperty); fe.ClearValue(Microsoft.UI.Xaml.Automation.AutomationProperties.PositionInSetProperty); diff --git a/src/Reactor/Core/Reconciler.cs b/src/Reactor/Core/Reconciler.cs index fc2983b18..ed4513e95 100644 --- a/src/Reactor/Core/Reconciler.cs +++ b/src/Reactor/Core/Reconciler.cs @@ -3787,6 +3787,12 @@ internal void ApplyModifiers(FrameworkElement fe, ElementModifiers? oldM, Elemen else if (m.AutomationId is null && oldM?.AutomationId is not null) fe.ClearValue(Microsoft.UI.Xaml.Automation.AutomationProperties.AutomationIdProperty); + // TitleBar.IsDragRegion (attached; inert outside a TitleBar). WinApp SDK ≥ 2.1.3. + if (m.IsDragRegion.HasValue && m.IsDragRegion != oldM?.IsDragRegion) + WinUI.TitleBar.SetIsDragRegion(fe, m.IsDragRegion.Value); + else if (!m.IsDragRegion.HasValue && oldM?.IsDragRegion.HasValue == true) + fe.ClearValue(WinUI.TitleBar.IsDragRegionProperty); + // ElementSoundMode (on Control, not FrameworkElement) if (m.ElementSoundMode.HasValue && m.ElementSoundMode != oldM?.ElementSoundMode && fe is WinUI.Control ctrl) ctrl.ElementSoundMode = m.ElementSoundMode.Value; diff --git a/src/Reactor/Elements/ElementExtensions.cs b/src/Reactor/Elements/ElementExtensions.cs index cce7063f7..5bfb7ed39 100644 --- a/src/Reactor/Elements/ElementExtensions.cs +++ b/src/Reactor/Elements/ElementExtensions.cs @@ -1719,6 +1719,15 @@ public static TitleBarElement Icon(this TitleBarElement el, IconData icon) => public static TitleBarElement Icon(this TitleBarElement el, string imageUri) => el with { Icon = new ImageIconData(new Uri(imageUri)) }; + /// + /// When true, the title bar re-derives its drag regions on every layout + /// pass — the right fit for content that changes across renders. WinApp SDK ≥ 2.1.3. + /// Interactive controls are auto-excluded from the drag region either way; use + /// on a child to override per element. + /// + public static TitleBarElement AutoRefreshDragRegions(this TitleBarElement el, bool value = true) => + el with { AutoRefreshDragRegions = value }; + /// /// Auto-syncs this TitleBar's back button with a NavigationHandle: sets /// IsBackButtonVisible and IsBackButtonEnabled from CanGoBack, @@ -2476,6 +2485,18 @@ public static T HeadingLevel(this T el, Microsoft.UI.Xaml.Automation.Peers.Au public static T IsTabStop(this T el, bool isTabStop = true) where T : Element => Modify(el, new ElementModifiers { IsTabStop = isTabStop }); + /// + /// Marks how this element participates in a window title bar's drag region + /// (Microsoft.UI.Xaml.Controls.TitleBar.IsDragRegion, WinApp SDK ≥ 2.1.3): + /// true = draggable, false = clickable, null = defer to the + /// title bar (interactive controls are auto-excluded from the drag region). Only + /// meaningful for an element inside a TitleBar's content; inert elsewhere. + /// The nullable parameter lets callers forward a bool? state without branching. + /// + /// Button("\uE713", OnSettings).IsDragRegion(false) + public static T IsDragRegion(this T el, bool? isDragRegion = true) where T : Element => + Modify(el, new ElementModifiers { IsDragRegion = isDragRegion }); + /// /// Sets Control.TabIndex — Tab order position. Lower values receive focus first. /// diff --git a/src/Reactor/Reactor.csproj b/src/Reactor/Reactor.csproj index 082b58a9f..7829fe5d4 100644 --- a/src/Reactor/Reactor.csproj +++ b/src/Reactor/Reactor.csproj @@ -237,6 +237,8 @@ Pack="true" PackagePath="agentkit/plugins/reactor/skills/reactor-recipes/references/" Visible="false" /> + - +