Repository: electron/electron
Pull request: fix: correct window sizing on Linux when constraints on resizability are enforced

Backport of #49903

See that PR for details.


Notes: <!-- Please add a one-line description for app developers to read in the release notes, or 'none' if no notes relevant to app developers. Examples and help on special cases: https://github.com/electron/clerk/blob/main/README.md#examples --> Fixed window sizing on Linux when min/max size constraints are set or `resizable` is `false`. Windows no longer shrink below the requested max size or grow taller when resizability is disabled.

Changed files:
filename: shell/browser/native_window_views.cc
patch: @@ -930,8 +930,10 @@ void NativeWindowViews::SetContentSizeConstraints(
   // of this to determine whether native widget has initialized.
   if (widget() && widget()->widget_delegate())
     widget()->OnSizeConstraintsChanged();
+#if BUILDFLAG(IS_LINUX)
   if (resizable_)
-    old_size_constraints_ = size_constraints;
+    old_size_constraints_ = GetSizeConstraints();
+#endif
 }
 
 #if BUILDFLAG(IS_WIN)
@@ -962,14 +964,18 @@ void NativeWindowViews::SetResizable(bool resizable) {
     resizable_ = resizable;
     // On Linux there is no "resizable" property of a window, we have to set
     // both the minimum and maximum size to the window size to achieve it.
+    // On Windows, the constraints don't directly enforce resizability, but
+    // changing them triggers the styling to be applied in Chromium
+    // (SizeConstraintsChanged).
     if (resizable) {
-      SetContentSizeConstraints(old_size_constraints_);
+      SetSizeConstraints(old_size_constraints_);
     } else {
-      old_size_constraints_ = GetContentSizeConstraints();
-      gfx::Size content_size = GetContentSize();
-      SetContentSizeConstraints(
-          extensions::SizeConstraints(content_size, content_size));
+      old_size_constraints_ = GetSizeConstraints();
+      gfx::Size window_size = GetSize();
+      SetSizeConstraints(extensions::SizeConstraints(window_size, window_size));
     }
+    if (widget() && widget()->widget_delegate())
+      widget()->OnSizeConstraintsChanged();
 #if BUILDFLAG(IS_WIN)
     UpdateThickFrame();
 #endif
@@ -1933,8 +1939,10 @@ std::unique_ptr<views::FrameView> NativeWindowViews::CreateFrameView(
 
 #if BUILDFLAG(IS_LINUX)
 LinuxFrameLayout* NativeWindowViews::GetLinuxFrameLayout() {
-  auto* view = views::AsViewClass<FramelessView>(
-      widget()->non_client_view()->frame_view());
+  auto* ncv = widget()->non_client_view();
+  if (!ncv)
+    return nullptr;
+  auto* view = views::AsViewClass<FramelessView>(ncv->frame_view());
   return view ? view->GetLinuxFrameLayout() : nullptr;
 }
 #endif

filename: shell/browser/ui/electron_desktop_window_tree_host_linux.cc
patch: @@ -93,6 +93,40 @@ gfx::Insets ElectronDesktopWindowTreeHostLinux::CalculateInsetsInDIP(
   return insets;
 }
 
+// Electron treats min/max constraints as the logical window size, but Chromium
+// expects widget bounds including CSD insets (WaylandToplevelWindow::
+// SetSizeConstraints). So we inflate constraints by insets to avoid double
+// subtraction. This is still OK for SSD frames or X11 where the insets are 0.
+std::optional<gfx::Size>
+ElectronDesktopWindowTreeHostLinux::GetMinimumSizeForWindow() const {
+  auto min_size = views::DesktopWindowTreeHostLinux::GetMinimumSizeForWindow();
+  if (min_size.has_value()) {
+    auto* const layout = native_window_view_->GetLinuxFrameLayout();
+    if (layout) {
+      gfx::Insets insets = layout->RestoredFrameBorderInsets();
+      min_size->Enlarge(insets.width(), insets.height());
+    }
+  }
+  return min_size;
+}
+
+std::optional<gfx::Size>
+ElectronDesktopWindowTreeHostLinux::GetMaximumSizeForWindow() const {
+  auto max_size = views::DesktopWindowTreeHostLinux::GetMaximumSizeForWindow();
+  if (max_size.has_value()) {
+    auto* const layout = native_window_view_->GetLinuxFrameLayout();
+    if (layout) {
+      gfx::Insets insets = layout->RestoredFrameBorderInsets();
+      // 0 means no constraint, so don't inflate.
+      if (max_size->width() > 0)
+        max_size->set_width(max_size->width() + insets.width());
+      if (max_size->height() > 0)
+        max_size->set_height(max_size->height() + insets.height());
+    }
+  }
+  return max_size;
+}
+
 void ElectronDesktopWindowTreeHostLinux::OnBoundsChanged(
     const BoundsChange& change) {
   views::DesktopWindowTreeHostLinux::OnBoundsChanged(change);

filename: shell/browser/ui/electron_desktop_window_tree_host_linux.h
patch: @@ -9,6 +9,8 @@
 #ifndef ELECTRON_SHELL_BROWSER_UI_ELECTRON_DESKTOP_WINDOW_TREE_HOST_LINUX_H_
 #define ELECTRON_SHELL_BROWSER_UI_ELECTRON_DESKTOP_WINDOW_TREE_HOST_LINUX_H_
 
+#include <optional>
+
 #include "base/memory/raw_ptr.h"
 #include "base/scoped_observation.h"
 #include "ui/gfx/image/image_skia.h"
@@ -52,6 +54,8 @@ class ElectronDesktopWindowTreeHostLinux
   // ui::PlatformWindowDelegate
   gfx::Insets CalculateInsetsInDIP(
       ui::PlatformWindowState window_state) const override;
+  std::optional<gfx::Size> GetMinimumSizeForWindow() const override;
+  std::optional<gfx::Size> GetMaximumSizeForWindow() const override;
   void OnBoundsChanged(const BoundsChange& change) override;
   void OnWindowStateChanged(ui::PlatformWindowState old_state,
                             ui::PlatformWindowState new_state) override;

filename: spec/api-browser-window-spec.ts
patch: @@ -1670,6 +1670,26 @@ describe('BrowserWindow module', () => {
         expectBoundsEqual(w.getMinimumSize(), [100, 100]);
         expectBoundsEqual(w.getMaximumSize(), [900, 600]);
       });
+
+      it('enforces minimum size', async () => {
+        w.setMinimumSize(300, 300);
+        const resize = once(w, 'resize');
+        w.setSize(100, 100);
+        await resize;
+        const size = w.getSize();
+        expect(size[0]).to.be.at.least(300);
+        expect(size[1]).to.be.at.least(300);
+      });
+
+      it('enforces maximum size', async () => {
+        w.setMaximumSize(200, 200);
+        const resize = once(w, 'resize');
+        w.setSize(500, 500);
+        await resize;
+        const size = w.getSize();
+        expect(size[0]).to.be.at.most(200);
+        expect(size[1]).to.be.at.most(200);
+      });
     });
 
     describe('BrowserWindow.setAspectRatio(ratio)', () => {
@@ -5462,6 +5482,20 @@ describe('BrowserWindow module', () => {
         expect(w.maximizable).to.be.true('maximizable');
       });
 
+      it('does not change window size when disabled and enabled', () => {
+        const w = new BrowserWindow({
+          show: false,
+          width: 400,
+          height: 300,
+          frame: true
+        });
+
+        w.setResizable(false);
+        expectBoundsEqual(w.getSize(), [400, 300]);
+        w.setResizable(true);
+        expectBoundsEqual(w.getSize(), [400, 300]);
+      });
+
       ifit(process.platform !== 'darwin')('works for a window smaller than 64x64', () => {
         const w = new BrowserWindow({
           show: false,