Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update Windows caption buttons to match Win11 style #34888

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions filenames.gni
Expand Up @@ -85,6 +85,8 @@ filenames = {
"shell/browser/ui/message_box_win.cc",
"shell/browser/ui/tray_icon_win.cc",
"shell/browser/ui/views/electron_views_delegate_win.cc",
"shell/browser/ui/views/win_icon_painter.cc",
"shell/browser/ui/views/win_icon_painter.h",
"shell/browser/ui/views/win_frame_view.cc",
"shell/browser/ui/views/win_frame_view.h",
"shell/browser/ui/views/win_caption_button.cc",
Expand Down
62 changes: 20 additions & 42 deletions shell/browser/ui/views/win_caption_button.cc
Expand Up @@ -10,6 +10,7 @@

#include "base/i18n/rtl.h"
#include "base/numerics/safe_conversions.h"
#include "base/win/windows_version.h"
#include "chrome/grit/theme_resources.h"
#include "shell/browser/ui/views/win_frame_view.h"
#include "shell/common/color_util.h"
Expand All @@ -28,13 +29,23 @@ WinCaptionButton::WinCaptionButton(PressedCallback callback,
const std::u16string& accessible_name)
: views::Button(std::move(callback)),
frame_view_(frame_view),
icon_painter_(CreateIconPainter()),
button_type_(button_type) {
SetAnimateOnStateChange(true);
// Not focusable by default, only for accessibility.
SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
SetAccessibleName(accessible_name);
}

WinCaptionButton::~WinCaptionButton() = default;

std::unique_ptr<WinIconPainter> WinCaptionButton::CreateIconPainter() {
if (base::win::GetVersion() >= base::win::Version::WIN11) {
return std::make_unique<Win11IconPainter>();
}
return std::make_unique<WinIconPainter>();
}

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.
Expand All @@ -50,9 +61,10 @@ void WinCaptionButton::OnPaintBackground(gfx::Canvas* canvas) {
const SkAlpha theme_alpha = SkColorGetA(bg_color);

gfx::Rect bounds = GetContentsBounds();
bounds.Inset(gfx::Insets::TLBR(0, 0, 0, 0));
bounds.Inset(gfx::Insets::TLBR(0, GetBetweenButtonSpacing(), 0, 0));

canvas->FillRect(bounds, SkColorSetA(bg_color, theme_alpha));
if (theme_alpha > 0)
canvas->FillRect(bounds, SkColorSetA(bg_color, theme_alpha));

SkColor base_color;
SkAlpha hovered_alpha, pressed_alpha;
Expand Down Expand Up @@ -136,21 +148,6 @@ int WinCaptionButton::GetButtonDisplayOrderIndex() const {
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(gfx::InsetsF::VH(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();

Expand Down Expand Up @@ -183,48 +180,29 @@ void WinCaptionButton::PaintSymbol(gfx::Canvas* canvas) {

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);
icon_painter_->PaintMinimizeIcon(canvas, symbol_rect, flags);
return;
}

case VIEW_ID_MAXIMIZE_BUTTON:
DrawRect(canvas, symbol_rect, flags);
case VIEW_ID_MAXIMIZE_BUTTON: {
icon_painter_->PaintMaximizeIcon(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(gfx::Insets::TLBR(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);
icon_painter_->PaintRestoreIcon(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);
icon_painter_->PaintCloseIcon(canvas, symbol_rect, flags);
return;
}

Expand Down
7 changes: 7 additions & 0 deletions shell/browser/ui/views/win_caption_button.h
Expand Up @@ -7,8 +7,11 @@
#ifndef ELECTRON_SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_H_
#define ELECTRON_SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_H_

#include <memory>

#include "chrome/browser/ui/frame/window_frame_util.h"
#include "chrome/browser/ui/view_ids.h"
#include "shell/browser/ui/views/win_icon_painter.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/gfx/canvas.h"
#include "ui/views/controls/button/button.h"
Expand All @@ -23,6 +26,8 @@ class WinCaptionButton : public views::Button {
WinFrameView* frame_view,
ViewID button_type,
const std::u16string& accessible_name);
~WinCaptionButton() override;

WinCaptionButton(const WinCaptionButton&) = delete;
WinCaptionButton& operator=(const WinCaptionButton&) = delete;

Expand All @@ -35,6 +40,7 @@ class WinCaptionButton : public views::Button {
void SetSize(gfx::Size size);

private:
std::unique_ptr<WinIconPainter> CreateIconPainter();
// 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.
Expand All @@ -53,6 +59,7 @@ class WinCaptionButton : public views::Button {
void PaintSymbol(gfx::Canvas* canvas);

WinFrameView* frame_view_;
std::unique_ptr<WinIconPainter> icon_painter_;
ViewID button_type_;

int base_width_ = WindowFrameUtil::kWindows10GlassCaptionButtonWidth;
Expand Down
143 changes: 143 additions & 0 deletions shell/browser/ui/views/win_icon_painter.cc
@@ -0,0 +1,143 @@
// Copyright (c) 2022 Microsoft, Inc. 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_icon_painter.h"

#include "base/numerics/safe_conversions.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/scoped_canvas.h"

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);
canvas->DrawRect(rect_f, flags);
}

void DrawRoundRect(gfx::Canvas* canvas,
const gfx::Rect& rect,
int radius,
const cc::PaintFlags& flags) {
gfx::RectF rect_f(rect);
float stroke_half_width = flags.getStrokeWidth() / 2;
rect_f.Inset(stroke_half_width);
canvas->DrawRoundRect(rect_f, radius, flags);
}

// Draws a path containing the top and right edges of a rounded rectangle.
void DrawRoundRectEdges(gfx::Canvas* canvas,
const gfx::Rect& rect,
float radius,
const cc::PaintFlags& flags) {
gfx::RectF symbol_rect_f(rect);
float stroke_half_width = flags.getStrokeWidth() / 2;
symbol_rect_f.Inset(stroke_half_width);
SkPath path;
path.moveTo(symbol_rect_f.x(), symbol_rect_f.y());
path.arcTo(symbol_rect_f.right(), symbol_rect_f.y(), symbol_rect_f.right(),
symbol_rect_f.y() + radius, radius);
path.lineTo(symbol_rect_f.right(), symbol_rect_f.bottom());
canvas->DrawPath(path, flags);
}

} // namespace

namespace electron {

WinIconPainter::WinIconPainter() = default;
WinIconPainter::~WinIconPainter() = default;

void WinIconPainter::PaintMinimizeIcon(gfx::Canvas* canvas,
const gfx::Rect& symbol_rect,
const cc::PaintFlags& flags) {
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);
}

void WinIconPainter::PaintMaximizeIcon(gfx::Canvas* canvas,
const gfx::Rect& symbol_rect,
const cc::PaintFlags& flags) {
DrawRect(canvas, symbol_rect, flags);
}

void WinIconPainter::PaintRestoreIcon(gfx::Canvas* canvas,
const gfx::Rect& symbol_rect,
const cc::PaintFlags& flags) {
const int separation = std::floor(2 * canvas->image_scale());
gfx::Rect icon_rect = symbol_rect;
icon_rect.Inset(gfx::Insets::TLBR(separation, 0, 0, separation));

// Bottom left ("in front") square.
DrawRect(canvas, icon_rect, flags);

// Top right ("behind") square.
canvas->ClipRect(icon_rect, SkClipOp::kDifference);
icon_rect.Offset(separation, -separation);
DrawRect(canvas, icon_rect, flags);
}

void WinIconPainter::PaintCloseIcon(gfx::Canvas* canvas,
const gfx::Rect& symbol_rect,
const cc::PaintFlags& flags) {
// TODO(bsep): This sometimes draws misaligned at fractional device scales
// because the button's origin isn't necessarily aligned to pixels.
cc::PaintFlags paint_flags = flags;
paint_flags.setAntiAlias(true);

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);
}

Win11IconPainter::Win11IconPainter() = default;
Win11IconPainter::~Win11IconPainter() = default;

void Win11IconPainter::PaintMaximizeIcon(gfx::Canvas* canvas,
const gfx::Rect& symbol_rect,
const cc::PaintFlags& flags) {
cc::PaintFlags paint_flags = flags;
paint_flags.setAntiAlias(true);

const float corner_radius = 2 * canvas->image_scale();
DrawRoundRect(canvas, symbol_rect, corner_radius, flags);
}

void Win11IconPainter::PaintRestoreIcon(gfx::Canvas* canvas,
const gfx::Rect& symbol_rect,
const cc::PaintFlags& flags) {
const int separation = std::floor(2 * canvas->image_scale());
gfx::Rect icon_rect = symbol_rect;
icon_rect.Inset(gfx::Insets::TLBR(separation, 0, 0, separation));

cc::PaintFlags paint_flags = flags;
paint_flags.setAntiAlias(true);

// Bottom left ("in front") rounded square.
const float bottom_rect_radius = 1 * canvas->image_scale();
DrawRoundRect(canvas, icon_rect, bottom_rect_radius, flags);

// Top right ("behind") top+right edges of rounded square (2.5x).
icon_rect.Offset(separation, -separation);
// Apply inset to left+bottom edges since we don't draw arcs for those edges
constexpr int top_rect_inset = 1;
icon_rect.Inset(gfx::Insets::TLBR(0, top_rect_inset, top_rect_inset, 0));

const float top_rect_radius = 2.5f * canvas->image_scale();
DrawRoundRectEdges(canvas, icon_rect, top_rect_radius, flags);
}
} // namespace electron
64 changes: 64 additions & 0 deletions shell/browser/ui/views/win_icon_painter.h
@@ -0,0 +1,64 @@
// Copyright (c) 2022 Microsoft, Inc. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef ELECTRON_SHELL_BROWSER_UI_VIEWS_WIN_ICON_PAINTER_H_
#define ELECTRON_SHELL_BROWSER_UI_VIEWS_WIN_ICON_PAINTER_H_

#include "base/memory/raw_ptr.h"
#include "ui/gfx/canvas.h"

namespace electron {

class WinIconPainter {
public:
WinIconPainter();
virtual ~WinIconPainter();

WinIconPainter(const WinIconPainter&) = delete;
WinIconPainter& operator=(const WinIconPainter&) = delete;

public:
// Paints the minimize icon for the button
virtual void PaintMinimizeIcon(gfx::Canvas* canvas,
const gfx::Rect& symbol_rect,
const cc::PaintFlags& flags);

// Paints the maximize icon for the button
virtual void PaintMaximizeIcon(gfx::Canvas* canvas,
const gfx::Rect& symbol_rect,
const cc::PaintFlags& flags);

// Paints the restore icon for the button
virtual void PaintRestoreIcon(gfx::Canvas* canvas,
const gfx::Rect& symbol_rect,
const cc::PaintFlags& flags);

// Paints the close icon for the button
virtual void PaintCloseIcon(gfx::Canvas* canvas,
const gfx::Rect& symbol_rect,
const cc::PaintFlags& flags);
};

class Win11IconPainter : public WinIconPainter {
public:
Win11IconPainter();
~Win11IconPainter() override;

Win11IconPainter(const Win11IconPainter&) = delete;
Win11IconPainter& operator=(const Win11IconPainter&) = delete;

public:
// Paints the maximize icon for the button
void PaintMaximizeIcon(gfx::Canvas* canvas,
const gfx::Rect& symbol_rect,
const cc::PaintFlags& flags) override;

// Paints the restore icon for the button
void PaintRestoreIcon(gfx::Canvas* canvas,
const gfx::Rect& symbol_rect,
const cc::PaintFlags& flags) override;
};
} // namespace electron

#endif // ELECTRON_SHELL_BROWSER_UI_VIEWS_WIN_ICON_PAINTER_H_