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
This commit is contained in:
Mitchell Cohen
2026-02-17 15:23:54 -05:00
committed by GitHub
parent 459a88f788
commit 931c257de7
11 changed files with 123 additions and 79 deletions

View File

@@ -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<FramelessView>(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()) {

View File

@@ -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,

View File

@@ -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<gfx::Rect> 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.

View File

@@ -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_;

View File

@@ -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<FramelessView>(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);

View File

@@ -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<ui::NativeTheme> theme_;
ThemeValues theme_values_;

View File

@@ -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

View File

@@ -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_; }

View File

@@ -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

View File

@@ -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

View File

@@ -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_;