From 7caa88c46f70d9600bd4f78fb902374e72a2a4bf Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Wed, 26 Jan 2022 15:59:09 -0600 Subject: [PATCH] fix: Add support for Wayland window decorations (#29618) Signed-off-by: Ryan Gonzalez Co-authored-by: Jeremy Rose --- filenames.gni | 5 + shell/browser/native_window.cc | 17 + shell/browser/native_window.h | 6 + shell/browser/native_window_features.cc | 10 + shell/browser/native_window_features.h | 14 + shell/browser/native_window_views.cc | 28 +- ...electron_desktop_window_tree_host_linux.cc | 149 ++++++ .../electron_desktop_window_tree_host_linux.h | 71 +++ .../ui/views/client_frame_view_linux.cc | 463 ++++++++++++++++++ .../ui/views/client_frame_view_linux.h | 143 ++++++ shell/browser/ui/views/frameless_view.cc | 17 +- shell/browser/ui/views/frameless_view.h | 7 +- 12 files changed, 914 insertions(+), 16 deletions(-) create mode 100644 shell/browser/native_window_features.cc create mode 100644 shell/browser/native_window_features.h create mode 100644 shell/browser/ui/electron_desktop_window_tree_host_linux.cc create mode 100644 shell/browser/ui/electron_desktop_window_tree_host_linux.h create mode 100644 shell/browser/ui/views/client_frame_view_linux.cc create mode 100644 shell/browser/ui/views/client_frame_view_linux.h diff --git a/filenames.gni b/filenames.gni index b7d61a9858cf5..eb30e8506ff0e 100644 --- a/filenames.gni +++ b/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", diff --git a/shell/browser/native_window.cc b/shell/browser/native_window.cc index 55fec650ce812..799b6ab9fa95a 100644 --- a/shell/browser/native_window.cc +++ b/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); } diff --git a/shell/browser/native_window.h b/shell/browser/native_window.h index d5c148a84661c..777b4a8507f0e 100644 --- a/shell/browser/native_window.h +++ b/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; diff --git a/shell/browser/native_window_features.cc b/shell/browser/native_window_features.cc new file mode 100644 index 0000000000000..d4a4c85dd8676 --- /dev/null +++ b/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}; +} diff --git a/shell/browser/native_window_features.h b/shell/browser/native_window_features.h new file mode 100644 index 0000000000000..661ed98e92b25 --- /dev/null +++ b/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_ diff --git a/shell/browser/native_window_views.cc b/shell/browser/native_window_views.cc index 0751f0e670d61..d7d4a9bd827f5 100644 --- a/shell/browser/native_window_views.cc +++ b/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(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(this, widget); } else { - auto frame_view = std::make_unique(); + auto frame_view = has_frame() && has_client_frame() + ? std::make_unique() + : std::make_unique(); frame_view->Init(this, widget); return frame_view; } diff --git a/shell/browser/ui/electron_desktop_window_tree_host_linux.cc b/shell/browser/ui/electron_desktop_window_tree_host_linux.cc new file mode 100644 index 0000000000000..f763c13dca689 --- /dev/null +++ b/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 + +#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( + 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 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 diff --git a/shell/browser/ui/electron_desktop_window_tree_host_linux.h b/shell/browser/ui/electron_desktop_window_tree_host_linux.h new file mode 100644 index 0000000000000..bbf5782dcb38a --- /dev/null +++ b/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 + theme_observation_{this}; + base::ScopedObservation + scale_observation_{this}; +}; + +} // namespace electron + +#endif // ELECTRON_SHELL_BROWSER_UI_ELECTRON_DESKTOP_WINDOW_TREE_HOST_LINUX_H_ diff --git a/shell/browser/ui/views/client_frame_view_linux.cc b/shell/browser/ui/views/client_frame_view_linux.cc new file mode 100644 index 0000000000000..b0d7210ff5db7 --- /dev/null +++ b/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 + +#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::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& leading_buttons, + const std::vector& 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(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 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(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 diff --git a/shell/browser/ui/views/client_frame_view_linux.h b/shell/browser/ui/views/client_frame_view_linux.h new file mode 100644 index 0000000000000..404c0e6b7db91 --- /dev/null +++ b/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 +#include +#include + +#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& leading_buttons, + const std::vector& 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 nav_button_provider_; + std::array nav_buttons_; + + std::vector leading_frame_buttons_; + std::vector trailing_frame_buttons_; + + bool host_supports_client_frame_shadow_ = false; + + views::WindowFrameProvider* frame_provider_; + + base::ScopedObservation + native_theme_observer_{this}; + base::ScopedObservation + 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_ diff --git a/shell/browser/ui/views/frameless_view.cc b/shell/browser/ui/views/frameless_view.cc index 76a8d21fe038d..d2d0b2fdc5e85 100644 --- a/shell/browser/ui/views/frameless_view.cc +++ b/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 { diff --git a/shell/browser/ui/views/frameless_view.h b/shell/browser/ui/views/frameless_view.h index 70c6d2c2d2c21..80868f90c89c0 100644 --- a/shell/browser/ui/views/frameless_view.h +++ b/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(