|
| 1 | +TitleBar Drag Region API Specification |
| 2 | +=== |
| 3 | + |
| 4 | +# Background |
| 5 | + |
| 6 | +Custom title bar layouts often combine **interactive controls** and **non‑interactive visual elements** using containers such as `Grid`, `StackPanel`, or deeper nested structures. While this flexibility enables rich and branded title bar designs, it also introduces ambiguity when determining **which parts of the title bar should behave as draggable regions** for moving the window. |
| 7 | + |
| 8 | +Under the **current default behavior**, the framework treats the entire `TitleBar.Content` area as the primary drag surface and then subtracts (or *"punches holes"* from) regions that should not initiate window dragging. This approach works reasonably well for dense, predictable layouts. However, it **fails in scenarios with empty gaps, uneven spacing, nested templates, or dynamically generated UI**, where the system cannot reliably infer developer intent. These situations can lead to **unexpected non‑draggable gaps**, creating inconsistent or unintuitive window‑drag behavior for users. |
| 9 | + |
| 10 | +This specification introduces **two changes** to address these issues: |
| 11 | + |
| 12 | +1. **Changing the default behavior** for deciding which parts of the TitleBar can be used to drag. The framework now recursively walks the visual tree and automatically excludes interactive controls from the drag region, making empty gaps and non‑interactive areas draggable by default. |
| 13 | +2. **Giving developers the ability to override the default behavior** on a per‑element basis using the `TitleBar.IsDragRegion` attached property, and control when drag regions are recomputed via `AutoRefreshDragRegions` and `RecomputeDragRegions()`. |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +# Conceptual pages (How To) |
| 18 | + |
| 19 | +## Change 1: Changes to default behavior |
| 20 | + |
| 21 | +```xml |
| 22 | +<TitleBar Title="Main Title" Subtitle="subtitle" x:Name="titleBar"> |
| 23 | + <TitleBar.Content> |
| 24 | + <Grid> |
| 25 | + <Grid.ColumnDefinitions> |
| 26 | + <ColumnDefinition Width="150" /> |
| 27 | + <ColumnDefinition Width="200" /> |
| 28 | + <ColumnDefinition Width="50" /> |
| 29 | + </Grid.ColumnDefinitions> |
| 30 | + <Border Grid.Column="0" Background="LightBlue" BorderBrush="Black" BorderThickness="1"> |
| 31 | + <AutoSuggestBox PlaceholderText="Search"/> |
| 32 | + </Border> |
| 33 | + <Border Grid.Column="1" /> |
| 34 | + <Border Grid.Column="2" Background="LightCoral" BorderBrush="Black" BorderThickness="1"> |
| 35 | + <TextBlock Text="Help" VerticalAlignment="Center" HorizontalAlignment="Center" /> |
| 36 | + </Border> |
| 37 | + </Grid> |
| 38 | + </TitleBar.Content> |
| 39 | +</TitleBar> |
| 40 | +``` |
| 41 | + |
| 42 | +Output: |
| 43 | + |
| 44 | + |
| 45 | + |
| 46 | +In this simple layout: |
| 47 | +- Column 0 contains **Sample Search Box** |
| 48 | +- Column 2 contains **Help** |
| 49 | +- Column 1 is **empty visual space** that becomes a non-draggable gap under the previous behavior |
| 50 | + |
| 51 | +Even in simple cases, it is non-trivial for the framework to automatically classify such gaps as draggable or non-draggable. More complex layouts—nested controls, templated UI, and dynamic content—make automatic detection even harder. |
| 52 | + |
| 53 | +**With the new default behavior**, the framework recursively traverses the visual tree and **excludes only interactive controls from drag**. Empty visual space (such as Column 1 above) and non‑interactive elements are now **draggable by default**, without any markup changes. The same XAML above now produces the correct result: |
| 54 | + |
| 55 | +Output (with new defaults): |
| 56 | + |
| 57 | + |
| 58 | + |
| 59 | +--- |
| 60 | + |
| 61 | +## Change 2: Per‑element overrides with `TitleBar.IsDragRegion` |
| 62 | + |
| 63 | +Developers can override the default behavior on any element using the `TitleBar.IsDragRegion` attached property: |
| 64 | +- Set `IsDragRegion="True"` to include an element in the drag region even if it is an interactive control (e.g., ribbon areas that should drag). |
| 65 | +- Set `IsDragRegion="False"` to exclude an element from drag even if the framework would not auto‑detect it as interactive. |
| 66 | +- If the property is not set (remains `null`), the framework uses the default behavior: interactive controls are excluded from drag, non‑interactive visuals are draggable. |
| 67 | + |
| 68 | +> **Implementation note:** `IsDragRegion` is a nullable boolean (`IReference<Boolean>`). The getter returns `null` when the property has not been set, `true` when explicitly set to `True`, and `false` when explicitly set to `False`. |
| 69 | +
|
| 70 | +**Advantages** |
| 71 | +- **Low developer effort** (good defaults). |
| 72 | +- **High flexibility** (simple overrides where needed). |
| 73 | +- **Consistent, accessible behavior** aligned with product expectations. |
| 74 | + |
| 75 | +IsDragRegion tri-state behavior: |
| 76 | + |
| 77 | +| State | How it's set | Getter returns | Meaning | |
| 78 | +|---|---|---|---| |
| 79 | +| **Not set** | Developer doesn't set it | `null` | "No opinion" — framework auto-detects based on control type | |
| 80 | +| `False` | `TitleBar.IsDragRegion="False"` | `false` | "Explicitly clickable" — always a passthrough hole | |
| 81 | +| `True` | `TitleBar.IsDragRegion="True"` | `true` | "Explicitly draggable" — never a passthrough, even if auto-detected as interactive | |
| 82 | + |
| 83 | +--- |
| 84 | + |
| 85 | +## Drag Region Refresh Behavior |
| 86 | + |
| 87 | +By default, the TitleBar refreshes drag regions in response to: |
| 88 | +- Content changes (the `Content` property is set or replaced) |
| 89 | +- Content loaded events (the content's visual tree becomes ready) |
| 90 | +- Size changes (the TitleBar is resized) |
| 91 | +- `IsDragRegion` attached property changes (at runtime, including hot reload) |
| 92 | + |
| 93 | +For scenarios where content is dynamically modified at runtime (controls added/removed/resized after initial load), there are two options: |
| 94 | + |
| 95 | +### Option 1: Manual refresh with `RecomputeDragRegions()` |
| 96 | + |
| 97 | +Call `RecomputeDragRegions()` in code-behind after making dynamic changes: |
| 98 | + |
| 99 | +```csharp |
| 100 | +// After dynamically adding a button to the title bar content |
| 101 | +myStackPanel.Children.Add(new Button { Content = "New" }); |
| 102 | +titleBar.RecomputeDragRegions(); |
| 103 | +``` |
| 104 | + |
| 105 | +### Option 2: Automatic refresh with `AutoRefreshDragRegions` |
| 106 | + |
| 107 | +Set `AutoRefreshDragRegions="True"` to subscribe to `LayoutUpdated` events for continuous automatic refresh. This is convenient but has a performance cost since it triggers a visual tree walk on every layout pass. |
| 108 | + |
| 109 | +```xml |
| 110 | +<TitleBar AutoRefreshDragRegions="True"> |
| 111 | + <TitleBar.Content> |
| 112 | + <!-- Dynamic content that changes at runtime --> |
| 113 | + </TitleBar.Content> |
| 114 | +</TitleBar> |
| 115 | +``` |
| 116 | + |
| 117 | +| `AutoRefreshDragRegions` | Behavior | |
| 118 | +|---|---| |
| 119 | +| `False` (default) | Refreshes on Content/Size/Loaded/IsDragRegion changes. Call `RecomputeDragRegions()` for edge cases. | |
| 120 | +| `True` | All of the above **plus** every `LayoutUpdated` event (continuous automatic refresh). | |
| 121 | + |
| 122 | +--- |
| 123 | + |
| 124 | +## Additional How‑To Topics |
| 125 | + |
| 126 | +### Styling and Containers |
| 127 | +You can apply `IsDragRegion` to containers to include/exclude large UI areas (e.g., toolbars). Use it sparingly to avoid accidentally disabling drag for entire subtrees; prefer marking the minimum necessary element. |
| 128 | + |
| 129 | +```xml |
| 130 | +<StackPanel Orientation="Horizontal" TitleBar.IsDragRegion="False"> |
| 131 | + <ComboBox PlaceholderText="Font"/> |
| 132 | + <ComboBox PlaceholderText="Size"/> |
| 133 | + <ToggleButton Content="Bold"/> |
| 134 | +</StackPanel> |
| 135 | +``` |
| 136 | + |
| 137 | +### Nested Layouts with Overrides |
| 138 | +A container can be marked as draggable, while a specific child overrides that to remain interactive. The two cases below show the difference side‑by‑side. |
| 139 | + |
| 140 | +Case A — Entire StackPanel is draggable (no overrides): |
| 141 | + |
| 142 | +```xml |
| 143 | +<!-- The entire StackPanel and all its children are part of the drag region --> |
| 144 | +<StackPanel Orientation="Horizontal" TitleBar.IsDragRegion="True"> |
| 145 | + <ComboBox PlaceholderText="Font"/> |
| 146 | + <ComboBox PlaceholderText="Size"/> |
| 147 | + <ToggleButton Content="Bold"/> |
| 148 | +</StackPanel> |
| 149 | +``` |
| 150 | + |
| 151 | +Case B — StackPanel is draggable, but one child overrides to remain clickable: |
| 152 | + |
| 153 | +```xml |
| 154 | +<!-- StackPanel is draggable, but the ComboBox overrides to stay interactive --> |
| 155 | +<StackPanel Orientation="Horizontal" TitleBar.IsDragRegion="True"> |
| 156 | + <ComboBox PlaceholderText="Font" TitleBar.IsDragRegion="False"/> |
| 157 | + <ComboBox PlaceholderText="Size"/> |
| 158 | + <ToggleButton Content="Bold"/> |
| 159 | +</StackPanel> |
| 160 | +``` |
| 161 | +In **Case B**, the "Font" `ComboBox` is explicitly excluded from drag (`IsDragRegion="False"`), so it remains clickable. The other children inherit the parent's `IsDragRegion="True"` and become part of the drag region. |
| 162 | + |
| 163 | + |
| 164 | +### Using in XAML, C#, and C++/WinRT |
| 165 | + |
| 166 | +<table> |
| 167 | + <tr> |
| 168 | + <th>Language</th> |
| 169 | + <th>Code Sample</th> |
| 170 | + <th>Notes</th> |
| 171 | + </tr> |
| 172 | + <tr> |
| 173 | + <td><b>XAML</b></td> |
| 174 | + <td> |
| 175 | +<pre lang="xml"><TitleBar AutoRefreshDragRegions="True"> |
| 176 | + <TitleBar.Content> |
| 177 | + <StackPanel Orientation="Horizontal"> |
| 178 | + <TextBlock Text="My App" VerticalAlignment="Center" /> |
| 179 | + <AutoSuggestBox TitleBar.IsDragRegion="True" /> |
| 180 | + </StackPanel> |
| 181 | + </TitleBar.Content> |
| 182 | +</TitleBar></pre> |
| 183 | + </td> |
| 184 | + <td>Enable automatic drag region refresh and opt a control into drag.</td> |
| 185 | + </tr> |
| 186 | + <tr> |
| 187 | + <td><b>C#</b></td> |
| 188 | + <td> |
| 189 | +<pre lang="csharp">// Per-element override (nullable bool) |
| 190 | +var search = new AutoSuggestBox(); |
| 191 | +TitleBar.SetIsDragRegion(search, true); |
| 192 | +// Manual refresh after dynamic changes |
| 193 | +titleBar.RecomputeDragRegions(); |
| 194 | + // Read the current override (returns null if not explicitly set) |
| 195 | + var isDrag = TitleBar.GetIsDragRegion(search)</pre> |
| 196 | + </td> |
| 197 | + <td>Override an element in code-behind and manually refresh.</td> |
| 198 | + </tr> |
| 199 | + <tr> |
| 200 | + <td><b>C++/WinRT</b></td> |
| 201 | + <td> |
| 202 | +<pre lang="cpp">using namespace winrt::Microsoft::UI::Xaml::Controls; |
| 203 | +AutoSuggestBox search{}; |
| 204 | +TitleBar::SetIsDragRegion(search, true); |
| 205 | +// Manual refresh after dynamic changes |
| 206 | +titleBar.RecomputeDragRegions(); |
| 207 | +// Read the current override |
| 208 | +auto isDrag = TitleBar::GetIsDragRegion(search);</pre> |
| 209 | + </td> |
| 210 | + <td>Equivalent usage in C++/WinRT.</td> |
| 211 | + </tr> |
| 212 | +</table> |
| 213 | + |
| 214 | +--- |
| 215 | + |
| 216 | +# API Pages |
| 217 | + |
| 218 | +## TitleBar.IsDragRegion attached property |
| 219 | +A nullable boolean (`IReference<Boolean>`) that marks an element as included in the window drag region (`True`) or excluded (`False`), overriding the framework default. When not set (`null`), the framework auto-detects based on control type. |
| 220 | + |
| 221 | +| Value | Meaning | |
| 222 | +|---|---| |
| 223 | +| **Not set** (`null`) | Framework decides: interactive controls are excluded from drag, non-interactive visuals are draggable. | |
| 224 | +| `False` | Explicitly clickable — always a passthrough hole, even if non-interactive. | |
| 225 | +| `True` | Explicitly draggable — never a passthrough, even if interactive. | |
| 226 | + |
| 227 | +```xml |
| 228 | +<ComboBox PlaceholderText="Font" TitleBar.IsDragRegion="False"/> |
| 229 | +``` |
| 230 | + |
| 231 | +### Example Usage |
| 232 | +```xml |
| 233 | +<TitleBar> |
| 234 | + <TitleBar.Content> |
| 235 | + <StackPanel Orientation="Horizontal" TitleBar.IsDragRegion="True"> |
| 236 | + <ComboBox PlaceholderText="Font" TitleBar.IsDragRegion="False"/> |
| 237 | + <ComboBox PlaceholderText="Size"/> |
| 238 | + <ToggleButton Content="Bold"/> |
| 239 | + </StackPanel> |
| 240 | + </TitleBar.Content> |
| 241 | +</TitleBar> |
| 242 | +``` |
| 243 | + |
| 244 | +## TitleBar.AutoRefreshDragRegions property |
| 245 | +When `True`, the TitleBar subscribes to `LayoutUpdated` events on the content and automatically refreshes drag regions on every layout pass. Default is `False`. |
| 246 | + |
| 247 | +```xml |
| 248 | +<TitleBar AutoRefreshDragRegions="True"/> |
| 249 | +``` |
| 250 | + |
| 251 | +## TitleBar.RecomputeDragRegions method |
| 252 | +Manually triggers a full recomputation of drag regions by walking the visual tree and updating passthrough rects. Use this when `AutoRefreshDragRegions` is `False` and content has been dynamically modified. |
| 253 | + |
| 254 | +```csharp |
| 255 | +titleBar.RecomputeDragRegions(); |
| 256 | +``` |
| 257 | + |
| 258 | +--- |
| 259 | + |
| 260 | +# API Details |
| 261 | + |
| 262 | +```c# (but really midl3) |
| 263 | +namespace Microsoft.UI.Xaml.Controls |
| 264 | +{ |
| 265 | + unsealed runtimeclass TitleBar : Microsoft.UI.Xaml.Controls.Control |
| 266 | + { |
| 267 | + // ... existing properties ... |
| 268 | +
|
| 269 | + // New in V10: |
| 270 | + // When true, drag regions are automatically refreshed on every layout update. |
| 271 | + // When false (default), drag regions are refreshed on Content/Size changes only; |
| 272 | + // call RecomputeDragRegions() for manual refresh. |
| 273 | + [MUX_PUBLIC_V10] |
| 274 | + { |
| 275 | + [MUX_DEFAULT_VALUE("false")] |
| 276 | + Boolean AutoRefreshDragRegions{ get; set; }; |
| 277 | + static Microsoft.UI.Xaml.DependencyProperty AutoRefreshDragRegionsProperty{ get; }; |
| 278 | + |
| 279 | + // Manually triggers a full recomputation of drag regions. |
| 280 | + void RecomputeDragRegions(); |
| 281 | + } |
| 282 | + |
| 283 | + // Attached property: nullable boolean for per-element drag region overrides. |
| 284 | + // null = unset (framework decides), false = clickable, true = draggable. |
| 285 | + [MUX_PUBLIC_V10] |
| 286 | + { |
| 287 | + [MUX_PROPERTY_CHANGED_CALLBACK_METHODNAME("OnIsDragRegionPropertyChanged")] |
| 288 | + static Microsoft.UI.Xaml.DependencyProperty IsDragRegionProperty{ get; }; |
| 289 | + static Windows.Foundation.IReference<Boolean> GetIsDragRegion(Microsoft.UI.Xaml.UIElement element); |
| 290 | + static void SetIsDragRegion(Microsoft.UI.Xaml.UIElement element, Windows.Foundation.IReference<Boolean> value); |
| 291 | + } |
| 292 | + } |
| 293 | +} |
| 294 | +``` |
| 295 | + |
| 296 | +--- |
| 297 | + |
| 298 | +## Appendix |
| 299 | + |
| 300 | +### Keyboard Behaviour |
| 301 | +This API affects **pointer hit‑testing** for window drag only; it does not change keyboard interaction. Ensure that interactive controls in `TitleBar.Content` remain fully focusable and operable. Keep a logical tab order in the title bar. |
| 302 | + |
| 303 | +### Automation Behaviour |
| 304 | +`IsDragRegion` **does not alter** UIA patterns or names of elements. Interactability for screen readers remains unchanged. Developers should verify that drag affordances are communicated visually and do not conflict with UIA expectations. |
| 305 | + |
| 306 | +### Backward Compatibility |
| 307 | +- The updated drag‑region model introduces a new global default: interactive controls are not draggable, and non‑interactive visuals are draggable, unless explicitly overridden. |
| 308 | +- `IsDragRegion` is a new nullable boolean (`IReference<Boolean>`) attached property introduced in V10. The getter returns `null` when not set, `true` when explicitly set to `True`, and `false` when explicitly set to `False`. |
| 309 | +- `AutoRefreshDragRegions` defaults to `False`, preserving the existing performance characteristics. Developers who need continuous automatic refresh for highly dynamic content should set `AutoRefreshDragRegions="True"` or call `RecomputeDragRegions()` after dynamic changes. |
0 commit comments