feat: enable Windows Control Overlay on Linux (#42682)

* feat: enable Windows Control Overlay on Linux

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* docs: update documentation for Linux WCO

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* fix: initial symbol painting

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* test: enable WCO tests for Linux

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* fix: add missing Layer include

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* chore: fix gn-check failure

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* fix: enable BrowserWindow.setTitleBarOverlay on Linux

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* test: fix test for maximize event on Linux

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* fix: geometry updating on BrowserWindow.setTitleBarOverlay

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* fix: crash when invalid titleBarStyle set

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* chore: clean up ordering and comments

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* Update docs/api/structures/base-window-options.md

Co-authored-by: Erick Zhao <erick@hotmail.ca>

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* feat: enable customizing symbolColor

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* docs: correct symbolColor reference

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* chore: update patches

Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>

* chore: remove Chrome-specific padding

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
This commit is contained in:
trop[bot] 2024-07-03 16:09:12 -04:00 committed by GitHub
parent 89d09922f7
commit 342ef8e7e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1026 additions and 107 deletions

View file

@ -108,6 +108,7 @@ static_library("chrome") {
"//chrome/browser/ui/frame/window_frame_util.h",
"//chrome/browser/ui/ui_features.cc",
"//chrome/browser/ui/ui_features.h",
"//chrome/browser/ui/view_ids.h",
"//chrome/browser/ui/views/eye_dropper/eye_dropper.cc",
"//chrome/browser/ui/views/eye_dropper/eye_dropper.h",
"//chrome/browser/ui/views/overlay/back_to_tab_label_button.cc",
@ -151,7 +152,6 @@ static_library("chrome") {
"//chrome/browser/media/webrtc/window_icon_util_win.cc",
"//chrome/browser/process_singleton_win.cc",
"//chrome/browser/ui/frame/window_frame_util.h",
"//chrome/browser/ui/view_ids.h",
"//chrome/browser/win/chrome_process_finder.cc",
"//chrome/browser/win/chrome_process_finder.h",
"//chrome/browser/win/titlebar_config.cc",

View file

@ -1370,15 +1370,16 @@ machine has a touch bar.
**Note:** The TouchBar API is currently experimental and may change or be
removed in future Electron releases.
#### `win.setTitleBarOverlay(options)` _Windows_
#### `win.setTitleBarOverlay(options)` _Windows_ _Linux_
* `options` Object
* `color` String (optional) _Windows_ - The CSS color of the Window Controls Overlay when enabled.
* `symbolColor` String (optional) _Windows_ - The CSS color of the symbols on the Window Controls Overlay when enabled.
* `height` Integer (optional) _Windows_ - The height of the title bar and Window Controls Overlay in pixels.
* `color` String (optional) - The CSS color of the Window Controls Overlay when enabled.
* `symbolColor` String (optional) - The CSS color of the symbols on the Window Controls Overlay when enabled.
* `height` Integer (optional) - The height of the title bar and Window Controls Overlay in pixels.
On a Window with Window Controls Overlay already enabled, this method updates
the style of the title bar overlay.
On a Window with Window Controls Overlay already enabled, this method updates the style of the title bar overlay.
On Linux, the `symbolColor` is automatically calculated to have minimum accessible contrast to the `color` if not explicitly set.
[quick-look]: https://en.wikipedia.org/wiki/Quick_Look
[vibrancy-docs]: https://developer.apple.com/documentation/appkit/nsvisualeffectview?preferredLanguage=objc

View file

@ -1641,15 +1641,16 @@ with `addBrowserView` or `setBrowserView`. The top-most BrowserView is the last
> The `BrowserView` class is deprecated, and replaced by the new
> [`WebContentsView`](web-contents-view.md) class.
#### `win.setTitleBarOverlay(options)` _Windows_
#### `win.setTitleBarOverlay(options)` _Windows_ _Linux_
* `options` Object
* `color` String (optional) _Windows_ - The CSS color of the Window Controls Overlay when enabled.
* `symbolColor` String (optional) _Windows_ - The CSS color of the symbols on the Window Controls Overlay when enabled.
* `height` Integer (optional) _macOS_ _Windows_ - The height of the title bar and Window Controls Overlay in pixels.
* `color` String (optional) - The CSS color of the Window Controls Overlay when enabled.
* `symbolColor` String (optional) - The CSS color of the symbols on the Window Controls Overlay when enabled.
* `height` Integer (optional) - The height of the title bar and Window Controls Overlay in pixels.
On a Window with Window Controls Overlay already enabled, this method updates
the style of the title bar overlay.
On a window with Window Controls Overlay already enabled, this method updates the style of the title bar overlay.
On Linux, the `symbolColor` is automatically calculated to have minimum accessible contrast to the `color` if not explicitly set.
[page-visibility-api]: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
[quick-look]: https://en.wikipedia.org/wiki/Quick_Look

View file

@ -80,14 +80,14 @@
* `followWindow` - The backdrop should automatically appear active when the window is active, and inactive when it is not. This is the default.
* `active` - The backdrop should always appear active.
* `inactive` - The backdrop should always appear inactive.
* `titleBarStyle` string (optional) _macOS_ _Windows_ - The style of window title bar.
* `titleBarStyle` string (optional) - The style of window title bar.
Default is `default`. Possible values are:
* `default` - Results in the standard title bar for macOS or Windows respectively.
* `hidden` - Results in a hidden title bar and a full size content window. On macOS, the window still has the standard window controls (“traffic lights”) in the top left. On Windows, when combined with `titleBarOverlay: true` it will activate the Window Controls Overlay (see `titleBarOverlay` for more information), otherwise no window controls will be shown.
* `hiddenInset` _macOS_ - Only on macOS, results in a hidden title bar
* `hidden` - Results in a hidden title bar and a full size content window. On macOS, the window still has the standard window controls (“traffic lights”) in the top left. On Windows and Linux, when combined with `titleBarOverlay: true` it will activate the Window Controls Overlay (see `titleBarOverlay` for more information), otherwise no window controls will be shown.
* `hiddenInset` _macOS_ - Results in a hidden title bar
with an alternative look where the traffic light buttons are slightly
more inset from the window edge.
* `customButtonsOnHover` _macOS_ - Only on macOS, results in a hidden
* `customButtonsOnHover` _macOS_ - Results in a hidden
title bar and a full size content window, the traffic light buttons will
display when being hovered over in the top left of the window.
**Note:** This option is currently experimental.

View file

@ -3,9 +3,9 @@
* `webPreferences` [WebPreferences](web-preferences.md?inline) (optional) - Settings of web page's features.
* `paintWhenInitiallyHidden` boolean (optional) - Whether the renderer should be active when `show` is `false` and it has just been created. In order for `document.visibilityState` to work correctly on first load with `show: false` you should set this to `false`. Setting this to `false` will cause the `ready-to-show` event to not fire. Default is `true`.
* `titleBarOverlay` Object | Boolean (optional) - When using a frameless window in conjunction with `win.setWindowButtonVisibility(true)` on macOS or using a `titleBarStyle` so that the standard window controls ("traffic lights" on macOS) are visible, this property enables the Window Controls Overlay [JavaScript APIs][overlay-javascript-apis] and [CSS Environment Variables][overlay-css-env-vars]. Specifying `true` will result in an overlay with default system colors. Default is `false`.
* `color` String (optional) _Windows_ - The CSS color of the Window Controls Overlay when enabled. Default is the system color.
* `color` String (optional) _Windows_ _Linux_ - The CSS color of the Window Controls Overlay when enabled. Default is the system color.
* `symbolColor` String (optional) _Windows_ - The CSS color of the symbols on the Window Controls Overlay when enabled. Default is the system color.
* `height` Integer (optional) _macOS_ _Windows_ - The height of the title bar and Window Controls Overlay in pixels. Default is system height.
* `height` Integer (optional) - The height of the title bar and Window Controls Overlay in pixels. Default is system height.
[overlay-css-env-vars]: https://github.com/WICG/window-controls-overlay/blob/main/explainer.md#css-environment-variables
[overlay-javascript-apis]: https://github.com/WICG/window-controls-overlay/blob/main/explainer.md#javascript-apis

View file

@ -91,7 +91,7 @@ win.setWindowButtonVisibility(false)
> combining `frame: false` with `win.setWindowButtonVisibility(true)` will yield the same
> layout outcome as setting `titleBarStyle: 'hidden'`.
## Window Controls Overlay _macOS_ _Windows_
## Window Controls Overlay
The [Window Controls Overlay API][] is a web standard that gives web apps the ability to
customize their title bar region when installed on desktop. Electron exposes this API
@ -115,12 +115,11 @@ const win = new BrowserWindow({
})
```
On either platform `titleBarOverlay` can also be an object. On both macOS and Windows, the height of the overlay can be specified with the `height` property. On Windows, the color of the overlay and its symbols can be specified using the `color` and `symbolColor` properties respectively. `rgba()`, `hsla()`, and `#RRGGBBAA` color formats are supported to apply transparency.
On either platform `titleBarOverlay` can also be an object. The height of the overlay can be specified with the `height` property. On Windows and Linux, the color of the overlay and can be specified using the `color` property. On Windows and Linux, the color of the overlay and its symbols can be specified using the `color` and `symbolColor` properties respectively. The `rgba()`, `hsla()`, and `#RRGGBBAA` color formats are supported to apply transparency.
If a color option is not specified, the color will default to its system color for the window control buttons. Similarly, if the height option is not specified it will default to the default height:
```js title='main.js'
// on Windows
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({
titleBarStyle: 'hidden',

View file

@ -44,6 +44,10 @@ filenames = {
"shell/browser/ui/status_icon_gtk.h",
"shell/browser/ui/tray_icon_linux.cc",
"shell/browser/ui/tray_icon_linux.h",
"shell/browser/ui/views/opaque_frame_view.cc",
"shell/browser/ui/views/opaque_frame_view.h",
"shell/browser/ui/views/caption_button_placeholder_container.cc",
"shell/browser/ui/views/caption_button_placeholder_container.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",

View file

@ -130,3 +130,4 @@ feat_add_support_for_missing_dialog_features_to_shell_dialogs.patch
fix_font_face_resolution_when_renderer_is_blocked.patch
feat_enable_passing_exit_code_on_service_process_crash.patch
x11_use_localized_display_label_only_for_browser_process.patch
feat_enable_customizing_symbol_color_in_framecaptionbutton.patch

View file

@ -0,0 +1,80 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Shelley Vohr <shelley.vohr@gmail.com>
Date: Fri, 5 Apr 2024 11:07:22 +0200
Subject: feat: enable customizing symbol color in FrameCaptionButton
This enables customizing the symbol color on a given FrameCaptionButton
for the Window Controls Overlay API on Linux. By default, the symbol color
is dynamically calculated based on the background color of the button to
ensure it has minimum contrast required to be accessible.
This should be upstreamed to Chromium if possible.
diff --git a/ui/views/window/frame_caption_button.cc b/ui/views/window/frame_caption_button.cc
index 73e6020e3b9b6e0d12a8dea991f189b3ddeab14c..b38e5bd1408c26cbbfc995fc2ac5dc5983cc0db7 100644
--- a/ui/views/window/frame_caption_button.cc
+++ b/ui/views/window/frame_caption_button.cc
@@ -107,7 +107,7 @@ FrameCaptionButton::FrameCaptionButton(PressedCallback callback,
FrameCaptionButton::~FrameCaptionButton() = default;
// static
-SkColor FrameCaptionButton::GetButtonColor(SkColor background_color) {
+SkColor FrameCaptionButton::GetAccessibleButtonColor(SkColor background_color) {
// Use IsDark() to change target colors instead of PickContrastingColor(), so
// that DefaultFrameHeader::GetTitleColor() (which uses different target
// colors) can change between light/dark targets at the same time. It looks
@@ -124,6 +124,22 @@ SkColor FrameCaptionButton::GetButtonColor(SkColor background_color) {
.color;
}
+SkColor FrameCaptionButton::GetButtonColor(SkColor background_color) {
+ // If the button color has been overridden, return that.
+ if (button_color_ != SkColor())
+ return button_color_;
+
+ return GetAccessibleButtonColor(background_color);
+}
+
+void FrameCaptionButton::SetButtonColor(SkColor button_color) {
+ if (button_color_ == button_color)
+ return;
+
+ button_color_ = button_color;
+ MaybeRefreshIconAndInkdropBaseColor();
+}
+
// static
float FrameCaptionButton::GetInactiveButtonColorAlphaRatio() {
return 0.38f;
diff --git a/ui/views/window/frame_caption_button.h b/ui/views/window/frame_caption_button.h
index 0ac923a3ca6052d499ed7c1a4f156b0f19ad4e64..3164f79828218d57843eba823e0f14ff456b2df4 100644
--- a/ui/views/window/frame_caption_button.h
+++ b/ui/views/window/frame_caption_button.h
@@ -44,8 +44,18 @@ class VIEWS_EXPORT FrameCaptionButton : public Button {
FrameCaptionButton& operator=(const FrameCaptionButton&) = delete;
~FrameCaptionButton() override;
+ // Gets the color to use for a frame caption button with accessible contrast
+ // to the given background color.
+ static SkColor GetAccessibleButtonColor(SkColor background_color);
+
// Gets the color to use for a frame caption button.
- static SkColor GetButtonColor(SkColor background_color);
+ SkColor GetButtonColor(SkColor background_color);
+
+ // Sets the color to use for a frame caption button.
+ // The color is by default calculated to be an accessible contrast
+ // to the background color, so you should keep that in mind when
+ // overriding that behavior.
+ void SetButtonColor(SkColor button_color);
// Gets the alpha ratio for the colors of inactive frame caption buttons.
static float GetInactiveButtonColorAlphaRatio();
@@ -134,6 +144,7 @@ class VIEWS_EXPORT FrameCaptionButton : public Button {
// TODO(b/292154873): Store the foreground color instead of the background
// color for the SkColor type.
absl::variant<ui::ColorId, SkColor> color_ = gfx::kPlaceholderColor;
+ SkColor button_color_ = SkColor();
// Whether the button should be painted as active.
bool paint_as_active_ = false;

View file

@ -40,6 +40,8 @@
#include "shell/browser/ui/views/win_frame_view.h"
#include "shell/browser/ui/win/taskbar_host.h"
#include "ui/base/win/shell.h"
#elif BUILDFLAG(IS_LINUX)
#include "shell/browser/ui/views/opaque_frame_view.h"
#endif
#if BUILDFLAG(IS_WIN)
@ -1041,11 +1043,13 @@ void BaseWindow::SetAppDetails(const gin_helper::Dictionary& options) {
relaunch_command, relaunch_display_name,
window_->GetAcceleratedWidget());
}
#endif
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
void BaseWindow::SetTitleBarOverlay(const gin_helper::Dictionary& options,
gin_helper::Arguments* args) {
// Ensure WCO is already enabled on this window
if (!window_->titlebar_overlay_enabled()) {
if (!window_->IsWindowControlsOverlayEnabled()) {
args->ThrowError("Titlebar overlay is not enabled");
return;
}
@ -1090,13 +1094,18 @@ void BaseWindow::SetTitleBarOverlay(const gin_helper::Dictionary& options,
updated = true;
}
// If anything was updated, invalidate the layout and schedule a paint of the
// window's frame view
if (updated) {
auto* frame_view = static_cast<WinFrameView*>(
window->widget()->non_client_view()->frame_view());
frame_view->InvalidateCaptionButtons();
}
if (!updated)
return;
// If anything was updated, ensure the overlay is repainted.
#if BUILDFLAG(IS_WIN)
auto* frame_view = static_cast<WinFrameView*>(
window->widget()->non_client_view()->frame_view());
#else
auto* frame_view = static_cast<OpaqueFrameView*>(
window->widget()->non_client_view()->frame_view());
#endif
frame_view->InvalidateCaptionButtons();
}
#endif
@ -1286,6 +1295,8 @@ void BaseWindow::BuildPrototype(v8::Isolate* isolate,
.SetMethod("setThumbnailClip", &BaseWindow::SetThumbnailClip)
.SetMethod("setThumbnailToolTip", &BaseWindow::SetThumbnailToolTip)
.SetMethod("setAppDetails", &BaseWindow::SetAppDetails)
#endif
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
.SetMethod("setTitleBarOverlay", &BaseWindow::SetTitleBarOverlay)
#endif
.SetProperty("id", &BaseWindow::GetID);

View file

@ -241,6 +241,9 @@ class BaseWindow : public gin_helper::TrackableObject<BaseWindow>,
bool SetThumbnailClip(const gfx::Rect& region);
bool SetThumbnailToolTip(const std::string& tooltip);
void SetAppDetails(const gin_helper::Dictionary& options);
#endif
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
void SetTitleBarOverlay(const gin_helper::Dictionary& options,
gin_helper::Arguments* args);
#endif

View file

@ -122,10 +122,6 @@ NativeWindow::NativeWindow(const gin_helper::Dictionary& options,
int height;
if (titlebar_overlay_dict.Get(options::kOverlayHeight, &height))
titlebar_overlay_height_ = height;
#if !(BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC))
DCHECK(false);
#endif
}
}

View file

@ -372,12 +372,23 @@ class NativeWindow : public base::SupportsUserData,
kHiddenInset,
kCustomButtonsOnHover,
};
TitleBarStyle title_bar_style() const { return title_bar_style_; }
bool IsWindowControlsOverlayEnabled() const {
bool valid_titlebar_style = title_bar_style() == TitleBarStyle::kHidden
#if BUILDFLAG(IS_MAC)
||
title_bar_style() == TitleBarStyle::kHiddenInset
#endif
;
return valid_titlebar_style && titlebar_overlay_;
}
int titlebar_overlay_height() const { return titlebar_overlay_height_; }
void set_titlebar_overlay_height(int height) {
titlebar_overlay_height_ = height;
}
bool titlebar_overlay_enabled() const { return titlebar_overlay_; }
bool has_frame() const { return has_frame_; }
void set_has_frame(bool has_frame) { has_frame_ = has_frame; }

View file

@ -26,6 +26,7 @@
#include "base/strings/utf_string_conversions.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/desktop_media_id.h"
#include "content/public/common/color_parser.h"
#include "shell/browser/api/electron_api_web_contents.h"
#include "shell/browser/ui/inspectable_web_contents.h"
#include "shell/browser/ui/inspectable_web_contents_view.h"
@ -59,6 +60,7 @@
#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 "shell/browser/ui/views/opaque_frame_view.h"
#include "shell/common/platform_util.h"
#include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h"
#include "ui/views/window/native_frame_view.h"
@ -76,7 +78,6 @@
#elif BUILDFLAG(IS_WIN)
#include "base/win/win_util.h"
#include "base/win/windows_version.h"
#include "content/public/common/color_parser.h"
#include "shell/browser/ui/views/win_frame_view.h"
#include "shell/browser/ui/win/electron_desktop_native_widget_aura.h"
#include "skia/ext/skia_utils_win.h"
@ -219,6 +220,7 @@ NativeWindowViews::NativeWindowViews(const gin_helper::Dictionary& options,
overlay_button_color_ = color_utils::GetSysSkColor(COLOR_BTNFACE);
overlay_symbol_color_ = color_utils::GetSysSkColor(COLOR_BTNTEXT);
#endif
v8::Local<v8::Value> titlebar_overlay;
if (options.Get(options::ktitleBarOverlay, &titlebar_overlay) &&
@ -244,9 +246,11 @@ NativeWindowViews::NativeWindowViews(const gin_helper::Dictionary& options,
}
}
if (title_bar_style_ != TitleBarStyle::kNormal)
// |hidden| is the only non-default titleBarStyle valid on Windows and Linux.
if (title_bar_style_ == TitleBarStyle::kHidden)
set_has_frame(false);
#if BUILDFLAG(IS_WIN)
// If the taskbar is re-created after we start up, we have to rebuild all of
// our buttons.
taskbar_created_message_ = RegisterWindowMessage(TEXT("TaskbarCreated"));
@ -1703,11 +1707,15 @@ NativeWindowViews::CreateNonClientFrameView(views::Widget* widget) {
if (has_frame() && !has_client_frame()) {
return std::make_unique<NativeFrameView>(this, widget);
} else {
auto frame_view = has_frame() && has_client_frame()
? std::make_unique<ClientFrameViewLinux>()
: std::make_unique<FramelessView>();
frame_view->Init(this, widget);
return frame_view;
if (has_frame() && has_client_frame()) {
auto frame_view = std::make_unique<ClientFrameViewLinux>();
frame_view->Init(this, widget);
return frame_view;
} else {
auto frame_view = std::make_unique<OpaqueFrameView>();
frame_view->Init(this, widget);
return frame_view;
}
}
#endif
}

View file

@ -167,13 +167,9 @@ class NativeWindowViews : public NativeWindow,
#if BUILDFLAG(IS_WIN)
TaskbarHost& taskbar_host() { return taskbar_host_; }
void UpdateThickFrame();
#endif
#if BUILDFLAG(IS_WIN)
bool IsWindowControlsOverlayEnabled() const {
return (title_bar_style_ == NativeWindowViews::TitleBarStyle::kHidden) &&
titlebar_overlay_;
}
SkColor overlay_button_color() const { return overlay_button_color_; }
void set_overlay_button_color(SkColor color) {
overlay_button_color_ = color;
@ -183,9 +179,6 @@ class NativeWindowViews : public NativeWindow,
overlay_symbol_color_ = color;
}
void UpdateThickFrame();
#endif
private:
// views::WidgetObserver:
void OnWidgetActivationChanged(views::Widget* widget, bool active) override;
@ -264,6 +257,10 @@ class NativeWindowViews : public NativeWindow,
std::unique_ptr<EventDisabler> event_disabler_;
#endif
// The color to use as the theme and symbol colors respectively for WCO.
SkColor overlay_button_color_ = SkColor();
SkColor overlay_symbol_color_ = SkColor();
#if BUILDFLAG(IS_WIN)
ui::WindowShowState last_window_state_;
@ -307,11 +304,6 @@ class NativeWindowViews : public NativeWindow,
std::optional<gfx::Rect> pending_bounds_change_;
// The color to use as the theme and symbol colors respectively for Window
// Controls Overlay if enabled on Windows.
SkColor overlay_button_color_;
SkColor overlay_symbol_color_;
// The message ID of the "TaskbarCreated" message, sent to us when we need to
// reset our thumbar buttons.
UINT taskbar_created_message_ = 0;

View file

@ -0,0 +1,20 @@
// Copyright 2024 Microsoft GmbH.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "shell/browser/ui/views/caption_button_placeholder_container.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/views/view.h"
CaptionButtonPlaceholderContainer::CaptionButtonPlaceholderContainer() {
SetPaintToLayer();
}
CaptionButtonPlaceholderContainer::~CaptionButtonPlaceholderContainer() =
default;
BEGIN_METADATA(CaptionButtonPlaceholderContainer)
END_METADATA

View file

@ -0,0 +1,26 @@
// Copyright 2024 Microsoft GmbH.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_UI_VIEWS_CAPTION_BUTTON_PLACEHOLDER_CONTAINER_H_
#define ELECTRON_SHELL_BROWSER_UI_VIEWS_CAPTION_BUTTON_PLACEHOLDER_CONTAINER_H_
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/views/view.h"
// A placeholder container for control buttons with window controls
// overlay display override. Does not interact with the buttons. It is just
// used to indicate that this is non-client-area.
class CaptionButtonPlaceholderContainer : public views::View {
METADATA_HEADER(CaptionButtonPlaceholderContainer, views::View)
public:
CaptionButtonPlaceholderContainer();
CaptionButtonPlaceholderContainer(const CaptionButtonPlaceholderContainer&) =
delete;
CaptionButtonPlaceholderContainer& operator=(
const CaptionButtonPlaceholderContainer&) = delete;
~CaptionButtonPlaceholderContainer() override;
};
#endif // ELECTRON_SHELL_BROWSER_UI_VIEWS_CAPTION_BUTTON_PLACEHOLDER_CONTAINER_H_

View file

@ -95,6 +95,8 @@ void FramelessView::ResetWindowControls() {}
void FramelessView::UpdateWindowIcon() {}
void FramelessView::InvalidateCaptionButtons() {}
void FramelessView::UpdateWindowTitle() {}
void FramelessView::SizeConstraintsChanged() {}

View file

@ -33,6 +33,13 @@ class FramelessView : public views::NonClientFrameView {
// Returns whether the |point| is on frameless window's resizing border.
virtual int ResizingBorderHitTest(const gfx::Point& point);
// Tells the NonClientView to invalidate caption buttons
// and forces a re-layout and re-paint.
virtual void InvalidateCaptionButtons();
NativeWindowViews* window() const { return window_; }
views::Widget* frame() const { return frame_; }
protected:
// Helper function for subclasses to implement ResizingBorderHitTest with a
// custom resize inset.

View file

@ -0,0 +1,552 @@
// Copyright (c) 2024 Microsoft GmbH.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/ui/views/opaque_frame_view.h"
#include "base/containers/adapters.h"
#include "base/i18n/rtl.h"
#include "chrome/grit/generated_resources.h"
#include "components/strings/grit/components_strings.h"
#include "shell/browser/native_window_views.h"
#include "shell/browser/ui/inspectable_web_contents_view.h"
#include "shell/browser/ui/views/caption_button_placeholder_container.h"
#include "ui/aura/window.h"
#include "ui/base/hit_test.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/linux/linux_ui.h"
#include "ui/views/background.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/window/frame_caption_button.h"
#include "ui/views/window/vector_icons/vector_icons.h"
namespace electron {
namespace {
// These values should be the same as Chromium uses.
constexpr int kCaptionButtonHeight = 18;
bool HitTestCaptionButton(views::Button* button, const gfx::Point& point) {
return button && button->GetVisible() &&
button->GetMirroredBounds().Contains(point);
}
// The frame has a 2 px 3D edge along the top. This is overridable by
// subclasses, so RestoredFrameEdgeInsets() should be used instead of using this
// constant directly.
const int kTopFrameEdgeThickness = 2;
// The frame has a 1 px 3D edge along the side. This is overridable by
// subclasses, so RestoredFrameEdgeInsets() should be used instead of using this
// constant directly.
const int kSideFrameEdgeThickness = 1;
// The minimum vertical padding between the bottom of the caption buttons and
// the top of the content shadow.
const int kCaptionButtonBottomPadding = 3;
} // namespace
// The content edge images have a shadow built into them.
const int OpaqueFrameView::kContentEdgeShadowThickness = 2;
OpaqueFrameView::OpaqueFrameView() = default;
OpaqueFrameView::~OpaqueFrameView() = default;
void OpaqueFrameView::Init(NativeWindowViews* window, views::Widget* frame) {
FramelessView::Init(window, frame);
if (!window->IsWindowControlsOverlayEnabled())
return;
caption_button_placeholder_container_ =
AddChildView(std::make_unique<CaptionButtonPlaceholderContainer>());
minimize_button_ = CreateButton(
VIEW_ID_MINIMIZE_BUTTON, IDS_ACCNAME_MINIMIZE,
views::CAPTION_BUTTON_ICON_MINIMIZE, HTMINBUTTON,
views::kWindowControlMinimizeIcon,
base::BindRepeating(&views::Widget::Minimize, base::Unretained(frame)));
maximize_button_ = CreateButton(
VIEW_ID_MAXIMIZE_BUTTON, IDS_ACCNAME_MAXIMIZE,
views::CAPTION_BUTTON_ICON_MAXIMIZE_RESTORE, HTMAXBUTTON,
views::kWindowControlMaximizeIcon,
base::BindRepeating(&views::Widget::Maximize, base::Unretained(frame)));
restore_button_ = CreateButton(
VIEW_ID_RESTORE_BUTTON, IDS_ACCNAME_RESTORE,
views::CAPTION_BUTTON_ICON_MAXIMIZE_RESTORE, HTMAXBUTTON,
views::kWindowControlRestoreIcon,
base::BindRepeating(&views::Widget::Restore, base::Unretained(frame)));
close_button_ = CreateButton(
VIEW_ID_CLOSE_BUTTON, IDS_ACCNAME_CLOSE, views::CAPTION_BUTTON_ICON_CLOSE,
HTMAXBUTTON, views::kWindowControlCloseIcon,
base::BindRepeating(&views::Widget::CloseWithReason,
base::Unretained(frame),
views::Widget::ClosedReason::kCloseButtonClicked));
// 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(
&OpaqueFrameView::PaintAsActiveChanged, base::Unretained(this)));
}
int OpaqueFrameView::ResizingBorderHitTest(const gfx::Point& point) {
return FramelessView::ResizingBorderHitTest(point);
}
void OpaqueFrameView::InvalidateCaptionButtons() {
UpdateCaptionButtonPlaceholderContainerBackground();
UpdateFrameCaptionButtons();
LayoutWindowControlsOverlay();
InvalidateLayout();
}
gfx::Rect OpaqueFrameView::GetBoundsForClientView() const {
if (window()->IsWindowControlsOverlayEnabled()) {
auto border_thickness = FrameBorderInsets(false);
int top_height = border_thickness.top();
return gfx::Rect(
border_thickness.left(), top_height,
std::max(0, width() - border_thickness.width()),
std::max(0, height() - top_height - border_thickness.bottom()));
}
return FramelessView::GetBoundsForClientView();
}
gfx::Rect OpaqueFrameView::GetWindowBoundsForClientBounds(
const gfx::Rect& client_bounds) const {
if (window()->IsWindowControlsOverlayEnabled()) {
int top_height = NonClientTopHeight(false);
auto border_insets = FrameBorderInsets(false);
return gfx::Rect(
std::max(0, client_bounds.x() - border_insets.left()),
std::max(0, client_bounds.y() - top_height),
client_bounds.width() + border_insets.width(),
client_bounds.height() + top_height + border_insets.bottom());
}
return FramelessView::GetWindowBoundsForClientBounds(client_bounds);
}
int OpaqueFrameView::NonClientHitTest(const gfx::Point& point) {
if (window()->IsWindowControlsOverlayEnabled()) {
if (HitTestCaptionButton(close_button_, point))
return HTCLOSE;
if (HitTestCaptionButton(restore_button_, point))
return HTMAXBUTTON;
if (HitTestCaptionButton(maximize_button_, point))
return HTMAXBUTTON;
if (HitTestCaptionButton(minimize_button_, point))
return HTMINBUTTON;
if (caption_button_placeholder_container_->GetMirroredBounds().Contains(
point)) {
return HTCAPTION;
}
}
return FramelessView::NonClientHitTest(point);
}
void OpaqueFrameView::ResetWindowControls() {
NonClientFrameView::ResetWindowControls();
if (restore_button_)
restore_button_->SetState(views::Button::STATE_NORMAL);
if (minimize_button_)
minimize_button_->SetState(views::Button::STATE_NORMAL);
if (maximize_button_)
maximize_button_->SetState(views::Button::STATE_NORMAL);
// The close button isn't affected by this constraint.
}
views::View* OpaqueFrameView::TargetForRect(views::View* root,
const gfx::Rect& rect) {
return views::NonClientFrameView::TargetForRect(root, rect);
}
void OpaqueFrameView::Layout(PassKey) {
LayoutSuperclass<FramelessView>(this);
if (!window()->IsWindowControlsOverlayEnabled())
return;
// Reset all our data so that everything is invisible.
TopAreaPadding top_area_padding = GetTopAreaPadding();
available_space_leading_x_ = top_area_padding.leading;
available_space_trailing_x_ = width() - top_area_padding.trailing;
minimum_size_for_buttons_ =
available_space_leading_x_ + width() - available_space_trailing_x_;
placed_leading_button_ = false;
placed_trailing_button_ = false;
LayoutWindowControls();
int height = NonClientTopHeight(false);
auto insets = FrameBorderInsets(false);
int container_x = placed_trailing_button_ ? available_space_trailing_x_ : 0;
caption_button_placeholder_container_->SetBounds(
container_x, insets.top(), minimum_size_for_buttons_ - insets.width(),
height - insets.top());
LayoutWindowControlsOverlay();
}
void OpaqueFrameView::OnPaint(gfx::Canvas* canvas) {
if (!window()->IsWindowControlsOverlayEnabled())
return;
if (frame()->IsFullscreen())
return;
UpdateFrameCaptionButtons();
}
void OpaqueFrameView::PaintAsActiveChanged() {
if (!window()->IsWindowControlsOverlayEnabled())
return;
UpdateCaptionButtonPlaceholderContainerBackground();
UpdateFrameCaptionButtons();
}
void OpaqueFrameView::UpdateFrameCaptionButtons() {
const bool active = ShouldPaintAsActive();
const SkColor symbol_color = window()->overlay_symbol_color();
const SkColor background_color = window()->overlay_button_color();
SkColor frame_color =
background_color == SkColor() ? GetFrameColor() : background_color;
for (views::Button* button :
{minimize_button_, maximize_button_, restore_button_, close_button_}) {
DCHECK_EQ(std::string(views::FrameCaptionButton::kViewClassName),
button->GetClassName());
views::FrameCaptionButton* frame_caption_button =
static_cast<views::FrameCaptionButton*>(button);
frame_caption_button->SetPaintAsActive(active);
frame_caption_button->SetButtonColor(symbol_color);
frame_caption_button->SetBackgroundColor(frame_color);
}
}
void OpaqueFrameView::UpdateCaptionButtonPlaceholderContainerBackground() {
if (caption_button_placeholder_container_) {
const SkColor obc = window()->overlay_button_color();
const SkColor bg_color = obc == SkColor() ? GetFrameColor() : obc;
caption_button_placeholder_container_->SetBackground(
views::CreateSolidBackground(bg_color));
}
}
void OpaqueFrameView::LayoutWindowControls() {
// Keep a list of all buttons that we don't show.
std::vector<views::FrameButton> buttons_not_shown;
buttons_not_shown.push_back(views::FrameButton::kMaximize);
buttons_not_shown.push_back(views::FrameButton::kMinimize);
buttons_not_shown.push_back(views::FrameButton::kClose);
for (const auto& button : leading_buttons_) {
ConfigureButton(button, ALIGN_LEADING);
std::erase(buttons_not_shown, button);
}
for (const auto& button : base::Reversed(trailing_buttons_)) {
ConfigureButton(button, ALIGN_TRAILING);
std::erase(buttons_not_shown, button);
}
for (const auto& button_id : buttons_not_shown)
HideButton(button_id);
}
void OpaqueFrameView::LayoutWindowControlsOverlay() {
int overlay_height = window()->titlebar_overlay_height();
if (overlay_height == 0) {
// Accounting for the 1 pixel margin at the top of the button container
overlay_height =
window()->IsMaximized()
? caption_button_placeholder_container_->size().height()
: caption_button_placeholder_container_->size().height() + 1;
}
int overlay_width = caption_button_placeholder_container_->size().width();
int bounding_rect_width = width() - overlay_width;
auto bounding_rect =
GetMirroredRect(gfx::Rect(0, 0, bounding_rect_width, overlay_height));
window()->SetWindowControlsOverlayRect(bounding_rect);
window()->NotifyLayoutWindowControlsOverlay();
}
views::Button* OpaqueFrameView::CreateButton(
ViewID view_id,
int accessibility_string_id,
views::CaptionButtonIcon icon_type,
int ht_component,
const gfx::VectorIcon& icon_image,
views::Button::PressedCallback callback) {
views::FrameCaptionButton* button = new views::FrameCaptionButton(
views::Button::PressedCallback(), icon_type, ht_component);
button->SetImage(button->GetIcon(), views::FrameCaptionButton::Animate::kNo,
icon_image);
button->SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
button->SetCallback(std::move(callback));
button->SetAccessibleName(l10n_util::GetStringUTF16(accessibility_string_id));
button->SetID(view_id);
AddChildView(button);
button->SetPaintToLayer();
button->layer()->SetFillsBoundsOpaquely(false);
return button;
}
gfx::Insets OpaqueFrameView::FrameBorderInsets(bool restored) const {
return !restored && IsFrameCondensed() ? gfx::Insets()
: RestoredFrameBorderInsets();
}
int OpaqueFrameView::FrameTopBorderThickness(bool restored) const {
int thickness = FrameBorderInsets(restored).top();
if ((restored || !IsFrameCondensed()) && thickness > 0)
thickness += NonClientExtraTopThickness();
return thickness;
}
OpaqueFrameView::TopAreaPadding OpaqueFrameView::GetTopAreaPadding(
bool has_leading_buttons,
bool has_trailing_buttons) const {
const auto padding = FrameBorderInsets(false);
return TopAreaPadding{padding.left(), padding.right()};
}
bool OpaqueFrameView::IsFrameCondensed() const {
return frame()->IsMaximized() || frame()->IsFullscreen();
}
gfx::Insets OpaqueFrameView::RestoredFrameBorderInsets() const {
return gfx::Insets();
}
gfx::Insets OpaqueFrameView::RestoredFrameEdgeInsets() const {
return gfx::Insets::TLBR(kTopFrameEdgeThickness, kSideFrameEdgeThickness,
kSideFrameEdgeThickness, kSideFrameEdgeThickness);
}
int OpaqueFrameView::NonClientExtraTopThickness() const {
return kNonClientExtraTopThickness;
}
int OpaqueFrameView::NonClientTopHeight(bool restored) const {
// Adding 2px of vertical padding puts at least 1 px of space on the top and
// bottom of the element.
constexpr int kVerticalPadding = 2;
const int icon_height = GetIconSize() + kVerticalPadding;
const int caption_button_height = DefaultCaptionButtonY(restored) +
kCaptionButtonHeight +
kCaptionButtonBottomPadding;
int custom_height = window()->titlebar_overlay_height();
return custom_height ? custom_height
: std::max(icon_height, caption_button_height) +
kContentEdgeShadowThickness;
}
int OpaqueFrameView::CaptionButtonY(views::FrameButton button_id,
bool restored) const {
return DefaultCaptionButtonY(restored);
}
int OpaqueFrameView::DefaultCaptionButtonY(bool restored) const {
// Maximized buttons start at window top, since the window has no border. This
// offset is for the image (the actual clickable bounds extend all the way to
// the top to take Fitts' Law into account).
const bool start_at_top_of_frame = !restored && IsFrameCondensed();
return start_at_top_of_frame
? FrameBorderInsets(false).top()
: views::NonClientFrameView::kFrameShadowThickness;
}
gfx::Insets OpaqueFrameView::FrameEdgeInsets(bool restored) const {
return RestoredFrameEdgeInsets();
}
int OpaqueFrameView::GetIconSize() const {
// The icon never shrinks below 16 px on a side.
const int kIconMinimumSize = 16;
return std::max(gfx::FontList().GetHeight(), kIconMinimumSize);
}
OpaqueFrameView::TopAreaPadding OpaqueFrameView::GetTopAreaPadding() const {
return GetTopAreaPadding(!leading_buttons_.empty(),
!trailing_buttons_.empty());
}
SkColor OpaqueFrameView::GetFrameColor() const {
return GetColorProvider()->GetColor(
ShouldPaintAsActive() ? ui::kColorFrameActive : ui::kColorFrameInactive);
}
void OpaqueFrameView::ConfigureButton(views::FrameButton button_id,
ButtonAlignment alignment) {
switch (button_id) {
case views::FrameButton::kMinimize: {
bool can_minimize = true; // delegate_->CanMinimize();
if (can_minimize) {
minimize_button_->SetVisible(true);
SetBoundsForButton(button_id, minimize_button_, alignment);
} else {
HideButton(button_id);
}
break;
}
case views::FrameButton::kMaximize: {
bool can_maximize = true; // delegate_->CanMaximize();
if (can_maximize) {
// When the window is restored, we show a maximized button; otherwise,
// we show a restore button.
bool is_restored = !window()->IsMaximized() && !window()->IsMinimized();
views::Button* invisible_button =
is_restored ? restore_button_ : maximize_button_;
invisible_button->SetVisible(false);
views::Button* visible_button =
is_restored ? maximize_button_ : restore_button_;
visible_button->SetVisible(true);
SetBoundsForButton(button_id, visible_button, alignment);
} else {
HideButton(button_id);
}
break;
}
case views::FrameButton::kClose: {
close_button_->SetVisible(true);
SetBoundsForButton(button_id, close_button_, alignment);
break;
}
}
}
void OpaqueFrameView::HideButton(views::FrameButton button_id) {
switch (button_id) {
case views::FrameButton::kMinimize:
minimize_button_->SetVisible(false);
break;
case views::FrameButton::kMaximize:
restore_button_->SetVisible(false);
maximize_button_->SetVisible(false);
break;
case views::FrameButton::kClose:
close_button_->SetVisible(false);
break;
}
}
void OpaqueFrameView::SetBoundsForButton(views::FrameButton button_id,
views::Button* button,
ButtonAlignment alignment) {
const int caption_y = CaptionButtonY(button_id, false);
// There should always be the same number of non-shadow pixels visible to the
// side of the caption buttons. In maximized mode we extend buttons to the
// screen top and the rightmost button to the screen right (or leftmost button
// to the screen left, for left-aligned buttons) to obey Fitts' Law.
const bool is_frame_condensed = IsFrameCondensed();
const int button_width = views::GetCaptionButtonWidth();
gfx::Size button_size = button->GetPreferredSize();
DCHECK_EQ(std::string(views::FrameCaptionButton::kViewClassName),
button->GetClassName());
const int caption_button_center_size =
button_width - 2 * views::kCaptionButtonInkDropDefaultCornerRadius;
const int height = GetTopAreaHeight() - FrameEdgeInsets(false).top();
const int corner_radius =
std::clamp((height - caption_button_center_size) / 2, 0,
views::kCaptionButtonInkDropDefaultCornerRadius);
button_size = gfx::Size(button_width, height);
button->SetPreferredSize(button_size);
static_cast<views::FrameCaptionButton*>(button)->SetInkDropCornerRadius(
corner_radius);
TopAreaPadding top_area_padding = GetTopAreaPadding();
switch (alignment) {
case ALIGN_LEADING: {
int extra_width = top_area_padding.leading;
int button_start_spacing =
GetWindowCaptionSpacing(button_id, true, !placed_leading_button_);
available_space_leading_x_ += button_start_spacing;
minimum_size_for_buttons_ += button_start_spacing;
bool top_spacing_clickable = is_frame_condensed;
bool start_spacing_clickable =
is_frame_condensed && !placed_leading_button_;
button->SetBounds(
available_space_leading_x_ - (start_spacing_clickable
? button_start_spacing + extra_width
: 0),
top_spacing_clickable ? 0 : caption_y,
button_size.width() + (start_spacing_clickable
? button_start_spacing + extra_width
: 0),
button_size.height() + (top_spacing_clickable ? caption_y : 0));
int button_end_spacing =
GetWindowCaptionSpacing(button_id, false, !placed_leading_button_);
available_space_leading_x_ += button_size.width() + button_end_spacing;
minimum_size_for_buttons_ += button_size.width() + button_end_spacing;
placed_leading_button_ = true;
break;
}
case ALIGN_TRAILING: {
int extra_width = top_area_padding.trailing;
int button_start_spacing =
GetWindowCaptionSpacing(button_id, true, !placed_trailing_button_);
available_space_trailing_x_ -= button_start_spacing;
minimum_size_for_buttons_ += button_start_spacing;
bool top_spacing_clickable = is_frame_condensed;
bool start_spacing_clickable =
is_frame_condensed && !placed_trailing_button_;
button->SetBounds(
available_space_trailing_x_ - button_size.width(),
top_spacing_clickable ? 0 : caption_y,
button_size.width() + (start_spacing_clickable
? button_start_spacing + extra_width
: 0),
button_size.height() + (top_spacing_clickable ? caption_y : 0));
int button_end_spacing =
GetWindowCaptionSpacing(button_id, false, !placed_trailing_button_);
available_space_trailing_x_ -= button_size.width() + button_end_spacing;
minimum_size_for_buttons_ += button_size.width() + button_end_spacing;
placed_trailing_button_ = true;
break;
}
}
}
int OpaqueFrameView::GetTopAreaHeight() const {
int top_height = NonClientTopHeight(false);
return top_height;
}
int OpaqueFrameView::GetWindowCaptionSpacing(views::FrameButton button_id,
bool leading_spacing,
bool is_leading_button) const {
return 0;
}
BEGIN_METADATA(OpaqueFrameView)
END_METADATA
} // namespace electron

View file

@ -0,0 +1,205 @@
// Copyright 2024 Microsoft GmbH.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_UI_VIEWS_OPAQUE_FRAME_VIEW_H_
#define ELECTRON_SHELL_BROWSER_UI_VIEWS_OPAQUE_FRAME_VIEW_H_
#include <memory>
#include "base/memory/raw_ptr.h"
#include "base/scoped_observation.h"
#include "chrome/browser/ui/view_ids.h"
#include "shell/browser/ui/views/frameless_view.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/gfx/font_list.h"
#include "ui/linux/nav_button_provider.h"
#include "ui/linux/window_button_order_observer.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/window/caption_button_types.h"
#include "ui/views/window/frame_buttons.h"
#include "ui/views/window/non_client_view.h"
class CaptionButtonPlaceholderContainer;
namespace electron {
class OpaqueFrameView : public FramelessView {
METADATA_HEADER(OpaqueFrameView, FramelessView)
public:
// Constants used by OpaqueBrowserFrameView as well.
static const int kContentEdgeShadowThickness;
static constexpr int kNonClientExtraTopThickness = 1;
OpaqueFrameView();
~OpaqueFrameView() override;
// FramelessView:
void Init(NativeWindowViews* window, views::Widget* frame) override;
int ResizingBorderHitTest(const gfx::Point& point) override;
void InvalidateCaptionButtons() override;
// 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 ResetWindowControls() override;
views::View* TargetForRect(views::View* root, const gfx::Rect& rect) override;
// views::View:
void Layout(PassKey) override;
void OnPaint(gfx::Canvas* canvas) override;
private:
enum ButtonAlignment { ALIGN_LEADING, ALIGN_TRAILING };
struct TopAreaPadding {
int leading;
int trailing;
};
void PaintAsActiveChanged();
void UpdateCaptionButtonPlaceholderContainerBackground();
void UpdateFrameCaptionButtons();
void LayoutWindowControls();
void LayoutWindowControlsOverlay();
// Creates and returns an ImageButton with |this| as its listener.
// Memory is owned by the caller.
views::Button* CreateButton(ViewID view_id,
int accessibility_string_id,
views::CaptionButtonIcon icon_type,
int ht_component,
const gfx::VectorIcon& icon_image,
views::Button::PressedCallback callback);
/** Layout-Related Utility Functions **/
// Returns the insets from the native window edge to the client view.
// This does not include any client edge. If |restored| is true, this
// is calculated as if the window was restored, regardless of its
// current node_data.
gfx::Insets FrameBorderInsets(bool restored) const;
// Returns the thickness of the border that makes up the window frame edge
// along the top of the frame. If |restored| is true, this acts as if the
// window is restored regardless of the actual mode.
int FrameTopBorderThickness(bool restored) const;
// Returns the spacing between the edge of the browser window and the first
// frame buttons.
TopAreaPadding GetTopAreaPadding(bool has_leading_buttons,
bool has_trailing_buttons) const;
// Determines whether the top frame is condensed vertically, as when the
// window is maximized. If true, the top frame is just the height of a tab,
// rather than having extra vertical space above the tabs.
bool IsFrameCondensed() const;
// The insets from the native window edge to the client view when the window
// is restored. This goes all the way to the web contents on the left, right,
// and bottom edges.
gfx::Insets RestoredFrameBorderInsets() const;
// The insets from the native window edge to the flat portion of the
// window border. That is, this function returns the "3D portion" of the
// border when the window is restored. The returned insets will not be larger
// than RestoredFrameBorderInsets().
gfx::Insets RestoredFrameEdgeInsets() const;
// Additional vertical padding between tabs and the top edge of the window
// when the window is restored.
int NonClientExtraTopThickness() const;
// Returns the height of the entire nonclient top border, from the edge of the
// window to the top of the tabs. If |restored| is true, this is calculated as
// if the window was restored, regardless of its current state.
int NonClientTopHeight(bool restored) const;
// Returns the y-coordinate of button |button_id|. If |restored| is true,
// acts as if the window is restored regardless of the real mode.
int CaptionButtonY(views::FrameButton button_id, bool restored) const;
// Returns the y-coordinate of the caption button when native frame buttons
// are disabled. If |restored| is true, acts as if the window is restored
// regardless of the real mode.
int DefaultCaptionButtonY(bool restored) const;
// Returns the insets from the native window edge to the flat portion of the
// window border. That is, this function returns the "3D portion" of the
// border. If |restored| is true, acts as if the window is restored
// regardless of the real mode.
gfx::Insets FrameEdgeInsets(bool restored) const;
// Returns the size of the window icon. This can be platform dependent
// because of differences in fonts.
int GetIconSize() const;
// Returns the spacing between the edge of the browser window and the first
// frame buttons.
TopAreaPadding GetTopAreaPadding() const;
// Returns the color of the frame.
SkColor GetFrameColor() const;
// Initializes the button with |button_id| to be aligned according to
// |alignment|.
void ConfigureButton(views::FrameButton button_id, ButtonAlignment alignment);
// Sets the visibility of all buttons associated with |button_id| to false.
void HideButton(views::FrameButton button_id);
// Adds a window caption button to either the leading or trailing side.
void SetBoundsForButton(views::FrameButton button_id,
views::Button* button,
ButtonAlignment alignment);
// Computes the height of the top area of the frame.
int GetTopAreaHeight() const;
// Returns the margin around button |button_id|. If |leading_spacing| is
// true, returns the left margin (in RTL), otherwise returns the right margin
// (in RTL). Extra margin may be added if |is_leading_button| is true.
int GetWindowCaptionSpacing(views::FrameButton button_id,
bool leading_spacing,
bool is_leading_button) const;
// Window controls.
raw_ptr<views::Button> minimize_button_;
raw_ptr<views::Button> maximize_button_;
raw_ptr<views::Button> restore_button_;
raw_ptr<views::Button> close_button_;
// The leading and trailing x positions of the empty space available for
// laying out titlebar elements.
int available_space_leading_x_ = 0;
int available_space_trailing_x_ = 0;
// Whether any of the window control buttons were packed on the leading or
// trailing sides. This state is only valid while layout is being performed.
bool placed_leading_button_ = false;
bool placed_trailing_button_ = false;
// The size of the window buttons. This does not count labels or other
// elements that should be counted in a minimal frame.
int minimum_size_for_buttons_ = 0;
std::vector<views::FrameButton> leading_buttons_;
std::vector<views::FrameButton> trailing_buttons_{
views::FrameButton::kMinimize, views::FrameButton::kMaximize,
views::FrameButton::kClose};
base::CallbackListSubscription paint_as_active_changed_subscription_;
// PlaceholderContainer beneath the controls button for WCO.
raw_ptr<CaptionButtonPlaceholderContainer>
caption_button_placeholder_container_;
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_UI_VIEWS_OPAQUE_FRAME_VIEW_H_

View file

@ -37,8 +37,6 @@ void WinFrameView::Init(NativeWindowViews* window, views::Widget* frame) {
if (window->IsWindowControlsOverlayEnabled()) {
caption_button_container_ =
AddChildView(std::make_unique<WinCaptionButtonContainer>(this));
} else {
caption_button_container_ = nullptr;
}
}
@ -82,24 +80,23 @@ views::View* WinFrameView::TargetForRect(views::View* root,
// Custom system titlebar returns non HTCLIENT value, however event should
// be handled by the view, not by the system, because there are no system
// buttons underneath.
if (!ShouldCustomDrawSystemTitlebar()) {
if (!window()->IsWindowControlsOverlayEnabled())
return this;
}
auto local_point = rect.origin();
ConvertPointToTarget(parent(), caption_button_container_, &local_point);
if (!caption_button_container_->HitTestPoint(local_point)) {
if (!caption_button_container_->HitTestPoint(local_point))
return this;
}
}
return NonClientFrameView::TargetForRect(root, rect);
}
int WinFrameView::NonClientHitTest(const gfx::Point& point) {
if (window_->has_frame())
if (window()->has_frame())
return frame_->client_view()->NonClientHitTest(point);
if (ShouldCustomDrawSystemTitlebar()) {
if (window()->IsWindowControlsOverlayEnabled()) {
// See if the point is within any of the window controls.
if (caption_button_container_) {
gfx::Point local_point = point;
@ -168,10 +165,6 @@ bool WinFrameView::IsMaximized() const {
return frame()->IsMaximized();
}
bool WinFrameView::ShouldCustomDrawSystemTitlebar() const {
return window()->IsWindowControlsOverlayEnabled();
}
void WinFrameView::Layout(PassKey) {
LayoutCaptionButtons();
if (window()->IsWindowControlsOverlayEnabled()) {
@ -197,7 +190,7 @@ int WinFrameView::FrameTopBorderThicknessPx(bool restored) const {
// See comments in BrowserDesktopWindowTreeHostWin::GetClientAreaInsets().
const bool needs_no_border =
(ShouldCustomDrawSystemTitlebar() && frame()->IsMaximized()) ||
(window()->IsWindowControlsOverlayEnabled() && frame()->IsMaximized()) ||
frame()->IsFullscreen();
if (needs_no_border && !restored)
return 0;
@ -245,7 +238,7 @@ void WinFrameView::LayoutCaptionButtons() {
return;
// Non-custom system titlebar already contains caption buttons.
if (!ShouldCustomDrawSystemTitlebar()) {
if (!window()->IsWindowControlsOverlayEnabled()) {
caption_button_container_->SetVisible(false);
return;
}

View file

@ -26,6 +26,7 @@ class WinFrameView : public FramelessView {
~WinFrameView() override;
void Init(NativeWindowViews* window, views::Widget* frame) override;
void InvalidateCaptionButtons() override;
// Alpha to use for features in the titlebar (the window title and caption
// buttons) when the window is inactive. They are opaque when active.
@ -33,24 +34,17 @@ class WinFrameView : public FramelessView {
SkColor GetReadableFeatureColor(SkColor background_color);
// Tells the NonClientView to invalidate the WinFrameView's caption buttons.
void InvalidateCaptionButtons();
// views::NonClientFrameView:
gfx::Rect GetWindowBoundsForClientBounds(
const gfx::Rect& client_bounds) const override;
int NonClientHitTest(const gfx::Point& point) override;
NativeWindowViews* window() const { return window_; }
views::Widget* frame() const { return frame_; }
WinCaptionButtonContainer* caption_button_container() {
return caption_button_container_;
}
bool IsMaximized() const;
bool ShouldCustomDrawSystemTitlebar() const;
// Visual height of the titlebar when the window is maximized (i.e. excluding
// the area above the top of the screen).
int TitlebarMaximizedVisualHeight() const;
@ -89,7 +83,7 @@ class WinFrameView : public FramelessView {
// The container holding the caption buttons (minimize, maximize, close, etc.)
// May be null if the caption button container is destroyed before the frame
// view. Always check for validity before using!
raw_ptr<WinCaptionButtonContainer> caption_button_container_;
raw_ptr<WinCaptionButtonContainer> caption_button_container_ = nullptr;
};
} // namespace electron

View file

@ -2958,7 +2958,7 @@ describe('BrowserWindow module', () => {
});
});
ifdescribe(['win32', 'darwin'].includes(process.platform))('"titleBarStyle" option', () => {
describe('"titleBarStyle" option', () => {
const testWindowsOverlay = async (style: any) => {
const w = new BrowserWindow({
show: false,
@ -2997,10 +2997,12 @@ describe('BrowserWindow module', () => {
const [, newOverlayRect] = await geometryChange;
expect(newOverlayRect.width).to.equal(overlayRect.width + 400);
};
afterEach(async () => {
await closeAllWindows();
ipcMain.removeAllListeners('geometrychange');
});
it('creates browser window with hidden title bar', () => {
const w = new BrowserWindow({
show: false,
@ -3011,6 +3013,7 @@ describe('BrowserWindow module', () => {
const contentSize = w.getContentSize();
expect(contentSize).to.deep.equal([400, 400]);
});
ifit(process.platform === 'darwin')('creates browser window with hidden inset title bar', () => {
const w = new BrowserWindow({
show: false,
@ -3021,14 +3024,16 @@ describe('BrowserWindow module', () => {
const contentSize = w.getContentSize();
expect(contentSize).to.deep.equal([400, 400]);
});
it('sets Window Control Overlay with hidden title bar', async () => {
await testWindowsOverlay('hidden');
});
ifit(process.platform === 'darwin')('sets Window Control Overlay with hidden inset title bar', async () => {
await testWindowsOverlay('hiddenInset');
});
ifdescribe(process.platform === 'win32')('when an invalid titleBarStyle is initially set', () => {
ifdescribe(process.platform !== 'darwin')('when an invalid titleBarStyle is initially set', () => {
let w: BrowserWindow;
beforeEach(() => {
@ -3064,7 +3069,7 @@ describe('BrowserWindow module', () => {
});
});
ifdescribe(['win32', 'darwin'].includes(process.platform))('"titleBarOverlay" option', () => {
describe('"titleBarOverlay" option', () => {
const testWindowsOverlayHeight = async (size: any) => {
const w = new BrowserWindow({
show: false,
@ -3079,6 +3084,7 @@ describe('BrowserWindow module', () => {
height: size
}
});
const overlayHTML = path.join(__dirname, 'fixtures', 'pages', 'overlay.html');
if (process.platform === 'darwin') {
await w.loadFile(overlayHTML);
@ -3087,48 +3093,52 @@ describe('BrowserWindow module', () => {
await w.loadFile(overlayHTML);
await overlayReady;
}
const overlayEnabled = await w.webContents.executeJavaScript('navigator.windowControlsOverlay.visible');
expect(overlayEnabled).to.be.true('overlayEnabled');
const overlayRectPreMax = await w.webContents.executeJavaScript('getJSOverlayProperties()');
if (!w.isMaximized()) {
const maximize = once(w, 'maximize');
w.show();
w.maximize();
await maximize;
}
expect(w.isMaximized()).to.be.true('not maximized');
const overlayRectPostMax = await w.webContents.executeJavaScript('getJSOverlayProperties()');
expect(overlayRectPreMax.y).to.equal(0);
if (process.platform === 'darwin') {
expect(overlayRectPreMax.x).to.be.greaterThan(0);
} else {
expect(overlayRectPreMax.x).to.equal(0);
}
expect(overlayRectPreMax.width).to.be.greaterThan(0);
expect(overlayRectPreMax.width).to.be.greaterThan(0);
expect(overlayRectPreMax.height).to.equal(size);
// Confirm that maximization only affected the height of the buttons and not the title bar
expect(overlayRectPostMax.height).to.equal(size);
// 'maximize' event is not emitted on Linux in CI.
if (process.platform !== 'linux' && !w.isMaximized()) {
const maximize = once(w, 'maximize');
w.show();
w.maximize();
await maximize;
expect(w.isMaximized()).to.be.true('not maximized');
const overlayRectPostMax = await w.webContents.executeJavaScript('getJSOverlayProperties()');
expect(overlayRectPostMax.height).to.equal(size);
}
};
afterEach(async () => {
await closeAllWindows();
ipcMain.removeAllListeners('geometrychange');
});
it('sets Window Control Overlay with title bar height of 40', async () => {
await testWindowsOverlayHeight(40);
});
});
ifdescribe(process.platform === 'win32')('BrowserWindow.setTitlebarOverlay', () => {
ifdescribe(process.platform !== 'darwin')('BrowserWindow.setTitlebarOverlay', () => {
afterEach(async () => {
await closeAllWindows();
ipcMain.removeAllListeners('geometrychange');
});
it('does not crash when an invalid titleBarStyle was initially set', () => {
it('throws when an invalid titleBarStyle is initially set', () => {
const win = new BrowserWindow({
show: false,
webPreferences: {
@ -3146,7 +3156,7 @@ describe('BrowserWindow module', () => {
win.setTitleBarOverlay({
color: '#000000'
});
}).to.not.throw();
}).to.throw('Titlebar overlay is not enabled');
});
it('correctly updates the height of the overlay', async () => {
@ -3160,22 +3170,25 @@ describe('BrowserWindow module', () => {
const overlayEnabled = await w.webContents.executeJavaScript('navigator.windowControlsOverlay.visible');
expect(overlayEnabled).to.be.true('overlayEnabled');
const { height: preMaxHeight } = await w.webContents.executeJavaScript('getJSOverlayProperties()');
if (!w.isMaximized()) {
const { height: preMaxHeight } = await w.webContents.executeJavaScript('getJSOverlayProperties()');
expect(preMaxHeight).to.equal(size);
// 'maximize' event is not emitted on Linux in CI.
if (process.platform !== 'linux' && !w.isMaximized()) {
const maximize = once(w, 'maximize');
w.show();
w.maximize();
await maximize;
}
expect(w.isMaximized()).to.be.true('not maximized');
const { x, y, width, height } = await w.webContents.executeJavaScript('getJSOverlayProperties()');
expect(x).to.equal(0);
expect(y).to.equal(0);
expect(width).to.be.greaterThan(0);
expect(height).to.equal(size);
expect(preMaxHeight).to.equal(size);
await maximize;
expect(w.isMaximized()).to.be.true('not maximized');
const { x, y, width, height } = await w.webContents.executeJavaScript('getJSOverlayProperties()');
expect(x).to.equal(0);
expect(y).to.equal(0);
expect(width).to.be.greaterThan(0);
expect(height).to.equal(size);
}
};
const INITIAL_SIZE = 40;