blob: 508cb2ee57203c012421ea7d4663f8bfc46586f3 [file] [log] [blame]
// Copyright 2013 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 "chrome/browser/ui/views/frame/opaque_browser_frame_view_layout.h"
#include <algorithm>
#include <string>
#include <vector>
#include "base/command_line.h"
#include "base/containers/adapters.h"
#include "base/numerics/ranges.h"
#include "base/stl_util.h"
#include "build/build_config.h"
#include "chrome/browser/ui/views/frame/hosted_app_button_container.h"
#include "chrome/common/chrome_switches.h"
#include "ui/gfx/font.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/window/caption_button_layout_constants.h"
#include "ui/views/window/frame_caption_button.h"
namespace {
constexpr int kCaptionButtonHeight = 18;
} // namespace
///////////////////////////////////////////////////////////////////////////////
// OpaqueBrowserFrameViewLayout, public:
// statics
// The content edge images have a shadow built into them.
const int OpaqueBrowserFrameViewLayout::kContentEdgeShadowThickness = 2;
// The frame border is only visible in restored mode and is hardcoded to 4 px on
// each side regardless of the system window border size.
const int OpaqueBrowserFrameViewLayout::kFrameBorderThickness = 4;
// The frame has a 2 px 3D edge along the top.
const int OpaqueBrowserFrameViewLayout::kTopFrameEdgeThickness = 2;
// The frame has a 1 px 3D edge along the top.
const int OpaqueBrowserFrameViewLayout::kSideFrameEdgeThickness = 1;
// The icon is inset 1 px from the left frame border.
const int OpaqueBrowserFrameViewLayout::kIconLeftSpacing = 1;
// There is a 4 px gap between the icon and the title text.
const int OpaqueBrowserFrameViewLayout::kIconTitleSpacing = 4;
// The horizontal spacing to use in most cases when laying out things near the
// caption button area.
const int OpaqueBrowserFrameViewLayout::kCaptionSpacing = 5;
// The minimum vertical padding between the bottom of the caption buttons and
// the top of the content shadow.
const int OpaqueBrowserFrameViewLayout::kCaptionButtonBottomPadding = 3;
OpaqueBrowserFrameViewLayout::OpaqueBrowserFrameViewLayout()
: available_space_leading_x_(0),
available_space_trailing_x_(0),
minimum_size_for_buttons_(0),
placed_leading_button_(false),
placed_trailing_button_(false),
forced_window_caption_spacing_(-1),
minimize_button_(nullptr),
maximize_button_(nullptr),
restore_button_(nullptr),
close_button_(nullptr),
window_icon_(nullptr),
window_title_(nullptr),
trailing_buttons_{views::FRAME_BUTTON_MINIMIZE,
views::FRAME_BUTTON_MAXIMIZE,
views::FRAME_BUTTON_CLOSE} {}
OpaqueBrowserFrameViewLayout::~OpaqueBrowserFrameViewLayout() {}
void OpaqueBrowserFrameViewLayout::SetButtonOrdering(
const std::vector<views::FrameButton>& leading_buttons,
const std::vector<views::FrameButton>& trailing_buttons) {
leading_buttons_ = leading_buttons;
trailing_buttons_ = trailing_buttons;
}
gfx::Rect OpaqueBrowserFrameViewLayout::GetBoundsForTabStrip(
const gfx::Size& tabstrip_preferred_size,
int total_width) const {
const int x = available_space_leading_x_;
const int available_width = available_space_trailing_x_ - x;
return gfx::Rect(x, GetTabStripInsetsTop(false), std::max(0, available_width),
tabstrip_preferred_size.height());
}
gfx::Size OpaqueBrowserFrameViewLayout::GetMinimumSize(
const views::View* host) const {
// Ensure that we can fit the main browser view.
gfx::Size min_size = delegate_->GetBrowserViewMinimumSize();
// Ensure that we can, at minimum, hold our window controls and a tab strip.
int top_width = minimum_size_for_buttons_;
if (delegate_->IsTabStripVisible())
top_width += delegate_->GetTabstripPreferredSize().width();
min_size.set_width(std::max(min_size.width(), top_width));
// Account for the frame.
const int border_thickness = FrameBorderThickness(false);
min_size.Enlarge(2 * border_thickness,
NonClientTopHeight(false) + border_thickness);
return min_size;
}
gfx::Rect OpaqueBrowserFrameViewLayout::GetWindowBoundsForClientBounds(
const gfx::Rect& client_bounds) const {
int top_height = NonClientTopHeight(false);
int border_thickness = FrameBorderThickness(false);
return gfx::Rect(std::max(0, client_bounds.x() - border_thickness),
std::max(0, client_bounds.y() - top_height),
client_bounds.width() + (2 * border_thickness),
client_bounds.height() + top_height + border_thickness);
}
int OpaqueBrowserFrameViewLayout::FrameBorderThickness(bool restored) const {
return !restored && delegate_->IsFrameCondensed() ? 0 : kFrameBorderThickness;
}
int OpaqueBrowserFrameViewLayout::FrameTopBorderThickness(bool restored) const {
int thickness = FrameBorderThickness(restored);
if (restored || !delegate_->IsFrameCondensed())
thickness += kNonClientExtraTopThickness;
return thickness;
}
int OpaqueBrowserFrameViewLayout::NonClientTopHeight(bool restored) const {
if (!delegate_->ShouldShowWindowTitle())
return FrameTopBorderThickness(restored);
// 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 =
FrameTopThickness(restored) + delegate_->GetIconSize() + kVerticalPadding;
const int caption_button_height = DefaultCaptionButtonY(restored) +
kCaptionButtonHeight +
kCaptionButtonBottomPadding;
int hosted_app_button_height = 0;
if (hosted_app_button_container_) {
hosted_app_button_height =
hosted_app_button_container_->GetPreferredSize().height() +
kVerticalPadding;
}
return std::max(std::max(icon_height, caption_button_height),
hosted_app_button_height) +
kContentEdgeShadowThickness;
}
int OpaqueBrowserFrameViewLayout::GetTabStripInsetsTop(bool restored) const {
const int top = NonClientTopHeight(restored);
return !restored && delegate_->IsFrameCondensed()
? top
: (top + GetNonClientRestoredExtraThickness());
}
int OpaqueBrowserFrameViewLayout::FrameTopThickness(bool restored) const {
return IsFrameEdgeVisible(restored) ? kTopFrameEdgeThickness : 0;
}
int OpaqueBrowserFrameViewLayout::FrameSideThickness(bool restored) const {
return IsFrameEdgeVisible(restored) ? kSideFrameEdgeThickness : 0;
}
int OpaqueBrowserFrameViewLayout::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).
return !restored && delegate_->IsFrameCondensed()
? FrameBorderThickness(false)
: views::NonClientFrameView::kFrameShadowThickness;
}
int OpaqueBrowserFrameViewLayout::CaptionButtonY(
chrome::FrameButtonDisplayType button_id,
bool restored) const {
return DefaultCaptionButtonY(restored);
}
gfx::Rect OpaqueBrowserFrameViewLayout::IconBounds() const {
return window_icon_bounds_;
}
gfx::Rect OpaqueBrowserFrameViewLayout::CalculateClientAreaBounds(
int width,
int height) const {
int top_height = NonClientTopHeight(false);
int border_thickness = FrameBorderThickness(false);
return gfx::Rect(border_thickness, top_height,
std::max(0, width - (2 * border_thickness)),
std::max(0, height - top_height - border_thickness));
}
chrome::FrameButtonDisplayType
OpaqueBrowserFrameViewLayout::GetButtonDisplayType(
views::FrameButton button_id) const {
switch (button_id) {
case views::FRAME_BUTTON_MINIMIZE:
return chrome::FrameButtonDisplayType::kMinimize;
case views::FRAME_BUTTON_MAXIMIZE:
return delegate_->IsMaximized()
? chrome::FrameButtonDisplayType::kRestore
: chrome::FrameButtonDisplayType::kMaximize;
case views::FRAME_BUTTON_CLOSE:
return chrome::FrameButtonDisplayType::kClose;
default:
NOTREACHED();
return chrome::FrameButtonDisplayType::kClose;
}
}
int OpaqueBrowserFrameViewLayout::GetWindowCaptionSpacing(
views::FrameButton button_id,
bool leading_spacing,
bool is_leading_button) const {
if (leading_spacing) {
if (is_leading_button) {
// If we're the first button and maximized, add width to the right
// hand side of the screen.
return delegate_->IsFrameCondensed() && is_leading_button
? kFrameBorderThickness -
views::NonClientFrameView::kFrameShadowThickness
: 0;
}
if (forced_window_caption_spacing_ >= 0)
return forced_window_caption_spacing_;
}
return 0;
}
int OpaqueBrowserFrameViewLayout::GetNonClientRestoredExtraThickness() const {
// Besides the frame border, there's empty space atop the window in restored
// mode, to use to drag the window around.
constexpr int kNonClientRestoredExtraThickness = 4;
int thickness = kNonClientRestoredExtraThickness;
if (delegate_->EverHasVisibleBackgroundTabShapes()) {
thickness =
std::max(thickness, BrowserNonClientFrameView::kMinimumDragHeight);
}
return thickness;
}
///////////////////////////////////////////////////////////////////////////////
// OpaqueBrowserFrameViewLayout, protected:
OpaqueBrowserFrameViewLayout::TopAreaPadding
OpaqueBrowserFrameViewLayout::GetTopAreaPadding(
bool has_leading_buttons,
bool has_trailing_buttons) const {
const int padding = FrameBorderThickness(false);
return {padding, padding};
}
bool OpaqueBrowserFrameViewLayout::IsFrameEdgeVisible(bool restored) const {
return delegate_->UseCustomFrame() &&
(restored || !delegate_->IsFrameCondensed());
}
///////////////////////////////////////////////////////////////////////////////
// OpaqueBrowserFrameViewLayout, private:
void OpaqueBrowserFrameViewLayout::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::FRAME_BUTTON_MAXIMIZE);
buttons_not_shown.push_back(views::FRAME_BUTTON_MINIMIZE);
buttons_not_shown.push_back(views::FRAME_BUTTON_CLOSE);
if (delegate_->ShouldShowCaptionButtons()) {
for (const auto& button : leading_buttons_) {
ConfigureButton(button, ALIGN_LEADING);
base::Erase(buttons_not_shown, button);
}
for (const auto& button : base::Reversed(trailing_buttons_)) {
ConfigureButton(button, ALIGN_TRAILING);
base::Erase(buttons_not_shown, button);
}
}
for (const auto& button : buttons_not_shown)
HideButton(button);
}
void OpaqueBrowserFrameViewLayout::LayoutTitleBar() {
bool use_hidden_icon_location = true;
int size = delegate_->GetIconSize();
bool should_show_icon = delegate_->ShouldShowWindowIcon() && window_icon_;
bool should_show_title = delegate_->ShouldShowWindowTitle() && window_title_;
if (should_show_icon || should_show_title || hosted_app_button_container_) {
use_hidden_icon_location = false;
// Our frame border has a different "3D look" than Windows'. Theirs has
// a more complex gradient on the top that they push their icon/title
// below; then the maximized window cuts this off and the icon/title are
// centered in the remaining space. Because the apparent shape of our
// border is simpler, using the same positioning makes things look
// slightly uncentered with restored windows, so when the window is
// restored, instead of calculating the remaining space from below the
// frame border, we calculate from below the 3D edge.
const int unavailable_px_at_top = FrameTopThickness(false);
// When the icon is shorter than the minimum space we reserve for the
// caption button, we vertically center it. We want to bias rounding to
// put extra space below the icon, since we'll use the same Y coordinate for
// the title, and the majority of the font weight is below the centerline.
const int available_height = NonClientTopHeight(false);
const int icon_height =
unavailable_px_at_top + size + kContentEdgeShadowThickness;
const int y = unavailable_px_at_top + (available_height - icon_height) / 2;
window_icon_bounds_ =
gfx::Rect(available_space_leading_x_ + kIconLeftSpacing, y, size, size);
available_space_leading_x_ += size + kIconLeftSpacing;
minimum_size_for_buttons_ += size + kIconLeftSpacing;
if (hosted_app_button_container_) {
available_space_trailing_x_ =
hosted_app_button_container_->LayoutInContainer(
available_space_leading_x_, available_space_trailing_x_, 0,
available_height);
}
}
if (should_show_icon)
window_icon_->SetBoundsRect(window_icon_bounds_);
if (window_title_) {
window_title_->SetVisible(should_show_title);
if (should_show_title) {
window_title_->SetText(delegate_->GetWindowTitle());
int text_width =
std::max(0, available_space_trailing_x_ - kCaptionSpacing -
available_space_leading_x_ - kIconTitleSpacing);
window_title_->SetBounds(available_space_leading_x_ + kIconTitleSpacing,
window_icon_bounds_.y(), text_width,
window_icon_bounds_.height());
available_space_leading_x_ += text_width + kIconTitleSpacing;
}
}
if (use_hidden_icon_location) {
if (placed_leading_button_) {
// There are window button icons on the left. Don't size the hidden window
// icon that people can double click on to close the window.
window_icon_bounds_ = gfx::Rect();
} else {
// We set the icon bounds to a small rectangle in the top leading corner
// if there are no icons on the leading side.
const int frame_thickness = FrameBorderThickness(false);
window_icon_bounds_ = gfx::Rect(
frame_thickness + kIconLeftSpacing, frame_thickness, size, size);
}
}
}
void OpaqueBrowserFrameViewLayout::ConfigureButton(views::FrameButton button_id,
ButtonAlignment alignment) {
switch (button_id) {
case views::FRAME_BUTTON_MINIMIZE: {
minimize_button_->SetVisible(true);
SetBoundsForButton(button_id, minimize_button_, alignment);
break;
}
case views::FRAME_BUTTON_MAXIMIZE: {
// When the window is restored, we show a maximized button; otherwise, we
// show a restore button.
bool is_restored = !delegate_->IsMaximized() && !delegate_->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);
break;
}
case views::FRAME_BUTTON_CLOSE: {
close_button_->SetVisible(true);
SetBoundsForButton(button_id, close_button_, alignment);
break;
}
}
}
void OpaqueBrowserFrameViewLayout::HideButton(views::FrameButton button_id) {
switch (button_id) {
case views::FRAME_BUTTON_MINIMIZE:
minimize_button_->SetVisible(false);
break;
case views::FRAME_BUTTON_MAXIMIZE:
restore_button_->SetVisible(false);
maximize_button_->SetVisible(false);
break;
case views::FRAME_BUTTON_CLOSE:
close_button_->SetVisible(false);
break;
}
}
void OpaqueBrowserFrameViewLayout::SetBoundsForButton(
views::FrameButton button_id,
views::Button* button,
ButtonAlignment alignment) {
const int caption_y = CaptionButtonY(GetButtonDisplayType(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 = delegate_->IsFrameCondensed();
gfx::Size button_size = button->GetPreferredSize();
if (delegate_->GetFrameButtonStyle() ==
OpaqueBrowserFrameViewLayoutDelegate::FrameButtonStyle::kMdButton) {
DCHECK_EQ(std::string(views::FrameCaptionButton::kViewClassName),
button->GetClassName());
constexpr int kCaptionButtonCenterSize =
views::kCaptionButtonWidth -
2 * views::kCaptionButtonInkDropDefaultCornerRadius;
const int height = delegate_->GetTopAreaHeight();
const int corner_radius =
base::ClampToRange((height - kCaptionButtonCenterSize) / 2, 0,
views::kCaptionButtonInkDropDefaultCornerRadius);
button_size = gfx::Size(views::kCaptionButtonWidth, height);
button->SetPreferredSize(button_size);
static_cast<views::FrameCaptionButton*>(button)->set_ink_drop_corner_radius(
corner_radius);
} else if (delegate_->GetFrameButtonStyle() ==
OpaqueBrowserFrameViewLayoutDelegate::FrameButtonStyle::
kImageButton) {
DCHECK_EQ(std::string(views::ImageButton::kViewClassName),
button->GetClassName());
static_cast<views::ImageButton*>(button)->SetImageAlignment(
(alignment == ALIGN_LEADING) ? views::ImageButton::ALIGN_RIGHT
: views::ImageButton::ALIGN_LEFT,
views::ImageButton::ALIGN_BOTTOM);
}
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;
}
}
}
void OpaqueBrowserFrameViewLayout::SetView(int id, views::View* view) {
// Why do things this way instead of having an Init() method, where we're
// passed the views we'll handle? Because OpaqueBrowserFrameView doesn't own
// all the views which are part of it.
switch (id) {
case VIEW_ID_MINIMIZE_BUTTON:
minimize_button_ = static_cast<views::Button*>(view);
break;
case VIEW_ID_MAXIMIZE_BUTTON:
maximize_button_ = static_cast<views::Button*>(view);
break;
case VIEW_ID_RESTORE_BUTTON:
restore_button_ = static_cast<views::Button*>(view);
break;
case VIEW_ID_CLOSE_BUTTON:
close_button_ = static_cast<views::Button*>(view);
break;
case VIEW_ID_WINDOW_ICON:
window_icon_ = view;
break;
case VIEW_ID_WINDOW_TITLE:
if (view) {
DCHECK_EQ(std::string(views::Label::kViewClassName),
view->GetClassName());
}
window_title_ = static_cast<views::Label*>(view);
break;
case VIEW_ID_HOSTED_APP_BUTTON_CONTAINER:
DCHECK_EQ(view->GetClassName(), HostedAppButtonContainer::kViewClassName);
hosted_app_button_container_ =
static_cast<HostedAppButtonContainer*>(view);
break;
default:
NOTREACHED() << "Unknown view id " << id;
break;
}
}
OpaqueBrowserFrameViewLayout::TopAreaPadding
OpaqueBrowserFrameViewLayout::GetTopAreaPadding() const {
return GetTopAreaPadding(!leading_buttons_.empty(),
!trailing_buttons_.empty());
}
///////////////////////////////////////////////////////////////////////////////
// OpaqueBrowserFrameViewLayout, views::LayoutManager:
void OpaqueBrowserFrameViewLayout::Layout(views::View* host) {
TRACE_EVENT0("views.frame", "OpaqueBrowserFrameViewLayout::Layout");
// 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_ = host->width() - top_area_padding.trailing;
minimum_size_for_buttons_ =
available_space_leading_x_ + host->width() - available_space_trailing_x_;
placed_leading_button_ = false;
placed_trailing_button_ = false;
LayoutWindowControls();
LayoutTitleBar();
// Any buttons/icon/title were laid out based on the frame border thickness,
// but the tabstrip bounds need to be based on the non-client border thickness
// on any side where there aren't other buttons forcing a larger inset.
const int old_button_size =
available_space_leading_x_ + host->width() - available_space_trailing_x_;
const int min_button_width = FrameBorderThickness(false);
available_space_leading_x_ =
std::max(available_space_leading_x_, min_button_width);
// The trailing corner is a mirror of the leading one.
available_space_trailing_x_ =
std::min(available_space_trailing_x_, host->width() - min_button_width);
minimum_size_for_buttons_ += (available_space_leading_x_ + host->width() -
available_space_trailing_x_ - old_button_size);
client_view_bounds_ = CalculateClientAreaBounds(
host->width(), host->height());
}
gfx::Size OpaqueBrowserFrameViewLayout::GetPreferredSize(
const views::View* host) const {
// This is never used; NonClientView::CalculatePreferredSize() will be called
// instead.
NOTREACHED();
return gfx::Size();
}
void OpaqueBrowserFrameViewLayout::ViewAdded(views::View* host,
views::View* view) {
SetView(view->id(), view);
}
void OpaqueBrowserFrameViewLayout::ViewRemoved(views::View* host,
views::View* view) {
SetView(view->id(), nullptr);
}