Skip to content

Commit 52b96df

Browse files
committed
Bring the snap layouts to the title bar example's own Maximize (#70)
With the title bar Hidden, flutter_window_title_bar_example's strip draws a Maximize / Restore chip wrapped in MaximizeButtonArea, following the window's maximized state however it changes. tools/gui/flutter_window_title_bar_snap_test.ps1 checks it end to end on Windows 11: resting on the chip opens the snap layouts, a click maximizes and another restores.
1 parent b3f92fa commit 52b96df

3 files changed

Lines changed: 152 additions & 1 deletion

File tree

‎examples/flutter_window_title_bar_example/lib/main.dart‎

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class _TitleBarPageState extends State<TitleBarPage> {
3434
Window? _window;
3535
String _note = 'Try the three states and watch the strip at the top';
3636
final List<String> _log = [];
37+
int? _listenerId;
3738

3839
@override
3940
void initState() {
@@ -45,6 +46,33 @@ class _TitleBarPageState extends State<TitleBarPage> {
4546
window.minimumSize = const Size(520, 420).toNative();
4647
window.contentSize = const Size(620, 520).toNative();
4748
window.center();
49+
// The strip's Maximize / Restore follows the window however it changes:
50+
// its own button, the snap layouts, a double-click, a shortcut.
51+
_listenerId = WindowManager.instance.addListener((event) {
52+
if (event.windowId != window.id) return;
53+
if (event is WindowMaximizedEvent || event is WindowRestoredEvent) {
54+
if (mounted) {
55+
setState(
56+
() => _note = event is WindowMaximizedEvent
57+
? 'Maximized'
58+
: 'Restored',
59+
);
60+
}
61+
}
62+
});
63+
}
64+
65+
@override
66+
void dispose() {
67+
final id = _listenerId;
68+
if (id != null) WindowManager.instance.removeListener(id);
69+
super.dispose();
70+
}
71+
72+
void _toggleMaximized() {
73+
final window = _window;
74+
if (window == null) return;
75+
window.isMaximized ? window.unmaximize() : window.maximize();
4876
}
4977

5078
void _act(String what, void Function(Window window) change) {
@@ -80,6 +108,9 @@ class _TitleBarPageState extends State<TitleBarPage> {
80108
children: [
81109
_Strip(
82110
underTitleBar: style == TitleBarStyle.normal && under,
111+
// Hidden takes the system's buttons away; the strip brings its own.
112+
maximized: style == TitleBarStyle.hidden ? window?.isMaximized : null,
113+
onToggleMaximized: _toggleMaximized,
83114
onQuit: () => Application.instance.quit(0),
84115
),
85116
OptionRow(
@@ -185,6 +216,12 @@ class _TitleBarPageState extends State<TitleBarPage> {
185216
'the same size; only contentSize above changes, by '
186217
'the height of the title bar.',
187218
),
219+
const _Bullet(
220+
'Hidden brings a Maximize / Restore chip into the '
221+
'strip. It is marked as the window\'s maximize '
222+
'button, so on Windows 11 resting the pointer on it '
223+
'opens the snap layouts, as on the system\'s own.',
224+
),
188225
const _Bullet(
189226
'Hidden also stops the system moving the window when '
190227
'you drag the top of the content — that is what the '
@@ -230,9 +267,19 @@ class _TitleBarPageState extends State<TitleBarPage> {
230267
/// up behind the window buttons, which is why its controls start clear of
231268
/// them.
232269
class _Strip extends StatelessWidget {
233-
const _Strip({required this.underTitleBar, required this.onQuit});
270+
const _Strip({
271+
required this.underTitleBar,
272+
required this.maximized,
273+
required this.onToggleMaximized,
274+
required this.onQuit,
275+
});
234276

235277
final bool underTitleBar;
278+
279+
/// Whether the window is maximized, or null to leave out the strip's own
280+
/// Maximize / Restore button (the system's title bar has one).
281+
final bool? maximized;
282+
final VoidCallback onToggleMaximized;
236283
final VoidCallback onQuit;
237284

238285
@override
@@ -274,6 +321,17 @@ class _Strip extends StatelessWidget {
274321
style: vars.labelStrong.copyWith(color: vars.colorContent),
275322
),
276323
),
324+
if (maximized != null) ...[
325+
// Marked as the window's maximize button: on Windows 11,
326+
// resting the pointer on it opens the snap layouts.
327+
MaximizeButtonArea(
328+
child: ActionChip(
329+
label: maximized! ? 'Restore' : 'Maximize',
330+
onTap: onToggleMaximized,
331+
),
332+
),
333+
SizedBox(width: vars.spacing15),
334+
],
277335
ActionChip(label: 'Quit', onTap: onQuit),
278336
],
279337
),

‎tools/gui/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ runner) lives in [`.agents/skills/`](../../.agents/skills); read
1818
| `core_window_title_bar_double_click_macos.py` | core title-bar double-click regression | macOS | Guarded real double clicks on bare hidden title bars: zoom/restore, Minimize, None and native Fill. Preferences are overridden only in the fixture process. Build `window_title_bar_double_click_gui_macos_test` in `core/build` first. |
1919
| `core_window_system_menu_test.ps1` | core system menu regression | Windows | Native popup states and guarded selections of custom, maximize, restore, minimize and close commands; content coordinates scale once. Override the executable with `CORE_SYSTEM_MENU_TEST_EXE`. |
2020
| `core_window_snap_layout_test.ps1` | core `window_maximize_button_test --serve` (#70) | Windows 11 | Real hover, no clicks: a hidden-title-bar window marks its own maximize button with `SetMaximizeButtonBounds`; resting the cursor there opens the snap layouts (another process's window appears below the button), the content window still receives the moves inside the button, and leaving closes the flyout and reaches the content as `WM_MOUSELEAVE`. Saves a screenshot. Override the executable with `CORE_SNAP_LAYOUT_TEST_EXE`. |
21+
| `flutter_window_title_bar_snap_test.ps1` | `window_title_bar_example` (#70) | Windows 11 | End to end in a real Flutter app: with the title bar Hidden the strip draws a Maximize / Restore chip wrapped in `MaximizeButtonArea`; resting the cursor on it opens the snap layouts and leaving closes them, a click maximizes (the chip reads Restore) and another restores to the earlier width. Owner-guarded clicks, no keyboard. Saves a screenshot. Set `TITLE_BAR_EXAMPLE_EXE` for a build outside `$RemoteWorkspace`. |
2122
| `core_window_shadow_layout_test_linux.cpp` | core shadow | Linux Wayland / X11 | No-input regression: stable shadow geometry, contour pixels inside transparent content, input pass-through, disable and decoration restoration |
2223
| `flutter_window_shape_smoke_macos.py` / `flutter_window_shape_test_windows.ps1` | `shaped_window_example` | macOS / Windows | macOS: no-input shape and pixel-alpha smoke test; Windows: native regions, real button clicks/drag, resize, clear/reapply, click-through to the underlying app window |
2324
| `flutter_detachable_window_test.py` / `.ps1` | `detachable_window_example` | macOS / Windows | tear a panel off, exact content size, header stays under the cursor, dock into the other window, `State` preserved |
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# End-to-end check of MaximizeButtonArea (#70) in a real Flutter app on
2+
# Windows 11: flutter_window_title_bar_example with its title bar hidden draws
3+
# its own Maximize / Restore chip, wrapped in MaximizeButtonArea. Resting the
4+
# cursor on it opens the snap layouts; clicking it maximizes and restores.
5+
# Owner-guarded clicks on the example's own window only, no keyboard input.
6+
$ErrorActionPreference = 'Stop'
7+
. "$PSScriptRoot\env.ps1"
8+
. "$PSScriptRoot\winput.ps1"
9+
. "$PSScriptRoot\guiapp.ps1"
10+
Add-Type @"
11+
using System;
12+
using System.Runtime.InteropServices;
13+
public static class SnapWin {
14+
[DllImport("user32.dll")] public static extern bool IsZoomed(IntPtr h);
15+
}
16+
"@
17+
Add-Type -AssemblyName System.Drawing
18+
19+
Start-Result "$RemoteScratch\flutter_window_title_bar_snap_test.result.txt"
20+
$exe = $env:TITLE_BAR_EXAMPLE_EXE
21+
if (-not $exe) {
22+
$exe = Get-FlutterExe "$RemoteWorkspace\examples\flutter_window_title_bar_example" 'window_title_bar_example'
23+
}
24+
Assert-Idle
25+
$app = Start-GuiApp $exe
26+
# The probe fails (a traceback on stderr) until the VM service is up, and
27+
# PowerShell 5.1 turns native stderr into a terminating error under Stop.
28+
function Get-ViewsQuietly {
29+
$saved = $ErrorActionPreference
30+
$ErrorActionPreference = 'Continue'
31+
try { @(Get-Views $app 2>$null) } catch { @() } finally { $ErrorActionPreference = $saved }
32+
}
33+
function Get-State {
34+
$script:win = Get-Win $app 'nativeapi*'
35+
if (-not $script:win) { throw 'The example window is missing' }
36+
$script:view = Get-ViewsQuietly | Select-Object -First 1
37+
if (-not $script:view) { throw 'The example has no Flutter view to probe' }
38+
}
39+
try {
40+
$deadline = (Get-Date).AddSeconds(40)
41+
while (@(Get-ViewsQuietly).Count -eq 0 -and (Get-Date) -lt $deadline) { Pause 0.5 }
42+
Get-State
43+
Invoke-Activate $app $win
44+
Invoke-Click $app (ConvertTo-Screen $win (Get-TextCenter $view 'Hidden'))
45+
Pause 1.5
46+
Get-State
47+
Check 'Hidden puts a Maximize chip into the strip' (Test-ViewText $view 'Maximize')
48+
$restoredWidth = $win.Right - $win.Left
49+
50+
# Rest on the chip: the snap layouts open below it, over this window.
51+
$button = ConvertTo-Screen $win (Get-TextCenter $view 'Maximize')
52+
$below = @($button[0], ($button[1] + [int](80 * $win.Scale)))
53+
$ownerBefore = [WInput]::Owner($below[0], $below[1])
54+
Assert-Owner $app.Proc.Id $button[0] $button[1]
55+
Move-Cursor $button 500
56+
Pause 2.0
57+
$ownerDuring = [WInput]::Owner($below[0], $below[1])
58+
$bmp = New-Object System.Drawing.Bitmap 900, 520
59+
$g = [System.Drawing.Graphics]::FromImage($bmp)
60+
$g.CopyFromScreen([Math]::Max(0, $button[0] - 650), [Math]::Max(0, $button[1] - 60), 0, 0, $bmp.Size)
61+
$bmp.Save("$RemoteScratch\flutter_window_title_bar_snap_test.png", [System.Drawing.Imaging.ImageFormat]::Png)
62+
$g.Dispose(); $bmp.Dispose()
63+
Check 'below the chip is the example before the hover' ($ownerBefore -eq $app.Proc.Id) "pid $ownerBefore"
64+
$process = (Get-Process -Id $ownerDuring -ErrorAction SilentlyContinue).ProcessName
65+
Check 'resting on the chip opens the snap layouts over it' `
66+
($ownerDuring -ne $app.Proc.Id -and $ownerDuring -ne 0) "pid $ownerDuring ($process)"
67+
68+
# Leave sideways along the strip, away from the flyout.
69+
$side = @(($button[0] - [int](220 * $win.Scale)), $button[1])
70+
Assert-Owner $app.Proc.Id $side[0] $side[1]
71+
Move-Cursor $side 400
72+
Pause 1.5
73+
Check 'leaving the chip closes the snap layouts' ([WInput]::Owner($below[0], $below[1]) -eq $app.Proc.Id)
74+
75+
# A click on the chip reaches Flutter, which maximizes the window.
76+
Invoke-Click $app $button 400
77+
Pause 1.5
78+
Get-State
79+
Check 'clicking Maximize maximizes the window' ([SnapWin]::IsZoomed([IntPtr]$win.Hwnd)) "width $($win.Right - $win.Left)"
80+
Check 'the chip now reads Restore' (Test-ViewText $view 'Restore')
81+
82+
$button = ConvertTo-Screen $win (Get-TextCenter $view 'Restore')
83+
Invoke-Click $app $button 400
84+
Pause 1.5
85+
Get-State
86+
Check 'clicking Restore restores the window' (-not [SnapWin]::IsZoomed([IntPtr]$win.Hwnd))
87+
Check 'at its earlier width' ([Math]::Abs(($win.Right - $win.Left) - $restoredWidth) -le 2) "$($win.Right - $win.Left) vs $restoredWidth"
88+
Check 'the chip reads Maximize again' (Test-ViewText $view 'Maximize')
89+
} finally {
90+
Stop-GuiApp $app | Out-Null
91+
}
92+
Say "$(if ($script:Failures) { 'FAILED' } else { 'OK' })"

0 commit comments

Comments
 (0)