Repository: electron/electron
Pull request: fix: glitchy rendering and maximize behavior with different GTK themes

Backport of #50550

See that PR for details.


Notes: Fixed the appearance of maximized windows on GNOME in Wayland, especially when non-default GTK themes like Breeze are set.


Changed files:
filename: shell/browser/ui/views/client_frame_view_linux.cc
patch: @@ -163,7 +163,7 @@ int ClientFrameViewLinux::ResizingBorderHitTest(const gfx::Point& point) {
 gfx::Rect ClientFrameViewLinux::GetBoundsForClientView() const {
   gfx::Rect client_bounds = bounds();
   if (!frame_->IsFullscreen()) {
-    client_bounds.Inset(RestoredFrameBorderInsets());
+    client_bounds.Inset(linux_frame_layout_->FrameBorderInsets(false));
     client_bounds.Inset(
         gfx::Insets::TLBR(GetTitlebarBounds().height(), 0, 0, 0));
   }
@@ -236,13 +236,40 @@ void ClientFrameViewLinux::Layout(PassKey) {
 }
 
 void ClientFrameViewLinux::OnPaint(gfx::Canvas* canvas) {
+  if (frame_->IsFullscreen()) {
+    return;
+  }
+
+  if (frame_->IsMaximized()) {
+    // Some GTK themes (Breeze) still render shadow/border assets when
+    // maximized, and we don't need a border when maximized anyway. Chromium
+    // switches on this too: OpaqueBrowserFrameView::PaintMaximizedFrameBorder.
+    PaintMaximizedFrameBorder(canvas);
+  } else {
+    PaintRestoredFrameBorder(canvas);
+  }
+}
+
+void ClientFrameViewLinux::PaintRestoredFrameBorder(gfx::Canvas* canvas) {
   if (auto* frame_provider = linux_frame_layout_->GetFrameProvider()) {
     frame_provider->PaintWindowFrame(
         canvas, GetLocalBounds(), GetTitlebarBounds().bottom(),
         ShouldPaintAsActive(), linux_frame_layout_->GetInputInsets());
   }
 }
 
+void ClientFrameViewLinux::PaintMaximizedFrameBorder(gfx::Canvas* canvas) {
+  ui::NativeTheme::FrameTopAreaExtraParams frame_top_area;
+  frame_top_area.use_custom_frame = true;
+  frame_top_area.is_active = ShouldPaintAsActive();
+  frame_top_area.default_background_color = SK_ColorTRANSPARENT;
+  ui::NativeTheme::ExtraParams params(frame_top_area);
+  GetNativeTheme()->Paint(
+      canvas->sk_canvas(), GetColorProvider(), ui::NativeTheme::kFrameTopArea,
+      ui::NativeTheme::kNormal,
+      gfx::Rect(0, 0, width(), GetTitlebarBounds().bottom()), params);
+}
+
 void ClientFrameViewLinux::PaintAsActiveChanged() {
   UpdateThemeValues();
 }
@@ -251,23 +278,15 @@ void ClientFrameViewLinux::UpdateThemeValues() {
   gtk::GtkCssContext window_context =
       gtk::AppendCssNodeToStyleContext({}, "window.background.csd");
   gtk::GtkCssContext headerbar_context = gtk::AppendCssNodeToStyleContext(
-      {}, "headerbar.default-decoration.titlebar");
+      window_context, "headerbar.default-decoration.titlebar");
   gtk::GtkCssContext title_context =
       gtk::AppendCssNodeToStyleContext(headerbar_context, "label.title");
-  gtk::GtkCssContext button_context = gtk::AppendCssNodeToStyleContext(
-      headerbar_context, "button.image-button");
-
-  gtk_style_context_set_parent(headerbar_context, window_context);
-  gtk_style_context_set_parent(title_context, headerbar_context);
-  gtk_style_context_set_parent(button_context, headerbar_context);
-
   // ShouldPaintAsActive asks the widget, so assume active if the widget is not
   // set yet.
   if (GetWidget() != nullptr && !ShouldPaintAsActive()) {
     gtk_style_context_set_state(window_context, GTK_STATE_FLAG_BACKDROP);
     gtk_style_context_set_state(headerbar_context, GTK_STATE_FLAG_BACKDROP);
     gtk_style_context_set_state(title_context, GTK_STATE_FLAG_BACKDROP);
-    gtk_style_context_set_state(button_context, GTK_STATE_FLAG_BACKDROP);
   }
 
   theme_values_.window_border_radius =
@@ -281,10 +300,6 @@ void ClientFrameViewLinux::UpdateThemeValues() {
   theme_values_.title_color = gtk::GtkStyleContextGetColor(title_context);
   theme_values_.title_padding = gtk::GtkStyleContextGetPadding(title_context);
 
-  gtk::GtkStyleContextGet(button_context, "min-height",
-                          &theme_values_.button_min_size, nullptr);
-  theme_values_.button_padding = gtk::GtkStyleContextGetPadding(button_context);
-
   title_->SetEnabledColor(theme_values_.title_color);
 
   InvalidateLayout();
@@ -299,8 +314,9 @@ ClientFrameViewLinux::GetButtonTypeToSkip() const {
 }
 
 void ClientFrameViewLinux::UpdateButtonImages() {
-  nav_button_provider_->RedrawImages(theme_values_.button_min_size,
-                                     frame_->IsMaximized(),
+  int top_area_height = theme_values_.titlebar_min_height +
+                        theme_values_.titlebar_padding.height();
+  nav_button_provider_->RedrawImages(top_area_height, frame_->IsMaximized(),
                                      ShouldPaintAsActive());
 
   ui::NavButtonProvider::FrameButtonDisplayType skip_type =
@@ -368,7 +384,14 @@ void ClientFrameViewLinux::LayoutButtonsOnSide(
 
     button->button->SetVisible(true);
 
-    int button_width = theme_values_.button_min_size;
+    // CSS min-size/height/width is not enough to determine the actual size of
+    // the buttons, so we sample the rendered image. See Chromium's
+    // BrowserFrameViewLinuxNative::MaybeUpdateCachedFrameButtonImages.
+    int button_width =
+        nav_button_provider_
+            ->GetImage(button->type,
+                       ui::NavButtonProvider::ButtonState::kNormal)
+            .width();
     int next_button_offset =
         button_width + nav_button_provider_->GetInterNavButtonSpacing();
 
@@ -404,7 +427,7 @@ gfx::Rect ClientFrameViewLinux::GetTitlebarBounds() const {
       std::max(font_height, theme_values_.titlebar_min_height) +
       GetTitlebarContentInsets().height();
 
-  gfx::Insets decoration_insets = RestoredFrameBorderInsets();
+  gfx::Insets decoration_insets = linux_frame_layout_->FrameBorderInsets(false);
 
   // We add the inset height here, so the .Inset() that follows won't reduce it
   // to be too small.

filename: shell/browser/ui/views/client_frame_view_linux.h
patch: @@ -91,12 +91,11 @@ class ClientFrameViewLinux : public FramelessView,
 
     SkColor title_color;
     gfx::Insets title_padding;
-
-    int button_min_size;
-    gfx::Insets button_padding;
   };
 
   void PaintAsActiveChanged();
+  void PaintRestoredFrameBorder(gfx::Canvas* canvas);
+  void PaintMaximizedFrameBorder(gfx::Canvas* canvas);
 
   void UpdateThemeValues();
 

filename: shell/browser/ui/views/linux_frame_layout.cc
patch: @@ -83,6 +83,12 @@ gfx::Insets LinuxFrameLayout::RestoredFrameBorderInsets() const {
   return gfx::Insets();
 }
 
+gfx::Insets LinuxFrameLayout::FrameBorderInsets(bool restored) const {
+  return !restored && (window_->IsMaximized() || window_->IsFullscreen())
+             ? gfx::Insets()
+             : RestoredFrameBorderInsets();
+}
+
 gfx::Insets LinuxFrameLayout::GetInputInsets() const {
   return gfx::Insets(kResizeInsideBoundsSize);
 }
@@ -106,7 +112,7 @@ void LinuxFrameLayout::set_tiled(bool tiled) {
 
 gfx::Rect LinuxFrameLayout::GetWindowBounds() const {
   gfx::Rect bounds = window_->widget()->GetWindowBoundsInScreen();
-  bounds.Inset(RestoredFrameBorderInsets());
+  bounds.Inset(FrameBorderInsets(false));
   return bounds;
 }
 

filename: shell/browser/ui/views/linux_frame_layout.h
patch: @@ -44,6 +44,9 @@ class LinuxFrameLayout {
                                                   CSDStyle csd_style);
 
   // Insets from the transparent widget border to the opaque part of the window.
+  // Returns empty insets when maximized or fullscreen unless |restored| is
+  // true. Matches Chromium's OpaqueBrowserFrameViewLayout::FrameBorderInsets.
+  gfx::Insets FrameBorderInsets(bool restored) const;
   virtual gfx::Insets RestoredFrameBorderInsets() const;
   // Insets for parts of the surface that should be counted for user input.
   virtual gfx::Insets GetInputInsets() const;

filename: shell/browser/ui/views/opaque_frame_view.cc
patch: @@ -210,7 +210,7 @@ void OpaqueFrameView::OnPaint(gfx::Canvas* canvas) {
     return;
 
   const bool active = ShouldPaintAsActive();
-  const gfx::Insets border = RestoredFrameBorderInsets();
+  const gfx::Insets border = FrameBorderInsets(false);
   const bool showing_shadow = linux_frame_layout_->IsShowingShadow();
   gfx::RectF bounds_dip(GetLocalBounds());
   if (showing_shadow) {
@@ -342,9 +342,7 @@ views::Button* OpaqueFrameView::CreateButton(
 }
 
 gfx::Insets OpaqueFrameView::FrameBorderInsets(bool restored) const {
-  return !restored && IsFrameCondensed()
-             ? gfx::Insets()
-             : linux_frame_layout_->RestoredFrameBorderInsets();
+  return linux_frame_layout_->FrameBorderInsets(restored);
 }
 
 int OpaqueFrameView::FrameTopBorderThickness(bool restored) const {