Skip to content

Commit ffb4a45

Browse files
committed
Window full-screen events, and a full-screen fix for the title bar on Windows
WindowEnteredFullScreenEvent and WindowExitedFullScreenEvent report a window entering and leaving full screen: on macOS from AppKit's did-enter/did-exit notifications (the green button, the View menu and SetFullScreen() alike), on Linux from the window manager's GDK_WINDOW_STATE_FULLSCREEN, and on Windows, which has no full-screen window state, from SetFullScreen(). They are appended after WindowClosedEvent, so the C event type values stay as they were. Windows: SetTitleBarStyle() on a full-screen window no longer adds WS_CAPTION to it, which put a title bar on the full-screen window; it records the caption for leaving full screen instead, also for a window that had none before. A maximized window with a hidden title bar now covers the taskbar in full screen: the maximized clamp to the work area in WM_NCCALCSIZE skips full-screen windows. Fixes #58
1 parent cd7cd6d commit ffb4a45

7 files changed

Lines changed: 150 additions & 6 deletions

File tree

‎src/capi/window_c.cpp‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,16 @@ bool to_c_window_event(const nativeapi::WindowEvent& event, native_window_event_
13901390
(void)typed;
13911391
return true;
13921392
}
1393+
if (const auto* typed = dynamic_cast<const nativeapi::WindowEnteredFullScreenEvent*>(&event)) {
1394+
out->type = NATIVE_WINDOW_EVENT_TYPE_ENTERED_FULL_SCREEN;
1395+
(void)typed;
1396+
return true;
1397+
}
1398+
if (const auto* typed = dynamic_cast<const nativeapi::WindowExitedFullScreenEvent*>(&event)) {
1399+
out->type = NATIVE_WINDOW_EVENT_TYPE_EXITED_FULL_SCREEN;
1400+
(void)typed;
1401+
return true;
1402+
}
13931403
return false;
13941404
}
13951405

‎src/capi/window_c.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ typedef enum {
8484
NATIVE_WINDOW_EVENT_TYPE_RESIZED = 6,
8585
NATIVE_WINDOW_EVENT_TYPE_CREATED = 7,
8686
NATIVE_WINDOW_EVENT_TYPE_CLOSED = 8,
87+
NATIVE_WINDOW_EVENT_TYPE_ENTERED_FULL_SCREEN = 9,
88+
NATIVE_WINDOW_EVENT_TYPE_EXITED_FULL_SCREEN = 10,
8789
} native_window_event_type_t;
8890

8991
/// One WindowEvent, tagged by its concrete type.

‎src/platform/linux/window_manager_linux.cpp‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ static void InstallGlobalSwizzling() {
284284
}
285285

286286
// Show-state and geometry tracking for WindowMinimizedEvent, WindowMaximizedEvent,
287-
// WindowRestoredEvent, WindowMovedEvent and WindowResizedEvent. The windows are
287+
// WindowRestoredEvent, the full-screen pair, WindowMovedEvent and WindowResizedEvent. The windows are
288288
// usually created by someone else (Flutter's runner, the host application), so
289289
// the two toplevel signals are watched with emission hooks, like the focus pair.
290290
static gulong g_window_state_hook_id = 0;
@@ -375,6 +375,11 @@ static gboolean on_window_state_emission_hook(GSignalInvocationHint* ihint,
375375
g_window_signal_fn(g_window_signal_context, id,
376376
(state & GDK_WINDOW_STATE_MAXIMIZED) ? "maximized" : "restored");
377377
}
378+
if (changed & GDK_WINDOW_STATE_FULLSCREEN) {
379+
g_window_signal_fn(g_window_signal_context, id,
380+
(state & GDK_WINDOW_STATE_FULLSCREEN) ? "entered_full_screen"
381+
: "exited_full_screen");
382+
}
378383
return TRUE; // Continue emission
379384
}
380385

@@ -610,6 +615,12 @@ class WindowManager::Impl {
610615
} else if (event_type == "restored") {
611616
WindowRestoredEvent event(window_id);
612617
manager_->DispatchWindowEvent(event);
618+
} else if (event_type == "entered_full_screen") {
619+
WindowEnteredFullScreenEvent event(window_id);
620+
manager_->DispatchWindowEvent(event);
621+
} else if (event_type == "exited_full_screen") {
622+
WindowExitedFullScreenEvent event(window_id);
623+
manager_->DispatchWindowEvent(event);
613624
} else if (event_type == "moved" || event_type == "resized") {
614625
// Report what the getters return (the frame, decorations included), not
615626
// the content rectangle configure-event talks about.

‎src/platform/macos/window_manager_macos.mm‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,20 @@ - (void)windowDidMove:(NSNotification*)notification {
196196
}
197197
}
198198

199+
- (void)windowDidEnterFullScreen:(NSNotification*)notification {
200+
NSWindow* window = [notification object];
201+
if (_impl && window && nativeapi::g_window_event_trampoline) {
202+
nativeapi::g_window_event_trampoline(_impl, window, "entered_full_screen");
203+
}
204+
}
205+
206+
- (void)windowDidExitFullScreen:(NSNotification*)notification {
207+
NSWindow* window = [notification object];
208+
if (_impl && window && nativeapi::g_window_event_trampoline) {
209+
nativeapi::g_window_event_trampoline(_impl, window, "exited_full_screen");
210+
}
211+
}
212+
199213
- (void)windowDidChangeOcclusionState:(NSNotification*)notification {
200214
// Posted when a window comes on screen, for which AppKit has no notification
201215
// of its own
@@ -279,6 +293,14 @@ static WindowId ResolveWindowId(NSWindow* ns_window) {
279293
selector:@selector(windowDidMove:)
280294
name:NSWindowDidMoveNotification
281295
object:nil];
296+
[center addObserver:delegate_
297+
selector:@selector(windowDidEnterFullScreen:)
298+
name:NSWindowDidEnterFullScreenNotification
299+
object:nil];
300+
[center addObserver:delegate_
301+
selector:@selector(windowDidExitFullScreen:)
302+
name:NSWindowDidExitFullScreenNotification
303+
object:nil];
282304
[center addObserver:delegate_
283305
selector:@selector(windowDidChangeOcclusionState:)
284306
name:NSWindowDidChangeOcclusionStateNotification
@@ -377,6 +399,12 @@ static WindowId ResolveWindowId(NSWindow* ns_window) {
377399
} else if (event_type == "restored") {
378400
WindowRestoredEvent event(window_id);
379401
manager_->DispatchWindowEvent(event);
402+
} else if (event_type == "entered_full_screen") {
403+
WindowEnteredFullScreenEvent event(window_id);
404+
manager_->DispatchWindowEvent(event);
405+
} else if (event_type == "exited_full_screen") {
406+
WindowExitedFullScreenEvent event(window_id);
407+
manager_->DispatchWindowEvent(event);
380408
} else if (event_type == "resized") {
381409
// The frame size, which is what Window::GetSize() returns
382410
NSRect frame = [window frame];

‎src/platform/windows/window_manager_windows.cpp‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,23 @@ using WindowChangedFn = void (*)(void* impl, DWORD event, HWND hwnd);
132132
static WindowChangedFn g_window_changed_fn = nullptr;
133133
static void* g_window_changed_context = nullptr;
134134

135+
// Window::SetFullScreen() reports its changes here: Windows has no full-screen
136+
// window state of its own, so there is nothing to be notified about.
137+
using FullScreenChangedFn = void (*)(void* impl, HWND hwnd, bool is_full_screen);
138+
static FullScreenChangedFn g_full_screen_changed_fn = nullptr;
139+
static void* g_full_screen_changed_context = nullptr;
140+
141+
} // namespace
142+
143+
// Declared in window_windows.cpp, hence outside the anonymous namespace.
144+
void NotifyWindowFullScreenChanged(HWND hwnd, bool is_full_screen) {
145+
if (g_full_screen_changed_fn && g_full_screen_changed_context) {
146+
g_full_screen_changed_fn(g_full_screen_changed_context, hwnd, is_full_screen);
147+
}
148+
}
149+
150+
namespace {
151+
135152
// Top-level windows that a listener can make sense of: anything already known
136153
// to the library, otherwise what the user sees as a window. Tooltips, menus and
137154
// other helper windows move and resize as well and must not get an id for it.
@@ -481,6 +498,11 @@ class WindowManager::Impl {
481498
g_window_changed_fn = [](void* impl, DWORD event, HWND hwnd) {
482499
static_cast<Impl*>(impl)->OnWindowChanged(event, hwnd);
483500
};
501+
g_full_screen_changed_context = this;
502+
g_full_screen_changed_fn = [](void* impl, HWND hwnd, bool is_full_screen) {
503+
static_cast<Impl*>(impl)->OnWindowEvent(
504+
hwnd, is_full_screen ? "entered_full_screen" : "exited_full_screen");
505+
};
484506

485507
// Unlike the foreground hook this only concerns our own windows, so it is
486508
// scoped to this process and costs nothing while other applications work.
@@ -526,6 +548,8 @@ class WindowManager::Impl {
526548
g_foreground_changed_context = nullptr;
527549
g_window_changed_fn = nullptr;
528550
g_window_changed_context = nullptr;
551+
g_full_screen_changed_fn = nullptr;
552+
g_full_screen_changed_context = nullptr;
529553
for (const auto& [hwnd, snapshot] : g_window_snapshots) {
530554
UnwatchWindowGeometry(hwnd);
531555
}
@@ -648,6 +672,12 @@ class WindowManager::Impl {
648672
} else if (event_type == "maximized") {
649673
WindowMaximizedEvent event(window_id);
650674
manager_->DispatchWindowEvent(event);
675+
} else if (event_type == "entered_full_screen") {
676+
WindowEnteredFullScreenEvent event(window_id);
677+
manager_->DispatchWindowEvent(event);
678+
} else if (event_type == "exited_full_screen") {
679+
WindowExitedFullScreenEvent event(window_id);
680+
manager_->DispatchWindowEvent(event);
651681
} else if (event_type == "resized" || event_type == "moved") {
652682
// Report what the getters return (logical pixels), not the raw rectangle
653683
auto window = WindowRegistry::GetInstance().Get(window_id);

‎src/platform/windows/window_windows.cpp‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ struct FullScreenState {
207207
};
208208
static std::unordered_map<HWND, FullScreenState> g_full_screen_windows;
209209

210+
// Emits the full-screen pair; defined with the rest of the window events in
211+
// window_manager_windows.cpp.
212+
void NotifyWindowFullScreenChanged(HWND hwnd, bool is_full_screen);
213+
210214
// The frame bits taken away while a window is full screen, and put back afterwards.
211215
static constexpr LONG_PTR kFullScreenRemovedStyle = WS_CAPTION | WS_THICKFRAME;
212216
static constexpr LONG_PTR kFullScreenRemovedExStyle =
@@ -297,7 +301,9 @@ static std::optional<LRESULT> HandleHiddenTitleBarFrame(HWND hwnd, UINT message,
297301
auto* params = reinterpret_cast<NCCALCSIZE_PARAMS*>(lp);
298302
const LONG top = params->rgrc[0].top;
299303
const LRESULT result = DefSubclassProc(hwnd, message, wp, lp);
300-
if (!IsZoomed(hwnd)) {
304+
// A full screen window keeps WS_MAXIMIZE when it was maximized, but it has no
305+
// frame to hang past the monitor and must cover the taskbar too.
306+
if (!IsZoomed(hwnd) || g_full_screen_windows.count(hwnd)) {
301307
params->rgrc[0].top = top;
302308
return result;
303309
}
@@ -690,6 +696,7 @@ void Window::SetFullScreen(bool is_full_screen) {
690696
monitor.rcMonitor.right - monitor.rcMonitor.left,
691697
monitor.rcMonitor.bottom - monitor.rcMonitor.top,
692698
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
699+
NotifyWindowFullScreenChanged(hwnd, true);
693700
return;
694701
}
695702

@@ -709,6 +716,7 @@ void Window::SetFullScreen(bool is_full_screen) {
709716
SetWindowPlacement(hwnd, &state.placement);
710717
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
711718
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
719+
NotifyWindowFullScreenChanged(hwnd, false);
712720
}
713721

714722
bool Window::IsFullScreen() const {
@@ -1393,13 +1401,19 @@ void Window::SetTitleBarStyle(TitleBarStyle style) {
13931401
#ifdef NATIVEAPI_ENABLE_WINUI3
13941402
if (!SetWinUI3TitleBarStyle(pimpl_->hwnd_, style)) return;
13951403
#else
1396-
LONG_PTR flags = GetWindowLongPtrW(pimpl_->hwnd_, GWL_STYLE);
13971404
// Keep the native caption style even when its non-client area is hidden by
13981405
// HandleHiddenTitleBarFrame. Removing it makes Windows maximize the outer
13991406
// window over the entire monitor, which also hides the taskbar. Clamping only
14001407
// the client area in WM_NCCALCSIZE cannot fix the shell's fullscreen detection.
1401-
flags |= WS_CAPTION;
1402-
SetWindowLongPtrW(pimpl_->hwnd_, GWL_STYLE, flags);
1408+
const auto full_screen = g_full_screen_windows.find(pimpl_->hwnd_);
1409+
if (full_screen != g_full_screen_windows.end()) {
1410+
// A full screen window shows no caption; leaving full screen puts it back,
1411+
// also on a window that had none when it went full screen.
1412+
full_screen->second.style |= WS_CAPTION;
1413+
} else {
1414+
SetWindowLongPtrW(pimpl_->hwnd_, GWL_STYLE,
1415+
GetWindowLongPtrW(pimpl_->hwnd_, GWL_STYLE) | WS_CAPTION);
1416+
}
14031417
#endif
14041418
// Read by WM_NCCALCSIZE during the frame change below.
14051419
if (style == TitleBarStyle::Hidden) {

‎src/window.h‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,10 @@ class Window : public NativeObjectProvider, public std::enable_shared_from_this<
342342
* @param is_full_screen true to enter fullscreen mode, false to exit
343343
*
344344
* In fullscreen mode, the window occupies the entire screen with no
345-
* window decorations (title bar, borders) visible.
345+
* window decorations (title bar, borders) visible. The change is reported by
346+
* WindowEnteredFullScreenEvent and WindowExitedFullScreenEvent; on macOS and
347+
* Linux it completes asynchronously, so IsFullScreen() may still return the old
348+
* state right after this call.
346349
*/
347350
void SetFullScreen(bool is_full_screen);
348351

@@ -1596,4 +1599,50 @@ class WindowClosedEvent : public WindowEvent {
15961599
std::string GetTypeName() const override { return "WindowClosedEvent"; }
15971600
};
15981601

1602+
/**
1603+
* Event class for a window entering full screen
1604+
*
1605+
* This event is emitted once a window is full screen, whoever made it so: SetFullScreen(),
1606+
* or the user through the system's own controls where there are any. IsFullScreen()
1607+
* already returns true. The new size is reported by WindowResizedEvent.
1608+
*
1609+
* @note Platform availability:
1610+
* - macOS: ✅ Fully supported - The green button, the View menu and SetFullScreen() alike;
1611+
* emitted once the full-screen animation has finished.
1612+
* - Windows: ⚠️ SetFullScreen() only - Windows has no full-screen window state, so a window
1613+
* another component makes borderless and monitor-sized is not reported.
1614+
* - Linux: ✅ Fully supported - Whatever the window manager reports, SetFullScreen() and
1615+
* its own shortcuts (F11 in many) alike; emitted when the window manager confirms it.
1616+
* - Android: ❌ Not applicable - Never emitted
1617+
* - iOS: ❌ Not applicable - Never emitted
1618+
* - OpenHarmony: ❌ Not applicable - Never emitted
1619+
*/
1620+
class WindowEnteredFullScreenEvent : public WindowEvent {
1621+
public:
1622+
explicit WindowEnteredFullScreenEvent(WindowId window_id) : WindowEvent(window_id) {}
1623+
1624+
std::string GetTypeName() const override { return "WindowEnteredFullScreenEvent"; }
1625+
};
1626+
1627+
/**
1628+
* Event class for a window leaving full screen
1629+
*
1630+
* This event is emitted once a window is no longer full screen, whoever ended it.
1631+
* IsFullScreen() already returns false.
1632+
*
1633+
* @note Platform availability:
1634+
* - macOS: ✅ Fully supported - Emitted once the animation back has finished.
1635+
* - Windows: ⚠️ SetFullScreen() only - See WindowEnteredFullScreenEvent.
1636+
* - Linux: ✅ Fully supported - Emitted when the window manager confirms it.
1637+
* - Android: ❌ Not applicable - Never emitted
1638+
* - iOS: ❌ Not applicable - Never emitted
1639+
* - OpenHarmony: ❌ Not applicable - Never emitted
1640+
*/
1641+
class WindowExitedFullScreenEvent : public WindowEvent {
1642+
public:
1643+
explicit WindowExitedFullScreenEvent(WindowId window_id) : WindowEvent(window_id) {}
1644+
1645+
std::string GetTypeName() const override { return "WindowExitedFullScreenEvent"; }
1646+
};
1647+
15991648
} // namespace nativeapi

0 commit comments

Comments
 (0)