feat: add WCO height option (#31222)

* feat: add WCO height option

* add docs and mac functionality

* add macOS functionality and height lowerbound

* Update docs/api/browser-window.md

Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>

* update macOS functionality

* add chromium related notes

* add test and fix pixel under button bug and fix typo

* revert changes to docs/api/frameless-window.md

* modify `useCustomHeight` calls

* update `useCustomHeight` and `getCurrentMargin`

* modify margin calculation

* fix minimum custom height on macOS

* Update window_buttons_proxy.mm

* fix specified traffic light positions

Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
This commit is contained in:
Michaela Laurencin 2022-01-24 14:09:21 -08:00 committed by GitHub
parent 2fe5d0e1e8
commit 2f0d5651a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 196 additions and 30 deletions

View file

@ -388,9 +388,10 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
contain the layout of the document—without requiring scrolling. Enabling
this will cause the `preferred-size-changed` event to be emitted on the
`WebContents` when the preferred size changes. Default is `false`.
* `titleBarOverlay` Object | boolean (optional) - When using a frameless window in conjuction 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.
* `symbolColor` string (optional) _Windows_ - The CSS color of the symbols on the Window Controls Overlay when enabled. Default is the system color.
* `titleBarOverlay` Object | Boolean (optional) - When using a frameless window in conjuction 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.
* `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.
When setting minimum or maximum window size with `minWidth`/`maxWidth`/
`minHeight`/`maxHeight`, it only constrains the users. It won't prevent you from

View file

@ -91,7 +91,15 @@ NativeWindow::NativeWindow(const gin_helper::Dictionary& options,
options.Get(options::ktitleBarOverlay, &titlebar_overlay_);
} else if (titlebar_overlay->IsObject()) {
titlebar_overlay_ = true;
#if !defined(OS_WIN)
gin_helper::Dictionary titlebar_overlay =
gin::Dictionary::CreateEmpty(options.isolate());
options.Get(options::ktitleBarOverlay, &titlebar_overlay);
int height;
if (titlebar_overlay.Get(options::kOverlayHeight, &height))
titlebar_overlay_height_ = height;
#if !(defined(OS_WIN) || defined(OS_MAC))
DCHECK(false);
#endif
}

View file

@ -327,6 +327,7 @@ class NativeWindow : public base::SupportsUserData,
kCustomButtonsOnHover,
};
TitleBarStyle title_bar_style() const { return title_bar_style_; }
int titlebar_overlay_height() const { return titlebar_overlay_height_; }
bool has_frame() const { return has_frame_; }
void set_has_frame(bool has_frame) { has_frame_ = has_frame; }
@ -362,6 +363,10 @@ class NativeWindow : public base::SupportsUserData,
// The boolean parsing of the "titleBarOverlay" option
bool titlebar_overlay_ = false;
// The custom height parsed from the "height" option in a Object
// "titleBarOverlay"
int titlebar_overlay_height_ = 0;
// The "titleBarStyle" option.
TitleBarStyle title_bar_style_ = TitleBarStyle::kNormal;

View file

@ -369,6 +369,7 @@ NativeWindowMac::NativeWindowMac(const gin_helper::Dictionary& options,
InternalSetWindowButtonVisibility(false);
} else {
buttons_proxy_.reset([[WindowButtonsProxy alloc] initWithWindow:window_]);
[buttons_proxy_ setHeight:titlebar_overlay_height()];
if (traffic_light_position_) {
[buttons_proxy_ setMargin:*traffic_light_position_];
} else if (title_bar_style_ == TitleBarStyle::kHiddenInset) {
@ -1836,7 +1837,12 @@ gfx::Rect NativeWindowMac::GetWindowControlsOverlayRect() {
NSRect buttons = [buttons_proxy_ getButtonsContainerBounds];
gfx::Rect overlay;
overlay.set_width(GetContentSize().width() - NSWidth(buttons));
overlay.set_height(NSHeight(buttons));
if ([buttons_proxy_ useCustomHeight]) {
overlay.set_height(titlebar_overlay_height());
} else {
overlay.set_height(NSHeight(buttons));
}
if (!base::i18n::IsRTL())
overlay.set_x(NSMaxX(buttons));
return overlay;

View file

@ -181,9 +181,8 @@ NativeWindowViews::NativeWindowViews(const gin_helper::Dictionary& options,
v8::Local<v8::Value> titlebar_overlay;
if (options.Get(options::ktitleBarOverlay, &titlebar_overlay) &&
titlebar_overlay->IsObject()) {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
gin_helper::Dictionary titlebar_overlay_obj =
gin::Dictionary::CreateEmpty(isolate);
gin::Dictionary::CreateEmpty(options.isolate());
options.Get(options::ktitleBarOverlay, &titlebar_overlay_obj);
std::string overlay_color_string;

View file

@ -30,6 +30,8 @@
gfx::Point margin_;
// The default left-top margin.
gfx::Point default_margin_;
// Current height of the title bar container.
float height_;
// Track mouse moves above window buttons.
BOOL show_on_hover_;
@ -49,6 +51,10 @@
// Set left-top margin of the window buttons..
- (void)setMargin:(const absl::optional<gfx::Point>&)margin;
// Set height of button container
- (void)setHeight:(const float)height;
- (BOOL)useCustomHeight;
// Return the bounds of all 3 buttons, with margin on all sides.
- (NSRect)getButtonsContainerBounds;

View file

@ -36,6 +36,8 @@
// Remember the default margin.
margin_ = default_margin_ = [self getCurrentMargin];
// Custom height will be used if set larger than default
height_ = 0;
return self;
}
@ -86,6 +88,17 @@
[self redraw];
}
- (void)setHeight:(const float)height {
height_ = height;
[self redraw];
}
- (BOOL)useCustomHeight {
NSView* left = [self leftButton];
float button_height = NSHeight(left.frame);
return height_ > button_height + 2 * default_margin_.y();
}
- (NSRect)getButtonsContainerBounds {
return NSInsetRect([self getButtonsBounds], -margin_.x(), -margin_.y());
}
@ -111,14 +124,18 @@
NSRect cbounds = titleBarContainer.frame;
cbounds.size.height = button_height + 2 * margin_.y();
// Custom height must be larger than the button height to use
if ([self useCustomHeight]) {
cbounds.size.height = height_;
}
cbounds.origin.y = NSHeight(window_.frame) - NSHeight(cbounds);
[titleBarContainer setFrame:cbounds];
[left setFrameOrigin:NSMakePoint(start, margin_.y())];
[left setFrameOrigin:NSMakePoint(start, [self getCurrentMargin].y())];
start += button_width + padding;
[middle setFrameOrigin:NSMakePoint(start, margin_.y())];
[middle setFrameOrigin:NSMakePoint(start, [self getCurrentMargin].y())];
start += button_width + padding;
[right setFrameOrigin:NSMakePoint(start, margin_.y())];
[right setFrameOrigin:NSMakePoint(start, [self getCurrentMargin].y())];
if (hover_view_)
[hover_view_ setFrame:[self getButtonsBounds]];
@ -167,6 +184,7 @@
- (NSRect)getButtonsBounds {
NSView* left = [self leftButton];
NSView* right = [self rightButton];
return NSMakeRect(NSMinX(left.frame), NSMinY(left.frame),
NSMaxX(right.frame) - NSMinX(left.frame),
NSHeight(left.frame));
@ -182,7 +200,17 @@
NSView* left = [self leftButton];
NSView* right = [self rightButton];
result.set_y((NSHeight(titleBarContainer.frame) - NSHeight(left.frame)) / 2);
if (height_ != 0) {
result.set_y((height_ - NSHeight(left.frame)) / 2);
// Do not center buttons if height and button position specified
if (margin_.y() != default_margin_.y())
result.set_y(height_ - NSHeight(left.frame) - margin_.y());
} else {
result.set_y((NSHeight(titleBarContainer.frame) - NSHeight(left.frame)) /
2);
}
if (base::i18n::IsRTL())
result.set_x(NSWidth(window_.frame) - NSMaxX(right.frame));

View file

@ -2,13 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Modified from chrome/browser/ui/views/frame/windows_10_caption_button.cc
#include "shell/browser/ui/views/win_caption_button.h"
#include <utility>
#include "base/i18n/rtl.h"
#include "base/numerics/safe_conversions.h"
#include "chrome/browser/ui/frame/window_frame_util.h"
#include "chrome/grit/theme_resources.h"
#include "shell/browser/ui/views/win_frame_view.h"
#include "shell/common/color_util.h"
@ -37,9 +38,8 @@ WinCaptionButton::WinCaptionButton(PressedCallback callback,
gfx::Size WinCaptionButton::CalculatePreferredSize() const {
// TODO(bsep): The sizes in this function are for 1x device scale and don't
// match Windows button sizes at hidpi.
int height = WindowFrameUtil::kWindows10GlassCaptionButtonHeightRestored;
int base_width = WindowFrameUtil::kWindows10GlassCaptionButtonWidth;
return gfx::Size(base_width + GetBetweenButtonSpacing(), height);
return gfx::Size(base_width_ + GetBetweenButtonSpacing(), height_);
}
void WinCaptionButton::OnPaintBackground(gfx::Canvas* canvas) {
@ -88,6 +88,20 @@ void WinCaptionButton::PaintButtonContents(gfx::Canvas* canvas) {
PaintSymbol(canvas);
}
gfx::Size WinCaptionButton::GetSize() const {
return gfx::Size(base_width_, height_);
}
void WinCaptionButton::SetSize(gfx::Size size) {
int width = size.width();
int height = size.height();
if (width > 0)
base_width_ = width;
if (height > 0)
height_ = height;
}
int WinCaptionButton::GetBetweenButtonSpacing() const {
const int display_order_index = GetButtonDisplayOrderIndex();
return display_order_index == 0

View file

@ -2,9 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Modified from chrome/browser/ui/views/frame/windows_10_caption_button.h
#ifndef ELECTRON_SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_H_
#define ELECTRON_SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_H_
#include "chrome/browser/ui/frame/window_frame_util.h"
#include "chrome/browser/ui/view_ids.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/gfx/canvas.h"
@ -28,7 +31,10 @@ class WinCaptionButton : public views::Button {
void OnPaintBackground(gfx::Canvas* canvas) override;
void PaintButtonContents(gfx::Canvas* canvas) override;
// private:
gfx::Size GetSize() const;
void SetSize(gfx::Size size);
private:
// Returns the amount we should visually reserve on the left (right in RTL)
// for spacing between buttons. We do this instead of repositioning the
// buttons to avoid the sliver of deadspace that would result.
@ -48,6 +54,9 @@ class WinCaptionButton : public views::Button {
WinFrameView* frame_view_;
ViewID button_type_;
int base_width_ = WindowFrameUtil::kWindows10GlassCaptionButtonWidth;
int height_ = WindowFrameUtil::kWindows10GlassCaptionButtonHeightRestored;
};
} // namespace electron

View file

@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Modified from
// chrome/browser/ui/views/frame/glass_browser_caption_button_container.cc
#include "shell/browser/ui/views/win_caption_button_container.h"
#include <memory>
@ -96,6 +99,18 @@ int WinCaptionButtonContainer::NonClientHitTest(const gfx::Point& point) const {
return HTCAPTION;
}
gfx::Size WinCaptionButtonContainer::GetButtonSize() const {
// Close button size is set the same as all the buttons
return close_button_->GetSize();
}
void WinCaptionButtonContainer::SetButtonSize(gfx::Size size) {
minimize_button_->SetSize(size);
maximize_button_->SetSize(size);
restore_button_->SetSize(size);
close_button_->SetSize(size);
}
void WinCaptionButtonContainer::ResetWindowControls() {
minimize_button_->SetState(views::Button::STATE_NORMAL);
maximize_button_->SetState(views::Button::STATE_NORMAL);

View file

@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Modified from
// chrome/browser/ui/views/frame/glass_browser_caption_button_container.h
#ifndef ELECTRON_SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_CONTAINER_H_
#define ELECTRON_SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_CONTAINER_H_
@ -35,6 +38,9 @@ class WinCaptionButtonContainer : public views::View,
// See also ClientView::NonClientHitTest.
int NonClientHitTest(const gfx::Point& point) const;
gfx::Size GetButtonSize() const;
void SetButtonSize(gfx::Size size);
private:
// views::View:
void AddedToWidget() override;

View file

@ -193,13 +193,20 @@ int WinFrameView::TitlebarMaximizedVisualHeight() const {
return maximized_height;
}
int WinFrameView::TitlebarHeight(bool restored) const {
if (frame()->IsFullscreen() && !restored)
// NOTE(@mlaurencin): Usage of IsWebUITabStrip simplified out from Chromium
int WinFrameView::TitlebarHeight(int custom_height) const {
if (frame()->IsFullscreen() && !IsMaximized())
return 0;
return TitlebarMaximizedVisualHeight() + FrameTopBorderThickness(false);
int height = TitlebarMaximizedVisualHeight() +
FrameTopBorderThickness(false) - WindowTopY();
if (custom_height > TitlebarMaximizedVisualHeight())
height = custom_height - WindowTopY();
return height;
}
// NOTE(@mlaurencin): Usage of IsWebUITabStrip simplified out from Chromium
int WinFrameView::WindowTopY() const {
// The window top is SM_CYSIZEFRAME pixels when maximized (see the comment in
// FrameTopBorderThickness()) and floor(system dsf) pixels when restored.
@ -222,27 +229,35 @@ void WinFrameView::LayoutCaptionButtons() {
}
caption_button_container_->SetVisible(true);
const gfx::Size preferred_size =
caption_button_container_->GetPreferredSize();
int height = preferred_size.height();
height = IsMaximized() ? TitlebarMaximizedVisualHeight()
: TitlebarHeight(false) - WindowTopY();
int custom_height = window()->titlebar_overlay_height();
int height = TitlebarHeight(custom_height);
// TODO(mlaurencin): This -1 creates a 1 pixel gap between the right
// edge of the overlay and the edge of the window, allowing for this edge
// portion to return the correct hit test and be manually resized properly.
// Alternatives can be explored, but the differences in view structures
// between Electron and Chromium may result in this as the best option.
// TODO(mlaurencin): This -1 creates a 1 pixel margin between the right
// edge of the button container and the edge of the window, allowing for this
// edge portion to return the correct hit test and be manually resized
// properly. Alternatives can be explored, but the differences in view
// structures between Electron and Chromium may result in this as the best
// option.
int variable_width =
IsMaximized() ? preferred_size.width() : preferred_size.width() - 1;
caption_button_container_->SetBounds(width() - preferred_size.width(),
WindowTopY(), variable_width, height);
// Needed for heights larger than default
caption_button_container_->SetButtonSize(gfx::Size(0, height));
}
void WinFrameView::LayoutWindowControlsOverlay() {
int overlay_height = caption_button_container_->size().height();
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 = IsMaximized()
? caption_button_container_->size().height()
: caption_button_container_->size().height() + 1;
}
int overlay_width = caption_button_container_->size().width();
int bounding_rect_width = width() - overlay_width;
auto bounding_rect =

View file

@ -67,7 +67,7 @@ class WinFrameView : public FramelessView {
// Returns the height of the titlebar for popups or other browser types that
// don't have tabs.
int TitlebarHeight(bool restored) const;
int TitlebarHeight(int custom_height) const;
// Returns the y coordinate for the top of the frame, which in maximized mode
// is the top of the screen and in restored mode is 1 pixel below the top of

View file

@ -36,6 +36,9 @@ const char kRoundedCorners[] = "roundedCorners";
const char kOverlayButtonColor[] = "color";
const char kOverlaySymbolColor[] = "symbolColor";
// The custom height for Window Controls Overlay.
const char kOverlayHeight[] = "height";
// Whether the window should show in taskbar.
const char kSkipTaskbar[] = "skipTaskbar";

View file

@ -60,6 +60,7 @@ extern const char kRoundedCorners[];
extern const char ktitleBarOverlay[];
extern const char kOverlayButtonColor[];
extern const char kOverlaySymbolColor[];
extern const char kOverlayHeight[];
// WebPreferences.
extern const char kZoomFactor[];

View file

@ -1981,6 +1981,56 @@ describe('BrowserWindow module', () => {
});
});
ifdescribe(process.platform === 'win32' || (process.platform === 'darwin' && semver.gte(os.release(), '14.0.0')))('"titleBarOverlay" option', () => {
const testWindowsOverlayHeight = async (size: any) => {
const w = new BrowserWindow({
show: false,
width: 400,
height: 400,
titleBarStyle: 'hidden',
webPreferences: {
nodeIntegration: true,
contextIsolation: false
},
titleBarOverlay: {
height: size
}
});
const overlayHTML = path.join(__dirname, 'fixtures', 'pages', 'overlay.html');
if (process.platform === 'darwin') {
await w.loadFile(overlayHTML);
} else {
const overlayReady = emittedOnce(ipcMain, 'geometrychange');
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()');
await w.maximize();
const max = await w.isMaximized();
expect(max).to.equal(true);
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.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);
};
afterEach(closeAllWindows);
afterEach(() => { ipcMain.removeAllListeners('geometrychange'); });
it('sets Window Control Overlay with title bar height of 40', async () => {
await testWindowsOverlayHeight(40);
});
});
ifdescribe(process.platform === 'darwin')('"enableLargerThanScreen" option', () => {
afterEach(closeAllWindows);
it('can move the window out of screen', () => {