66#include < commctrl.h>
77#include < cmath>
88#include < iostream>
9+ #include < optional>
910#include " ../../foundation/id_allocator.h"
1011#include " ../../window.h"
1112#include " ../../window_manager.h"
@@ -20,17 +21,111 @@ namespace nativeapi {
2021
2122// Property name for storing window ID in HWND
2223static const wchar_t * kWindowIdProperty = L" NativeAPIWindowId" ;
24+ // Set while the title bar is hidden. Kept on the HWND, like the style itself, so every
25+ // wrapper of the window agrees and the frame handling below outlives any one wrapper.
26+ static const wchar_t * kTitleBarHiddenProperty = L" NativeAPITitleBarHidden" ;
27+
28+ #ifndef NATIVEAPI_ENABLE_WINUI3
29+ // A window with WS_THICKFRAME but no WS_CAPTION keeps its sizing frame on all sides.
30+ // The left, right and bottom parts are invisible, but the top part is painted as a
31+ // band above the content. That band is given to the client area (WM_NCCALCSIZE), and
32+ // the top edge stays resizable through hit testing: the window answers HTTOP there,
33+ // and child windows covering the content let the hit test through to it.
34+
35+ // The resize hit code for a screen point in the top band of `root`, or 0.
36+ static LRESULT TopResizeHit (HWND root, LPARAM point) {
37+ if (!GetPropW (root, kTitleBarHiddenProperty ) || IsZoomed (root) ||
38+ !(GetWindowLongPtrW (root, GWL_STYLE ) & WS_THICKFRAME ))
39+ return 0 ;
40+ RECT window;
41+ GetWindowRect (root, &window);
42+ // The sizing frame is as thick at the top as on the left, where it is still in place.
43+ POINT client_origin = {0 , 0 };
44+ ClientToScreen (root, &client_origin);
45+ const int frame = client_origin.x - window.left ;
46+ // GET_X_LPARAM / GET_Y_LPARAM, without <windowsx.h>: its IsMaximized() and
47+ // IsMinimized() macros would rename Window's methods of the same name.
48+ const int x = static_cast <short >(LOWORD (point));
49+ const int y = static_cast <short >(HIWORD (point));
50+ if (y < window.top || y >= window.top + frame || x < window.left || x >= window.right )
51+ return 0 ;
52+ if (x < window.left + frame) return HTTOPLEFT ;
53+ if (x >= window.right - frame) return HTTOPRIGHT ;
54+ return HTTOP ;
55+ }
56+
57+ // Subclass of the child windows of a window with a hidden title bar. A child that
58+ // covers the content (a Flutter view, for one) is hit-tested before its parent;
59+ // in the top resize band it steps aside so the parent can answer HTTOP.
60+ static LRESULT CALLBACK TopEdgeChildProc (HWND child, UINT message, WPARAM wp, LPARAM lp,
61+ UINT_PTR subclass_id, DWORD_PTR ) {
62+ if (message == WM_NCHITTEST ) {
63+ // HTTRANSPARENT passes the hit test on to windows of the same thread only.
64+ HWND root = GetAncestor (child, GA_ROOT );
65+ if (root && TopResizeHit (root, lp) != 0 &&
66+ GetWindowThreadProcessId (root, nullptr ) == GetCurrentThreadId ())
67+ return HTTRANSPARENT ;
68+ } else if (message == WM_NCDESTROY ) {
69+ RemoveWindowSubclass (child, TopEdgeChildProc, subclass_id);
70+ }
71+ return DefSubclassProc (child, message, wp, lp);
72+ }
73+
74+ static BOOL CALLBACK AttachTopEdgeChild (HWND child, LPARAM ) {
75+ // Fails for windows of other threads, which cannot be subclassed; that is fine.
76+ SetWindowSubclass (child, TopEdgeChildProc, 1 , 0 );
77+ return TRUE ;
78+ }
79+
80+ static std::optional<LRESULT > HandleHiddenTitleBarFrame (HWND hwnd, UINT message, WPARAM wp,
81+ LPARAM lp) {
82+ if (message == WM_PARENTNOTIFY && LOWORD (wp) == WM_CREATE ) {
83+ if (GetPropW (hwnd, kTitleBarHiddenProperty )) AttachTopEdgeChild (reinterpret_cast <HWND >(lp), 0 );
84+ return std::nullopt ;
85+ }
86+ if (!GetPropW (hwnd, kTitleBarHiddenProperty )) return std::nullopt ;
87+ if (message == WM_NCCALCSIZE && wp) {
88+ auto * params = reinterpret_cast <NCCALCSIZE_PARAMS *>(lp);
89+ const LONG top = params->rgrc [0 ].top ;
90+ const LRESULT result = DefSubclassProc (hwnd, message, wp, lp);
91+ if (!IsZoomed (hwnd)) {
92+ params->rgrc [0 ].top = top;
93+ return result;
94+ }
95+ // Without a caption, maximizing sizes the whole window to the monitor, so the
96+ // content would run under the taskbar and stop short on the right. Fit it to
97+ // the work area instead.
98+ MONITORINFO monitor = {sizeof (monitor)};
99+ if (GetMonitorInfoW (MonitorFromWindow (hwnd, MONITOR_DEFAULTTONEAREST ), &monitor))
100+ params->rgrc [0 ] = monitor.rcWork ;
101+ return result;
102+ }
103+ if (message == WM_NCHITTEST ) {
104+ const LRESULT hit = DefSubclassProc (hwnd, message, wp, lp);
105+ if (hit != HTCLIENT ) return hit;
106+ const LRESULT top = TopResizeHit (hwnd, lp);
107+ return top != 0 ? top : hit;
108+ }
109+ return std::nullopt ;
110+ }
111+ #endif
23112
24113// Registry entries follow the HWND lifetime, not any one C++ wrapper.
25114static LRESULT CALLBACK WindowLifetimeProc (HWND hwnd, UINT message, WPARAM wp, LPARAM lp,
26115 UINT_PTR subclass_id, DWORD_PTR reference) {
27116 if (message == WM_NCDESTROY ) {
28117 RemoveWindowSubclass (hwnd, WindowLifetimeProc, subclass_id);
29118 RemovePropW (hwnd, kWindowIdProperty );
119+ RemovePropW (hwnd, kTitleBarHiddenProperty );
30120 const auto result = DefSubclassProc (hwnd, message, wp, lp);
31121 WindowRegistry::GetInstance ().Remove (static_cast <WindowId>(reference));
32122 return result;
33123 }
124+ #ifndef NATIVEAPI_ENABLE_WINUI3
125+ if (message == WM_NCCALCSIZE || message == WM_NCHITTEST || message == WM_PARENTNOTIFY ) {
126+ if (auto handled = HandleHiddenTitleBarFrame (hwnd, message, wp, lp)) return *handled;
127+ }
128+ #endif
34129 return DefSubclassProc (hwnd, message, wp, lp);
35130}
36131static void TrackWindowLifetime (HWND hwnd, WindowId id) {
@@ -44,13 +139,9 @@ static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM l
44139class Window ::Impl {
45140 public:
46141 Impl (HWND hwnd, WindowId id)
47- : hwnd_(hwnd),
48- window_id_ (id),
49- title_bar_style_(TitleBarStyle::Normal),
50- visual_effect_(VisualEffect::None) {}
142+ : hwnd_(hwnd), window_id_(id), visual_effect_(VisualEffect::None) {}
51143 HWND hwnd_;
52144 WindowId window_id_;
53- TitleBarStyle title_bar_style_;
54145 VisualEffect visual_effect_;
55146 Size min_size_{0 , 0 };
56147 Size max_size_{0 , 0 };
@@ -963,7 +1054,16 @@ void Window::SetTitleBarStyle(TitleBarStyle style) {
9631054 else flags |= WS_CAPTION ;
9641055 SetWindowLongPtrW (pimpl_->hwnd_ , GWL_STYLE , flags);
9651056#endif
966- pimpl_->title_bar_style_ = style;
1057+ // Read by WM_NCCALCSIZE during the frame change below.
1058+ if (style == TitleBarStyle::Hidden) {
1059+ SetPropW (pimpl_->hwnd_ , kTitleBarHiddenProperty , reinterpret_cast <HANDLE >(1 ));
1060+ #ifndef NATIVEAPI_ENABLE_WINUI3
1061+ // Existing children; later ones are attached as they are created (WM_PARENTNOTIFY).
1062+ EnumChildWindows (pimpl_->hwnd_ , AttachTopEdgeChild, 0 );
1063+ #endif
1064+ } else {
1065+ RemovePropW (pimpl_->hwnd_ , kTitleBarHiddenProperty );
1066+ }
9671067
9681068 // Get current window rect
9691069 RECT rect;
@@ -980,7 +1080,9 @@ void Window::SetTitleBarStyle(TitleBarStyle style) {
9801080}
9811081
9821082TitleBarStyle Window::GetTitleBarStyle () const {
983- return pimpl_->title_bar_style_ ;
1083+ if (pimpl_->hwnd_ && GetPropW (pimpl_->hwnd_ , kTitleBarHiddenProperty ))
1084+ return TitleBarStyle::Hidden;
1085+ return TitleBarStyle::Normal;
9841086}
9851087
9861088void Window::SetHasShadow (bool has_shadow) {
0 commit comments