Browse Source

fix: Add support for Wayland window decorations (#29618)

Signed-off-by: Ryan Gonzalez <[email protected]>

Co-authored-by: Jeremy Rose <[email protected]>
Ryan Gonzalez 3 years ago
parent
commit
7caa88c46f

+ 5 - 0
filenames.gni

@@ -32,10 +32,13 @@ filenames = {
     "shell/browser/notifications/linux/notification_presenter_linux.cc",
     "shell/browser/notifications/linux/notification_presenter_linux.h",
     "shell/browser/relauncher_linux.cc",
+    "shell/browser/ui/electron_desktop_window_tree_host_linux.cc",
     "shell/browser/ui/file_dialog_gtk.cc",
     "shell/browser/ui/message_box_gtk.cc",
     "shell/browser/ui/tray_icon_gtk.cc",
     "shell/browser/ui/tray_icon_gtk.h",
+    "shell/browser/ui/views/client_frame_view_linux.cc",
+    "shell/browser/ui/views/client_frame_view_linux.h",
     "shell/common/application_info_linux.cc",
     "shell/common/language_util_linux.cc",
     "shell/common/node_bindings_linux.cc",
@@ -413,6 +416,8 @@ filenames = {
     "shell/browser/native_browser_view.h",
     "shell/browser/native_window.cc",
     "shell/browser/native_window.h",
+    "shell/browser/native_window_features.cc",
+    "shell/browser/native_window_features.h",
     "shell/browser/native_window_observer.h",
     "shell/browser/net/asar/asar_file_validator.cc",
     "shell/browser/net/asar/asar_file_validator.h",

+ 17 - 0
shell/browser/native_window.cc

@@ -13,6 +13,7 @@
 #include "base/values.h"
 #include "content/public/browser/web_contents_user_data.h"
 #include "shell/browser/browser.h"
+#include "shell/browser/native_window_features.h"
 #include "shell/browser/window_list.h"
 #include "shell/common/color_util.h"
 #include "shell/common/gin_helper/dictionary.h"
@@ -25,6 +26,11 @@
 #include "ui/display/win/screen_win.h"
 #endif
 
+#if defined(USE_OZONE) || defined(USE_X11)
+#include "ui/base/ui_base_features.h"
+#include "ui/ozone/public/ozone_platform.h"
+#endif
+
 namespace gin {
 
 template <>
@@ -108,6 +114,17 @@ NativeWindow::NativeWindow(const gin_helper::Dictionary& options,
   if (parent)
     options.Get("modal", &is_modal_);
 
+#if defined(USE_OZONE)
+  // Ozone X11 likes to prefer custom frames, but we don't need them unless
+  // on Wayland.
+  if (base::FeatureList::IsEnabled(features::kWaylandWindowDecorations) &&
+      !ui::OzonePlatform::GetInstance()
+           ->GetPlatformRuntimeProperties()
+           .supports_server_side_window_decorations) {
+    has_client_frame_ = true;
+  }
+#endif
+
   WindowList::AddWindow(this);
 }
 

+ 6 - 0
shell/browser/native_window.h

@@ -332,6 +332,7 @@ class NativeWindow : public base::SupportsUserData,
   bool has_frame() const { return has_frame_; }
   void set_has_frame(bool has_frame) { has_frame_ = has_frame; }
 
+  bool has_client_frame() const { return has_client_frame_; }
   bool transparent() const { return transparent_; }
   bool enable_larger_than_screen() const { return enable_larger_than_screen_; }
 
@@ -381,6 +382,11 @@ class NativeWindow : public base::SupportsUserData,
   // Whether window has standard frame.
   bool has_frame_ = true;
 
+  // Whether window has standard frame, but it's drawn by Electron (the client
+  // application) instead of the OS. Currently only has meaning on Linux for
+  // Wayland hosts.
+  bool has_client_frame_ = false;
+
   // Whether window is transparent.
   bool transparent_ = false;
 

+ 10 - 0
shell/browser/native_window_features.cc

@@ -0,0 +1,10 @@
+// Copyright (c) 2022 Slack Technologies, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/native_window_features.h"
+
+namespace features {
+const base::Feature kWaylandWindowDecorations{
+    "WaylandWindowDecorations", base::FEATURE_DISABLED_BY_DEFAULT};
+}

+ 14 - 0
shell/browser/native_window_features.h

@@ -0,0 +1,14 @@
+// Copyright (c) 2022 Slack Technologies, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_BROWSER_NATIVE_WINDOW_FEATURES_H_
+#define ELECTRON_SHELL_BROWSER_NATIVE_WINDOW_FEATURES_H_
+
+#include "base/feature_list.h"
+
+namespace features {
+extern const base::Feature kWaylandWindowDecorations;
+}
+
+#endif  // ELECTRON_SHELL_BROWSER_NATIVE_WINDOW_FEATURES_H_

+ 20 - 8
shell/browser/native_window_views.cc

@@ -19,6 +19,7 @@
 #include "content/public/browser/desktop_media_id.h"
 #include "shell/browser/api/electron_api_web_contents.h"
 #include "shell/browser/native_browser_view_views.h"
+#include "shell/browser/native_window_features.h"
 #include "shell/browser/ui/drag_util.h"
 #include "shell/browser/ui/inspectable_web_contents.h"
 #include "shell/browser/ui/inspectable_web_contents_view.h"
@@ -47,9 +48,11 @@
 #include "base/strings/string_util.h"
 #include "shell/browser/browser.h"
 #include "shell/browser/linux/unity_service.h"
+#include "shell/browser/ui/electron_desktop_window_tree_host_linux.h"
+#include "shell/browser/ui/views/client_frame_view_linux.h"
 #include "shell/browser/ui/views/frameless_view.h"
 #include "shell/browser/ui/views/native_frame_view.h"
-#include "ui/views/widget/desktop_aura/desktop_window_tree_host_linux.h"
+#include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h"
 #include "ui/views/window/native_frame_view.h"
 
 #if defined(USE_X11)
@@ -78,7 +81,6 @@
 #include "ui/display/screen.h"
 #include "ui/display/win/screen_win.h"
 #include "ui/gfx/color_utils.h"
-#include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h"
 #endif
 
 namespace electron {
@@ -227,9 +229,10 @@ NativeWindowViews::NativeWindowViews(const gin_helper::Dictionary& options,
   params.bounds = bounds;
   params.delegate = this;
   params.type = views::Widget::InitParams::TYPE_WINDOW;
-  params.remove_standard_frame = !has_frame();
+  params.remove_standard_frame = !has_frame() || has_client_frame();
 
-  if (transparent())
+  // If a client frame, we need to draw our own shadows.
+  if (transparent() || has_client_frame())
     params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
 
   // The given window is most likely not rectangular since it uses
@@ -253,6 +256,13 @@ NativeWindowViews::NativeWindowViews(const gin_helper::Dictionary& options,
   // Set WM_CLASS.
   params.wm_class_name = base::ToLowerASCII(name);
   params.wm_class_class = name;
+
+  if (base::FeatureList::IsEnabled(features::kWaylandWindowDecorations)) {
+    auto* native_widget = new views::DesktopNativeWidgetAura(widget());
+    params.native_widget = native_widget;
+    params.desktop_window_tree_host =
+        new ElectronDesktopWindowTreeHostLinux(this, native_widget);
+  }
 #endif
 
   widget()->Init(std::move(params));
@@ -337,7 +347,7 @@ NativeWindowViews::NativeWindowViews(const gin_helper::Dictionary& options,
   ::SetWindowLong(GetAcceleratedWidget(), GWL_EXSTYLE, ex_style);
 #endif
 
-  if (has_frame()) {
+  if (has_frame() && !has_client_frame()) {
     // TODO(zcbenz): This was used to force using native frame on Windows 2003,
     // we should check whether setting it in InitParams can work.
     widget()->set_frame_type(views::Widget::FrameType::kForceNative);
@@ -1553,7 +1563,7 @@ bool NativeWindowViews::ShouldDescendIntoChildForEventHandling(
     return false;
 
   // And the events on border for dragging resizable frameless window.
-  if (!has_frame() && resizable_) {
+  if ((!has_frame() || has_client_frame()) && resizable_) {
     auto* frame =
         static_cast<FramelessView*>(widget()->non_client_view()->frame_view());
     return frame->ResizingBorderHitTest(location) == HTNOWHERE;
@@ -1573,10 +1583,12 @@ NativeWindowViews::CreateNonClientFrameView(views::Widget* widget) {
   frame_view->Init(this, widget);
   return frame_view;
 #else
-  if (has_frame()) {
+  if (has_frame() && !has_client_frame()) {
     return std::make_unique<NativeFrameView>(this, widget);
   } else {
-    auto frame_view = std::make_unique<FramelessView>();
+    auto frame_view = has_frame() && has_client_frame()
+                          ? std::make_unique<ClientFrameViewLinux>()
+                          : std::make_unique<FramelessView>();
     frame_view->Init(this, widget);
     return frame_view;
   }

+ 149 - 0
shell/browser/ui/electron_desktop_window_tree_host_linux.cc

@@ -0,0 +1,149 @@
+// Copyright (c) 2021 Ryan Gonzalez.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+// Portions of this file are sourced from
+// chrome/browser/ui/views/frame/browser_desktop_window_tree_host_linux.cc,
+// Copyright (c) 2019 The Chromium Authors,
+// which is governed by a BSD-style license
+
+#include "shell/browser/ui/electron_desktop_window_tree_host_linux.h"
+
+#include <vector>
+
+#include "base/i18n/rtl.h"
+#include "shell/browser/ui/views/client_frame_view_linux.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/geometry/skia_conversions.h"
+#include "ui/platform_window/platform_window.h"
+#include "ui/views/linux_ui/linux_ui.h"
+#include "ui/views/widget/desktop_aura/desktop_window_tree_host.h"
+#include "ui/views/widget/desktop_aura/desktop_window_tree_host_linux.h"
+#include "ui/views/window/non_client_view.h"
+
+namespace electron {
+
+ElectronDesktopWindowTreeHostLinux::ElectronDesktopWindowTreeHostLinux(
+    NativeWindowViews* native_window_view,
+    views::DesktopNativeWidgetAura* desktop_native_widget_aura)
+    : views::DesktopWindowTreeHostLinux(native_window_view->widget(),
+                                        desktop_native_widget_aura),
+      native_window_view_(native_window_view) {}
+
+ElectronDesktopWindowTreeHostLinux::~ElectronDesktopWindowTreeHostLinux() =
+    default;
+
+bool ElectronDesktopWindowTreeHostLinux::SupportsClientFrameShadow() const {
+  return platform_window()->CanSetDecorationInsets() &&
+         platform_window()->IsTranslucentWindowOpacitySupported();
+}
+
+void ElectronDesktopWindowTreeHostLinux::OnWidgetInitDone() {
+  views::DesktopWindowTreeHostLinux::OnWidgetInitDone();
+  UpdateFrameHints();
+}
+
+void ElectronDesktopWindowTreeHostLinux::OnBoundsChanged(
+    const BoundsChange& change) {
+  views::DesktopWindowTreeHostLinux::OnBoundsChanged(change);
+  UpdateFrameHints();
+}
+
+void ElectronDesktopWindowTreeHostLinux::OnWindowStateChanged(
+    ui::PlatformWindowState old_state,
+    ui::PlatformWindowState new_state) {
+  views::DesktopWindowTreeHostLinux::OnWindowStateChanged(old_state, new_state);
+  UpdateFrameHints();
+}
+
+void ElectronDesktopWindowTreeHostLinux::OnNativeThemeUpdated(
+    ui::NativeTheme* observed_theme) {
+  UpdateFrameHints();
+}
+
+void ElectronDesktopWindowTreeHostLinux::OnDeviceScaleFactorChanged() {
+  UpdateFrameHints();
+}
+
+void ElectronDesktopWindowTreeHostLinux::UpdateFrameHints() {
+  if (SupportsClientFrameShadow() && native_window_view_->has_frame() &&
+      native_window_view_->has_client_frame()) {
+    UpdateClientDecorationHints(static_cast<ClientFrameViewLinux*>(
+        native_window_view_->widget()->non_client_view()->frame_view()));
+  }
+
+  SizeConstraintsChanged();
+}
+
+void ElectronDesktopWindowTreeHostLinux::UpdateClientDecorationHints(
+    ClientFrameViewLinux* view) {
+  ui::PlatformWindow* window = platform_window();
+  bool showing_frame = !native_window_view_->IsFullscreen();
+  float scale = device_scale_factor();
+
+  bool should_set_opaque_region = window->IsTranslucentWindowOpacitySupported();
+
+  gfx::Insets insets;
+  gfx::Insets input_insets;
+  if (showing_frame) {
+    insets = view->GetBorderDecorationInsets();
+    if (base::i18n::IsRTL()) {
+      insets.Set(insets.top(), insets.right(), insets.bottom(), insets.left());
+    }
+
+    input_insets = view->GetInputInsets();
+  }
+
+  gfx::Insets scaled_insets = gfx::ScaleToCeiledInsets(insets, scale);
+  window->SetDecorationInsets(&scaled_insets);
+
+  gfx::Rect input_bounds(view->GetWidget()->GetWindowBoundsInScreen().size());
+  input_bounds.Inset(insets + input_insets);
+  gfx::Rect scaled_bounds = gfx::ScaleToEnclosingRect(input_bounds, scale);
+  window->SetInputRegion(&scaled_bounds);
+
+  if (should_set_opaque_region) {
+    // 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.
+    SkRRect rrect = view->GetRoundedWindowContentBounds();
+    gfx::RectF rectf(view->GetWindowContentBounds());
+    rectf.Scale(scale);
+    // It is acceptable to omit some pixels that are opaque, but the region
+    // must not include any translucent pixels.  Therefore, we must
+    // conservatively scale to the enclosed rectangle.
+    gfx::Rect rect = gfx::ToEnclosedRect(rectf);
+
+    // Create the initial region from the clipping rectangle without rounded
+    // corners.
+    SkRegion region(gfx::RectToSkIRect(rect));
+
+    // Now subtract out the small rectangles that cover the corners.
+    struct {
+      SkRRect::Corner corner;
+      bool left;
+      bool upper;
+    } kCorners[] = {
+        {SkRRect::kUpperLeft_Corner, true, true},
+        {SkRRect::kUpperRight_Corner, false, true},
+        {SkRRect::kLowerLeft_Corner, true, false},
+        {SkRRect::kLowerRight_Corner, false, false},
+    };
+    for (const auto& corner : kCorners) {
+      auto radii = rrect.radii(corner.corner);
+      auto rx = std::ceil(scale * radii.x());
+      auto ry = std::ceil(scale * radii.y());
+      auto corner_rect = SkIRect::MakeXYWH(
+          corner.left ? rect.x() : rect.right() - rx,
+          corner.upper ? rect.y() : rect.bottom() - ry, rx, ry);
+      region.op(corner_rect, SkRegion::kDifference_Op);
+    }
+
+    // Convert the region to a list of rectangles.
+    std::vector<gfx::Rect> opaque_region;
+    for (SkRegion::Iterator i(region); !i.done(); i.next())
+      opaque_region.push_back(gfx::SkIRectToRect(i.rect()));
+    window->SetOpaqueRegion(&opaque_region);
+  }
+}
+
+}  // namespace electron

+ 71 - 0
shell/browser/ui/electron_desktop_window_tree_host_linux.h

@@ -0,0 +1,71 @@
+// Copyright (c) 2021 Ryan Gonzalez.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+// Portions of this file are sourced from
+// chrome/browser/ui/views/frame/browser_desktop_window_tree_host_linux.h,
+// Copyright (c) 2019 The Chromium Authors,
+// which is governed by a BSD-style license
+
+#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 "base/scoped_observation.h"
+#include "shell/browser/native_window_views.h"
+#include "shell/browser/ui/views/client_frame_view_linux.h"
+#include "ui/native_theme/native_theme_observer.h"
+#include "ui/views/linux_ui/device_scale_factor_observer.h"
+#include "ui/views/widget/desktop_aura/desktop_window_tree_host_linux.h"
+
+namespace electron {
+
+class ElectronDesktopWindowTreeHostLinux
+    : public views::DesktopWindowTreeHostLinux,
+      public ui::NativeThemeObserver,
+      public views::DeviceScaleFactorObserver {
+ public:
+  ElectronDesktopWindowTreeHostLinux(
+      NativeWindowViews* native_window_view,
+      views::DesktopNativeWidgetAura* desktop_native_widget_aura);
+  ~ElectronDesktopWindowTreeHostLinux() override;
+
+  // disable copy
+  ElectronDesktopWindowTreeHostLinux(
+      const ElectronDesktopWindowTreeHostLinux&) = delete;
+  ElectronDesktopWindowTreeHostLinux& operator=(
+      const ElectronDesktopWindowTreeHostLinux&) = delete;
+
+  bool SupportsClientFrameShadow() const;
+
+ protected:
+  // views::DesktopWindowTreeHostLinuxImpl:
+  void OnWidgetInitDone() override;
+
+  // ui::PlatformWindowDelegate
+  void OnBoundsChanged(const BoundsChange& change) override;
+  void OnWindowStateChanged(ui::PlatformWindowState old_state,
+                            ui::PlatformWindowState new_state) override;
+
+  // ui::NativeThemeObserver:
+  void OnNativeThemeUpdated(ui::NativeTheme* observed_theme) override;
+
+  // views::OnDeviceScaleFactorChanged:
+  void OnDeviceScaleFactorChanged() override;
+
+ private:
+  void UpdateFrameHints();
+  void UpdateClientDecorationHints(ClientFrameViewLinux* view);
+
+  NativeWindowViews* native_window_view_;  // weak ref
+
+  base::ScopedObservation<ui::NativeTheme, ui::NativeThemeObserver>
+      theme_observation_{this};
+  base::ScopedObservation<views::LinuxUI,
+                          views::DeviceScaleFactorObserver,
+                          &views::LinuxUI::AddDeviceScaleFactorObserver,
+                          &views::LinuxUI::RemoveDeviceScaleFactorObserver>
+      scale_observation_{this};
+};
+
+}  // namespace electron
+
+#endif  // ELECTRON_SHELL_BROWSER_UI_ELECTRON_DESKTOP_WINDOW_TREE_HOST_LINUX_H_

+ 463 - 0
shell/browser/ui/views/client_frame_view_linux.cc

@@ -0,0 +1,463 @@
+// Copyright (c) 2021 Ryan Gonzalez.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/ui/views/client_frame_view_linux.h"
+
+#include <algorithm>
+
+#include "base/strings/utf_string_conversions.h"
+#include "cc/paint/paint_filter.h"
+#include "cc/paint/paint_flags.h"
+#include "shell/browser/native_window_views.h"
+#include "shell/browser/ui/electron_desktop_window_tree_host_linux.h"
+#include "shell/browser/ui/views/frameless_view.h"
+#include "ui/base/hit_test.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "ui/gfx/canvas.h"
+#include "ui/gfx/font_list.h"
+#include "ui/gfx/geometry/insets.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/geometry/skia_conversions.h"
+#include "ui/gfx/skia_util.h"
+#include "ui/gfx/text_constants.h"
+#include "ui/gtk/gtk_compat.h"  // nogncheck
+#include "ui/gtk/gtk_util.h"
+#include "ui/native_theme/native_theme.h"
+#include "ui/strings/grit/ui_strings.h"
+#include "ui/views/controls/button/image_button.h"
+#include "ui/views/linux_ui/linux_ui.h"
+#include "ui/views/linux_ui/nav_button_provider.h"
+#include "ui/views/style/typography.h"
+#include "ui/views/widget/widget.h"
+#include "ui/views/window/frame_buttons.h"
+
+namespace electron {
+
+namespace {
+
+// These values should be the same as Chromium uses.
+constexpr int kResizeOutsideBorderSize = 10;
+constexpr int kResizeInsideBoundsSize = 5;
+
+}  // namespace
+
+// static
+const char ClientFrameViewLinux::kViewClassName[] = "ClientFrameView";
+
+ClientFrameViewLinux::ClientFrameViewLinux()
+    : theme_(ui::NativeTheme::GetInstanceForNativeUi()),
+      nav_button_provider_(
+          views::LinuxUI::instance()->CreateNavButtonProvider()),
+      nav_buttons_{
+          NavButton{views::NavButtonProvider::FrameButtonDisplayType::kClose,
+                    views::FrameButton::kClose, &views::Widget::Close,
+                    IDS_APP_ACCNAME_CLOSE, HTCLOSE},
+          NavButton{views::NavButtonProvider::FrameButtonDisplayType::kMaximize,
+                    views::FrameButton::kMaximize, &views::Widget::Maximize,
+                    IDS_APP_ACCNAME_MAXIMIZE, HTMAXBUTTON},
+          NavButton{views::NavButtonProvider::FrameButtonDisplayType::kRestore,
+                    views::FrameButton::kMaximize, &views::Widget::Restore,
+                    IDS_APP_ACCNAME_RESTORE, HTMAXBUTTON},
+          NavButton{views::NavButtonProvider::FrameButtonDisplayType::kMinimize,
+                    views::FrameButton::kMinimize, &views::Widget::Minimize,
+                    IDS_APP_ACCNAME_MINIMIZE, HTMINBUTTON},
+      },
+      trailing_frame_buttons_{views::FrameButton::kMinimize,
+                              views::FrameButton::kMaximize,
+                              views::FrameButton::kClose} {
+  for (auto& button : nav_buttons_) {
+    button.button = new views::ImageButton();
+    button.button->SetImageVerticalAlignment(views::ImageButton::ALIGN_MIDDLE);
+    button.button->SetAccessibleName(
+        l10n_util::GetStringUTF16(button.accessibility_id));
+    AddChildView(button.button);
+  }
+
+  title_ = new views::Label();
+  title_->SetSubpixelRenderingEnabled(false);
+  title_->SetAutoColorReadabilityEnabled(false);
+  title_->SetHorizontalAlignment(gfx::ALIGN_CENTER);
+  title_->SetVerticalAlignment(gfx::ALIGN_MIDDLE);
+  title_->SetTextStyle(views::style::STYLE_TAB_ACTIVE);
+  AddChildView(title_);
+
+  native_theme_observer_.Observe(theme_);
+  window_button_order_observer_.Observe(views::LinuxUI::instance());
+}
+
+ClientFrameViewLinux::~ClientFrameViewLinux() {
+  views::LinuxUI::instance()->RemoveWindowButtonOrderObserver(this);
+  theme_->RemoveObserver(this);
+}
+
+void ClientFrameViewLinux::Init(NativeWindowViews* window,
+                                views::Widget* frame) {
+  FramelessView::Init(window, frame);
+
+  // Unretained() is safe because the subscription is saved into an instance
+  // member and thus will be cancelled upon the instance's destruction.
+  paint_as_active_changed_subscription_ =
+      frame_->RegisterPaintAsActiveChangedCallback(base::BindRepeating(
+          &ClientFrameViewLinux::PaintAsActiveChanged, base::Unretained(this)));
+
+  auto* tree_host = static_cast<ElectronDesktopWindowTreeHostLinux*>(
+      ElectronDesktopWindowTreeHostLinux::GetHostForWidget(
+          window->GetAcceleratedWidget()));
+  host_supports_client_frame_shadow_ = tree_host->SupportsClientFrameShadow();
+
+  frame_provider_ = views::LinuxUI::instance()->GetWindowFrameProvider(
+      !host_supports_client_frame_shadow_);
+
+  UpdateWindowTitle();
+
+  for (auto& button : nav_buttons_) {
+    // Unretained() is safe because the buttons are added as children to, and
+    // thus owned by, this view. Thus, the buttons themselves will be destroyed
+    // when this view is destroyed, and the frame's life must never outlive the
+    // view.
+    button.button->SetCallback(
+        base::BindRepeating(button.callback, base::Unretained(frame)));
+  }
+
+  UpdateThemeValues();
+}
+
+gfx::Insets ClientFrameViewLinux::GetBorderDecorationInsets() const {
+  return frame_provider_->GetFrameThicknessDip();
+}
+
+gfx::Insets ClientFrameViewLinux::GetInputInsets() const {
+  return gfx::Insets(
+      host_supports_client_frame_shadow_ ? -kResizeOutsideBorderSize : 0);
+}
+
+gfx::Rect ClientFrameViewLinux::GetWindowContentBounds() const {
+  gfx::Rect content_bounds = bounds();
+  content_bounds.Inset(GetBorderDecorationInsets());
+  return content_bounds;
+}
+
+SkRRect ClientFrameViewLinux::GetRoundedWindowContentBounds() const {
+  SkRect rect = gfx::RectToSkRect(GetWindowContentBounds());
+  SkRRect rrect;
+
+  if (!frame_->IsMaximized()) {
+    SkPoint round_point{theme_values_.window_border_radius,
+                        theme_values_.window_border_radius};
+    SkPoint radii[] = {round_point, round_point, {}, {}};
+    rrect.setRectRadii(rect, radii);
+  } else {
+    rrect.setRect(rect);
+  }
+
+  return rrect;
+}
+
+void ClientFrameViewLinux::OnNativeThemeUpdated(
+    ui::NativeTheme* observed_theme) {
+  UpdateThemeValues();
+}
+
+void ClientFrameViewLinux::OnWindowButtonOrderingChange(
+    const std::vector<views::FrameButton>& leading_buttons,
+    const std::vector<views::FrameButton>& trailing_buttons) {
+  leading_frame_buttons_ = leading_buttons;
+  trailing_frame_buttons_ = trailing_buttons;
+
+  InvalidateLayout();
+}
+
+int ClientFrameViewLinux::ResizingBorderHitTest(const gfx::Point& point) {
+  return ResizingBorderHitTestImpl(
+      point,
+      GetBorderDecorationInsets() + gfx::Insets(kResizeInsideBoundsSize));
+}
+
+gfx::Rect ClientFrameViewLinux::GetBoundsForClientView() const {
+  gfx::Rect client_bounds = bounds();
+  if (!frame_->IsFullscreen()) {
+    client_bounds.Inset(GetBorderDecorationInsets());
+    client_bounds.Inset(0, GetTitlebarBounds().height(), 0, 0);
+  }
+  return client_bounds;
+}
+
+gfx::Rect ClientFrameViewLinux::GetWindowBoundsForClientBounds(
+    const gfx::Rect& client_bounds) const {
+  gfx::Insets insets = bounds().InsetsFrom(GetBoundsForClientView());
+  return gfx::Rect(std::max(0, client_bounds.x() - insets.left()),
+                   std::max(0, client_bounds.y() - insets.top()),
+                   client_bounds.width() + insets.width(),
+                   client_bounds.height() + insets.height());
+}
+
+int ClientFrameViewLinux::NonClientHitTest(const gfx::Point& point) {
+  int component = ResizingBorderHitTest(point);
+  if (component != HTNOWHERE) {
+    return component;
+  }
+
+  for (auto& button : nav_buttons_) {
+    if (button.button->GetVisible() &&
+        button.button->GetMirroredBounds().Contains(point)) {
+      return button.hit_test_id;
+    }
+  }
+
+  if (GetTitlebarBounds().Contains(point)) {
+    return HTCAPTION;
+  }
+
+  return FramelessView::NonClientHitTest(point);
+}
+
+void ClientFrameViewLinux::GetWindowMask(const gfx::Size& size,
+                                         SkPath* window_mask) {
+  // Nothing to do here, as transparency is used for decorations, not masks.
+}
+
+void ClientFrameViewLinux::UpdateWindowTitle() {
+  title_->SetText(base::UTF8ToUTF16(window_->GetTitle()));
+}
+
+void ClientFrameViewLinux::SizeConstraintsChanged() {
+  InvalidateLayout();
+}
+
+gfx::Size ClientFrameViewLinux::CalculatePreferredSize() const {
+  return SizeWithDecorations(FramelessView::CalculatePreferredSize());
+}
+
+gfx::Size ClientFrameViewLinux::GetMinimumSize() const {
+  return SizeWithDecorations(FramelessView::GetMinimumSize());
+}
+
+gfx::Size ClientFrameViewLinux::GetMaximumSize() const {
+  return SizeWithDecorations(FramelessView::GetMaximumSize());
+}
+
+void ClientFrameViewLinux::Layout() {
+  FramelessView::Layout();
+
+  if (frame_->IsFullscreen()) {
+    // Just hide everything and return.
+    for (NavButton& button : nav_buttons_) {
+      button.button->SetVisible(false);
+    }
+
+    title_->SetVisible(false);
+    return;
+  }
+
+  UpdateButtonImages();
+  LayoutButtons();
+
+  gfx::Rect title_bounds(GetTitlebarContentBounds());
+  title_bounds.Inset(theme_values_.title_padding);
+
+  title_->SetVisible(true);
+  title_->SetBounds(title_bounds.x(), title_bounds.y(), title_bounds.width(),
+                    title_bounds.height());
+}
+
+void ClientFrameViewLinux::OnPaint(gfx::Canvas* canvas) {
+  if (!frame_->IsFullscreen()) {
+    frame_provider_->PaintWindowFrame(canvas, GetLocalBounds(),
+                                      GetTitlebarBounds().bottom(),
+                                      ShouldPaintAsActive());
+  }
+}
+
+const char* ClientFrameViewLinux::GetClassName() const {
+  return kViewClassName;
+}
+
+void ClientFrameViewLinux::PaintAsActiveChanged() {
+  UpdateThemeValues();
+}
+
+void ClientFrameViewLinux::UpdateThemeValues() {
+  gtk::GtkCssContext window_context =
+      gtk::AppendCssNodeToStyleContext({}, "GtkWindow#window.background.csd");
+  gtk::GtkCssContext headerbar_context = gtk::AppendCssNodeToStyleContext(
+      {}, "GtkHeaderBar#headerbar.default-decoration.titlebar");
+  gtk::GtkCssContext title_context = gtk::AppendCssNodeToStyleContext(
+      headerbar_context, "GtkLabel#label.title");
+  gtk::GtkCssContext button_context = gtk::AppendCssNodeToStyleContext(
+      headerbar_context, "GtkButton#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 = frame_provider_->GetTopCornerRadiusDip();
+
+  gtk::GtkStyleContextGet(headerbar_context, "min-height",
+                          &theme_values_.titlebar_min_height, nullptr);
+  theme_values_.titlebar_padding =
+      gtk::GtkStyleContextGetPadding(headerbar_context);
+
+  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();
+  SchedulePaint();
+}
+
+views::NavButtonProvider::FrameButtonDisplayType
+ClientFrameViewLinux::GetButtonTypeToSkip() const {
+  return frame_->IsMaximized()
+             ? views::NavButtonProvider::FrameButtonDisplayType::kMaximize
+             : views::NavButtonProvider::FrameButtonDisplayType::kRestore;
+}
+
+void ClientFrameViewLinux::UpdateButtonImages() {
+  nav_button_provider_->RedrawImages(theme_values_.button_min_size,
+                                     frame_->IsMaximized(),
+                                     ShouldPaintAsActive());
+
+  views::NavButtonProvider::FrameButtonDisplayType skip_type =
+      GetButtonTypeToSkip();
+
+  for (NavButton& button : nav_buttons_) {
+    if (button.type == skip_type) {
+      continue;
+    }
+
+    for (size_t state_id = 0; state_id < views::Button::STATE_COUNT;
+         state_id++) {
+      views::Button::ButtonState state =
+          static_cast<views::Button::ButtonState>(state_id);
+      button.button->SetImage(
+          state, nav_button_provider_->GetImage(button.type, state));
+    }
+  }
+}
+
+void ClientFrameViewLinux::LayoutButtons() {
+  for (NavButton& button : nav_buttons_) {
+    button.button->SetVisible(false);
+  }
+
+  gfx::Rect remaining_content_bounds = GetTitlebarContentBounds();
+  LayoutButtonsOnSide(ButtonSide::kLeading, &remaining_content_bounds);
+  LayoutButtonsOnSide(ButtonSide::kTrailing, &remaining_content_bounds);
+}
+
+void ClientFrameViewLinux::LayoutButtonsOnSide(
+    ButtonSide side,
+    gfx::Rect* remaining_content_bounds) {
+  views::NavButtonProvider::FrameButtonDisplayType skip_type =
+      GetButtonTypeToSkip();
+
+  std::vector<views::FrameButton> frame_buttons;
+
+  switch (side) {
+    case ButtonSide::kLeading:
+      frame_buttons = leading_frame_buttons_;
+      break;
+    case ButtonSide::kTrailing:
+      frame_buttons = trailing_frame_buttons_;
+      // We always lay buttons out going from the edge towards the center, but
+      // they are given to us as left-to-right, so reverse them.
+      std::reverse(frame_buttons.begin(), frame_buttons.end());
+      break;
+    default:
+      NOTREACHED();
+  }
+
+  for (views::FrameButton frame_button : frame_buttons) {
+    auto* button = std::find_if(
+        nav_buttons_.begin(), nav_buttons_.end(), [&](const NavButton& test) {
+          return test.type != skip_type && test.frame_button == frame_button;
+        });
+    CHECK(button != nav_buttons_.end())
+        << "Failed to find frame button: " << static_cast<int>(frame_button);
+
+    if (button->type == skip_type) {
+      continue;
+    }
+
+    button->button->SetVisible(true);
+
+    int button_width = theme_values_.button_min_size;
+    int next_button_offset =
+        button_width + nav_button_provider_->GetInterNavButtonSpacing();
+
+    int x_position = 0;
+    gfx::Insets inset_after_placement;
+
+    switch (side) {
+      case ButtonSide::kLeading:
+        x_position = remaining_content_bounds->x();
+        inset_after_placement.set_left(next_button_offset);
+        break;
+      case ButtonSide::kTrailing:
+        x_position = remaining_content_bounds->right() - button_width;
+        inset_after_placement.set_right(next_button_offset);
+        break;
+      default:
+        NOTREACHED();
+    }
+
+    button->button->SetBounds(x_position, remaining_content_bounds->y(),
+                              button_width, remaining_content_bounds->height());
+    remaining_content_bounds->Inset(inset_after_placement);
+  }
+}
+
+gfx::Rect ClientFrameViewLinux::GetTitlebarBounds() const {
+  if (frame_->IsFullscreen()) {
+    return gfx::Rect();
+  }
+
+  int font_height = gfx::FontList().GetHeight();
+  int titlebar_height =
+      std::max(font_height, theme_values_.titlebar_min_height) +
+      GetTitlebarContentInsets().height();
+
+  gfx::Insets decoration_insets = GetBorderDecorationInsets();
+
+  // We add the inset height here, so the .Inset() that follows won't reduce it
+  // to be too small.
+  gfx::Rect titlebar(width(), titlebar_height + decoration_insets.height());
+  titlebar.Inset(decoration_insets);
+  return titlebar;
+}
+
+gfx::Insets ClientFrameViewLinux::GetTitlebarContentInsets() const {
+  return theme_values_.titlebar_padding +
+         nav_button_provider_->GetTopAreaSpacing();
+}
+
+gfx::Rect ClientFrameViewLinux::GetTitlebarContentBounds() const {
+  gfx::Rect titlebar(GetTitlebarBounds());
+  titlebar.Inset(GetTitlebarContentInsets());
+  return titlebar;
+}
+
+gfx::Size ClientFrameViewLinux::SizeWithDecorations(gfx::Size size) const {
+  gfx::Insets decoration_insets = GetBorderDecorationInsets();
+
+  size.Enlarge(0, GetTitlebarBounds().height());
+  size.Enlarge(decoration_insets.width(), decoration_insets.height());
+  return size;
+}
+
+}  // namespace electron

+ 143 - 0
shell/browser/ui/views/client_frame_view_linux.h

@@ -0,0 +1,143 @@
+// Copyright (c) 2021 Ryan Gonzalez.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_BROWSER_UI_VIEWS_CLIENT_FRAME_VIEW_LINUX_H_
+#define ELECTRON_SHELL_BROWSER_UI_VIEWS_CLIENT_FRAME_VIEW_LINUX_H_
+
+#include <array>
+#include <memory>
+#include <vector>
+
+#include "base/scoped_observation.h"
+#include "shell/browser/ui/views/frameless_view.h"
+#include "ui/native_theme/native_theme.h"
+#include "ui/native_theme/native_theme_observer.h"
+#include "ui/views/controls/button/image_button.h"
+#include "ui/views/controls/label.h"
+#include "ui/views/linux_ui/linux_ui.h"
+#include "ui/views/linux_ui/nav_button_provider.h"
+#include "ui/views/linux_ui/window_button_order_observer.h"
+#include "ui/views/linux_ui/window_frame_provider.h"
+#include "ui/views/widget/widget.h"
+#include "ui/views/window/frame_buttons.h"
+
+namespace electron {
+
+class ClientFrameViewLinux : public FramelessView,
+                             public ui::NativeThemeObserver,
+                             public views::WindowButtonOrderObserver {
+ public:
+  static const char kViewClassName[];
+  ClientFrameViewLinux();
+  ~ClientFrameViewLinux() override;
+
+  void Init(NativeWindowViews* window, views::Widget* frame) override;
+
+  // These are here for ElectronDesktopWindowTreeHostLinux to use.
+  gfx::Insets GetBorderDecorationInsets() const;
+  gfx::Insets GetInputInsets() const;
+  gfx::Rect GetWindowContentBounds() const;
+  SkRRect GetRoundedWindowContentBounds() const;
+
+ protected:
+  // ui::NativeThemeObserver:
+  void OnNativeThemeUpdated(ui::NativeTheme* observed_theme) override;
+
+  // views::WindowButtonOrderObserver:
+  void OnWindowButtonOrderingChange(
+      const std::vector<views::FrameButton>& leading_buttons,
+      const std::vector<views::FrameButton>& trailing_buttons) override;
+
+  // Overriden from FramelessView:
+  int ResizingBorderHitTest(const gfx::Point& point) override;
+
+  // Overriden from views::NonClientFrameView:
+  gfx::Rect GetBoundsForClientView() const override;
+  gfx::Rect GetWindowBoundsForClientBounds(
+      const gfx::Rect& client_bounds) const override;
+  int NonClientHitTest(const gfx::Point& point) override;
+  void GetWindowMask(const gfx::Size& size, SkPath* window_mask) override;
+  void UpdateWindowTitle() override;
+  void SizeConstraintsChanged() override;
+
+  // Overridden from View:
+  gfx::Size CalculatePreferredSize() const override;
+  gfx::Size GetMinimumSize() const override;
+  gfx::Size GetMaximumSize() const override;
+  void Layout() override;
+  void OnPaint(gfx::Canvas* canvas) override;
+  const char* GetClassName() const override;
+
+ private:
+  static constexpr int kNavButtonCount = 4;
+
+  struct NavButton {
+    views::NavButtonProvider::FrameButtonDisplayType type;
+    views::FrameButton frame_button;
+    void (views::Widget::*callback)();
+    int accessibility_id;
+    int hit_test_id;
+    views::ImageButton* button{nullptr};
+  };
+
+  struct ThemeValues {
+    float window_border_radius;
+
+    int titlebar_min_height;
+    gfx::Insets titlebar_padding;
+
+    SkColor title_color;
+    gfx::Insets title_padding;
+
+    int button_min_size;
+    gfx::Insets button_padding;
+  };
+
+  void PaintAsActiveChanged();
+
+  void UpdateThemeValues();
+
+  enum class ButtonSide { kLeading, kTrailing };
+
+  views::NavButtonProvider::FrameButtonDisplayType GetButtonTypeToSkip() const;
+  void UpdateButtonImages();
+  void LayoutButtons();
+  void LayoutButtonsOnSide(ButtonSide side,
+                           gfx::Rect* remaining_content_bounds);
+
+  gfx::Rect GetTitlebarBounds() const;
+  gfx::Insets GetTitlebarContentInsets() const;
+  gfx::Rect GetTitlebarContentBounds() const;
+
+  gfx::Size SizeWithDecorations(gfx::Size size) const;
+
+  ui::NativeTheme* theme_;
+  ThemeValues theme_values_;
+
+  views::Label* title_;
+
+  std::unique_ptr<views::NavButtonProvider> nav_button_provider_;
+  std::array<NavButton, kNavButtonCount> nav_buttons_;
+
+  std::vector<views::FrameButton> leading_frame_buttons_;
+  std::vector<views::FrameButton> trailing_frame_buttons_;
+
+  bool host_supports_client_frame_shadow_ = false;
+
+  views::WindowFrameProvider* frame_provider_;
+
+  base::ScopedObservation<ui::NativeTheme, ui::NativeThemeObserver>
+      native_theme_observer_{this};
+  base::ScopedObservation<views::LinuxUI,
+                          views::WindowButtonOrderObserver,
+                          &views::LinuxUI::AddWindowButtonOrderObserver,
+                          &views::LinuxUI::RemoveWindowButtonOrderObserver>
+      window_button_order_observer_{this};
+
+  base::CallbackListSubscription paint_as_active_changed_subscription_;
+};
+
+}  // namespace electron
+
+#endif  // ELECTRON_SHELL_BROWSER_UI_VIEWS_CLIENT_FRAME_VIEW_LINUX_H_

+ 10 - 7
shell/browser/ui/views/frameless_view.cc

@@ -33,7 +33,11 @@ void FramelessView::Init(NativeWindowViews* window, views::Widget* frame) {
 }
 
 int FramelessView::ResizingBorderHitTest(const gfx::Point& point) {
-  // Check the frame first, as we allow a small area overlapping the contents
+  return ResizingBorderHitTestImpl(point, gfx::Insets(kResizeInsideBoundsSize));
+}
+
+int FramelessView::ResizingBorderHitTestImpl(const gfx::Point& point,
+                                             const gfx::Insets& resize_border) {
   // to be used for resize handles.
   bool can_ever_resize = frame_->widget_delegate()
                              ? frame_->widget_delegate()->CanResize()
@@ -47,12 +51,11 @@ int FramelessView::ResizingBorderHitTest(const gfx::Point& point) {
 
   // Don't allow overlapping resize handles when the window is maximized or
   // fullscreen, as it can't be resized in those states.
-  int resize_border = frame_->IsMaximized() || frame_->IsFullscreen()
-                          ? 0
-                          : kResizeInsideBoundsSize;
-  return GetHTComponentForFrame(point, gfx::Insets(resize_border),
-                                kResizeAreaCornerSize, kResizeAreaCornerSize,
-                                can_ever_resize);
+  bool allow_overlapping_handles =
+      !frame_->IsMaximized() && !frame_->IsFullscreen();
+  return GetHTComponentForFrame(
+      point, allow_overlapping_handles ? resize_border : gfx::Insets(),
+      kResizeAreaCornerSize, kResizeAreaCornerSize, can_ever_resize);
 }
 
 gfx::Rect FramelessView::GetBoundsForClientView() const {

+ 6 - 1
shell/browser/ui/views/frameless_view.h

@@ -28,9 +28,14 @@ class FramelessView : public views::NonClientFrameView {
   virtual void Init(NativeWindowViews* window, views::Widget* frame);
 
   // Returns whether the |point| is on frameless window's resizing border.
-  int ResizingBorderHitTest(const gfx::Point& point);
+  virtual int ResizingBorderHitTest(const gfx::Point& point);
 
  protected:
+  // Helper function for subclasses to implement ResizingBorderHitTest with a
+  // custom resize inset.
+  int ResizingBorderHitTestImpl(const gfx::Point& point,
+                                const gfx::Insets& resize_border);
+
   // views::NonClientFrameView:
   gfx::Rect GetBoundsForClientView() const override;
   gfx::Rect GetWindowBoundsForClientBounds(