Repository: electron/electron
Pull request: fix: Wayland resizing border

#### Description of Change

Closes https://github.com/electron/electron/issues/44543.
Closes https://github.com/electron/electron/issues/42894.
Closes https://github.com/electron/electron/issues/45834.

Refs CL:5180720

Fixes an issue where the resizing border didn't work as expected on Wayland windows. This happened as collateral damage in changes we made to account for CL:5180720.

<details><summary>After Change</summary>
<p>

![PHOTO-2025-03-20-10-21-52](https://github.com/user-attachments/assets/e147f144-d039-4d77-8116-e3e44f8adbca)

</p>
</details> 

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes:  Fixed an issue where the resizing border didn't work as expected on Wayland windows.


Changed files:
filename: shell/browser/ui/electron_desktop_window_tree_host_linux.cc
patch: @@ -47,22 +47,28 @@ void ElectronDesktopWindowTreeHostLinux::OnWidgetInitDone() {
   UpdateFrameHints();
 }
 
+bool ElectronDesktopWindowTreeHostLinux::IsShowingFrame() const {
+  return !native_window_view_->IsFullscreen() &&
+         !native_window_view_->IsMaximized() &&
+         !native_window_view_->IsMinimized();
+}
+
 gfx::Insets ElectronDesktopWindowTreeHostLinux::CalculateInsetsInDIP(
     ui::PlatformWindowState window_state) const {
   // If we are not showing frame, the insets should be zero.
-  if (native_window_view_->IsFullscreen()) {
-    return {};
+  if (!IsShowingFrame()) {
+    return gfx::Insets();
   }
 
   if (!native_window_view_->has_frame() ||
       !native_window_view_->has_client_frame()) {
-    return {};
+    return gfx::Insets();
   }
 
   auto* view = static_cast<ClientFrameViewLinux*>(
       native_window_view_->widget()->non_client_view()->frame_view());
 
-  gfx::Insets insets = view->GetBorderDecorationInsets();
+  gfx::Insets insets = view->RestoredMirroredFrameBorderInsets();
   if (base::i18n::IsRTL())
     insets.set_left_right(insets.right(), insets.left());
   return insets;
@@ -99,9 +105,14 @@ void ElectronDesktopWindowTreeHostLinux::OnWindowTiledStateChanged(
   // of view.
   if (native_window_view_->has_frame() &&
       native_window_view_->has_client_frame()) {
-    static_cast<ClientFrameViewLinux*>(
-        native_window_view_->widget()->non_client_view()->frame_view())
-        ->set_tiled_edges(new_tiled_edges);
+    ClientFrameViewLinux* frame = static_cast<ClientFrameViewLinux*>(
+        native_window_view_->widget()->non_client_view()->frame_view());
+
+    bool maximized = new_tiled_edges.top && new_tiled_edges.left &&
+                     new_tiled_edges.bottom && new_tiled_edges.right;
+    bool tiled = new_tiled_edges.top || new_tiled_edges.left ||
+                 new_tiled_edges.bottom || new_tiled_edges.right;
+    frame->set_tiled(tiled && !maximized);
   }
   UpdateFrameHints();
 }
@@ -181,7 +192,7 @@ void ElectronDesktopWindowTreeHostLinux::UpdateFrameHints() {
     if (ui::OzonePlatform::GetInstance()->IsWindowCompositingSupported()) {
       // Set the opaque region.
       std::vector<gfx::Rect> opaque_region;
-      if (!native_window_view_->IsFullscreen()) {
+      if (!IsShowingFrame()) {
         // 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.

filename: shell/browser/ui/electron_desktop_window_tree_host_linux.h
patch: @@ -65,6 +65,8 @@ class ElectronDesktopWindowTreeHostLinux
  private:
   void UpdateWindowState(ui::PlatformWindowState new_state);
 
+  bool IsShowingFrame() const;
+
   raw_ptr<NativeWindowViews> native_window_view_;  // weak ref
 
   base::ScopedObservation<ui::NativeTheme, ui::NativeThemeObserver>

filename: shell/browser/ui/views/client_frame_view_linux.cc
patch: @@ -39,7 +39,7 @@ namespace electron {
 namespace {
 
 // These values should be the same as Chromium uses.
-constexpr int kResizeOutsideBorderSize = 10;
+constexpr int kResizeBorder = 10;
 constexpr int kResizeInsideBoundsSize = 5;
 
 ui::NavButtonProvider::ButtonState ButtonStateToNavButtonProviderState(
@@ -142,25 +142,28 @@ void ClientFrameViewLinux::Init(NativeWindowViews* window,
   UpdateThemeValues();
 }
 
-gfx::Insets ClientFrameViewLinux::GetBorderDecorationInsets() const {
-  const auto insets = GetFrameProvider()->GetFrameThicknessDip();
+gfx::Insets ClientFrameViewLinux::RestoredMirroredFrameBorderInsets() const {
+  auto border = RestoredFrameBorderInsets();
+  return base::i18n::IsRTL() ? gfx::Insets::TLBR(border.top(), border.right(),
+                                                 border.bottom(), border.left())
+                             : border;
+}
 
-  // We shouldn't draw frame decorations for the tiled edges.
-  // See https://wayland.app/protocols/xdg-shell#xdg_toplevel:enum:state
-  const auto& edges = tiled_edges();
-  return gfx::Insets::TLBR(
-      edges.top ? 0 : insets.top(), edges.left ? 0 : insets.left(),
-      edges.bottom ? 0 : insets.bottom(), edges.right ? 0 : insets.right());
+gfx::Insets ClientFrameViewLinux::RestoredFrameBorderInsets() const {
+  gfx::Insets insets = GetFrameProvider()->GetFrameThicknessDip();
+  insets.SetToMax(GetInputInsets());
+  return insets;
 }
 
 gfx::Insets ClientFrameViewLinux::GetInputInsets() const {
-  return gfx::Insets{
-      host_supports_client_frame_shadow_ ? -kResizeOutsideBorderSize : 0};
+  bool showing_shadow = host_supports_client_frame_shadow_ &&
+                        !frame_->IsMaximized() && !frame_->IsFullscreen();
+  return gfx::Insets(showing_shadow ? kResizeBorder : 0);
 }
 
 gfx::Rect ClientFrameViewLinux::GetWindowContentBounds() const {
   gfx::Rect content_bounds = bounds();
-  content_bounds.Inset(GetBorderDecorationInsets());
+  content_bounds.Inset(RestoredMirroredFrameBorderInsets());
   return content_bounds;
 }
 
@@ -194,15 +197,15 @@ void ClientFrameViewLinux::OnWindowButtonOrderingChange() {
 }
 
 int ClientFrameViewLinux::ResizingBorderHitTest(const gfx::Point& point) {
-  return ResizingBorderHitTestImpl(
-      point,
-      GetBorderDecorationInsets() + gfx::Insets(kResizeInsideBoundsSize));
+  return ResizingBorderHitTestImpl(point,
+                                   RestoredMirroredFrameBorderInsets() +
+                                       gfx::Insets(kResizeInsideBoundsSize));
 }
 
 gfx::Rect ClientFrameViewLinux::GetBoundsForClientView() const {
   gfx::Rect client_bounds = bounds();
   if (!frame_->IsFullscreen()) {
-    client_bounds.Inset(GetBorderDecorationInsets());
+    client_bounds.Inset(RestoredMirroredFrameBorderInsets());
     client_bounds.Inset(
         gfx::Insets::TLBR(GetTitlebarBounds().height(), 0, 0, 0));
   }
@@ -239,10 +242,8 @@ int ClientFrameViewLinux::NonClientHitTest(const gfx::Point& point) {
 }
 
 ui::WindowFrameProvider* ClientFrameViewLinux::GetFrameProvider() const {
-  const bool tiled = tiled_edges().top || tiled_edges().left ||
-                     tiled_edges().bottom || tiled_edges().right;
   return ui::LinuxUiTheme::GetForProfile(nullptr)->GetWindowFrameProvider(
-      !host_supports_client_frame_shadow_, tiled, frame_->IsMaximized());
+      !host_supports_client_frame_shadow_, tiled(), frame_->IsMaximized());
 }
 
 void ClientFrameViewLinux::GetWindowMask(const gfx::Size& size,
@@ -465,7 +466,7 @@ gfx::Rect ClientFrameViewLinux::GetTitlebarBounds() const {
       std::max(font_height, theme_values_.titlebar_min_height) +
       GetTitlebarContentInsets().height();
 
-  gfx::Insets decoration_insets = GetBorderDecorationInsets();
+  gfx::Insets decoration_insets = RestoredMirroredFrameBorderInsets();
 
   // We add the inset height here, so the .Inset() that follows won't reduce it
   // to be too small.
@@ -486,7 +487,7 @@ gfx::Rect ClientFrameViewLinux::GetTitlebarContentBounds() const {
 }
 
 gfx::Size ClientFrameViewLinux::SizeWithDecorations(gfx::Size size) const {
-  gfx::Insets decoration_insets = GetBorderDecorationInsets();
+  gfx::Insets decoration_insets = RestoredMirroredFrameBorderInsets();
 
   size.Enlarge(0, GetTitlebarBounds().height());
   size.Enlarge(decoration_insets.width(), decoration_insets.height());

filename: shell/browser/ui/views/client_frame_view_linux.h
patch: @@ -43,16 +43,16 @@ class ClientFrameViewLinux : public FramelessView,
   void Init(NativeWindowViews* window, views::Widget* frame) override;
 
   // These are here for ElectronDesktopWindowTreeHostLinux to use.
-  gfx::Insets GetBorderDecorationInsets() const;
+  gfx::Insets RestoredMirroredFrameBorderInsets() const;
+  gfx::Insets RestoredFrameBorderInsets() const;
   gfx::Insets GetInputInsets() const;
   gfx::Rect GetWindowContentBounds() const;
   SkRRect GetRoundedWindowContentBounds() const;
   int GetTranslucentTopAreaHeight() const;
-  // Returns which edges of the frame are tiled.
-  const ui::WindowTiledEdges& tiled_edges() const { return tiled_edges_; }
-  void set_tiled_edges(ui::WindowTiledEdges tiled_edges) {
-    tiled_edges_ = tiled_edges;
-  }
+
+  // Returns whether the frame is in a tiled state.
+  bool tiled() const { return tiled_; }
+  void set_tiled(bool tiled) { tiled_ = tiled; }
 
  protected:
   // ui::NativeThemeObserver:
@@ -149,7 +149,7 @@ class ClientFrameViewLinux : public FramelessView,
 
   base::CallbackListSubscription paint_as_active_changed_subscription_;
 
-  ui::WindowTiledEdges tiled_edges_;
+  bool tiled_ = false;
 };
 
 }  // namespace electron