From 931c257de7f077f104362122f2202810612dd347 Mon Sep 17 00:00:00 2001 From: Mitchell Cohen Date: Tue, 17 Feb 2026 15:23:54 -0500 Subject: [PATCH] fix: accurate window sizing and support for content sizing on Linux/Wayland with CSD (#49209) * fix window sizing and content sizing on Linux when CSD is in use * fixed size constraints * simplify min/max size calculation * use base window size for min/max * moved windows min/max size overrides * remove unnecessary checks for client frame * cleanup --- shell/browser/native_window_views.cc | 76 +++++++++++++++---- shell/browser/native_window_views.h | 9 +++ ...electron_desktop_window_tree_host_linux.cc | 17 ++--- .../electron_desktop_window_tree_host_linux.h | 2 +- .../ui/views/client_frame_view_linux.cc | 42 ++-------- .../ui/views/client_frame_view_linux.h | 12 +-- shell/browser/ui/views/frameless_view.cc | 16 ++-- shell/browser/ui/views/frameless_view.h | 5 ++ shell/browser/ui/views/opaque_frame_view.h | 6 +- shell/browser/ui/views/win_frame_view.cc | 15 ++++ shell/browser/ui/views/win_frame_view.h | 2 + 11 files changed, 123 insertions(+), 79 deletions(-) diff --git a/shell/browser/native_window_views.cc b/shell/browser/native_window_views.cc index 701cdc9435..aecba65935 100644 --- a/shell/browser/native_window_views.cc +++ b/shell/browser/native_window_views.cc @@ -29,6 +29,7 @@ #include "shell/browser/api/electron_api_system_preferences.h" #include "shell/browser/api/electron_api_web_contents.h" #include "shell/browser/ui/inspectable_web_contents_view.h" +#include "shell/browser/ui/views/frameless_view.h" #include "shell/browser/ui/views/root_view.h" #include "shell/browser/web_contents_preferences.h" #include "shell/browser/web_view_manager.h" @@ -41,14 +42,19 @@ #include "ui/base/hit_test.h" #include "ui/compositor/compositor.h" #include "ui/display/screen.h" +#include "ui/gfx/geometry/insets.h" +#include "ui/gfx/geometry/outsets.h" #include "ui/gfx/image/image.h" #include "ui/gfx/native_ui_types.h" #include "ui/ozone/public/ozone_platform.h" #include "ui/views/background.h" #include "ui/views/controls/webview/webview.h" +#include "ui/views/view_utils.h" #include "ui/views/widget/native_widget_private.h" #include "ui/views/widget/widget.h" #include "ui/views/window/client_view.h" +#include "ui/views/window/frame_view.h" +#include "ui/views/window/non_client_view.h" #include "ui/wm/core/shadow_types.h" #include "ui/wm/core/window_util.h" @@ -432,9 +438,12 @@ NativeWindowViews::NativeWindowViews(const int32_t base_window_id, window->AddPreTargetHandler(this); #if BUILDFLAG(IS_LINUX) - // On linux after the widget is initialized we might have to force set the - // bounds if the bounds are smaller than the current display - SetBounds(gfx::Rect(GetPosition(), bounds.size()), false); + // We need to set bounds again after widget init for two reasons: + // 1. For CSD windows, user-specified bounds need to be inflated by frame + // insets, but the frame view isn't available at first. + // 2. The widget clamps bounds to fit the screen, but we want to allow + // windows larger than the display. + SetBounds(gfx::Rect(GetPosition(), size), false); #endif } @@ -875,16 +884,14 @@ void NativeWindowViews::SetBounds(const gfx::Rect& bounds, bool animate) { } #endif - widget()->SetBounds(bounds); + widget()->SetBounds(LogicalToWidgetBounds(bounds)); } gfx::Rect NativeWindowViews::GetBounds() const { -#if BUILDFLAG(IS_WIN) if (IsMinimized()) - return widget()->GetRestoredBounds(); -#endif + return WidgetToLogicalBounds(widget()->GetRestoredBounds()); - return widget()->GetWindowBoundsInScreen(); + return WidgetToLogicalBounds(widget()->GetWindowBoundsInScreen()); } gfx::Rect NativeWindowViews::GetContentBounds() const { @@ -905,7 +912,7 @@ gfx::Rect NativeWindowViews::GetNormalBounds() const { if (IsMaximized() && transparent()) return restore_bounds_; #endif - return widget()->GetRestoredBounds(); + return WidgetToLogicalBounds(widget()->GetRestoredBounds()); } void NativeWindowViews::SetContentSizeConstraints( @@ -1481,6 +1488,22 @@ gfx::NativeWindow NativeWindowViews::GetNativeWindow() const { return widget()->GetNativeWindow(); } +gfx::Insets NativeWindowViews::GetRestoredFrameBorderInsets() const { + auto* non_client_view = widget()->non_client_view(); + if (!non_client_view) + return gfx::Insets(); + + auto* frame_view = non_client_view->frame_view(); + if (!frame_view) + return gfx::Insets(); + + if (auto* frameless = views::AsViewClass(frame_view)) { + return frameless->RestoredFrameBorderInsets(); + } + + return gfx::Insets(); +} + void NativeWindowViews::SetProgressBar(double progress, NativeWindow::ProgressState state) { #if BUILDFLAG(IS_WIN) @@ -1646,21 +1669,42 @@ NativeWindowHandle NativeWindowViews::GetNativeWindowHandle() const { return GetAcceleratedWidget(); } +gfx::Rect NativeWindowViews::LogicalToWidgetBounds( + const gfx::Rect& bounds) const { + gfx::Rect widget_bounds(bounds); + const gfx::Insets frame_insets = GetRestoredFrameBorderInsets(); + widget_bounds.Outset( + gfx::Outsets::TLBR(frame_insets.top(), frame_insets.left(), + frame_insets.bottom(), frame_insets.right())); + + return widget_bounds; +} + +gfx::Rect NativeWindowViews::WidgetToLogicalBounds( + const gfx::Rect& bounds) const { + gfx::Rect logical_bounds(bounds); + logical_bounds.Inset(GetRestoredFrameBorderInsets()); + return logical_bounds; +} + gfx::Rect NativeWindowViews::ContentBoundsToWindowBounds( const gfx::Rect& bounds) const { if (!has_frame()) return bounds; gfx::Rect window_bounds(bounds); + + if (auto* ncv = widget()->non_client_view()) { #if BUILDFLAG(IS_WIN) - if (widget()->non_client_view()) { HWND hwnd = GetAcceleratedWidget(); gfx::Rect dpi_bounds = DIPToScreenRect(hwnd, bounds); - window_bounds = ScreenToDIPRect( - hwnd, widget()->non_client_view()->GetWindowBoundsForClientBounds( - dpi_bounds)); - } + window_bounds = + ScreenToDIPRect(hwnd, ncv->GetWindowBoundsForClientBounds(dpi_bounds)); +#else + window_bounds = WidgetToLogicalBounds( + ncv->GetWindowBoundsForClientBounds(window_bounds)); #endif + } if (root_view_.HasMenu() && root_view_.is_menu_bar_visible()) { int menu_bar_height = root_view_.GetMenuBarHeight(); @@ -1687,6 +1731,10 @@ gfx::Rect NativeWindowViews::WindowBoundsToContentBounds( content_bounds.set_width(content_bounds.width() - (rect.right - rect.left)); content_bounds.set_height(content_bounds.height() - (rect.bottom - rect.top)); content_bounds.set_size(ScreenToDIPRect(hwnd, content_bounds).size()); +#else + if (auto* frame_view = widget()->non_client_view()->frame_view()) { + content_bounds = frame_view->GetBoundsForClientView(); + } #endif if (root_view_.HasMenu() && root_view_.is_menu_bar_visible()) { diff --git a/shell/browser/native_window_views.h b/shell/browser/native_window_views.h index 2ff5436e32..ec672c1ef3 100644 --- a/shell/browser/native_window_views.h +++ b/shell/browser/native_window_views.h @@ -16,6 +16,7 @@ #include "shell/browser/ui/views/root_view.h" #include "third_party/abseil-cpp/absl/container/flat_hash_set.h" #include "ui/base/ozone_buildflags.h" +#include "ui/gfx/geometry/insets.h" #include "ui/views/controls/webview/unhandled_keyboard_event_handler.h" #include "ui/views/widget/widget_observer.h" @@ -156,6 +157,12 @@ class NativeWindowViews : public NativeWindow, gfx::Rect ContentBoundsToWindowBounds(const gfx::Rect& bounds) const override; gfx::Rect WindowBoundsToContentBounds(const gfx::Rect& bounds) const override; + // Translates between logical/opaque window bounds exposed to callers + // and the absolute bounds of the underlying widget, which can be larger to + // fit CSD, e.g. transparent outer regions for shadows and resize targets. + gfx::Rect LogicalToWidgetBounds(const gfx::Rect& bounds) const; + gfx::Rect WidgetToLogicalBounds(const gfx::Rect& bounds) const; + void IncrementChildModals(); void DecrementChildModals(); @@ -205,6 +212,8 @@ class NativeWindowViews : public NativeWindow, overlay_symbol_color_ = color; } + gfx::Insets GetRestoredFrameBorderInsets() const; + // views::WidgetObserver: void OnWidgetActivationChanged(views::Widget* widget, bool active) override; void OnWidgetBoundsChanged(views::Widget* widget, diff --git a/shell/browser/ui/electron_desktop_window_tree_host_linux.cc b/shell/browser/ui/electron_desktop_window_tree_host_linux.cc index ad58894392..e5b4676e39 100644 --- a/shell/browser/ui/electron_desktop_window_tree_host_linux.cc +++ b/shell/browser/ui/electron_desktop_window_tree_host_linux.cc @@ -53,10 +53,11 @@ void ElectronDesktopWindowTreeHostLinux::OnWidgetInitDone() { UpdateFrameHints(); } -bool ElectronDesktopWindowTreeHostLinux::IsShowingFrame() const { - return !native_window_view_->IsFullscreen() && - !native_window_view_->IsMaximized() && - !native_window_view_->IsMinimized(); +bool ElectronDesktopWindowTreeHostLinux::IsShowingFrame( + ui::PlatformWindowState window_state) const { + return window_state != ui::PlatformWindowState::kFullScreen && + window_state != ui::PlatformWindowState::kMaximized && + window_state != ui::PlatformWindowState::kMinimized; } void ElectronDesktopWindowTreeHostLinux::SetWindowIcons( @@ -80,7 +81,7 @@ void ElectronDesktopWindowTreeHostLinux::Show( gfx::Insets ElectronDesktopWindowTreeHostLinux::CalculateInsetsInDIP( ui::PlatformWindowState window_state) const { // If we are not showing frame, the insets should be zero. - if (!IsShowingFrame()) { + if (!IsShowingFrame(window_state)) { return gfx::Insets(); } @@ -88,9 +89,7 @@ gfx::Insets ElectronDesktopWindowTreeHostLinux::CalculateInsetsInDIP( if (!view) return {}; - gfx::Insets insets = view->RestoredMirroredFrameBorderInsets(); - if (base::i18n::IsRTL()) - insets.set_left_right(insets.right(), insets.left()); + gfx::Insets insets = view->RestoredFrameBorderInsets(); return insets; } @@ -207,7 +206,7 @@ void ElectronDesktopWindowTreeHostLinux::UpdateFrameHints() { if (ui::OzonePlatform::GetInstance()->IsWindowCompositingSupported()) { // Set the opaque region. std::vector opaque_region; - if (IsShowingFrame()) { + if (IsShowingFrame(window_state)) { // The opaque region is a list of rectangles that contain only fully // opaque pixels of the window. We need to convert the clipping // rounded-rect into this format. diff --git a/shell/browser/ui/electron_desktop_window_tree_host_linux.h b/shell/browser/ui/electron_desktop_window_tree_host_linux.h index 208cd5e42c..7d184afd13 100644 --- a/shell/browser/ui/electron_desktop_window_tree_host_linux.h +++ b/shell/browser/ui/electron_desktop_window_tree_host_linux.h @@ -74,7 +74,7 @@ class ElectronDesktopWindowTreeHostLinux private: void UpdateWindowState(ui::PlatformWindowState new_state); - bool IsShowingFrame() const; + bool IsShowingFrame(ui::PlatformWindowState window_state) const; gfx::ImageSkia saved_window_icon_; diff --git a/shell/browser/ui/views/client_frame_view_linux.cc b/shell/browser/ui/views/client_frame_view_linux.cc index 7886c91f19..ca5cc8040a 100644 --- a/shell/browser/ui/views/client_frame_view_linux.cc +++ b/shell/browser/ui/views/client_frame_view_linux.cc @@ -142,13 +142,6 @@ void ClientFrameViewLinux::Init(NativeWindowViews* window, UpdateThemeValues(); } -gfx::Insets ClientFrameViewLinux::RestoredMirroredFrameBorderInsets() const { - auto border = RestoredFrameBorderInsets(); - return base::i18n::IsRTL() ? gfx::Insets::TLBR(border.top(), border.right(), - border.bottom(), border.left()) - : border; -} - gfx::Insets ClientFrameViewLinux::RestoredFrameBorderInsets() const { gfx::Insets insets = GetFrameProvider()->GetFrameThicknessDip(); const gfx::Insets input = GetInputInsets(); @@ -163,7 +156,9 @@ gfx::Insets ClientFrameViewLinux::RestoredFrameBorderInsets() const { merged.set_bottom(expand_if_visible(insets.bottom(), input.bottom())); merged.set_right(expand_if_visible(insets.right(), input.right())); - return merged; + return base::i18n::IsRTL() ? gfx::Insets::TLBR(merged.top(), merged.right(), + merged.bottom(), merged.left()) + : merged; } gfx::Insets ClientFrameViewLinux::GetInputInsets() const { @@ -174,7 +169,7 @@ gfx::Insets ClientFrameViewLinux::GetInputInsets() const { gfx::Rect ClientFrameViewLinux::GetWindowContentBounds() const { gfx::Rect content_bounds = bounds(); - content_bounds.Inset(RestoredMirroredFrameBorderInsets()); + content_bounds.Inset(RestoredFrameBorderInsets()); return content_bounds; } @@ -208,13 +203,13 @@ void ClientFrameViewLinux::OnWindowButtonOrderingChange() { } int ClientFrameViewLinux::ResizingBorderHitTest(const gfx::Point& point) { - return ResizingBorderHitTestImpl(point, RestoredMirroredFrameBorderInsets()); + return ResizingBorderHitTestImpl(point, RestoredFrameBorderInsets()); } gfx::Rect ClientFrameViewLinux::GetBoundsForClientView() const { gfx::Rect client_bounds = bounds(); if (!frame_->IsFullscreen()) { - client_bounds.Inset(RestoredMirroredFrameBorderInsets()); + client_bounds.Inset(RestoredFrameBorderInsets()); client_bounds.Inset( gfx::Insets::TLBR(GetTitlebarBounds().height(), 0, 0, 0)); } @@ -268,20 +263,6 @@ void ClientFrameViewLinux::SizeConstraintsChanged() { InvalidateLayout(); } -gfx::Size ClientFrameViewLinux::CalculatePreferredSize( - const views::SizeBounds& available_size) const { - return SizeWithDecorations( - FramelessView::CalculatePreferredSize(available_size)); -} - -gfx::Size ClientFrameViewLinux::GetMinimumSize() const { - return SizeWithDecorations(FramelessView::GetMinimumSize()); -} - -gfx::Size ClientFrameViewLinux::GetMaximumSize() const { - return SizeWithDecorations(FramelessView::GetMaximumSize()); -} - void ClientFrameViewLinux::Layout(PassKey) { LayoutSuperclass(this); @@ -474,7 +455,7 @@ gfx::Rect ClientFrameViewLinux::GetTitlebarBounds() const { std::max(font_height, theme_values_.titlebar_min_height) + GetTitlebarContentInsets().height(); - gfx::Insets decoration_insets = RestoredMirroredFrameBorderInsets(); + gfx::Insets decoration_insets = RestoredFrameBorderInsets(); // We add the inset height here, so the .Inset() that follows won't reduce it // to be too small. @@ -493,15 +474,6 @@ gfx::Rect ClientFrameViewLinux::GetTitlebarContentBounds() const { titlebar.Inset(GetTitlebarContentInsets()); return titlebar; } - -gfx::Size ClientFrameViewLinux::SizeWithDecorations(gfx::Size size) const { - gfx::Insets decoration_insets = RestoredMirroredFrameBorderInsets(); - - size.Enlarge(0, GetTitlebarBounds().height()); - size.Enlarge(decoration_insets.width(), decoration_insets.height()); - return size; -} - views::View* ClientFrameViewLinux::TargetForRect(views::View* root, const gfx::Rect& rect) { return views::FrameView::TargetForRect(root, rect); diff --git a/shell/browser/ui/views/client_frame_view_linux.h b/shell/browser/ui/views/client_frame_view_linux.h index 54cbc9a1c3..bd09cfeb54 100644 --- a/shell/browser/ui/views/client_frame_view_linux.h +++ b/shell/browser/ui/views/client_frame_view_linux.h @@ -42,9 +42,9 @@ class ClientFrameViewLinux : public FramelessView, void Init(NativeWindowViews* window, views::Widget* frame) override; - // These are here for ElectronDesktopWindowTreeHostLinux to use. - gfx::Insets RestoredMirroredFrameBorderInsets() const; - gfx::Insets RestoredFrameBorderInsets() const; + // FramelessView: + gfx::Insets RestoredFrameBorderInsets() const override; + gfx::Insets GetInputInsets() const; gfx::Rect GetWindowContentBounds() const; SkRRect GetRoundedWindowContentBounds() const; @@ -74,10 +74,6 @@ class ClientFrameViewLinux : public FramelessView, void SizeConstraintsChanged() override; // Overridden from View: - gfx::Size CalculatePreferredSize( - const views::SizeBounds& available_size) const override; - gfx::Size GetMinimumSize() const override; - gfx::Size GetMaximumSize() const override; void Layout(PassKey) override; void OnPaint(gfx::Canvas* canvas) override; @@ -127,8 +123,6 @@ class ClientFrameViewLinux : public FramelessView, gfx::Insets GetTitlebarContentInsets() const; gfx::Rect GetTitlebarContentBounds() const; - gfx::Size SizeWithDecorations(gfx::Size size) const; - raw_ptr theme_; ThemeValues theme_values_; diff --git a/shell/browser/ui/views/frameless_view.cc b/shell/browser/ui/views/frameless_view.cc index 420a825f27..faecb60751 100644 --- a/shell/browser/ui/views/frameless_view.cc +++ b/shell/browser/ui/views/frameless_view.cc @@ -30,6 +30,10 @@ void FramelessView::Init(NativeWindowViews* window, views::Widget* frame) { frame_ = frame; } +gfx::Insets FramelessView::RestoredFrameBorderInsets() const { + return gfx::Insets(); +} + int FramelessView::ResizingBorderHitTest(const gfx::Point& point) { return ResizingBorderHitTestImpl(point, gfx::Insets(kResizeInsideBoundsSize)); } @@ -108,16 +112,16 @@ gfx::Size FramelessView::CalculatePreferredSize( } gfx::Size FramelessView::GetMinimumSize() const { - return window_->GetContentMinimumSize(); + if (!window_) + return gfx::Size(); + return window_->GetMinimumSize(); } gfx::Size FramelessView::GetMaximumSize() const { - gfx::Size size = window_->GetContentMaximumSize(); - // Electron public APIs returns (0, 0) when maximum size is not set, but it - // would break internal window APIs like HWNDMessageHandler::SetAspectRatio. - return size.IsEmpty() ? gfx::Size(INT_MAX, INT_MAX) : size; + if (!window_) + return gfx::Size(); + return window_->GetMaximumSize(); } - BEGIN_METADATA(FramelessView) END_METADATA diff --git a/shell/browser/ui/views/frameless_view.h b/shell/browser/ui/views/frameless_view.h index e391cb9bd7..e80cfe4148 100644 --- a/shell/browser/ui/views/frameless_view.h +++ b/shell/browser/ui/views/frameless_view.h @@ -7,6 +7,7 @@ #include "base/memory/raw_ptr.h" #include "ui/base/metadata/metadata_header_macros.h" +#include "ui/gfx/geometry/insets.h" #include "ui/views/window/non_client_view.h" namespace views { @@ -37,6 +38,10 @@ class FramelessView : public views::FrameView { // and forces a re-layout and re-paint. virtual void InvalidateCaptionButtons() {} + // Any insets from the (transparent) widget bounds to the logical/opaque + // bounds of the view, used for CSD and resize targets on some platforms. + virtual gfx::Insets RestoredFrameBorderInsets() const; + NativeWindowViews* window() const { return window_; } views::Widget* frame() const { return frame_; } diff --git a/shell/browser/ui/views/opaque_frame_view.h b/shell/browser/ui/views/opaque_frame_view.h index 94da7e8766..7c9c4bb36b 100644 --- a/shell/browser/ui/views/opaque_frame_view.h +++ b/shell/browser/ui/views/opaque_frame_view.h @@ -39,6 +39,7 @@ class OpaqueFrameView : public FramelessView { void Init(NativeWindowViews* window, views::Widget* frame) override; int ResizingBorderHitTest(const gfx::Point& point) override; void InvalidateCaptionButtons() override; + gfx::Insets RestoredFrameBorderInsets() const override; // views::FrameView: gfx::Rect GetBoundsForClientView() const override; @@ -99,11 +100,6 @@ class OpaqueFrameView : public FramelessView { // rather than having extra vertical space above the tabs. bool IsFrameCondensed() const; - // The insets from the native window edge to the client view when the window - // is restored. This goes all the way to the web contents on the left, right, - // and bottom edges. - gfx::Insets RestoredFrameBorderInsets() const; - // The insets from the native window edge to the flat portion of the // window border. That is, this function returns the "3D portion" of the // border when the window is restored. The returned insets will not be larger diff --git a/shell/browser/ui/views/win_frame_view.cc b/shell/browser/ui/views/win_frame_view.cc index 697243c4ad..1a6512d912 100644 --- a/shell/browser/ui/views/win_frame_view.cc +++ b/shell/browser/ui/views/win_frame_view.cc @@ -274,6 +274,21 @@ bool WinFrameView::GetShouldPaintAsActive() { return ShouldPaintAsActive(); } +gfx::Size WinFrameView::GetMinimumSize() const { + // Chromium expects minimum size to be in content dimensions on Windows + // because it adds the frame border automatically in OnGetMinMaxInfo. + return window_->GetContentMinimumSize(); +} + +gfx::Size WinFrameView::GetMaximumSize() const { + // Chromium expects minimum size to be in content dimensions on Windows + // because it adds the frame border automatically in OnGetMinMaxInfo. + gfx::Size size = window_->GetContentMaximumSize(); + // Electron public APIs returns (0, 0) when maximum size is not set, but it + // would break internal window APIs like HWNDMessageHandler::SetAspectRatio. + return size.IsEmpty() ? gfx::Size(INT_MAX, INT_MAX) : size; +} + BEGIN_METADATA(WinFrameView) END_METADATA diff --git a/shell/browser/ui/views/win_frame_view.h b/shell/browser/ui/views/win_frame_view.h index c47550aef0..3759138b4b 100644 --- a/shell/browser/ui/views/win_frame_view.h +++ b/shell/browser/ui/views/win_frame_view.h @@ -35,6 +35,8 @@ class WinFrameView : public FramelessView { gfx::Rect GetWindowBoundsForClientBounds( const gfx::Rect& client_bounds) const override; int NonClientHitTest(const gfx::Point& point) override; + gfx::Size GetMinimumSize() const override; + gfx::Size GetMaximumSize() const override; WinCaptionButtonContainer* caption_button_container() { return caption_button_container_;