Skip to content

Commit 790ae7d

Browse files
committed
Sync with core ca4a61c5d: confirm the macOS close button
On macOS 26 the title bar's close button sends -[NSWindow __close], not performClose:, and core let it close the window past every WindowCloseRequestedEvent listener. Found by the new window_manager GUI test; core now confirms it like performClose:. The Dart and Rust changelogs note it as Unreleased. New GUI tests: flutter_window_title_bar_close_test.py, the macOS twin of the Windows and Linux close tests, with real clicks on the close button; and flutter_window_manager_gui_test.py / .ps1 / _hyprland.py, which drive window_manager's WindowCaption, VirtualWindowFrame, prevent-close, system menu, mouse pass-through and destroy() on a fixture app (window_manager_flutter.dart). flutter_window_manager_test.py finds window_manager in its submodule.
1 parent 769d7f8 commit 790ae7d

10 files changed

Lines changed: 1083 additions & 3 deletions

‎bindings/dart/nativeapi/CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
## Unreleased
22

33
- Add desktop clipboard text, HTML, images, file paths, multi-format writes and change notifications.
4+
- macOS: the title bar's close button is a cancellable close request again.
5+
On macOS 26 it bypassed `WindowCloseRequestedEvent` and closed the window
6+
even when a listener cancelled.
47

58
## 0.5.2
69

‎bindings/rust/CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Unreleased
2+
3+
* macOS: the title bar's close button is a cancellable close request again.
4+
On macOS 26 it bypassed `WindowEvent::CloseRequested` and closed the window
5+
even when a listener cancelled.
6+
17
## 0.5.2
28

39
* Windows: `Display::name` is the monitor's name ("DELL U2720Q", or "Generic PnP

‎core‎

‎tools/gui/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ runner) lives in [`.agents/skills/`](../../.agents/skills); read
2222
| `flutter_window_title_bar_system_menu_test.ps1` | `window_title_bar_example` (#66) | Windows | End to end in a real Flutter app: a secondary click on the `DragToMoveArea` strip opens the native system menu at the pointer; its item states match the window (restored: Restore off; maximized: Move, Size, Maximize off); choosing Maximize maximizes and, from a second menu, Restore restores to the earlier width. Items are taken by their fixed order, so any display language works. Owner-guarded clicks, no keyboard. Set `TITLE_BAR_EXAMPLE_EXE` for a build outside `$RemoteWorkspace`. |
2323
| `flutter_window_title_bar_close_test.ps1` | `window_title_bar_example` (#65) | Windows | End to end in a real Flutter app: with Closing set to Keep open, the example cancels `WindowCloseRequestedEvent`; a real click on the window's own close button (found through DWM's caption button bounds) and the system menu's Close each leave the window open and count one cancelled request; with Allow, the close button closes the window and the app exits with code 0. Owner-guarded clicks, no keyboard. Set `TITLE_BAR_EXAMPLE_EXE` for a build outside `$RemoteWorkspace`. |
2424
| `flutter_window_title_bar_close_test_linux.py` | `window_title_bar_example` (#65) | Linux (GNOME) | The Linux twin of the close test: with Keep open, two real clicks on the header bar's close button (found by its accessible name through AT-SPI) are cancelled and counted; with Allow the click closes the window and the app exits with code 0. The Flutter view is located through AT-SPI too, since the runner's header bar sits inside the X11 client area. The system menu is gnome-shell's and is not driven. Set `TITLE_BAR_EXAMPLE_EXE` for a build outside `$REMOTE_WORKSPACE`. |
25+
| `flutter_window_title_bar_close_test.py` | `window_title_bar_example` (#65) | macOS | The macOS twin of the close test: with Keep open, two real clicks on the window's close button (found through Accessibility) are cancelled and counted; with Allow the click closes the window and the app exits with code 0. On macOS 26 the button sends the private `-[NSWindow __close]` instead of `performClose:`, which core confirms too; this is the test that caught it. |
26+
| `flutter_window_manager_gui_test.py` / `.ps1` / `_hyprland.py` | window_manager example, fixture `window_manager_flutter.dart` | macOS / Windows / Linux (Hyprland) | leanflutter/window_manager's widgets and close handling through its 0.5.x compatible API: `WindowCaption` drags, maximizes / restores and minimizes the window and, with `setPreventClose(true)`, its close button and the window's own close button (DWM's caption button bounds on Windows, Accessibility on macOS, GTK's header bar and the compositor's close request on Hyprland) report `onWindowClose` and keep it; `VirtualWindowFrame`'s edge resizes it; `popUpWindowMenu()` opens the system menu (Windows); `setIgnoreMouseEvents(true, forward: true)` passes hit testing through while hovering still reaches the window, with nothing pressed meanwhile; `destroy()` closes past prevent-close. The fixture is copied to `example/lib/gui_test_main.dart` and built with `-t`. On Hyprland a temporary rule floats the window; maximize, minimize and the resize edge are skipped there (suppressed by the default config, absent on Wayland, and the window reported maximized). Set `WINDOW_MANAGER_DIR` (macOS), `WINDOW_MANAGER_EXE` (Windows) or `WINDOW_MANAGER_BUNDLE` (Hyprland) for builds elsewhere. |
2527
| `flutter_window_reports_test.ps1` | fixture `window_reports_flutter.dart` (#75, #76, #77, #78) | Windows | The window_manager reports carried over to nativeapi-core, replayed through the Flutter Window API in a fixture app built from the workspace's Dart packages (`REPORTS_ARCHIVE`). The app paints solid colours and announces each step; the runner measures geometry, styles, DWM caption buttons, pixels and frame-by-frame samples. Scenarios (`REPORTS_SCENARIOS`): `startup-center` (#428 no white or black frame, #504/#223 appears where it stays, centred), `startup-maximize` (#412/#572 maximized before the first show appears maximized and stays so), `flashes` (#383/#153/#155 no white or black frame while maximizing, restoring or showing after hide, #578 a part dragged back from off screen painted within 100 ms), `fullscreen` (#458 start in full screen and leave it, #579 caption back, #330 no stale content at the bottom), `hidden` (#547 top border as thick as the sides, #554/#450 maximized exactly to the work area, #378 no padding in full screen, #397 no caption flash when leaving it), `transparent` (#576 caption buttons and title with a transparent background), `events` (#294 focus last after restoring, #207 double-click on the caption maximizes and restores with Resized, #511 maximize() from onDoubleTapDown, #534 setIcon() from flutter_assets). Owner-guarded clicks on the app's own window only, no keyboard. Not covered: #480 (keyboard) and #245 (two monitors). |
2628
| `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 |
2729
| `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 |
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# GUI test (Windows) of window_manager's widgets and close handling, on the
2+
# fixture window_manager_flutter.dart built into the window_manager example
3+
# (see flutter_window_manager_gui_test.py for the macOS twin):
4+
#
5+
# - WindowCaption: its title strip drags the window, its maximize button
6+
# maximizes and restores, its minimize button minimizes (the fixture restores
7+
# after 2 s), and with setPreventClose(true) its close button reports
8+
# onWindowClose and keeps the window. VirtualWindowFrame's top edge resizes.
9+
# - With the native title bar, the window's own close button (DWM's caption
10+
# button bounds) is prevented the same way.
11+
# - popUpWindowMenu() opens the system menu at the cursor.
12+
# - setIgnoreMouseEvents(true, forward: true): the window drops out of hit
13+
# testing (WindowFromPoint passes it) while hovering still reaches it. Nothing
14+
# is pressed while it ignores the mouse.
15+
# - destroy() closes the window past setPreventClose and the app exits.
16+
#
17+
# Owner-guarded clicks on the app's own window and menu only, no keyboard.
18+
# Set WINDOW_MANAGER_EXE to the built example (flutter build windows --debug
19+
# -t lib/gui_test_main.dart, with the fixture copied there).
20+
$ErrorActionPreference = 'Stop'
21+
. "$PSScriptRoot\env.ps1"
22+
. "$PSScriptRoot\winput.ps1"
23+
. "$PSScriptRoot\guiapp.ps1"
24+
Add-Type @"
25+
using System;
26+
using System.Runtime.InteropServices;
27+
public static class WmWin {
28+
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; }
29+
[DllImport("dwmapi.dll")] static extern int DwmGetWindowAttribute(IntPtr h, int a, out RECT r, int size);
30+
[DllImport("user32.dll")] public static extern bool IsWindow(IntPtr h);
31+
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
32+
[DllImport("user32.dll")] public static extern bool IsZoomed(IntPtr h);
33+
// DWMWA_CAPTION_BUTTON_BOUNDS, relative to the window: minimize, maximize, close.
34+
public static int[] CaptionButtons(IntPtr h) {
35+
RECT r; if (DwmGetWindowAttribute(h, 5, out r, Marshal.SizeOf(typeof(RECT))) != 0) return null;
36+
return new[] { r.L, r.T, r.R, r.B };
37+
}
38+
}
39+
"@
40+
41+
Start-Result "$RemoteScratch\flutter_window_manager_gui_test.result.txt"
42+
$exe = $env:WINDOW_MANAGER_EXE
43+
if (-not $exe) { throw 'Set WINDOW_MANAGER_EXE to the built fixture' }
44+
$caption = 'WM GUI caption'
45+
$button = 46
46+
47+
Assert-Idle
48+
$app = Start-GuiApp $exe
49+
$null = $app.Proc.Handle
50+
function Get-ViewsQuietly {
51+
$saved = $ErrorActionPreference
52+
$ErrorActionPreference = 'Continue'
53+
try { @(Get-Views $app 2>$null) } catch { @() } finally { $ErrorActionPreference = $saved }
54+
}
55+
function Get-State {
56+
$script:win = Get-Win $app 'wm gui test'
57+
if (-not $script:win) { throw 'The fixture window is missing' }
58+
$script:view = Find-View (Get-ViewsQuietly) 'TARGET'
59+
if (-not $script:view) { throw 'The fixture has no Flutter view to probe' }
60+
}
61+
function Get-Value([string]$Name) {
62+
Get-State
63+
$t = @($view.texts | % { $_[0] } | ? { $_ -like "$Name *" }) | Select-Object -First 1
64+
if ($null -eq $t) { throw "The $Name line is missing" }
65+
$t.Substring($Name.Length + 1)
66+
}
67+
function Press([string]$Label, [double]$Settle = 1.2) {
68+
Get-State
69+
Invoke-Click $app (ConvertTo-Screen $win (Get-TextCenter $view $Label)) 400
70+
Pause $Settle
71+
}
72+
# 0: close, 1: maximize / restore, 2: minimize.
73+
function Get-CaptionButton([int]$Index) {
74+
Get-State
75+
$c = Get-TextCenter $view $caption
76+
ConvertTo-Screen $win @(($view.size[0] - $button / 2 - $Index * $button), $c[1])
77+
}
78+
79+
try {
80+
$deadline = (Get-Date).AddSeconds(40)
81+
while (-not (Find-View (Get-ViewsQuietly) 'TARGET') -and (Get-Date) -lt $deadline) { Pause 0.5 }
82+
Get-State
83+
Invoke-Activate $app $win ([int](20 * $win.Scale))
84+
Check 'the fixture starts with WindowCaption' (Test-ViewText $view $caption)
85+
86+
# DragToMoveArea inside WindowCaption.
87+
Get-State
88+
$before = $win
89+
$start = ConvertTo-Screen $win (Get-TextCenter $view $caption)
90+
Invoke-Drag $app $start @(@(($start[0] + 40), ($start[1] + 20), 400), @(($start[0] + 160), ($start[1] + 100), 800))
91+
Pause 1.5
92+
Get-State
93+
Check 'dragging the caption moved the window' ([math]::Abs($win.Left - $before.Left - 160) -le 8 -and [math]::Abs($win.Top - $before.Top - 100) -le 8) "from $($before.Left),$($before.Top) to $($win.Left),$($win.Top)"
94+
95+
# VirtualWindowFrame's top edge (DragToResizeArea).
96+
$before = $win
97+
$edge = ConvertTo-Screen $win @(($view.size[0] / 2 - 120), 3)
98+
Invoke-Drag $app $edge @(@($edge[0], ($edge[1] - 20), 400), @($edge[0], ($edge[1] - 80), 800))
99+
Pause 1.5
100+
Get-State
101+
# The drag legs are physical pixels: 80 up.
102+
Check 'the top edge resized the window' ([math]::Abs(($win.ClientH - $before.ClientH) - 80) -le 6) "client height $($before.ClientH) -> $($win.ClientH)"
103+
104+
# Maximize and restore with the caption's own button.
105+
$before = $win
106+
Invoke-Click $app (Get-CaptionButton 1) 400
107+
Pause 1.8
108+
Get-State
109+
Check 'the caption maximize button maximized the window' ([WmWin]::IsZoomed([IntPtr]$win.Hwnd))
110+
Check 'and isMaximized says so' ((Get-Value 'maximized') -eq 'true') (Get-Value 'maximized')
111+
Invoke-Click $app (Get-CaptionButton 1) 400
112+
Pause 1.8
113+
Get-State
114+
Check 'pressing it again restored the window' (-not [WmWin]::IsZoomed([IntPtr]$win.Hwnd) -and [math]::Abs($win.ClientW - $before.ClientW) -le 4) "client width $($before.ClientW) -> $($win.ClientW)"
115+
$ev = Get-Value 'events'
116+
Check 'and the legacy events saw maximize, unmaximize' ($ev -like '*maximize*' -and $ev -like '*unmaximize*') $ev
117+
118+
# Minimize; the fixture restores after 2 s.
119+
Invoke-Click $app (Get-CaptionButton 2) 400
120+
Pause 0.8
121+
$iconic = [WmWin]::IsIconic([IntPtr]$win.Hwnd)
122+
Pause 3
123+
$ev = Get-Value 'events'
124+
Check 'the caption minimize button minimized the window' $iconic
125+
Check 'and it came back, with minimize and restore reported' (-not [WmWin]::IsIconic([IntPtr]$win.Hwnd) -and $ev -like '*minimize*' -and $ev -like '*restore*') $ev
126+
Get-State
127+
Invoke-Activate $app $win ([int](20 * $win.Scale))
128+
129+
# Prevented close through the caption's close button.
130+
Press 'Prevent close on'
131+
Invoke-Click $app (Get-CaptionButton 0) 400
132+
Pause 1.5
133+
Check 'with preventClose, the caption close button reports onWindowClose' ((Get-Value 'closeReports') -eq '1') (Get-Value 'closeReports')
134+
Check 'and the window stays' ([WmWin]::IsWindow([IntPtr]$win.Hwnd) -and -not $app.Proc.HasExited)
135+
136+
# Prevented close through the window's own close button.
137+
Press 'Native title bar' 1.5
138+
Get-State
139+
$b = [WmWin]::CaptionButtons([IntPtr]$win.Hwnd)
140+
if (-not $b) { throw 'No caption button bounds' }
141+
$third = [int](($b[2] - $b[0]) / 3)
142+
Invoke-Click $app @(($win.Left + $b[2] - [int]($third / 2)), ($win.Top + [int](($b[1] + $b[3]) / 2))) 400
143+
Pause 1.5
144+
Check 'with preventClose, the native close button reports onWindowClose' ((Get-Value 'closeReports') -eq '2') (Get-Value 'closeReports')
145+
Check 'and the window stays' ([WmWin]::IsWindow([IntPtr]$win.Hwnd) -and -not $app.Proc.HasExited)
146+
Press 'Hidden title bar' 1.5
147+
148+
# popUpWindowMenu at the cursor, which rests on the chip.
149+
Press 'Menu in 1.5s' 0.2
150+
$opened = Wait-Menu $app
151+
Check 'popUpWindowMenu() opened the system menu' $opened
152+
if ($opened) {
153+
$menu = @(Get-OpenMenus $app)[0]
154+
Check 'with the system menu items' ($menu.Items.Count -ge 5) "$($menu.Items.Count) items"
155+
# Close it by clicking the app's own window clear of the menu.
156+
Get-State
157+
Invoke-Click $app (ConvertTo-Screen $win @(($view.size[0] - 40), ($view.size[1] - 30))) 400
158+
Check 'and it closes again' (Wait-Menu $app -Closed)
159+
}
160+
161+
# Ignore the mouse, forwarding moves; no presses while it lasts.
162+
$hovers = [int](Get-Value 'hovers')
163+
Press 'Ignore mouse 8s' 0.8
164+
Check 'setIgnoreMouseEvents is on' ((Get-Value 'ignoring') -eq 'true')
165+
Get-State
166+
$c = Get-TextCenter $view 'TARGET'
167+
$pt = ConvertTo-Screen $win $c
168+
$passed = $true
169+
foreach ($d in @(@(-80, -30), @(60, 20), @(-20, 40), @(90, -10))) {
170+
$p = ConvertTo-Screen $win @(($c[0] + $d[0]), ($c[1] + $d[1]))
171+
Move-Cursor $p 300
172+
if ([WInput]::Owner($p[0], $p[1]) -eq $app.Proc.Id) { $passed = $false }
173+
}
174+
Pause 0.5
175+
Check 'hit testing passes through the window' $passed
176+
$now = [int](Get-Value 'hovers')
177+
Check 'with forward, hovering still reaches the window' ($now -gt $hovers) "$hovers -> $now"
178+
Pause 8
179+
Check 'the window takes the mouse back after 8 s' ((Get-Value 'ignoring') -eq 'false' -and [WInput]::Owner($pt[0], $pt[1]) -eq $app.Proc.Id)
180+
$clicks = [int](Get-Value 'clicks')
181+
Press 'TARGET'
182+
Check 'a click lands again' ([int](Get-Value 'clicks') -eq $clicks + 1) (Get-Value 'clicks')
183+
184+
# destroy() past preventClose.
185+
Check 'preventClose is still on' ((Get-Value 'preventClose') -eq 'true')
186+
Press 'destroy()' 0.2
187+
$exited = $app.Proc.WaitForExit(10000)
188+
Check 'destroy() closed the window and the app exited' $exited
189+
} catch {
190+
Check 'the scenario ran to the end' $false "$($_.Exception.Message) at line $($_.InvocationInfo.ScriptLineNumber)"
191+
} finally {
192+
if (-not $app.Proc.HasExited) { Stop-GuiApp $app | Out-Null }
193+
}
194+
Say "$(if ($script:Failures) { 'FAILED' } else { 'OK' })"

0 commit comments

Comments
 (0)