Skip to content

Commit 5f9e851

Browse files
[API Spec] TitleBar's Content Custom Drag Regions (#10936)
1 parent ca7a243 commit 5f9e851

3 files changed

Lines changed: 309 additions & 0 deletions

File tree

32.7 KB
Loading
32.6 KB
Loading
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
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+
![Non draggable gaps in TitleBar Content](./images/titlebar-drag-issue.png)
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+
![Non draggable gaps in TitleBar Content](./images/titlebar-drag-issue-fixed.png)
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">&lt;TitleBar AutoRefreshDragRegions="True"&gt;
176+
&lt;TitleBar.Content&gt;
177+
&lt;StackPanel Orientation="Horizontal"&gt;
178+
&lt;TextBlock Text="My App" VerticalAlignment="Center" /&gt;
179+
&lt;AutoSuggestBox TitleBar.IsDragRegion="True" /&gt;
180+
&lt;/StackPanel&gt;
181+
&lt;/TitleBar.Content&gt;
182+
&lt;/TitleBar&gt;</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

Comments
 (0)