Compare commits

...

5 Commits

Author SHA1 Message Date
Michaela Laurencin
2be495f70e docs: add further disposition description 2026-03-13 11:44:28 -04:00
Mitchell Cohen
11f28ac3ac fix: improved the appearance of shadows and borders on frameless windows on Wayland (#50007)
* remove painting from linux frame layout

* use chromium csd strategy for frameless windows

* Apply suggestions from code review

Remove unneeded virtual methods

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* removed inline destructors

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-03-11 15:42:09 -04:00
Erick Zhao
5ec589a1de chore: remove remark-cli from markdown linting (#50165)
* chore: remove stale `.remarkrc` file

* build(deps): remove unused deps

* cleanup again
2026-03-11 15:39:38 -04:00
Charles Kerr
4fe3752fae refactor: move electron::api::Tray to cppgc (#50187)
* refactor: migrate electron::api::tray to cppgc

* chore: add Tray to wrappable_pointer_tags.h patch

* fixup! refactor: migrate electron::api::tray to cppgc

clear keep_alive_ if error is thrown in constructor

* refactor: make Tray::menu_ a cppgc::Member<Menu>
2026-03-11 15:38:08 -04:00
Shelley Vohr
c8dd0b99ee fix: prevent traffic light buttons flashing on deminiaturize (#50183)
* fix: prevent traffic light buttons flashing on deminiaturize

When a window with a custom `trafficLightPosition` is minimized and
restored, macOS re-layouts the title bar container during the
deminiaturize animation, causing the traffic light buttons to briefly
appear at their default position before being repositioned.

Fix this by hiding the buttons container in `windowWillMiniaturize` and
restoring them (with a redraw to the correct position) in
`windowDidDeminiaturize`.

* chore: address feedback from review
2026-03-11 13:02:51 -04:00
19 changed files with 369 additions and 1755 deletions

View File

@@ -1,6 +0,0 @@
{
"plugins": [
["remark-lint-code-block-style", "fenced"],
["remark-lint-fenced-code-flag"]
]
}

View File

@@ -245,6 +245,10 @@ static_library("chrome") {
"//chrome/browser/ui/views/dark_mode_manager_linux.cc",
"//chrome/browser/ui/views/dark_mode_manager_linux.h",
]
sources += [
"//chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.cc",
"//chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h",
]
public_deps += [ "//components/dbus" ]
}

View File

@@ -226,7 +226,8 @@ Returns:
Only defined when the window is being created by a form that set
`target=_blank`.
* `disposition` string - Can be `default`, `foreground-tab`,
`background-tab`, `new-window` or `other`.
`background-tab`, `new-window` or `other`. Corresponds to the manner an associated link was clicked. See Chromium's
[WindowOpenDisposition](https://source.chromium.org/chromium/chromium/src/+/main:ui/base/window_open_disposition.h).
Emitted _after_ successful creation of a window via `window.open` in the renderer.
Not emitted if the creation of the window is canceled from
@@ -1450,7 +1451,8 @@ Ignore application menu shortcuts while this web contents is focused.
* `frameName` string - Name of the window provided in `window.open()`
* `features` string - Comma separated list of window features provided to `window.open()`.
* `disposition` string - Can be `default`, `foreground-tab`, `background-tab`,
`new-window` or `other`.
`new-window` or `other`. Corresponds to the manner an associated link was clicked. See Chromium's
[WindowOpenDisposition](https://source.chromium.org/chromium/chromium/src/+/main:ui/base/window_open_disposition.h).
* `referrer` [Referrer](structures/referrer.md) - The referrer that will be
passed to the new window. May or may not result in the `Referer` header being
sent, depending on the referrer policy.

View File

@@ -39,7 +39,7 @@ etc.
## Documentation
* Write [remark](https://github.com/remarkjs/remark) markdown style.
* Write prose according to our [documentation style guide](./style-guide.md).
You can run `npm run lint:docs` to ensure that your documentation changes are
formatted correctly.

View File

@@ -45,8 +45,6 @@
"null-loader": "^4.0.1",
"pre-flight": "^2.0.0",
"process": "^0.11.10",
"remark-cli": "^12.0.1",
"remark-preset-lint-markdown-style-guide": "^6.0.1",
"semver": "^7.6.3",
"stream-json": "^1.9.1",
"tap-xunit": "^2.4.1",
@@ -73,7 +71,7 @@
"lint:objc": "node ./script/lint.js --objc",
"lint:py": "node ./script/lint.js --py",
"lint:gn": "node ./script/lint.js --gn",
"lint:docs": "remark docs -qf && npm run lint:js-in-markdown && npm run create-typescript-definitions && npm run lint:ts-check-js-in-markdown && npm run lint:docs-fiddles && npm run lint:docs-relative-links && npm run lint:markdown && npm run lint:api-history",
"lint:docs": "npm run lint:js-in-markdown && npm run create-typescript-definitions && npm run lint:ts-check-js-in-markdown && npm run lint:docs-fiddles && npm run lint:docs-relative-links && npm run lint:markdown && npm run lint:api-history",
"lint:docs-fiddles": "standard \"docs/fiddles/**/*.js\"",
"lint:docs-relative-links": "lint-roller-markdown-links --resource-root . --root docs \"**/*.md\"",
"lint:markdown": "node ./script/lint.js --md",

View File

@@ -8,10 +8,10 @@ electron objects that extend gin::Wrappable and gets
allocated on the cpp heap
diff --git a/gin/public/wrappable_pointer_tags.h b/gin/public/wrappable_pointer_tags.h
index c29e8554933994ff56ccea394af34e17c4e9fc2c..42512541b36ceb353483a29eca2c858b9628854b 100644
index c29e8554933994ff56ccea394af34e17c4e9fc2c..6befb717f83d93d97033c240aa281e0bcb94e69c 100644
--- a/gin/public/wrappable_pointer_tags.h
+++ b/gin/public/wrappable_pointer_tags.h
@@ -76,7 +76,19 @@ enum WrappablePointerTag : uint16_t {
@@ -76,7 +76,20 @@ enum WrappablePointerTag : uint16_t {
kTextInputControllerBindings, // content::TextInputControllerBindings
kWebAXObjectProxy, // content::WebAXObjectProxy
kWrappedExceptionHandler, // extensions::WrappedExceptionHandler
@@ -27,6 +27,7 @@ index c29e8554933994ff56ccea394af34e17c4e9fc2c..42512541b36ceb353483a29eca2c858b
+ kElectronReplyChannel, // gin_helper::internal::ReplyChannel
+ kElectronScreen, // electron::api::Screen
+ kElectronSession, // electron::api::Session
+ kElectronTray, // electron::api::Tray
+ kElectronWebRequest, // electron::api::WebRequest
+ kLastPointerTag = kElectronWebRequest,
};

View File

@@ -23,6 +23,8 @@
#include "shell/common/gin_helper/error_thrower.h"
#include "shell/common/gin_helper/handle.h"
#include "shell/common/node_includes.h"
#include "v8/include/cppgc/allocation.h"
#include "v8/include/v8-cppgc.h"
namespace gin {
@@ -48,12 +50,13 @@ struct Converter<electron::TrayIcon::IconType> {
namespace electron::api {
gin::DeprecatedWrapperInfo Tray::kWrapperInfo = {gin::kEmbedderNativeGin};
const gin::WrapperInfo Tray::kWrapperInfo = {{gin::kEmbedderNativeGin},
gin::kElectronTray};
Tray::Tray(v8::Isolate* isolate,
v8::Local<v8::Value> image,
std::optional<base::Uuid> guid)
: guid_(guid), tray_icon_(TrayIcon::Create(guid)) {
: guid_{guid}, tray_icon_{TrayIcon::Create(guid)} {
SetImage(isolate, image);
tray_icon_->AddObserver(this);
if (guid.has_value())
@@ -63,10 +66,10 @@ Tray::Tray(v8::Isolate* isolate,
Tray::~Tray() = default;
// static
gin_helper::Handle<Tray> Tray::New(gin_helper::ErrorThrower thrower,
v8::Local<v8::Value> image,
std::optional<base::Uuid> guid,
gin::Arguments* args) {
Tray* Tray::New(gin_helper::ErrorThrower thrower,
v8::Local<v8::Value> image,
std::optional<base::Uuid> guid,
gin::Arguments* args) {
if (!Browser::Get()->is_ready()) {
thrower.ThrowError("Cannot create Tray before app is ready");
return {};
@@ -80,17 +83,17 @@ gin_helper::Handle<Tray> Tray::New(gin_helper::ErrorThrower thrower,
// Error thrown by us will be dropped when entering V8.
// Make sure to abort early and propagate the error to JS.
// Refs https://chromium-review.googlesource.com/c/v8/v8/+/5050065
v8::TryCatch try_catch(args->isolate());
auto* tray = new Tray(args->isolate(), image, guid);
v8::Isolate* isolate = args->isolate();
v8::TryCatch try_catch{isolate};
Tray* tray = cppgc::MakeGarbageCollected<Tray>(
isolate->GetCppHeap()->GetAllocationHandle(), isolate, image, guid);
if (try_catch.HasCaught()) {
delete tray;
tray->keep_alive_.Clear();
try_catch.ReThrow();
return {};
}
auto handle = gin_helper::CreateHandle(args->isolate(), tray);
handle->Pin(args->isolate());
return handle;
return tray;
}
void Tray::OnClicked(const gfx::Rect& bounds,
@@ -186,9 +189,9 @@ void Tray::OnDragEnded() {
}
void Tray::Destroy() {
Unpin();
menu_.Reset();
menu_.Clear();
tray_icon_.reset();
keep_alive_.Clear();
}
bool Tray::IsDestroyed() {
@@ -374,12 +377,13 @@ void Tray::SetContextMenu(gin_helper::ErrorThrower thrower,
v8::Local<v8::Value> arg) {
if (!CheckAlive())
return;
gin_helper::Handle<Menu> menu;
if (arg->IsNull()) {
menu_.Reset();
menu_.Clear();
tray_icon_->SetContextMenu(nullptr);
} else if (gin::ConvertFromV8(thrower.isolate(), arg, &menu)) {
menu_.Reset(thrower.isolate(), menu.ToV8());
} else if (Menu* menu = nullptr;
gin::ConvertFromV8(thrower.isolate(), arg, &menu)) {
menu_ = menu;
tray_icon_->SetContextMenu(menu->model());
} else {
thrower.ThrowTypeError("Must pass Menu or null");
@@ -437,12 +441,17 @@ void Tray::FillObjectTemplate(v8::Isolate* isolate,
.Build();
}
const char* Tray::GetTypeName() {
return GetClassName();
void Tray::Trace(cppgc::Visitor* visitor) const {
gin::Wrappable<Tray>::Trace(visitor);
visitor->Trace(menu_);
}
void Tray::WillBeDestroyed() {
ClearWeak();
const gin::WrapperInfo* Tray::wrapper_info() const {
return &kWrapperInfo;
}
const char* Tray::GetHumanReadableName() const {
return "Electron / Tray";
}
} // namespace electron::api
@@ -457,7 +466,7 @@ void Initialize(v8::Local<v8::Object> exports,
void* priv) {
v8::Isolate* const isolate = electron::JavascriptEnvironment::GetIsolate();
gin::Dictionary dict{isolate, exports};
dict.Set("Tray", Tray::GetConstructor(isolate, context));
dict.Set("Tray", Tray::GetConstructor(isolate, context, &Tray::kWrapperInfo));
}
} // namespace

View File

@@ -10,14 +10,13 @@
#include <string>
#include <vector>
#include "gin/wrappable.h"
#include "shell/browser/event_emitter_mixin.h"
#include "shell/browser/ui/tray_icon.h"
#include "shell/browser/ui/tray_icon_observer.h"
#include "shell/common/gin_converters/guid_converter.h"
#include "shell/common/gin_helper/cleaned_up_at_exit.h"
#include "shell/common/gin_helper/constructible.h"
#include "shell/common/gin_helper/pinnable.h"
#include "shell/common/gin_helper/wrappable.h"
#include "shell/common/gin_helper/self_keep_alive.h"
namespace gfx {
class Image;
@@ -27,47 +26,43 @@ class Image;
namespace gin_helper {
class Dictionary;
class ErrorThrower;
template <typename T>
class Handle;
} // namespace gin_helper
namespace electron::api {
class Menu;
class Tray final : public gin_helper::DeprecatedWrappable<Tray>,
class Tray final : public gin::Wrappable<Tray>,
public gin_helper::EventEmitterMixin<Tray>,
public gin_helper::Constructible<Tray>,
public gin_helper::CleanedUpAtExit,
public gin_helper::Pinnable<Tray>,
private TrayIconObserver {
public:
// gin_helper::Constructible
static gin_helper::Handle<Tray> New(gin_helper::ErrorThrower thrower,
v8::Local<v8::Value> image,
std::optional<base::Uuid> guid,
gin::Arguments* args);
static Tray* New(gin_helper::ErrorThrower thrower,
v8::Local<v8::Value> image,
std::optional<base::Uuid> guid,
gin::Arguments* args);
static void FillObjectTemplate(v8::Isolate*, v8::Local<v8::ObjectTemplate>);
static const char* GetClassName() { return "Tray"; }
// gin_helper::Wrappable
static gin::DeprecatedWrapperInfo kWrapperInfo;
const char* GetTypeName() override;
// gin::Wrappable
static const gin::WrapperInfo kWrapperInfo;
void Trace(cppgc::Visitor*) const override;
const gin::WrapperInfo* wrapper_info() const override;
const char* GetHumanReadableName() const override;
// gin_helper::CleanedUpAtExit
void WillBeDestroyed() override;
// Make public for cppgc::MakeGarbageCollected.
Tray(v8::Isolate* isolate,
v8::Local<v8::Value> image,
std::optional<base::Uuid> guid);
~Tray() override;
// disable copy
Tray(const Tray&) = delete;
Tray& operator=(const Tray&) = delete;
private:
Tray(v8::Isolate* isolate,
v8::Local<v8::Value> image,
std::optional<base::Uuid> guid);
~Tray() override;
// TrayIconObserver:
void OnClicked(const gfx::Rect& bounds,
const gfx::Point& location,
@@ -115,9 +110,10 @@ class Tray final : public gin_helper::DeprecatedWrappable<Tray>,
bool CheckAlive();
v8::Global<v8::Value> menu_;
cppgc::Member<Menu> menu_;
std::optional<base::Uuid> guid_;
std::unique_ptr<TrayIcon> tray_icon_;
gin_helper::SelfKeepAlive<Tray> keep_alive_{this};
};
} // namespace electron::api

View File

@@ -172,6 +172,12 @@ class NativeWindowMac : public NativeWindow,
void NotifyWindowDidFailToEnterFullScreen();
void NotifyWindowWillLeaveFullScreen();
// Hide/show traffic light buttons around miniaturize/deminiaturize to
// prevent them from flashing at the default position during the restore
// animation when a custom trafficLightPosition is configured.
void HideTrafficLights();
void RestoreTrafficLights();
// Cleanup observers when window is getting closed. Note that the destructor
// can be called much later after window gets closed, so we should not do
// cleanup in destructor.

View File

@@ -1535,6 +1535,18 @@ void NativeWindowMac::RedrawTrafficLights() {
[buttons_proxy_ redraw];
}
void NativeWindowMac::HideTrafficLights() {
if (buttons_proxy_)
[buttons_proxy_ setVisible:NO];
}
void NativeWindowMac::RestoreTrafficLights() {
if (buttons_proxy_ && window_button_visibility_.value_or(true)) {
[buttons_proxy_ redraw];
[buttons_proxy_ setVisible:YES];
}
}
// In simpleFullScreen mode, update the frame for new bounds.
void NativeWindowMac::UpdateFrame() {
NSWindow* window = GetNativeWindow().GetNativeNSWindow();

View File

@@ -256,6 +256,10 @@ using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
shell_->SetWindowLevel(NSNormalWindowLevel);
shell_->UpdateWindowOriginalFrame();
shell_->DetachChildren();
// Hide the traffic light buttons container before miniaturize so that
// when the window is restored, macOS does not render the buttons at
// their default position during the deminiaturize animation.
shell_->HideTrafficLights();
}
- (void)windowDidMiniaturize:(NSNotification*)notification {
@@ -273,6 +277,10 @@ using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
shell_->set_wants_to_be_visible(true);
shell_->AttachChildren();
shell_->SetWindowLevel(level_);
// Reposition traffic light buttons and make them visible again.
// They were hidden in windowWillMiniaturize to prevent a flash at
// the default (0,0) position during the restore animation.
shell_->RestoreTrafficLights();
shell_->NotifyWindowRestore();
}

View File

@@ -243,8 +243,8 @@ void ElectronDesktopWindowTreeHostLinux::UpdateFrameHints() {
// 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 = layout->GetRoundedWindowContentBounds();
gfx::RectF rectf(layout->GetWindowContentBounds());
SkRRect rrect = layout->GetRoundedWindowBounds();
gfx::RectF rectf(layout->GetWindowBounds());
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

View File

@@ -112,7 +112,7 @@ ClientFrameViewLinux::~ClientFrameViewLinux() {
void ClientFrameViewLinux::Init(NativeWindowViews* window,
views::Widget* frame) {
FramelessView::Init(window, frame);
linux_frame_layout_ = std::make_unique<LinuxCSDFrameLayout>(window);
linux_frame_layout_ = std::make_unique<LinuxCSDNativeFrameLayout>(window);
// Unretained() is safe because the subscription is saved into an instance
// member and thus will be cancelled upon the instance's destruction.
@@ -156,7 +156,8 @@ void ClientFrameViewLinux::OnWindowButtonOrderingChange() {
}
int ClientFrameViewLinux::ResizingBorderHitTest(const gfx::Point& point) {
return ResizingBorderHitTestImpl(point, RestoredFrameBorderInsets());
return ResizingBorderHitTestImpl(
point, linux_frame_layout_->GetResizeBorderInsets());
}
gfx::Rect ClientFrameViewLinux::GetBoundsForClientView() const {
@@ -235,8 +236,11 @@ void ClientFrameViewLinux::Layout(PassKey) {
}
void ClientFrameViewLinux::OnPaint(gfx::Canvas* canvas) {
linux_frame_layout_->PaintWindowFrame(
canvas, GetLocalBounds(), GetTitlebarBounds(), ShouldPaintAsActive());
if (auto* frame_provider = linux_frame_layout_->GetFrameProvider()) {
frame_provider->PaintWindowFrame(
canvas, GetLocalBounds(), GetTitlebarBounds().bottom(),
ShouldPaintAsActive(), linux_frame_layout_->GetInputInsets());
}
}
void ClientFrameViewLinux::PaintAsActiveChanged() {
@@ -267,7 +271,7 @@ void ClientFrameViewLinux::UpdateThemeValues() {
}
theme_values_.window_border_radius =
linux_frame_layout_->GetFrameProvider()->GetTopCornerRadiusDip();
linux_frame_layout_->GetTopCornerRadiusDip();
gtk::GtkStyleContextGet(headerbar_context, "min-height",
&theme_values_.titlebar_min_height, nullptr);

View File

@@ -112,7 +112,7 @@ class ClientFrameViewLinux : public FramelessView,
gfx::Insets GetTitlebarContentInsets() const;
gfx::Rect GetTitlebarContentBounds() const;
std::unique_ptr<LinuxFrameLayout> linux_frame_layout_;
std::unique_ptr<LinuxCSDNativeFrameLayout> linux_frame_layout_;
raw_ptr<ui::NativeTheme> theme_;
ThemeValues theme_values_;

View File

@@ -4,14 +4,20 @@
// found in the LICENSE file.
#include "shell/browser/ui/views/linux_frame_layout.h"
#include <algorithm>
#include "base/i18n/rtl.h"
#include "chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h" // nogncheck
#include "shell/browser/linux/x11_util.h"
#include "shell/browser/native_window_views.h"
#include "shell/browser/ui/electron_desktop_window_tree_host_linux.h"
#include "ui/gfx/canvas.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/linux/linux_ui.h"
#include "ui/native_theme/native_theme.h"
#include "ui/linux/window_frame_provider.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/widget/widget.h"
namespace electron {
@@ -21,151 +27,174 @@ namespace {
constexpr int kResizeBorder = 10;
// This should match FramelessView's inside resize band.
constexpr int kResizeInsideBoundsSize = 5;
// These should match Chromium's restored frame edge thickness.
constexpr gfx::Insets kDefaultCustomFrameBorder = gfx::Insets::TLBR(2, 1, 1, 1);
bool CheckClientFrameShadowSupport(NativeWindowViews* window) {
auto* tree_host = static_cast<ElectronDesktopWindowTreeHostLinux*>(
ElectronDesktopWindowTreeHostLinux::GetHostForWidget(
window->GetAcceleratedWidget()));
return tree_host && tree_host->SupportsClientFrameShadow();
}
} // namespace
// static
std::unique_ptr<LinuxFrameLayout> LinuxFrameLayout::Create(
NativeWindowViews* window,
bool wants_shadow) {
bool wants_shadow,
CSDStyle csd_style) {
if (x11_util::IsX11() || window->IsTranslucent() || !wants_shadow) {
return std::make_unique<LinuxUndecoratedFrameLayout>(window);
return std::make_unique<LinuxFrameLayout>(window);
} else if (csd_style == CSDStyle::kCustom) {
return std::make_unique<LinuxCSDCustomFrameLayout>(window);
} else {
return std::make_unique<LinuxCSDFrameLayout>(window);
return std::make_unique<LinuxCSDNativeFrameLayout>(window);
}
}
LinuxCSDFrameLayout::LinuxCSDFrameLayout(NativeWindowViews* window)
: window_(window) {
host_supports_client_frame_shadow_ = SupportsClientFrameShadow();
gfx::Insets LinuxFrameLayout::GetResizeBorderInsets() const {
gfx::Insets insets = RestoredFrameBorderInsets();
return insets.IsEmpty() ? GetInputInsets() : insets;
}
bool LinuxCSDFrameLayout::tiled() const {
return tiled_;
}
void LinuxCSDFrameLayout::set_tiled(bool tiled) {
tiled_ = tiled;
}
gfx::Insets LinuxCSDFrameLayout::RestoredFrameBorderInsets() const {
gfx::Insets insets = GetFrameProvider()->GetFrameThicknessDip();
const gfx::Insets input = GetInputInsets();
auto expand_if_visible = [](int side_thickness, int min_band) {
return side_thickness > 0 ? std::max(side_thickness, min_band) : 0;
};
gfx::Insets merged;
merged.set_top(expand_if_visible(insets.top(), input.top()));
merged.set_left(expand_if_visible(insets.left(), input.left()));
merged.set_bottom(expand_if_visible(insets.bottom(), input.bottom()));
merged.set_right(expand_if_visible(insets.right(), input.right()));
return base::i18n::IsRTL() ? gfx::Insets::TLBR(merged.top(), merged.right(),
merged.bottom(), merged.left())
: merged;
}
gfx::Insets LinuxCSDFrameLayout::GetInputInsets() const {
bool showing_shadow = host_supports_client_frame_shadow_ &&
!window_->IsMaximized() && !window_->IsFullscreen();
return gfx::Insets(showing_shadow ? kResizeBorder : 0);
}
bool LinuxCSDFrameLayout::SupportsClientFrameShadow() const {
auto* tree_host = static_cast<ElectronDesktopWindowTreeHostLinux*>(
ElectronDesktopWindowTreeHostLinux::GetHostForWidget(
window_->GetAcceleratedWidget()));
return tree_host->SupportsClientFrameShadow();
}
void LinuxCSDFrameLayout::PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) {
GetFrameProvider()->PaintWindowFrame(
canvas, local_bounds, titlebar_bounds.bottom(), active, GetInputInsets());
}
gfx::Rect LinuxCSDFrameLayout::GetWindowContentBounds() const {
gfx::Rect content_bounds = window_->widget()->GetWindowBoundsInScreen();
content_bounds.Inset(RestoredFrameBorderInsets());
return content_bounds;
}
SkRRect LinuxCSDFrameLayout::GetRoundedWindowContentBounds() const {
SkRect rect = gfx::RectToSkRect(GetWindowContentBounds());
SkRRect LinuxFrameLayout::GetRoundedWindowBounds() const {
SkRect rect = gfx::RectToSkRect(GetWindowBounds());
SkRRect rrect;
if (!window_->IsMaximized()) {
float radius = GetFrameProvider()->GetTopCornerRadiusDip();
float radius = GetTopCornerRadiusDip();
if (radius > 0) {
SkPoint round_point{radius, radius};
SkPoint radii[] = {round_point, round_point, {}, {}};
rrect.setRectRadii(rect, radii);
} else {
rrect.setRect(rect);
}
return rrect;
}
int LinuxCSDFrameLayout::GetTranslucentTopAreaHeight() const {
// Base implementation is suitable for X11/views without shadows
LinuxFrameLayout::LinuxFrameLayout(NativeWindowViews* window)
: window_(window) {
host_supports_client_frame_shadow_ = false;
}
LinuxFrameLayout::~LinuxFrameLayout() = default;
gfx::Insets LinuxFrameLayout::RestoredFrameBorderInsets() const {
return gfx::Insets();
}
gfx::Insets LinuxFrameLayout::GetInputInsets() const {
return gfx::Insets(kResizeInsideBoundsSize);
}
bool LinuxFrameLayout::IsShowingShadow() const {
return host_supports_client_frame_shadow_ && !window_->IsMaximized() &&
!window_->IsFullscreen();
}
bool LinuxFrameLayout::SupportsClientFrameShadow() const {
return host_supports_client_frame_shadow_;
}
bool LinuxFrameLayout::tiled() const {
return tiled_;
}
void LinuxFrameLayout::set_tiled(bool tiled) {
tiled_ = tiled;
}
gfx::Rect LinuxFrameLayout::GetWindowBounds() const {
gfx::Rect bounds = window_->widget()->GetWindowBoundsInScreen();
bounds.Inset(RestoredFrameBorderInsets());
return bounds;
}
float LinuxFrameLayout::GetTopCornerRadiusDip() const {
return 0;
}
ui::WindowFrameProvider* LinuxCSDFrameLayout::GetFrameProvider() const {
int LinuxFrameLayout::GetTranslucentTopAreaHeight() const {
return 0;
}
gfx::Insets LinuxFrameLayout::NormalizeBorderInsets(
const gfx::Insets& frame_insets,
const gfx::Insets& input_insets) const {
auto expand_if_visible = [](int side_thickness, int min_band) {
return side_thickness > 0 ? std::max(side_thickness, min_band) : 0;
};
// Ensure hit testing for resize targets works
// even if borders/shadows are absent on some edges.
gfx::Insets merged;
merged.set_top(expand_if_visible(frame_insets.top(), input_insets.top()));
merged.set_left(expand_if_visible(frame_insets.left(), input_insets.left()));
merged.set_bottom(
expand_if_visible(frame_insets.bottom(), input_insets.bottom()));
merged.set_right(
expand_if_visible(frame_insets.right(), input_insets.right()));
return base::i18n::IsRTL() ? gfx::Insets::TLBR(merged.top(), merged.right(),
merged.bottom(), merged.left())
: merged;
}
// Used for a native-like frame with a FrameProvider
LinuxCSDNativeFrameLayout::LinuxCSDNativeFrameLayout(NativeWindowViews* window)
: LinuxFrameLayout(window) {
host_supports_client_frame_shadow_ = CheckClientFrameShadowSupport(window);
}
LinuxCSDNativeFrameLayout::~LinuxCSDNativeFrameLayout() = default;
gfx::Insets LinuxCSDNativeFrameLayout::RestoredFrameBorderInsets() const {
const gfx::Insets input_insets = GetInputInsets();
const gfx::Insets frame_insets = GetFrameProvider()->GetFrameThicknessDip();
return NormalizeBorderInsets(frame_insets, input_insets);
}
gfx::Insets LinuxCSDNativeFrameLayout::GetInputInsets() const {
return gfx::Insets(IsShowingShadow() ? kResizeBorder : 0);
}
float LinuxCSDNativeFrameLayout::GetTopCornerRadiusDip() const {
return window_->IsMaximized() ? 0
: GetFrameProvider()->GetTopCornerRadiusDip();
}
ui::WindowFrameProvider* LinuxCSDNativeFrameLayout::GetFrameProvider() const {
return ui::LinuxUiTheme::GetForProfile(nullptr)->GetWindowFrameProvider(
!host_supports_client_frame_shadow_, tiled(), window_->IsMaximized());
}
LinuxUndecoratedFrameLayout::LinuxUndecoratedFrameLayout(
NativeWindowViews* window)
: window_(window) {}
gfx::Insets LinuxUndecoratedFrameLayout::RestoredFrameBorderInsets() const {
return gfx::Insets();
// Used for Chromium-like custom CSD
LinuxCSDCustomFrameLayout::LinuxCSDCustomFrameLayout(NativeWindowViews* window)
: LinuxFrameLayout(window) {
host_supports_client_frame_shadow_ = CheckClientFrameShadowSupport(window);
}
gfx::Insets LinuxUndecoratedFrameLayout::GetInputInsets() const {
return gfx::Insets(kResizeInsideBoundsSize);
LinuxCSDCustomFrameLayout::~LinuxCSDCustomFrameLayout() = default;
gfx::Insets LinuxCSDCustomFrameLayout::RestoredFrameBorderInsets() const {
const gfx::Insets input_insets = GetInputInsets();
const bool showing_shadow = IsShowingShadow();
const auto shadow_values = (showing_shadow && !tiled())
? GetFrameShadowValuesLinux(/*active=*/true)
: gfx::ShadowValues();
const gfx::Insets frame_insets = GetRestoredFrameBorderInsetsLinux(
showing_shadow, kDefaultCustomFrameBorder, shadow_values, input_insets);
return NormalizeBorderInsets(frame_insets, input_insets);
}
bool LinuxUndecoratedFrameLayout::SupportsClientFrameShadow() const {
return false;
gfx::Insets LinuxCSDCustomFrameLayout::GetInputInsets() const {
return gfx::Insets(IsShowingShadow() ? kResizeBorder : 0);
}
bool LinuxUndecoratedFrameLayout::tiled() const {
return tiled_;
}
void LinuxUndecoratedFrameLayout::set_tiled(bool tiled) {
tiled_ = tiled;
}
void LinuxUndecoratedFrameLayout::PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) {
// No-op
}
gfx::Rect LinuxUndecoratedFrameLayout::GetWindowContentBounds() const {
// With no transparent insets, widget bounds and logical bounds match.
return window_->widget()->GetWindowBoundsInScreen();
}
SkRRect LinuxUndecoratedFrameLayout::GetRoundedWindowContentBounds() const {
SkRRect rrect;
rrect.setRect(gfx::RectToSkRect(GetWindowContentBounds()));
return rrect;
}
int LinuxUndecoratedFrameLayout::GetTranslucentTopAreaHeight() const {
return 0;
}
ui::WindowFrameProvider* LinuxUndecoratedFrameLayout::GetFrameProvider() const {
return nullptr;
gfx::ShadowValues GetFrameShadowValuesLinux(bool active) {
const int elevation = views::LayoutProvider::Get()->GetShadowElevationMetric(
active ? views::Emphasis::kMaximum : views::Emphasis::kMedium);
return gfx::ShadowValue::MakeMdShadowValues(elevation);
}
} // namespace electron

View File

@@ -8,110 +8,96 @@
#include <memory>
#include "base/i18n/rtl.h"
#include "shell/browser/linux/x11_util.h"
#include "shell/browser/native_window_views.h"
#include "shell/browser/ui/electron_desktop_window_tree_host_linux.h"
#include "base/memory/raw_ptr.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "ui/base/ozone_buildflags.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/linux/linux_ui.h"
#include "ui/gfx/shadow_value.h"
#include "ui/linux/window_frame_provider.h"
namespace gfx {
class Insets;
class Rect;
} // namespace gfx
namespace electron {
class NativeWindowViews;
// Shared helper for CSD layout and frame painting on Linux (shadows, resize
// regions, titlebars, etc.). Also helps views determine insets and perform
// bounds conversions between widget and logical coordinates.
// Shared helper for CSD layout on Linux (shadows, resize regions, titlebars,
// etc.). Also helps views determine insets and perform bounds conversions
// between widget and logical coordinates.
//
// The base class is concrete and suitable as-is for the undecorated case (X11,
// translucent windows, or windows without shadows). CSD subclasses override
// the methods that differ.
class LinuxFrameLayout {
public:
virtual ~LinuxFrameLayout() = default;
enum class CSDStyle {
kNativeFrame,
kCustom,
};
explicit LinuxFrameLayout(NativeWindowViews* window);
virtual ~LinuxFrameLayout();
static std::unique_ptr<LinuxFrameLayout> Create(NativeWindowViews* window,
bool wants_shadow);
bool wants_shadow,
CSDStyle csd_style);
// Insets from the transparent widget border to the opaque part of the window
virtual gfx::Insets RestoredFrameBorderInsets() const = 0;
// Insets for parts of the surface that should be counted for user input
virtual gfx::Insets GetInputInsets() const = 0;
// Insets from the transparent widget border to the opaque part of the window.
virtual gfx::Insets RestoredFrameBorderInsets() const;
// Insets for parts of the surface that should be counted for user input.
virtual gfx::Insets GetInputInsets() const;
// Insets to use for non-client resize hit-testing.
gfx::Insets GetResizeBorderInsets() const;
virtual bool SupportsClientFrameShadow() const = 0;
bool IsShowingShadow() const;
bool SupportsClientFrameShadow() const;
virtual bool tiled() const = 0;
virtual void set_tiled(bool tiled) = 0;
bool tiled() const;
void set_tiled(bool tiled);
virtual void PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) = 0;
// The logical bounds of the window interior.
gfx::Rect GetWindowBounds() const;
// The logical window bounds as a rounded rect with corner radii applied.
SkRRect GetRoundedWindowBounds() const;
// The corner radius of the top corners of the window, in DIPs.
virtual float GetTopCornerRadiusDip() const;
// The logical bounds of the window
virtual gfx::Rect GetWindowContentBounds() const = 0;
// The logical bounds as a rounded rect with corner radii applied
virtual SkRRect GetRoundedWindowContentBounds() const = 0;
int GetTranslucentTopAreaHeight() const;
virtual int GetTranslucentTopAreaHeight() const = 0;
protected:
gfx::Insets NormalizeBorderInsets(const gfx::Insets& frame_insets,
const gfx::Insets& input_insets) const;
virtual ui::WindowFrameProvider* GetFrameProvider() const = 0;
};
// Client-side decoration (CSD) Linux frame layout implementation.
class LinuxCSDFrameLayout : public LinuxFrameLayout {
public:
explicit LinuxCSDFrameLayout(NativeWindowViews* window);
~LinuxCSDFrameLayout() override = default;
gfx::Insets RestoredFrameBorderInsets() const override;
gfx::Insets GetInputInsets() const override;
bool SupportsClientFrameShadow() const override;
bool tiled() const override;
void set_tiled(bool tiled) override;
void PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) override;
gfx::Rect GetWindowContentBounds() const override;
SkRRect GetRoundedWindowContentBounds() const override;
int GetTranslucentTopAreaHeight() const override;
ui::WindowFrameProvider* GetFrameProvider() const override;
private:
raw_ptr<NativeWindowViews> window_;
bool tiled_ = false;
bool host_supports_client_frame_shadow_ = false;
};
// No-decoration Linux frame layout implementation.
//
// Intended for cases where we do not allocate a transparent inset area around
// the window (e.g. X11 / server-side decorations, or when insets are disabled).
// All inset math returns 0 and frame painting is skipped.
class LinuxUndecoratedFrameLayout : public LinuxFrameLayout {
// CSD strategy that uses the GTK window frame provider for metrics.
class LinuxCSDNativeFrameLayout : public LinuxFrameLayout {
public:
explicit LinuxUndecoratedFrameLayout(NativeWindowViews* window);
~LinuxUndecoratedFrameLayout() override = default;
explicit LinuxCSDNativeFrameLayout(NativeWindowViews* window);
~LinuxCSDNativeFrameLayout() override;
gfx::Insets RestoredFrameBorderInsets() const override;
gfx::Insets GetInputInsets() const override;
bool SupportsClientFrameShadow() const override;
bool tiled() const override;
void set_tiled(bool tiled) override;
void PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) override;
gfx::Rect GetWindowContentBounds() const override;
SkRRect GetRoundedWindowContentBounds() const override;
int GetTranslucentTopAreaHeight() const override;
ui::WindowFrameProvider* GetFrameProvider() const override;
private:
raw_ptr<NativeWindowViews> window_;
bool tiled_ = false;
float GetTopCornerRadiusDip() const override;
ui::WindowFrameProvider* GetFrameProvider() const;
};
// CSD strategy that uses custom metrics, similar to those used in Chromium.
class LinuxCSDCustomFrameLayout : public LinuxFrameLayout {
public:
explicit LinuxCSDCustomFrameLayout(NativeWindowViews* window);
~LinuxCSDCustomFrameLayout() override;
gfx::Insets RestoredFrameBorderInsets() const override;
gfx::Insets GetInputInsets() const override;
};
gfx::ShadowValues GetFrameShadowValuesLinux(bool active);
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_UI_VIEWS_LINUX_FRAME_LAYOUT_H_

View File

@@ -5,22 +5,24 @@
#include "shell/browser/ui/views/opaque_frame_view.h"
#include "base/containers/adapters.h"
#include "base/i18n/rtl.h"
#include "chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h" // nogncheck
#include "chrome/browser/ui/views/frame/opaque_browser_frame_view_layout.h" // nogncheck
#include "chrome/grit/generated_resources.h"
#include "components/strings/grit/components_strings.h"
#include "shell/browser/native_window_views.h"
#include "shell/browser/ui/views/caption_button_placeholder_container.h"
#include "third_party/skia/include/core/SkRRect.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/gfx/font_list.h"
#include "ui/linux/linux_ui.h"
#include "ui/gfx/geometry/insets_f.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/window/frame_background.h"
#include "ui/views/window/frame_caption_button.h"
#include "ui/views/window/vector_icons/vector_icons.h"
@@ -55,12 +57,14 @@ const int kCaptionButtonBottomPadding = 3;
// The content edge images have a shadow built into them.
const int OpaqueFrameView::kContentEdgeShadowThickness = 2;
OpaqueFrameView::OpaqueFrameView() = default;
OpaqueFrameView::OpaqueFrameView()
: frame_background_(std::make_unique<views::FrameBackground>()) {}
OpaqueFrameView::~OpaqueFrameView() = default;
void OpaqueFrameView::Init(NativeWindowViews* window, views::Widget* frame) {
FramelessView::Init(window, frame);
linux_frame_layout_ = LinuxFrameLayout::Create(window, window->HasShadow());
linux_frame_layout_ = LinuxFrameLayout::Create(
window, window->HasShadow(), LinuxFrameLayout::CSDStyle::kCustom);
// Unretained() is safe because the subscription is saved into an instance
// member and thus will be cancelled upon the instance's destruction.
@@ -98,9 +102,8 @@ void OpaqueFrameView::Init(NativeWindowViews* window, views::Widget* frame) {
}
int OpaqueFrameView::ResizingBorderHitTest(const gfx::Point& point) {
auto insets = RestoredFrameBorderInsets();
return ResizingBorderHitTestImpl(
point, insets.IsEmpty() ? linux_frame_layout_->GetInputInsets() : insets);
point, linux_frame_layout_->GetResizeBorderInsets());
}
void OpaqueFrameView::InvalidateCaptionButtons() {
@@ -200,14 +203,31 @@ void OpaqueFrameView::OnPaint(gfx::Canvas* canvas) {
if (frame()->IsFullscreen())
return;
// Titlebar height must be at least the frame border insets to avoid
// a negative height calculation in the GTK frame provider. We add 1 to
// ensure it's always positive even when insets are 0.
int top_area_height = RestoredFrameBorderInsets().top() + 1;
const bool active = ShouldPaintAsActive();
const gfx::Insets border = RestoredFrameBorderInsets();
const bool showing_shadow = linux_frame_layout_->IsShowingShadow();
gfx::RectF bounds_dip(GetLocalBounds());
if (showing_shadow) {
bounds_dip.Inset(gfx::InsetsF(border));
}
linux_frame_layout_->PaintWindowFrame(
canvas, GetLocalBounds(), gfx::Rect(0, 0, width(), top_area_height),
ShouldPaintAsActive());
// TODO: support roundedCorners.
float radius_dip = 0;
SkVector radii[4]{{radius_dip, radius_dip}, {radius_dip, radius_dip}, {}, {}};
SkRRect clip;
clip.setRectRadii(gfx::RectFToSkRect(bounds_dip), radii);
frame_background_->set_frame_color(GetFrameColor());
frame_background_->set_use_custom_frame(true);
frame_background_->set_is_active(active);
frame_background_->set_top_area_height(GetTopAreaHeight());
const bool draw_shadow = showing_shadow && !linux_frame_layout_->tiled();
auto shadow_values =
draw_shadow ? GetFrameShadowValuesLinux(active) : gfx::ShadowValues();
::PaintRestoredFrameBorderLinux(*canvas, *this, frame_background_.get(), clip,
showing_shadow, active, border, shadow_values,
linux_frame_layout_->tiled());
if (!window()->IsWindowControlsOverlayEnabled())
return;

View File

@@ -20,6 +20,10 @@
class CaptionButtonPlaceholderContainer;
namespace views {
class FrameBackground;
}
namespace electron {
class NativeWindowViews;
@@ -166,6 +170,7 @@ class OpaqueFrameView : public FramelessView {
bool is_leading_button) const;
std::unique_ptr<LinuxFrameLayout> linux_frame_layout_;
std::unique_ptr<views::FrameBackground> frame_background_;
// Window controls.
raw_ptr<views::Button> minimize_button_;

1510
yarn.lock

File diff suppressed because it is too large Load Diff