From 8aced2deb522d31d1eceb3c4d0b42f7035c67cae Mon Sep 17 00:00:00 2001 From: John Kleinschmidt Date: Mon, 30 Aug 2021 10:08:00 -0400 Subject: [PATCH] feat: enable windows control overlay on Windows (#30678) cherry-picked from 41646d1 Co-Authored-By: Michaela Laurencin <35157522+mlaurencin@users.noreply.github.com> Co-authored-by: Michaela Laurencin <35157522+mlaurencin@users.noreply.github.com> --- chromium_src/BUILD.gn | 3 + docs/api/browser-window.md | 18 +- docs/api/frameless-window.md | 32 ++- docs/api/structures/overlay-options.md | 4 + electron_strings.grdp | 14 ++ filenames.auto.gni | 1 + filenames.gni | 4 + shell/browser/native_window.cc | 42 +++- shell/browser/native_window.h | 12 + shell/browser/native_window_mac.h | 11 - shell/browser/native_window_mac.mm | 26 +-- shell/browser/native_window_views.cc | 33 +++ shell/browser/native_window_views.h | 15 ++ shell/browser/ui/views/frameless_view.cc | 10 +- shell/browser/ui/views/frameless_view.h | 2 + shell/browser/ui/views/win_caption_button.cc | 220 ++++++++++++++++++ shell/browser/ui/views/win_caption_button.h | 54 +++++ .../ui/views/win_caption_button_container.cc | 143 ++++++++++++ .../ui/views/win_caption_button_container.h | 70 ++++++ shell/browser/ui/views/win_frame_view.cc | 220 +++++++++++++++++- shell/browser/ui/views/win_frame_view.h | 58 +++++ shell/common/options_switches.cc | 5 + shell/common/options_switches.h | 2 + spec-main/api-browser-window-spec.ts | 21 +- 24 files changed, 952 insertions(+), 68 deletions(-) create mode 100644 docs/api/structures/overlay-options.md create mode 100644 shell/browser/ui/views/win_caption_button.cc create mode 100644 shell/browser/ui/views/win_caption_button.h create mode 100644 shell/browser/ui/views/win_caption_button_container.cc create mode 100644 shell/browser/ui/views/win_caption_button_container.h diff --git a/chromium_src/BUILD.gn b/chromium_src/BUILD.gn index 42b71245719a7..68269400d9a50 100644 --- a/chromium_src/BUILD.gn +++ b/chromium_src/BUILD.gn @@ -66,8 +66,11 @@ static_library("chrome") { "//chrome/browser/extensions/global_shortcut_listener_win.cc", "//chrome/browser/extensions/global_shortcut_listener_win.h", "//chrome/browser/icon_loader_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.h", "//chrome/child/v8_crashpad_support_win.cc", "//chrome/child/v8_crashpad_support_win.h", ] diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md index b47014b5f618f..a316d671e49d9 100644 --- a/docs/api/browser-window.md +++ b/docs/api/browser-window.md @@ -213,16 +213,13 @@ It creates a new `BrowserWindow` with native properties as set by the `options`. * `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) - The style of window title bar. + * `titleBarStyle` String (optional) _macOS_ _Windows_ - The style of window title bar. Default is `default`. Possible values are: - * `default` - Results in the standard gray opaque Mac title - bar. - * `hidden` - Results in a hidden title bar and a full size content window, yet - the title bar still has the standard window controls ("traffic lights") in - the top left. - * `hiddenInset` - Results in a hidden title bar with an alternative look + * `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` - Only on 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` - Results in a hidden title bar and a full size + * `customButtonsOnHover` - Only on 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. @@ -403,10 +400,7 @@ 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` Boolean (optional) - On macOS, when using a frameless window in conjunction with - `win.setWindowButtonVisibility(true)` or using a `titleBarStyle` so that the traffic lights are visible, - this property enables the Window Controls Overlay [JavaScript APIs][overlay-javascript-apis] and - [CSS Environment Variables][overlay-css-env-vars]. Default is `false`. + * `titleBarOverlay` [OverlayOptions](structures/overlay-options.md) | 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`. On Windows, the [OverlayOptions](structures/overlay-options.md) can be used instead of a boolean to specify colors for the overlay. When setting minimum or maximum window size with `minWidth`/`maxWidth`/ `minHeight`/`maxHeight`, it only constrains the users. It won't prevent you from diff --git a/docs/api/frameless-window.md b/docs/api/frameless-window.md index 7a73c8925e200..7f327904b36e7 100644 --- a/docs/api/frameless-window.md +++ b/docs/api/frameless-window.md @@ -18,17 +18,17 @@ const win = new BrowserWindow({ width: 800, height: 600, frame: false }) win.show() ``` -### Alternatives on macOS +### Alternatives -There's an alternative way to specify a chromeless window. +There's an alternative way to specify a chromeless window on macOS and Windows. Instead of setting `frame` to `false` which disables both the titlebar and window controls, you may want to have the title bar hidden and your content extend to the full window size, -yet still preserve the window controls ("traffic lights") for standard window actions. +yet still preserve the window controls ("traffic lights" on macOS) for standard window actions. You can do so by specifying the `titleBarStyle` option: #### `hidden` -Results in a hidden title bar and a full size content window, yet the title bar still has the standard window controls (“traffic lights”) in the top left. +Results in a hidden title bar and a full size content window. On macOS, the title bar still has the standard window controls (“traffic lights”) in the top left. ```javascript const { BrowserWindow } = require('electron') @@ -36,6 +36,8 @@ const win = new BrowserWindow({ titleBarStyle: 'hidden' }) win.show() ``` +### Alternatives on macOS + #### `hiddenInset` Results in a hidden title bar with an alternative look where the traffic light buttons are slightly more inset from the window edge. @@ -63,19 +65,33 @@ win.show() ## Windows Control Overlay -On macOS, when using a frameless window in conjuction with `win.setWindowButtonVisibility(true)` or using one of the `titleBarStyle`s described above so -that the traffic lights are visible, you can access the Window Controls Overlay [JavaScript APIs][overlay-javascript-apis] and -[CSS Environment Variables][overlay-css-env-vars] by setting the `titleBarOverlay` option to true: +When using a frameless window in conjuction with `win.setWindowButtonVisibility(true)` on macOS, using one of the `titleBarStyle`s as described above so +that the traffic lights are visible, or using `titleBarStyle: hidden` on Windows, you can access the Window Controls Overlay [JavaScript APIs][overlay-javascript-apis] and +[CSS Environment Variables][overlay-css-env-vars] by setting the `titleBarOverlay` option to true. Specifying `true` will result in an overlay with default system colors. + +On Windows, you can also specify the color of the overlay and its symbols by setting `titleBarOverlay` to an object with the options `color` and `symbolColor`. If an option is not specified, the color will default to its system color for the window control buttons: ```javascript const { BrowserWindow } = require('electron') const win = new BrowserWindow({ - titleBarStyle: 'hiddenInset', + titleBarStyle: 'hidden', titleBarOverlay: true }) win.show() ``` +```javascript +const { BrowserWindow } = require('electron') +const win = new BrowserWindow({ + titleBarStyle: 'hidden', + titleBarOverlay: { + color: '#2f3241', + symbolColor: '#74b1be' + } +}) +win.show() +``` + ## Transparent window By setting the `transparent` option to `true`, you can also make the frameless diff --git a/docs/api/structures/overlay-options.md b/docs/api/structures/overlay-options.md new file mode 100644 index 0000000000000..9e0e4815ea59c --- /dev/null +++ b/docs/api/structures/overlay-options.md @@ -0,0 +1,4 @@ +# OverlayOptions Object + +* `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. diff --git a/electron_strings.grdp b/electron_strings.grdp index b86aaf9d849cc..6fc83d90e5ea9 100644 --- a/electron_strings.grdp +++ b/electron_strings.grdp @@ -1,5 +1,19 @@ + + + Close + + + Minimize + + + Maximize + + + Restore + + Printing Service diff --git a/filenames.auto.gni b/filenames.auto.gni index 829a794466aa9..0bf902fd04f0a 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -101,6 +101,7 @@ auto_filenames = { "docs/api/structures/new-window-web-contents-event.md", "docs/api/structures/notification-action.md", "docs/api/structures/notification-response.md", + "docs/api/structures/overlay-options.md", "docs/api/structures/point.md", "docs/api/structures/post-body.md", "docs/api/structures/printer-info.md", diff --git a/filenames.gni b/filenames.gni index 3894978859e9b..271513816fda4 100644 --- a/filenames.gni +++ b/filenames.gni @@ -90,6 +90,10 @@ filenames = { "shell/browser/ui/views/electron_views_delegate_win.cc", "shell/browser/ui/views/win_frame_view.cc", "shell/browser/ui/views/win_frame_view.h", + "shell/browser/ui/views/win_caption_button.cc", + "shell/browser/ui/views/win_caption_button.h", + "shell/browser/ui/views/win_caption_button_container.cc", + "shell/browser/ui/views/win_caption_button_container.h", "shell/browser/ui/win/dialog_thread.cc", "shell/browser/ui/win/dialog_thread.h", "shell/browser/ui/win/electron_desktop_native_widget_aura.cc", diff --git a/shell/browser/native_window.cc b/shell/browser/native_window.cc index eb51d80134e49..dd80d4367ce5c 100644 --- a/shell/browser/native_window.cc +++ b/shell/browser/native_window.cc @@ -24,6 +24,34 @@ #include "ui/display/win/screen_win.h" #endif +namespace gin { + +template <> +struct Converter { + static bool FromV8(v8::Isolate* isolate, + v8::Handle val, + electron::NativeWindow::TitleBarStyle* out) { + using TitleBarStyle = electron::NativeWindow::TitleBarStyle; + std::string title_bar_style; + if (!ConvertFromV8(isolate, val, &title_bar_style)) + return false; + if (title_bar_style == "hidden") { + *out = TitleBarStyle::kHidden; +#if defined(OS_MAC) + } else if (title_bar_style == "hiddenInset") { + *out = TitleBarStyle::kHiddenInset; + } else if (title_bar_style == "customButtonsOnHover") { + *out = TitleBarStyle::kCustomButtonsOnHover; +#endif + } else { + return false; + } + return true; + } +}; + +} // namespace gin + namespace electron { namespace { @@ -54,7 +82,19 @@ NativeWindow::NativeWindow(const gin_helper::Dictionary& options, options.Get(options::kFrame, &has_frame_); options.Get(options::kTransparent, &transparent_); options.Get(options::kEnableLargerThanScreen, &enable_larger_than_screen_); - options.Get(options::ktitleBarOverlay, &titlebar_overlay_); + options.Get(options::kTitleBarStyle, &title_bar_style_); + + v8::Local titlebar_overlay; + if (options.Get(options::ktitleBarOverlay, &titlebar_overlay)) { + if (titlebar_overlay->IsBoolean()) { + options.Get(options::ktitleBarOverlay, &titlebar_overlay_); + } else if (titlebar_overlay->IsObject()) { + titlebar_overlay_ = true; +#if !defined(OS_WIN) + DCHECK(false); +#endif + } + } if (parent) options.Get("modal", &is_modal_); diff --git a/shell/browser/native_window.h b/shell/browser/native_window.h index 2515b2c9c1b18..77cf99e2824f1 100644 --- a/shell/browser/native_window.h +++ b/shell/browser/native_window.h @@ -315,6 +315,14 @@ class NativeWindow : public base::SupportsUserData, views::Widget* widget() const { return widget_.get(); } views::View* content_view() const { return content_view_; } + enum class TitleBarStyle { + kNormal, + kHidden, + kHiddenInset, + kCustomButtonsOnHover, + }; + TitleBarStyle title_bar_style() const { return title_bar_style_; } + bool has_frame() const { return has_frame_; } void set_has_frame(bool has_frame) { has_frame_ = has_frame; } @@ -346,8 +354,12 @@ class NativeWindow : public base::SupportsUserData, [&browser_view](NativeBrowserView* n) { return (n == browser_view); }); } + // The boolean parsing of the "titleBarOverlay" option bool titlebar_overlay_ = false; + // The "titleBarStyle" option. + TitleBarStyle title_bar_style_ = TitleBarStyle::kNormal; + private: std::unique_ptr widget_; diff --git a/shell/browser/native_window_mac.h b/shell/browser/native_window_mac.h index 71e5936bd512b..04d2279cca4ec 100644 --- a/shell/browser/native_window_mac.h +++ b/shell/browser/native_window_mac.h @@ -183,14 +183,6 @@ class NativeWindowMac : public NativeWindow, kInactive, }; - enum class TitleBarStyle { - kNormal, - kHidden, - kHiddenInset, - kCustomButtonsOnHover, - }; - TitleBarStyle title_bar_style() const { return title_bar_style_; } - ElectronPreviewItem* preview_item() const { return preview_item_.get(); } ElectronTouchBar* touch_bar() const { return touch_bar_.get(); } bool zoom_to_page_width() const { return zoom_to_page_width_; } @@ -250,9 +242,6 @@ class NativeWindowMac : public NativeWindow, // The presentation options before entering kiosk mode. NSApplicationPresentationOptions kiosk_options_; - // The "titleBarStyle" option. - TitleBarStyle title_bar_style_ = TitleBarStyle::kNormal; - // The "visualEffectState" option. VisualEffectState visual_effect_state_ = VisualEffectState::kFollowWindow; diff --git a/shell/browser/native_window_mac.mm b/shell/browser/native_window_mac.mm index 24dcdbda1e569..8d5a9f5fd3e6e 100644 --- a/shell/browser/native_window_mac.mm +++ b/shell/browser/native_window_mac.mm @@ -164,28 +164,6 @@ - (void)drawRect:(NSRect)dirtyRect { namespace gin { -template <> -struct Converter { - static bool FromV8(v8::Isolate* isolate, - v8::Handle val, - electron::NativeWindowMac::TitleBarStyle* out) { - using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle; - std::string title_bar_style; - if (!ConvertFromV8(isolate, val, &title_bar_style)) - return false; - if (title_bar_style == "hidden") { - *out = TitleBarStyle::kHidden; - } else if (title_bar_style == "hiddenInset") { - *out = TitleBarStyle::kHiddenInset; - } else if (title_bar_style == "customButtonsOnHover") { - *out = TitleBarStyle::kCustomButtonsOnHover; - } else { - return false; - } - return true; - } -}; - template <> struct Converter { static bool FromV8(v8::Isolate* isolate, @@ -273,8 +251,8 @@ void ViewDidMoveToSuperview(NSView* self, SEL _cmd) { round((NSHeight(main_screen_rect) - height) / 2), width, height); - options.Get(options::kResizable, &resizable_); - options.Get(options::kTitleBarStyle, &title_bar_style_); + bool resizable = true; + options.Get(options::kResizable, &resizable); options.Get(options::kZoomToPageWidth, &zoom_to_page_width_); options.Get(options::kSimpleFullScreen, &always_simple_fullscreen_); options.GetOptional(options::kTrafficLightPosition, &traffic_light_position_); diff --git a/shell/browser/native_window_views.cc b/shell/browser/native_window_views.cc index d0ccc82df64a5..5209aee8e98cf 100644 --- a/shell/browser/native_window_views.cc +++ b/shell/browser/native_window_views.cc @@ -71,12 +71,14 @@ #elif defined(OS_WIN) #include "base/win/win_util.h" +#include "extensions/common/image_util.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" #include "ui/base/win/shell.h" #include "ui/display/screen.h" #include "ui/display/win/screen_win.h" +#include "ui/gfx/color_utils.h" #include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h" #endif @@ -165,6 +167,37 @@ NativeWindowViews::NativeWindowViews(const gin_helper::Dictionary& options, options.Get("thickFrame", &thick_frame_); if (transparent()) thick_frame_ = false; + + overlay_button_color_ = color_utils::GetSysSkColor(COLOR_BTNFACE); + overlay_symbol_color_ = color_utils::GetSysSkColor(COLOR_BTNTEXT); + + v8::Local 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); + options.Get(options::ktitleBarOverlay, &titlebar_overlay_obj); + + std::string overlay_color_string; + if (titlebar_overlay_obj.Get(options::kOverlayButtonColor, + &overlay_color_string)) { + bool success = extensions::image_util::ParseCssColorString( + overlay_color_string, &overlay_button_color_); + DCHECK(success); + } + + std::string overlay_symbol_color_string; + if (titlebar_overlay_obj.Get(options::kOverlaySymbolColor, + &overlay_symbol_color_string)) { + bool success = extensions::image_util::ParseCssColorString( + overlay_symbol_color_string, &overlay_symbol_color_); + DCHECK(success); + } + } + + if (title_bar_style_ != TitleBarStyle::kNormal) + set_has_frame(false); #endif if (enable_larger_than_screen()) diff --git a/shell/browser/native_window_views.h b/shell/browser/native_window_views.h index 937667bd452df..4b9d61e8a2603 100644 --- a/shell/browser/native_window_views.h +++ b/shell/browser/native_window_views.h @@ -19,6 +19,7 @@ #if defined(OS_WIN) #include "base/win/scoped_gdi_object.h" #include "shell/browser/ui/win/taskbar_host.h" + #endif namespace views { @@ -174,6 +175,15 @@ class NativeWindowViews : public NativeWindow, TaskbarHost& taskbar_host() { return taskbar_host_; } #endif +#if defined(OS_WIN) + bool IsWindowControlsOverlayEnabled() const { + return (title_bar_style_ == NativeWindowViews::TitleBarStyle::kHidden) && + titlebar_overlay_; + } + SkColor overlay_button_color() const { return overlay_button_color_; } + SkColor overlay_symbol_color() const { return overlay_symbol_color_; } +#endif + private: // views::WidgetObserver: void OnWidgetActivationChanged(views::Widget* widget, bool active) override; @@ -294,6 +304,11 @@ class NativeWindowViews : public NativeWindow, // Whether the window is currently being moved. bool is_moving_ = false; + + // 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_; #endif // Handles unhandled keyboard messages coming back from the renderer process. diff --git a/shell/browser/ui/views/frameless_view.cc b/shell/browser/ui/views/frameless_view.cc index 734896a66ee53..ca81a55cfc0ab 100644 --- a/shell/browser/ui/views/frameless_view.cc +++ b/shell/browser/ui/views/frameless_view.cc @@ -85,17 +85,17 @@ int FramelessView::NonClientHitTest(const gfx::Point& cursor) { return HTCAPTION; } + // Support resizing frameless window by dragging the border. + int frame_component = ResizingBorderHitTest(cursor); + if (frame_component != HTNOWHERE) + return frame_component; + // Check for possible draggable region in the client area for the frameless // window. SkRegion* draggable_region = window_->draggable_region(); if (draggable_region && draggable_region->contains(cursor.x(), cursor.y())) return HTCAPTION; - // Support resizing frameless window by dragging the border. - int frame_component = ResizingBorderHitTest(cursor); - if (frame_component != HTNOWHERE) - return frame_component; - return HTCLIENT; } diff --git a/shell/browser/ui/views/frameless_view.h b/shell/browser/ui/views/frameless_view.h index 7719fa19c32f2..6fbd21a853b4b 100644 --- a/shell/browser/ui/views/frameless_view.h +++ b/shell/browser/ui/views/frameless_view.h @@ -48,6 +48,8 @@ class FramelessView : public views::NonClientFrameView { NativeWindowViews* window_ = nullptr; views::Widget* frame_ = nullptr; + friend class NativeWindowsViews; + private: DISALLOW_COPY_AND_ASSIGN(FramelessView); }; diff --git a/shell/browser/ui/views/win_caption_button.cc b/shell/browser/ui/views/win_caption_button.cc new file mode 100644 index 0000000000000..ec202a227481e --- /dev/null +++ b/shell/browser/ui/views/win_caption_button.cc @@ -0,0 +1,220 @@ +// Copyright (c) 2016 The Chromium Authors. All rights reserved. +// 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/win_caption_button.h" + +#include + +#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" +#include "ui/base/metadata/metadata_impl_macros.h" +#include "ui/base/theme_provider.h" +#include "ui/gfx/animation/tween.h" +#include "ui/gfx/color_utils.h" +#include "ui/gfx/geometry/rect_conversions.h" +#include "ui/gfx/scoped_canvas.h" + +namespace electron { + +WinCaptionButton::WinCaptionButton(PressedCallback callback, + WinFrameView* frame_view, + ViewID button_type, + const std::u16string& accessible_name) + : views::Button(std::move(callback)), + frame_view_(frame_view), + button_type_(button_type) { + SetAnimateOnStateChange(true); + // Not focusable by default, only for accessibility. + SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY); + SetAccessibleName(accessible_name); +} + +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); +} + +void WinCaptionButton::OnPaintBackground(gfx::Canvas* canvas) { + // Paint the background of the button (the semi-transparent rectangle that + // appears when you hover or press the button). + + const SkColor bg_color = frame_view_->window()->overlay_button_color(); + const SkAlpha theme_alpha = SkColorGetA(bg_color); + + gfx::Rect bounds = GetContentsBounds(); + bounds.Inset(0, 0, 0, 0); + + canvas->FillRect(bounds, SkColorSetA(bg_color, theme_alpha)); + + SkColor base_color; + SkAlpha hovered_alpha, pressed_alpha; + if (button_type_ == VIEW_ID_CLOSE_BUTTON) { + base_color = SkColorSetRGB(0xE8, 0x11, 0x23); + hovered_alpha = SK_AlphaOPAQUE; + pressed_alpha = 0x98; + } else { + // Match the native buttons. + base_color = frame_view_->GetReadableFeatureColor(bg_color); + hovered_alpha = 0x1A; + pressed_alpha = 0x33; + + if (theme_alpha > 0) { + // Theme buttons have slightly increased opacity to make them stand out + // against a visually-busy frame image. + constexpr float kAlphaScale = 1.3f; + hovered_alpha = base::ClampRound(hovered_alpha * kAlphaScale); + pressed_alpha = base::ClampRound(pressed_alpha * kAlphaScale); + } + } + + SkAlpha alpha; + if (GetState() == STATE_PRESSED) + alpha = pressed_alpha; + else + alpha = gfx::Tween::IntValueBetween(hover_animation().GetCurrentValue(), + SK_AlphaTRANSPARENT, hovered_alpha); + canvas->FillRect(bounds, SkColorSetA(base_color, alpha)); +} + +void WinCaptionButton::PaintButtonContents(gfx::Canvas* canvas) { + PaintSymbol(canvas); +} + +int WinCaptionButton::GetBetweenButtonSpacing() const { + const int display_order_index = GetButtonDisplayOrderIndex(); + return display_order_index == 0 + ? 0 + : WindowFrameUtil::kWindows10GlassCaptionButtonVisualSpacing; +} + +int WinCaptionButton::GetButtonDisplayOrderIndex() const { + int button_display_order = 0; + switch (button_type_) { + case VIEW_ID_MINIMIZE_BUTTON: + button_display_order = 0; + break; + case VIEW_ID_MAXIMIZE_BUTTON: + case VIEW_ID_RESTORE_BUTTON: + button_display_order = 1; + break; + case VIEW_ID_CLOSE_BUTTON: + button_display_order = 2; + break; + default: + NOTREACHED(); + return 0; + } + + // Reverse the ordering if we're in RTL mode + if (base::i18n::IsRTL()) + button_display_order = 2 - button_display_order; + + return button_display_order; +} + +namespace { + +// Canvas::DrawRect's stroke can bleed out of |rect|'s bounds, so this draws a +// rectangle inset such that the result is constrained to |rect|'s size. +void DrawRect(gfx::Canvas* canvas, + const gfx::Rect& rect, + const cc::PaintFlags& flags) { + gfx::RectF rect_f(rect); + float stroke_half_width = flags.getStrokeWidth() / 2; + rect_f.Inset(stroke_half_width, stroke_half_width); + canvas->DrawRect(rect_f, flags); +} + +} // namespace + +void WinCaptionButton::PaintSymbol(gfx::Canvas* canvas) { + SkColor symbol_color = frame_view_->window()->overlay_symbol_color(); + + if (button_type_ == VIEW_ID_CLOSE_BUTTON && + hover_animation().is_animating()) { + symbol_color = gfx::Tween::ColorValueBetween( + hover_animation().GetCurrentValue(), symbol_color, SK_ColorWHITE); + } else if (button_type_ == VIEW_ID_CLOSE_BUTTON && + (GetState() == STATE_HOVERED || GetState() == STATE_PRESSED)) { + symbol_color = SK_ColorWHITE; + } + + gfx::ScopedCanvas scoped_canvas(canvas); + const float scale = canvas->UndoDeviceScaleFactor(); + + const int symbol_size_pixels = std::round(10 * scale); + gfx::RectF bounds_rect(GetContentsBounds()); + bounds_rect.Scale(scale); + gfx::Rect symbol_rect(gfx::ToEnclosingRect(bounds_rect)); + symbol_rect.ClampToCenteredSize( + gfx::Size(symbol_size_pixels, symbol_size_pixels)); + + cc::PaintFlags flags; + flags.setAntiAlias(false); + flags.setColor(symbol_color); + flags.setStyle(cc::PaintFlags::kStroke_Style); + // Stroke width jumps up a pixel every time we reach a new integral scale. + const int stroke_width = std::floor(scale); + flags.setStrokeWidth(stroke_width); + + switch (button_type_) { + case VIEW_ID_MINIMIZE_BUTTON: { + const int y = symbol_rect.CenterPoint().y(); + const gfx::Point p1 = gfx::Point(symbol_rect.x(), y); + const gfx::Point p2 = gfx::Point(symbol_rect.right(), y); + canvas->DrawLine(p1, p2, flags); + return; + } + + case VIEW_ID_MAXIMIZE_BUTTON: + DrawRect(canvas, symbol_rect, flags); + return; + + case VIEW_ID_RESTORE_BUTTON: { + // Bottom left ("in front") square. + const int separation = std::floor(2 * scale); + symbol_rect.Inset(0, separation, separation, 0); + DrawRect(canvas, symbol_rect, flags); + + // Top right ("behind") square. + canvas->ClipRect(symbol_rect, SkClipOp::kDifference); + symbol_rect.Offset(separation, -separation); + DrawRect(canvas, symbol_rect, flags); + return; + } + + case VIEW_ID_CLOSE_BUTTON: { + flags.setAntiAlias(true); + // The close button's X is surrounded by a "halo" of transparent pixels. + // When the X is white, the transparent pixels need to be a bit brighter + // to be visible. + const float stroke_halo = + stroke_width * (symbol_color == SK_ColorWHITE ? 0.1f : 0.05f); + flags.setStrokeWidth(stroke_width + stroke_halo); + + // TODO(bsep): This sometimes draws misaligned at fractional device scales + // because the button's origin isn't necessarily aligned to pixels. + canvas->ClipRect(symbol_rect); + SkPath path; + path.moveTo(symbol_rect.x(), symbol_rect.y()); + path.lineTo(symbol_rect.right(), symbol_rect.bottom()); + path.moveTo(symbol_rect.right(), symbol_rect.y()); + path.lineTo(symbol_rect.x(), symbol_rect.bottom()); + canvas->DrawPath(path, flags); + return; + } + + default: + NOTREACHED(); + return; + } +} +} // namespace electron diff --git a/shell/browser/ui/views/win_caption_button.h b/shell/browser/ui/views/win_caption_button.h new file mode 100644 index 0000000000000..f57918538ec95 --- /dev/null +++ b/shell/browser/ui/views/win_caption_button.h @@ -0,0 +1,54 @@ +// Copyright (c) 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_H_ +#define SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_H_ + +#include "chrome/browser/ui/view_ids.h" +#include "ui/base/metadata/metadata_header_macros.h" +#include "ui/gfx/canvas.h" +#include "ui/views/controls/button/button.h" + +namespace electron { + +class WinFrameView; + +class WinCaptionButton : public views::Button { + public: + WinCaptionButton(PressedCallback callback, + WinFrameView* frame_view, + ViewID button_type, + const std::u16string& accessible_name); + WinCaptionButton(const WinCaptionButton&) = delete; + WinCaptionButton& operator=(const WinCaptionButton&) = delete; + + // // views::Button: + gfx::Size CalculatePreferredSize() const override; + void OnPaintBackground(gfx::Canvas* canvas) override; + void PaintButtonContents(gfx::Canvas* canvas) override; + + // 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. + int GetBetweenButtonSpacing() const; + + // Returns the order in which this button will be displayed (with 0 being + // drawn farthest to the left, and larger indices being drawn to the right of + // smaller indices). + int GetButtonDisplayOrderIndex() const; + + // The base color to use for the button symbols and background blending. Uses + // the more readable of black and white. + SkColor GetBaseColor() const; + + // Paints the minimize/maximize/restore/close icon for the button. + void PaintSymbol(gfx::Canvas* canvas); + + WinFrameView* frame_view_; + ViewID button_type_; +}; +} // namespace electron + +#endif // SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_H_ diff --git a/shell/browser/ui/views/win_caption_button_container.cc b/shell/browser/ui/views/win_caption_button_container.cc new file mode 100644 index 0000000000000..aba141e722512 --- /dev/null +++ b/shell/browser/ui/views/win_caption_button_container.cc @@ -0,0 +1,143 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// 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/win_caption_button_container.h" + +#include +#include + +#include "shell/browser/ui/views/win_caption_button.h" +#include "shell/browser/ui/views/win_frame_view.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/strings/grit/ui_strings.h" +#include "ui/views/layout/flex_layout.h" +#include "ui/views/view_class_properties.h" + +namespace electron { + +namespace { + +std::unique_ptr CreateCaptionButton( + views::Button::PressedCallback callback, + WinFrameView* frame_view, + ViewID button_type, + int accessible_name_resource_id) { + return std::make_unique( + std::move(callback), frame_view, button_type, + l10n_util::GetStringUTF16(accessible_name_resource_id)); +} + +bool HitTestCaptionButton(WinCaptionButton* button, const gfx::Point& point) { + return button && button->GetVisible() && button->bounds().Contains(point); +} + +} // anonymous namespace + +WinCaptionButtonContainer::WinCaptionButtonContainer(WinFrameView* frame_view) + : frame_view_(frame_view), + minimize_button_(AddChildView(CreateCaptionButton( + base::BindRepeating(&views::Widget::Minimize, + base::Unretained(frame_view_->frame())), + frame_view_, + VIEW_ID_MINIMIZE_BUTTON, + IDS_APP_ACCNAME_MINIMIZE))), + maximize_button_(AddChildView(CreateCaptionButton( + base::BindRepeating(&views::Widget::Maximize, + base::Unretained(frame_view_->frame())), + frame_view_, + VIEW_ID_MAXIMIZE_BUTTON, + IDS_APP_ACCNAME_MAXIMIZE))), + restore_button_(AddChildView(CreateCaptionButton( + base::BindRepeating(&views::Widget::Restore, + base::Unretained(frame_view_->frame())), + frame_view_, + VIEW_ID_RESTORE_BUTTON, + IDS_APP_ACCNAME_RESTORE))), + close_button_(AddChildView(CreateCaptionButton( + base::BindRepeating(&views::Widget::CloseWithReason, + base::Unretained(frame_view_->frame()), + views::Widget::ClosedReason::kCloseButtonClicked), + frame_view_, + VIEW_ID_CLOSE_BUTTON, + IDS_APP_ACCNAME_CLOSE))) { + // Layout is horizontal, with buttons placed at the trailing end of the view. + // This allows the container to expand to become a faux titlebar/drag handle. + auto* const layout = SetLayoutManager(std::make_unique()); + layout->SetOrientation(views::LayoutOrientation::kHorizontal) + .SetMainAxisAlignment(views::LayoutAlignment::kEnd) + .SetCrossAxisAlignment(views::LayoutAlignment::kStart) + .SetDefault( + views::kFlexBehaviorKey, + views::FlexSpecification(views::LayoutOrientation::kHorizontal, + views::MinimumFlexSizeRule::kPreferred, + views::MaximumFlexSizeRule::kPreferred, + /* adjust_width_for_height */ false, + views::MinimumFlexSizeRule::kScaleToZero)); +} + +WinCaptionButtonContainer::~WinCaptionButtonContainer() {} + +int WinCaptionButtonContainer::NonClientHitTest(const gfx::Point& point) const { + DCHECK(HitTestPoint(point)) + << "should only be called with a point inside this view's bounds"; + if (HitTestCaptionButton(minimize_button_, point)) { + return HTMINBUTTON; + } + if (HitTestCaptionButton(maximize_button_, point)) { + return HTMAXBUTTON; + } + if (HitTestCaptionButton(restore_button_, point)) { + return HTMAXBUTTON; + } + if (HitTestCaptionButton(close_button_, point)) { + return HTCLOSE; + } + return HTCAPTION; +} + +void WinCaptionButtonContainer::ResetWindowControls() { + minimize_button_->SetState(views::Button::STATE_NORMAL); + maximize_button_->SetState(views::Button::STATE_NORMAL); + restore_button_->SetState(views::Button::STATE_NORMAL); + close_button_->SetState(views::Button::STATE_NORMAL); + InvalidateLayout(); +} + +void WinCaptionButtonContainer::AddedToWidget() { + views::Widget* const widget = GetWidget(); + + DCHECK(!widget_observation_.IsObserving()); + widget_observation_.Observe(widget); + + UpdateButtons(); + + if (frame_view_->window()->IsWindowControlsOverlayEnabled()) { + SetPaintToLayer(); + } +} + +void WinCaptionButtonContainer::RemovedFromWidget() { + DCHECK(widget_observation_.IsObserving()); + widget_observation_.Reset(); +} + +void WinCaptionButtonContainer::OnWidgetBoundsChanged( + views::Widget* widget, + const gfx::Rect& new_bounds) { + UpdateButtons(); +} + +void WinCaptionButtonContainer::UpdateButtons() { + const bool is_maximized = frame_view_->frame()->IsMaximized(); + restore_button_->SetVisible(is_maximized); + maximize_button_->SetVisible(!is_maximized); + + // In touch mode, windows cannot be taken out of fullscreen or tiled mode, so + // the maximize/restore button should be disabled. + const bool is_touch = ui::TouchUiController::Get()->touch_ui(); + restore_button_->SetEnabled(!is_touch); + maximize_button_->SetEnabled(!is_touch); + InvalidateLayout(); +} +} // namespace electron diff --git a/shell/browser/ui/views/win_caption_button_container.h b/shell/browser/ui/views/win_caption_button_container.h new file mode 100644 index 0000000000000..ed36a106b3cf9 --- /dev/null +++ b/shell/browser/ui/views/win_caption_button_container.h @@ -0,0 +1,70 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_CONTAINER_H_ +#define SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_CONTAINER_H_ + +#include "base/scoped_observation.h" +#include "ui/base/metadata/metadata_header_macros.h" +#include "ui/base/pointer/touch_ui_controller.h" +#include "ui/views/controls/button/button.h" +#include "ui/views/view.h" +#include "ui/views/widget/widget.h" +#include "ui/views/widget/widget_observer.h" + +namespace electron { + +class WinFrameView; +class WinCaptionButton; + +// Provides a container for Windows 10 caption buttons that can be moved between +// frame and browser window as needed. When extended horizontally, becomes a +// grab bar for moving the window. +class WinCaptionButtonContainer : public views::View, + public views::WidgetObserver { + public: + explicit WinCaptionButtonContainer(WinFrameView* frame_view); + ~WinCaptionButtonContainer() override; + + // Tests to see if the specified |point| (which is expressed in this view's + // coordinates and which must be within this view's bounds) is within one of + // the caption buttons. Returns one of HitTestCompat enum defined in + // ui/base/hit_test.h, HTCAPTION if the area hit would be part of the window's + // drag handle, and HTNOWHERE otherwise. + // See also ClientView::NonClientHitTest. + int NonClientHitTest(const gfx::Point& point) const; + + private: + // views::View: + void AddedToWidget() override; + void RemovedFromWidget() override; + + // views::WidgetObserver: + void OnWidgetBoundsChanged(views::Widget* widget, + const gfx::Rect& new_bounds) override; + + void ResetWindowControls(); + + // Sets caption button visibility and enabled state based on window state. + // Only one of maximize or restore button should ever be visible at the same + // time, and both are disabled in tablet UI mode. + void UpdateButtons(); + + WinFrameView* const frame_view_; + WinCaptionButton* const minimize_button_; + WinCaptionButton* const maximize_button_; + WinCaptionButton* const restore_button_; + WinCaptionButton* const close_button_; + + base::ScopedObservation + widget_observation_{this}; + + base::CallbackListSubscription subscription_ = + ui::TouchUiController::Get()->RegisterCallback( + base::BindRepeating(&WinCaptionButtonContainer::UpdateButtons, + base::Unretained(this))); +}; +} // namespace electron + +#endif // SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_CONTAINER_H_ diff --git a/shell/browser/ui/views/win_frame_view.cc b/shell/browser/ui/views/win_frame_view.cc index 55119cf1e4222..f2f6acc367aa5 100644 --- a/shell/browser/ui/views/win_frame_view.cc +++ b/shell/browser/ui/views/win_frame_view.cc @@ -1,11 +1,24 @@ // Copyright (c) 2014 GitHub, Inc. // Use of this source code is governed by the MIT license that can be // found in the LICENSE file. +// +// Portions of this file are sourced from +// chrome/browser/ui/views/frame/glass_browser_frame_view.cc, +// Copyright (c) 2012 The Chromium Authors, +// which is governed by a BSD-style license #include "shell/browser/ui/views/win_frame_view.h" +#include +#include + #include "base/win/windows_version.h" #include "shell/browser/native_window_views.h" +#include "shell/browser/ui/views/win_caption_button_container.h" +#include "ui/base/win/hwnd_metrics.h" +#include "ui/display/win/dpi.h" +#include "ui/display/win/screen_win.h" +#include "ui/gfx/geometry/dip_util.h" #include "ui/views/widget/widget.h" #include "ui/views/win/hwnd_util.h" @@ -17,6 +30,30 @@ WinFrameView::WinFrameView() {} WinFrameView::~WinFrameView() {} +void WinFrameView::Init(NativeWindowViews* window, views::Widget* frame) { + window_ = window; + frame_ = frame; + + if (window->IsWindowControlsOverlayEnabled()) { + caption_button_container_ = + AddChildView(std::make_unique(this)); + } else { + caption_button_container_ = nullptr; + } +} + +SkColor WinFrameView::GetReadableFeatureColor(SkColor background_color) { + // color_utils::GetColorWithMaxContrast()/IsDark() aren't used here because + // they switch based on the Chrome light/dark endpoints, while we want to use + // the system native behavior below. + const auto windows_luma = [](SkColor c) { + return 0.25f * SkColorGetR(c) + 0.625f * SkColorGetG(c) + + 0.125f * SkColorGetB(c); + }; + return windows_luma(background_color) <= 128.0f ? SK_ColorWHITE + : SK_ColorBLACK; +} + gfx::Rect WinFrameView::GetWindowBoundsForClientBounds( const gfx::Rect& client_bounds) const { return views::GetWindowBoundsForClientBounds( @@ -24,15 +61,194 @@ gfx::Rect WinFrameView::GetWindowBoundsForClientBounds( client_bounds); } +int WinFrameView::FrameBorderThickness() const { + return (IsMaximized() || frame()->IsFullscreen()) + ? 0 + : display::win::ScreenWin::GetSystemMetricsInDIP(SM_CXSIZEFRAME); +} + int WinFrameView::NonClientHitTest(const gfx::Point& point) { if (window_->has_frame()) return frame_->client_view()->NonClientHitTest(point); - else - return FramelessView::NonClientHitTest(point); + + if (ShouldCustomDrawSystemTitlebar()) { + // See if the point is within any of the window controls. + if (caption_button_container_) { + gfx::Point local_point = point; + + ConvertPointToTarget(parent(), caption_button_container_, &local_point); + if (caption_button_container_->HitTestPoint(local_point)) { + const int hit_test_result = + caption_button_container_->NonClientHitTest(local_point); + if (hit_test_result != HTNOWHERE) + return hit_test_result; + } + } + + // On Windows 8+, the caption buttons are almost butted up to the top right + // corner of the window. This code ensures the mouse isn't set to a size + // cursor while hovering over the caption buttons, thus giving the incorrect + // impression that the user can resize the window. + if (base::win::GetVersion() >= base::win::Version::WIN8) { + RECT button_bounds = {0}; + if (SUCCEEDED(DwmGetWindowAttribute( + views::HWNDForWidget(frame()), DWMWA_CAPTION_BUTTON_BOUNDS, + &button_bounds, sizeof(button_bounds)))) { + gfx::RectF button_bounds_in_dips = gfx::ConvertRectToDips( + gfx::Rect(button_bounds), display::win::GetDPIScale()); + // TODO(crbug.com/1131681): GetMirroredRect() requires an integer rect, + // but the size in DIPs may not be an integer with a fractional device + // scale factor. If we want to keep using integers, the choice to use + // ToFlooredRectDeprecated() seems to be doing the wrong thing given the + // comment below about insetting 1 DIP instead of 1 physical pixel. We + // should probably use ToEnclosedRect() and then we could have inset 1 + // physical pixel here. + gfx::Rect buttons = GetMirroredRect( + gfx::ToFlooredRectDeprecated(button_bounds_in_dips)); + + // There is a small one-pixel strip right above the caption buttons in + // which the resize border "peeks" through. + constexpr int kCaptionButtonTopInset = 1; + // The sizing region at the window edge above the caption buttons is + // 1 px regardless of scale factor. If we inset by 1 before converting + // to DIPs, the precision loss might eliminate this region entirely. The + // best we can do is to inset after conversion. This guarantees we'll + // show the resize cursor when resizing is possible. The cost of which + // is also maybe showing it over the portion of the DIP that isn't the + // outermost pixel. + buttons.Inset(0, kCaptionButtonTopInset, 0, 0); + if (buttons.Contains(point)) + return HTNOWHERE; + } + } + + int top_border_thickness = FrameTopBorderThickness(false); + // At the window corners the resize area is not actually bigger, but the 16 + // pixels at the end of the top and bottom edges trigger diagonal resizing. + constexpr int kResizeCornerWidth = 16; + int window_component = GetHTComponentForFrame( + point, gfx::Insets(top_border_thickness, 0, 0, 0), top_border_thickness, + kResizeCornerWidth - FrameBorderThickness(), + frame()->widget_delegate()->CanResize()); + if (window_component != HTNOWHERE) + return window_component; + } + + // Use the parent class's hittest last + return FramelessView::NonClientHitTest(point); } const char* WinFrameView::GetClassName() const { return kViewClassName; } +bool WinFrameView::IsMaximized() const { + return frame()->IsMaximized(); +} + +bool WinFrameView::ShouldCustomDrawSystemTitlebar() const { + return window()->IsWindowControlsOverlayEnabled(); +} + +void WinFrameView::Layout() { + LayoutCaptionButtons(); + if (window()->IsWindowControlsOverlayEnabled()) { + LayoutWindowControlsOverlay(); + } + NonClientFrameView::Layout(); +} + +int WinFrameView::FrameTopBorderThickness(bool restored) const { + // Mouse and touch locations are floored but GetSystemMetricsInDIP is rounded, + // so we need to floor instead or else the difference will cause the hittest + // to fail when it ought to succeed. + return std::floor( + FrameTopBorderThicknessPx(restored) / + display::win::ScreenWin::GetScaleFactorForHWND(HWNDForView(this))); +} + +int WinFrameView::FrameTopBorderThicknessPx(bool restored) const { + // Distinct from FrameBorderThickness() because we can't inset the top + // border, otherwise Windows will give us a standard titlebar. + // For maximized windows this is not true, and the top border must be + // inset in order to avoid overlapping the monitor above. + + // See comments in BrowserDesktopWindowTreeHostWin::GetClientAreaInsets(). + const bool needs_no_border = + (ShouldCustomDrawSystemTitlebar() && frame()->IsMaximized()) || + frame()->IsFullscreen(); + if (needs_no_border && !restored) + return 0; + + // Note that this method assumes an equal resize handle thickness on all + // sides of the window. + // TODO(dfried): Consider having it return a gfx::Insets object instead. + return ui::GetFrameThickness( + MonitorFromWindow(HWNDForView(this), MONITOR_DEFAULTTONEAREST)); +} + +int WinFrameView::TitlebarMaximizedVisualHeight() const { + int maximized_height = + display::win::ScreenWin::GetSystemMetricsInDIP(SM_CYCAPTION); + return maximized_height; +} + +int WinFrameView::TitlebarHeight(bool restored) const { + if (frame()->IsFullscreen() && !restored) + return 0; + + return TitlebarMaximizedVisualHeight() + FrameTopBorderThickness(false); +} + +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. + // Unfortunately we can't represent either of those at hidpi without using + // non-integral dips, so we return the closest reasonable values instead. + if (IsMaximized()) + return FrameTopBorderThickness(false); + + return 1; +} + +void WinFrameView::LayoutCaptionButtons() { + if (!caption_button_container_) + return; + + // Non-custom system titlebar already contains caption buttons. + if (!ShouldCustomDrawSystemTitlebar()) { + caption_button_container_->SetVisible(false); + return; + } + + 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(); + + // 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. + caption_button_container_->SetBounds(width() - preferred_size.width(), + WindowTopY(), preferred_size.width() - 1, + height); +} + +void WinFrameView::LayoutWindowControlsOverlay() { + int overlay_height = caption_button_container_->size().height(); + int overlay_width = caption_button_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(); +} + } // namespace electron diff --git a/shell/browser/ui/views/win_frame_view.h b/shell/browser/ui/views/win_frame_view.h index 884a13e506c3e..c0311a38f11f8 100644 --- a/shell/browser/ui/views/win_frame_view.h +++ b/shell/browser/ui/views/win_frame_view.h @@ -1,11 +1,18 @@ // Copyright (c) 2014 GitHub, Inc. // Use of this source code is governed by the MIT license that can be // found in the LICENSE file. +// +// Portions of this file are sourced from +// chrome/browser/ui/views/frame/glass_browser_frame_view.h, +// Copyright (c) 2012 The Chromium Authors, +// which is governed by a BSD-style license #ifndef SHELL_BROWSER_UI_VIEWS_WIN_FRAME_VIEW_H_ #define SHELL_BROWSER_UI_VIEWS_WIN_FRAME_VIEW_H_ +#include "shell/browser/native_window_views.h" #include "shell/browser/ui/views/frameless_view.h" +#include "shell/browser/ui/views/win_caption_button.h" namespace electron { @@ -15,6 +22,14 @@ class WinFrameView : public FramelessView { WinFrameView(); ~WinFrameView() override; + void Init(NativeWindowViews* window, views::Widget* frame) 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. + static constexpr SkAlpha kInactiveTitlebarFeatureAlpha = 0x66; + + SkColor GetReadableFeatureColor(SkColor background_color); + // views::NonClientFrameView: gfx::Rect GetWindowBoundsForClientBounds( const gfx::Rect& client_bounds) const override; @@ -23,7 +38,50 @@ class WinFrameView : public FramelessView { // views::View: const char* GetClassName() const override; + NativeWindowViews* window() const { return window_; } + views::Widget* frame() const { return frame_; } + + 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; + + protected: + // views::View: + void Layout() override; + private: + friend class WinCaptionButtonContainer; + + int FrameBorderThickness() const; + + // Returns the thickness of the window border for the top edge of the frame, + // which is sometimes different than FrameBorderThickness(). Does not include + // the titlebar/tabstrip area. If |restored| is true, this is calculated as if + // the window was restored, regardless of its current state. + int FrameTopBorderThickness(bool restored) const; + int FrameTopBorderThicknessPx(bool restored) const; + + // Returns the height of the titlebar for popups or other browser types that + // don't have tabs. + int TitlebarHeight(bool restored) 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 + // the window to leave room for the visual border that Windows draws. + int WindowTopY() const; + + void LayoutCaptionButtons(); + void LayoutWindowControlsOverlay(); + + // 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! + WinCaptionButtonContainer* caption_button_container_; + DISALLOW_COPY_AND_ASSIGN(WinFrameView); }; diff --git a/shell/common/options_switches.cc b/shell/common/options_switches.cc index f2f575f43bcd9..c4bbdd528c61a 100644 --- a/shell/common/options_switches.cc +++ b/shell/common/options_switches.cc @@ -31,6 +31,11 @@ const char kFullscreen[] = "fullscreen"; const char kTrafficLightPosition[] = "trafficLightPosition"; const char kRoundedCorners[] = "roundedCorners"; +// The color to use as the theme and symbol colors respectively for Window +// Controls Overlay if enabled on Windows. +const char kOverlayButtonColor[] = "color"; +const char kOverlaySymbolColor[] = "symbolColor"; + // Whether the window should show in taskbar. const char kSkipTaskbar[] = "skipTaskbar"; diff --git a/shell/common/options_switches.h b/shell/common/options_switches.h index 16b2aa079dc9d..704e5b4b58e41 100644 --- a/shell/common/options_switches.h +++ b/shell/common/options_switches.h @@ -58,6 +58,8 @@ extern const char kVisualEffectState[]; extern const char kTrafficLightPosition[]; extern const char kRoundedCorners[]; extern const char ktitleBarOverlay[]; +extern const char kOverlayButtonColor[]; +extern const char kOverlaySymbolColor[]; // WebPreferences. extern const char kZoomFactor[]; diff --git a/spec-main/api-browser-window-spec.ts b/spec-main/api-browser-window-spec.ts index b7b2264385425..009a36aa5ae33 100644 --- a/spec-main/api-browser-window-spec.ts +++ b/spec-main/api-browser-window-spec.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as qs from 'querystring'; import * as http from 'http'; +import * as semver from 'semver'; import { AddressInfo } from 'net'; import { app, BrowserWindow, BrowserView, dialog, ipcMain, OnBeforeSendHeadersListenerDetails, protocol, screen, webContents, session, WebContents, BrowserWindowConstructorOptions } from 'electron/main'; @@ -1864,7 +1865,7 @@ describe('BrowserWindow module', () => { }); }); - ifdescribe(process.platform === 'darwin' && parseInt(os.release().split('.')[0]) >= 14)('"titleBarStyle" option', () => { + ifdescribe(process.platform === 'win32' || (process.platform === 'darwin' && semver.gte(os.release(), '14.0.0')))('"titleBarStyle" option', () => { const testWindowsOverlay = async (style: any) => { const w = new BrowserWindow({ show: false, @@ -1878,12 +1879,22 @@ describe('BrowserWindow module', () => { titleBarOverlay: true }); const overlayHTML = path.join(__dirname, 'fixtures', 'pages', 'overlay.html'); - await w.loadFile(overlayHTML); + 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 overlayRect = await w.webContents.executeJavaScript('getJSOverlayProperties()'); expect(overlayRect.y).to.equal(0); - expect(overlayRect.x).to.be.greaterThan(0); + if (process.platform === 'darwin') { + expect(overlayRect.x).to.be.greaterThan(0); + } else { + expect(overlayRect.x).to.equal(0); + } expect(overlayRect.width).to.be.greaterThan(0); expect(overlayRect.height).to.be.greaterThan(0); const cssOverlayRect = await w.webContents.executeJavaScript('getCssOverlayProperties();'); @@ -1905,7 +1916,7 @@ describe('BrowserWindow module', () => { const contentSize = w.getContentSize(); expect(contentSize).to.deep.equal([400, 400]); }); - it('creates browser window with hidden inset title bar', () => { + ifit(process.platform === 'darwin')('creates browser window with hidden inset title bar', () => { const w = new BrowserWindow({ show: false, width: 400, @@ -1918,7 +1929,7 @@ describe('BrowserWindow module', () => { it('sets Window Control Overlay with hidden title bar', async () => { await testWindowsOverlay('hidden'); }); - it('sets Window Control Overlay with hidden inset title bar', async () => { + ifit(process.platform === 'darwin')('sets Window Control Overlay with hidden inset title bar', async () => { await testWindowsOverlay('hiddenInset'); }); });