@@ -121,6 +121,13 @@ static const wchar_t* kFocusReturnWindowProperty = L"NativeAPIFocusReturnWindow"
121121// hands the foreground back to it. Distinct from the focus-policy property above,
122122// which only a no-activate window uses and its policy change clears.
123123static const wchar_t * kBlurReturnWindowProperty = L" NativeAPIBlurReturnWindow" ;
124+ // The app-drawn maximize button (MaximizeButtonArea), owned by the HWND.
125+ static const wchar_t * kMaximizeButtonProperty = L" NativeAPIMaximizeButton" ;
126+ struct MaximizeButtonArea {
127+ Rectangle bounds; // Content-area logical coordinates.
128+ bool tracking_leave = false ; // TME_NONCLIENT | TME_LEAVE is armed.
129+ HWND hovered = nullptr ; // Content window that was sent the last move.
130+ };
124131// The translucent background color of the window, as 0x1AARRGGBB (the leading 1 tells
125132// a transparent black from "no property"). Set while the window is see-through.
126133static const wchar_t * kTranslucentBackgroundProperty = L" NativeAPITranslucentBackground" ;
@@ -379,6 +386,98 @@ static LRESULT HiddenFrameResizeHit(HWND root, LPARAM point) {
379386 return 0 ;
380387}
381388
389+ // Whether a screen point (WM_NCHITTEST's lParam) is on the app-drawn maximize
390+ // button of |root|, and where in its content area, in physical pixels.
391+ static bool IsOverMaximizeButton (HWND root, LPARAM point, POINT * client = nullptr ) {
392+ const auto * area = static_cast <MaximizeButtonArea*>(GetPropW (root, kMaximizeButtonProperty ));
393+ if (!area) return false ;
394+ POINT local = {static_cast <short >(LOWORD (point)), static_cast <short >(HIWORD (point))};
395+ if (!ScreenToClient (root, &local)) return false ;
396+ const double scale = GetScaleFactorForWindow (root);
397+ const double x = local.x / scale, y = local.y / scale;
398+ const Rectangle& b = area->bounds ;
399+ if (x < b.x || y < b.y || x >= b.x + b.width || y >= b.y + b.height ) return false ;
400+ if (client) *client = local;
401+ return true ;
402+ }
403+
404+ // The innermost visible, enabled content window under a client point of |root|.
405+ static HWND ContentWindowAt (HWND root, POINT client, POINT * local) {
406+ HWND target = root;
407+ POINT at = client;
408+ for (int depth = 0 ; depth < 32 ; ++depth) {
409+ HWND child = ChildWindowFromPointEx (target, at, CWP_SKIPINVISIBLE | CWP_SKIPDISABLED |
410+ CWP_SKIPTRANSPARENT );
411+ if (!child || child == target) break ;
412+ MapWindowPoints (target, child, &at, 1 );
413+ target = child;
414+ }
415+ *local = at;
416+ return target;
417+ }
418+
419+ // Passes a non-client mouse message over the maximize button on to the content
420+ // as the client message it would have been, so the app's button sees it.
421+ static void ForwardToContent (HWND root, MaximizeButtonArea* area, UINT message, LPARAM point) {
422+ POINT client = {static_cast <short >(LOWORD (point)), static_cast <short >(HIWORD (point))};
423+ ScreenToClient (root, &client);
424+ POINT local = {};
425+ HWND target = ContentWindowAt (root, client, &local);
426+ WPARAM keys = 0 ;
427+ if (GetKeyState (VK_LBUTTON ) < 0 ) keys |= MK_LBUTTON ;
428+ if (GetKeyState (VK_RBUTTON ) < 0 ) keys |= MK_RBUTTON ;
429+ if (GetKeyState (VK_MBUTTON ) < 0 ) keys |= MK_MBUTTON ;
430+ if (GetKeyState (VK_SHIFT ) < 0 ) keys |= MK_SHIFT ;
431+ if (GetKeyState (VK_CONTROL ) < 0 ) keys |= MK_CONTROL ;
432+ if (message == WM_LBUTTONDOWN ) keys |= MK_LBUTTON ;
433+ if (message == WM_LBUTTONUP ) keys &= ~static_cast <WPARAM >(MK_LBUTTON );
434+ area->hovered = target;
435+ SendMessageW (target, message, keys, MAKELPARAM (local.x , local.y ));
436+ }
437+
438+ // The maximize button's share of the window procedure: answer the hit test,
439+ // and hand hover, press and release to the content instead of letting
440+ // DefWindowProc run its caption-button loop (it would draw a classic button
441+ // and maximize on its own).
442+ static std::optional<LRESULT > HandleMaximizeButtonArea (HWND hwnd, UINT message, WPARAM wp,
443+ LPARAM lp) {
444+ auto * area = static_cast <MaximizeButtonArea*>(GetPropW (hwnd, kMaximizeButtonProperty ));
445+ if (!area) return std::nullopt ;
446+ switch (message) {
447+ case WM_NCHITTEST :
448+ if (!IsOverMaximizeButton (hwnd, lp)) return std::nullopt ;
449+ // A resize border overlapping the button keeps working.
450+ if (GetPropW (hwnd, kTitleBarHiddenProperty ) && HiddenFrameResizeHit (hwnd, lp) != 0 )
451+ return std::nullopt ;
452+ return HTMAXBUTTON ;
453+ case WM_NCMOUSEMOVE :
454+ if (wp != HTMAXBUTTON ) break ;
455+ if (!area->tracking_leave ) {
456+ TRACKMOUSEEVENT track = {sizeof (track), TME_LEAVE | TME_NONCLIENT , hwnd, 0 };
457+ area->tracking_leave = TrackMouseEvent (&track) != FALSE ;
458+ }
459+ ForwardToContent (hwnd, area, WM_MOUSEMOVE , lp);
460+ return 0 ;
461+ case WM_NCMOUSELEAVE :
462+ area->tracking_leave = false ;
463+ if (area->hovered && IsWindow (area->hovered )) SendMessageW (area->hovered , WM_MOUSELEAVE , 0 , 0 );
464+ area->hovered = nullptr ;
465+ break ; // DefWindowProc has its own bookkeeping for it.
466+ case WM_NCLBUTTONDOWN :
467+ case WM_NCLBUTTONDBLCLK :
468+ if (wp != HTMAXBUTTON ) break ;
469+ ForwardToContent (hwnd, area, WM_LBUTTONDOWN , lp);
470+ return 0 ;
471+ case WM_NCLBUTTONUP :
472+ if (wp != HTMAXBUTTON ) break ;
473+ ForwardToContent (hwnd, area, WM_LBUTTONUP , lp);
474+ return 0 ;
475+ default :
476+ break ;
477+ }
478+ return std::nullopt ;
479+ }
480+
382481// Subclass of the child windows of a window with a hidden title bar. A child that
383482// covers the content (a Flutter view, for one) is hit-tested before its parent;
384483// on a resize edge it steps aside so the parent can answer it.
@@ -387,7 +486,7 @@ static LRESULT CALLBACK TopEdgeChildProc(HWND child, UINT message, WPARAM wp, LP
387486 if (message == WM_NCHITTEST ) {
388487 // HTTRANSPARENT passes the hit test on to windows of the same thread only.
389488 HWND root = GetAncestor (child, GA_ROOT );
390- if (root && HiddenFrameResizeHit (root, lp) != 0 &&
489+ if (root && ( HiddenFrameResizeHit (root, lp) != 0 || IsOverMaximizeButton (root, lp)) &&
391490 GetWindowThreadProcessId (root, nullptr ) == GetCurrentThreadId ())
392491 return HTTRANSPARENT ;
393492 } else if (message == WM_NCDESTROY ) {
@@ -618,6 +717,7 @@ static LRESULT WindowLifetimeMessage(HWND hwnd, UINT message, WPARAM wp, LPARAM
618717 RemovePropW (hwnd, kNonActivatingProperty );
619718 RemovePropW (hwnd, kFocusReturnWindowProperty );
620719 RemovePropW (hwnd, kBlurReturnWindowProperty );
720+ delete static_cast <MaximizeButtonArea*>(RemovePropW (hwnd, kMaximizeButtonProperty ));
621721 RemovePropW (hwnd, kVisualEffectProperty );
622722 RemovePropW (hwnd, kCornerPreferenceProperty );
623723 RemovePropW (hwnd, kBackgroundColorProperty );
@@ -652,6 +752,10 @@ static LRESULT WindowLifetimeMessage(HWND hwnd, UINT message, WPARAM wp, LPARAM
652752 return result;
653753 }
654754#ifndef NATIVEAPI_ENABLE_WINUI3
755+ if (auto handled = HandleMaximizeButtonArea (hwnd, message, wp, lp)) return *handled;
756+ if (message == WM_PARENTNOTIFY && LOWORD (wp) == WM_CREATE &&
757+ GetPropW (hwnd, kMaximizeButtonProperty ))
758+ AttachTopEdgeChild (reinterpret_cast <HWND >(lp), 0 );
655759 if (message == WM_NCCALCSIZE || message == WM_NCHITTEST || message == WM_PARENTNOTIFY ||
656760 message == WM_NCACTIVATE || message == WM_NCPAINT || message == 0x00AE ||
657761 message == 0x00AF || message == WM_SETTEXT || message == WM_SETICON ) {
@@ -2373,6 +2477,50 @@ bool Window::IsSystemMenuSupported() {
23732477 return true ;
23742478}
23752479
2480+ bool Window::SetMaximizeButtonBounds (Rectangle bounds) {
2481+ #ifdef NATIVEAPI_ENABLE_WINUI3
2482+ (void )bounds;
2483+ return false ;
2484+ #else
2485+ HWND hwnd = pimpl_->hwnd_ ;
2486+ if (!hwnd || !IsWindow (hwnd) || !std::isfinite (bounds.x ) || !std::isfinite (bounds.y ) ||
2487+ !std::isfinite (bounds.width ) || !std::isfinite (bounds.height ))
2488+ return false ;
2489+ auto * area = static_cast <MaximizeButtonArea*>(GetPropW (hwnd, kMaximizeButtonProperty ));
2490+ if (bounds.width <= 0 || bounds.height <= 0 ) {
2491+ delete static_cast <MaximizeButtonArea*>(RemovePropW (hwnd, kMaximizeButtonProperty ));
2492+ return true ;
2493+ }
2494+ if (!area) {
2495+ area = new MaximizeButtonArea ();
2496+ if (!SetPropW (hwnd, kMaximizeButtonProperty , area)) {
2497+ delete area;
2498+ return false ;
2499+ }
2500+ // Content windows (a Flutter view) are hit-tested first: let them step
2501+ // aside over the button so this window can answer HTMAXBUTTON.
2502+ EnumChildWindows (hwnd, AttachTopEdgeChild, 0 );
2503+ }
2504+ area->bounds = bounds;
2505+ return true ;
2506+ #endif
2507+ }
2508+
2509+ Rectangle Window::GetMaximizeButtonBounds () const {
2510+ HWND hwnd = pimpl_->hwnd_ ;
2511+ const auto * area =
2512+ hwnd ? static_cast <MaximizeButtonArea*>(GetPropW (hwnd, kMaximizeButtonProperty )) : nullptr ;
2513+ return area ? area->bounds : Rectangle{0 , 0 , 0 , 0 };
2514+ }
2515+
2516+ bool Window::IsMaximizeButtonBoundsSupported () {
2517+ #ifdef NATIVEAPI_ENABLE_WINUI3
2518+ return false ;
2519+ #else
2520+ return true ;
2521+ #endif
2522+ }
2523+
23762524bool Window::PerformTitleBarDoubleClick () {
23772525 HWND hwnd = pimpl_->hwnd_ ;
23782526 if (!hwnd || !IsWindow (hwnd) || IsFullScreen () || IsIconic (hwnd) || !IsMaximizable ())
0 commit comments