electron/shell/browser/ui/views/opaque_frame_view.cc
trop[bot] 040e9c317e
fix: WCO buttons hidden on Linux in fullscreen (#44640)
Closes https://github.com/electron/electron/issues/44569.

Fixes an issue where the WCO buttons were hidden on Linux in  fullscreen mode
but not on Windows or macOS. The Windows behavior is the expected one, so this
commit makes the Linux behavior consistent.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2024-11-13 16:54:37 +01:00

561 lines
20 KiB
C++

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