blob: 375eea327018f5bc09358e948262b3fb65a22acc [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// 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/side_panel/side_panel.h"
#include <memory>
#include "base/functional/bind.h"
#include "base/i18n/number_formatting.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/top_container_background.h"
#include "chrome/browser/ui/views/side_panel/side_panel_entry.h"
#include "chrome/browser/ui/views/side_panel/side_panel_enums.h"
#include "chrome/browser/ui/views/side_panel/side_panel_resize_area.h"
#include "chrome/browser/ui/views/side_panel/side_panel_ui.h"
#include "chrome/browser/ui/views/side_panel/side_panel_util.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/lens/lens_features.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/ui_base_features.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/dip_util.h"
#include "ui/gfx/geometry/insets_conversions.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/border.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_observer.h"
namespace {
// This thickness includes the solid-color background and the inner round-rect
// border-color stroke. It does not include the outer-color separator.
int GetBorderThickness() {
return 8 + views::Separator::kThickness;
}
// This is how many units of the toolbar are essentially expected to be
// background.
constexpr int kOverlapFromToolbar = 4;
// We want the border to visually look like GetBorderThickness() units on all
// sides except the top. On the top side, background is drawn on top of the
// top-content separator and some units of background inside the toolbar (or
// bookmarks bar) itself. Subtract both of those to not get visually-excessive
// padding.
gfx::Insets GetBorderInsets() {
int border_thickness = GetBorderThickness();
return gfx::Insets::TLBR(-kOverlapFromToolbar, border_thickness,
border_thickness, border_thickness);
}
constexpr base::TimeDelta kContentsHeightSidePanelAnimationDuration =
base::Milliseconds(450);
constexpr base::TimeDelta kToolbarHeightSidePanelAnimationDuration =
base::Milliseconds(350);
// This border paints the toolbar color around the side panel content and draws
// a roundrect viewport around the side panel content. The border can have
// rounded corners of its own.
class SidePanelBorder : public views::Border {
public:
explicit SidePanelBorder(BrowserView* browser_view)
: browser_view_(browser_view) {
SetColor(kColorSidePanelContentAreaSeparator);
}
SidePanelBorder(const SidePanelBorder&) = delete;
SidePanelBorder& operator=(const SidePanelBorder&) = delete;
void SetHeaderHeight(int height) { header_height_ = height; }
void SetBorderRadii(const gfx::RoundedCornersF& radii) {
border_radii_ = radii;
}
void SetOutlineVisibility(bool visible) { outline_visible_ = visible; }
// views::Border:
void Paint(const views::View& view, gfx::Canvas* canvas) override {
// Undo DSF so that we can be sure to draw an integral number of pixels for
// the border. Integral scale factors should be unaffected by this, but for
// fractional scale factors this ensures sharp lines.
gfx::ScopedCanvas scoped_unscale(canvas);
float dsf = canvas->UndoDeviceScaleFactor();
const gfx::RectF scaled_view_bounds_f = gfx::ConvertRectToPixels(
view.GetLocalBounds(), view.layer()->device_scale_factor());
gfx::RectF scaled_contents_bounds_f = scaled_view_bounds_f;
const float corner_radius =
dsf * view.GetLayoutProvider()->GetCornerRadiusMetric(
views::ShapeContextTokens::kSidePanelContentRadius);
const gfx::InsetsF insets_in_pixels(
gfx::ConvertInsetsToPixels(GetInsets(), dsf));
scaled_contents_bounds_f.Inset(insets_in_pixels);
// Use ToEnclosedRect to make sure that the clip bounds never end up larger
// than the child view.
gfx::Rect clip_bounds = ToEnclosedRect(scaled_contents_bounds_f);
SkRRect rect = SkRRect::MakeRectXY(gfx::RectToSkRect(clip_bounds),
corner_radius, corner_radius);
// Clip out the content area from the background about to be painted.
canvas->sk_canvas()->clipRRect(rect, SkClipOp::kDifference,
/*do_anti_alias=*/true);
{
// Redo the device scale factor. The theme background and clip for the
// outer corners are drawn in DIPs. Note that the clip area above is in
// pixels because `UndoDeviceScaleFactor()` was called before this.
gfx::ScopedCanvas scoped_rescale(canvas);
canvas->Scale(dsf, dsf);
const SkVector border_radii[4] = {
{border_radii_.upper_left(), border_radii_.upper_left()},
{border_radii_.upper_right(), border_radii_.upper_right()},
{border_radii_.lower_right(), border_radii_.lower_right()},
{border_radii_.lower_left(), border_radii_.lower_left()}};
const SkPath rounded_border_path = SkPath::RRect(SkRRect::MakeRectRadii(
gfx::RectToSkRect(view.GetLocalBounds()), border_radii));
// Add another clip to the canvas that rounds the outer corners of the
// border. This is done in DIPs because for some device scale factors, the
// conversion to pixels can cause the clip to be off by a pixel, resulting
// in a pixel gap between the side panel border and web contents.
canvas->ClipPath(rounded_border_path, /*do_anti_alias=*/true);
// Draw the top-container background.
TopContainerBackground::PaintBackground(canvas, &view, browser_view_);
}
// TODO(b/453702066): Avoid drawing a zero width rectangle.
// Paint the inner border around SidePanel content. Since half the stroke
// gets painted in the clipped area, make this twice as thick, and scale
// the thickness by device scale factor since we're working in pixels.
const float stroke_thickness =
outline_visible_ ? views::Separator::kThickness * 2 * dsf : 0;
cc::PaintFlags flags;
flags.setStrokeWidth(stroke_thickness);
flags.setColor(color().ResolveToSkColor(view.GetColorProvider()));
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setAntiAlias(true);
canvas->sk_canvas()->drawRRect(rect, flags);
}
gfx::Insets GetInsets() const override {
// This additional inset matches the growth inside BorderView::Layout()
// below to let us paint on top of the toolbar separator. This additional
// inset is outside the SidePanel itself, but not outside the BorderView. If
// there is a header we want to increase the top inset to give room for the
// header to paint on top of the border area.
int top_inset =
views::Separator::kThickness + header_height_ - GetBorderInsets().top();
return GetBorderInsets() + gfx::Insets::TLBR(top_inset, 0, 0, 0);
}
gfx::Size GetMinimumSize() const override {
return gfx::Size(GetInsets().width(), GetInsets().height());
}
private:
int header_height_ = 0;
gfx::RoundedCornersF border_radii_;
bool outline_visible_ = true;
const raw_ptr<BrowserView> browser_view_;
};
// ContentParentView is the parent view for views hosted in the
// side panel.
class ContentParentView : public views::View {
METADATA_HEADER(ContentParentView, views::View)
public:
explicit ContentParentView(bool should_round_corners)
: should_round_corners_(should_round_corners) {
SetUseDefaultFillLayout(true);
SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero,
views::MaximumFlexSizeRule::kUnbounded));
}
~ContentParentView() override = default;
private:
void AddedToWidget() override {
SetBackground(views::CreateRoundedRectBackground(kColorSidePanelBackground,
GetRoundedCorners()));
}
void ViewHierarchyChanged(
const views::ViewHierarchyChangedDetails& details) override {
// If a child view is added and we should round corners.
if (should_round_corners_ && details.is_add && details.parent == this) {
views::View* child = details.child;
// If the child is a WebView or paints to a layer, round its corners.
if (views::IsViewClass<views::WebView>(child)) {
views::AsViewClass<views::WebView>(child)->holder()->SetCornerRadii(
GetRoundedCorners());
}
if (child->layer()) {
child->layer()->SetRoundedCornerRadius(GetRoundedCorners());
child->layer()->SetIsFastRoundedCorner(true);
}
}
}
gfx::RoundedCornersF GetRoundedCorners() {
return should_round_corners_ && GetLayoutProvider()
? gfx::RoundedCornersF(
GetLayoutProvider()->GetCornerRadiusMetric(
views::ShapeContextTokens::kSidePanelContentRadius))
: gfx::RoundedCornersF();
}
bool should_round_corners_ = false;
};
BEGIN_METADATA(ContentParentView)
END_METADATA
} // namespace
class SidePanel::BorderView : public views::View {
METADATA_HEADER(BorderView, views::View)
public:
explicit BorderView(BrowserView* browser_view) {
SetVisible(false);
auto border = std::make_unique<SidePanelBorder>(browser_view);
border_ = border.get();
SetBorder(std::move(border));
// Don't allow the view to process events. If we do allow this then events
// won't get passed on to the side panel hosted content.
SetCanProcessEventsWithinSubtree(false);
}
void HeaderViewChanged(views::View* header_view) {
border_->SetHeaderHeight(
header_view ? header_view->GetPreferredSize().height() : 0);
}
void SetOutlineVisibilty(bool visible) {
border_->SetOutlineVisibility(visible);
SchedulePaint();
}
void SetBorderRadii(const gfx::RoundedCornersF& radii) {
border_->SetBorderRadii(radii);
SchedulePaint();
}
void Layout(PassKey) override {
// Let BorderView grow slightly taller so that it overlaps the divider into
// the toolbar or bookmarks bar above it.
gfx::Rect bounds = parent()->GetLocalBounds();
bounds.Inset(gfx::Insets::TLBR(-views::Separator::kThickness, 0, 0, 0));
SetBoundsRect(bounds);
}
void OnThemeChanged() override {
SchedulePaint();
View::OnThemeChanged();
}
private:
raw_ptr<SidePanelBorder> border_;
};
BEGIN_METADATA(SidePanel, BorderView)
END_METADATA
// Ensures immediate children of the SidePanel have their layers clipped to
// their visible bounds to prevent incorrect clipping during animation.
// TODO: 344626785 - Remove this once WebView layer behavior has been fixed.
class SidePanel::VisibleBoundsViewClipper : public views::ViewObserver {
public:
explicit VisibleBoundsViewClipper(SidePanel* side_panel)
: side_panel_(side_panel) {
view_observations_.AddObservation(side_panel);
}
VisibleBoundsViewClipper(const VisibleBoundsViewClipper&) = delete;
VisibleBoundsViewClipper& operator=(const VisibleBoundsViewClipper&) = delete;
~VisibleBoundsViewClipper() override = default;
// views::ViewObserver:
void OnChildViewAdded(View* observed_view, View* child) override {
if (observed_view == side_panel_) {
view_observations_.AddObservation(child);
}
}
void OnViewBoundsChanged(views::View* observed_view) override {
ui::Layer* layer = observed_view->layer();
if (observed_view != side_panel_ && layer) {
gfx::Rect clip_bounds = observed_view->GetVisibleBounds();
// Let side panel grow slightly taller so that it overlaps the divider
// into the toolbar or bookmarks bar above it.
// TODO: Explore extending the side panel bounds directly in
// BrowserViewLayout.
clip_bounds.Inset(
gfx::Insets::TLBR(-views::Separator::kThickness, 0, 0, 0));
// Only clip the bounds while animating. This makes sure we don't
// incorrectly clip things like focus rings for header buttons or the
// resize handle.
layer->SetClipRect(side_panel_->GetAnimationValue() < 1 ? clip_bounds
: gfx::Rect());
layer->SetVisible(clip_bounds.width() != 0);
}
}
void OnViewIsDeleting(views::View* observed_view) override {
view_observations_.RemoveObservation(observed_view);
}
private:
// Owns this.
const raw_ptr<SidePanel> side_panel_;
base::ScopedMultiSourceObservation<views::View, views::ViewObserver>
view_observations_{this};
};
SidePanel::SidePanel(BrowserView* browser_view,
SidePanelEntry::PanelType type,
bool has_border,
HorizontalAlignment horizontal_alignment)
: views::AnimationDelegateViews(this),
browser_view_(browser_view),
type_(type),
visible_bounds_view_clipper_(
std::make_unique<VisibleBoundsViewClipper>(this)),
horizontal_alignment_(horizontal_alignment) {
if (has_border) {
std::unique_ptr<BorderView> border_view =
std::make_unique<BorderView>(browser_view);
border_view_ = border_view.get();
AddChildView(std::move(border_view));
}
std::unique_ptr<views::SidePanelResizeArea> resize_area =
std::make_unique<views::SidePanelResizeArea>(this);
resize_area_ = resize_area.get();
AddChildView(std::move(resize_area));
pref_change_registrar_.Init(browser_view->GetProfile()->GetPrefs());
// base::Unretained is safe since the side panel must be attached to some
// BrowserView. Deleting BrowserView will also delete the SidePanel.
pref_change_registrar_.Add(
prefs::kSidePanelHorizontalAlignment,
base::BindRepeating(&BrowserView::UpdateSidePanelHorizontalAlignment,
base::Unretained(browser_view)));
if (type_ == SidePanelEntry::PanelType::kContent) {
animation_.SetTweenType(gfx::Tween::Type::EASE_IN_OUT_EMPHASIZED);
animation_.SetSlideDuration(kContentsHeightSidePanelAnimationDuration);
} else {
animation_.SetTweenType(gfx::Tween::Type::ACCEL_45_DECEL_88);
animation_.SetSlideDuration(kToolbarHeightSidePanelAnimationDuration);
}
SetVisible(false);
SetLayoutManager(std::make_unique<views::FillLayout>());
// Set the panel width from the preference or use the minimum size as the
// default.
SetPanelWidth(GetMinimumSize().width());
if (has_border) {
SetBorder(views::CreateEmptyBorder(GetBorderInsets()));
}
SetProperty(views::kElementIdentifierKey, kSidePanelElementId);
content_parent_view_ = AddChildView(std::make_unique<ContentParentView>(
/*should_round_corners=*/!has_border));
content_parent_view_->SetVisible(false);
}
SidePanel::~SidePanel() = default;
void SidePanel::SetPanelWidth(int width) {
// Only the width is used by BrowserViewLayout.
SetPreferredSize(gfx::Size(width, 1));
}
bool SidePanel::ShouldRestrictMaxWidth() const {
// TODO(crbug.com/394339052): Only restricting width for only non-read
// anything content is a temporary solution and UX will investigate a better
// long term solution.
SidePanelUI* side_panel_ui =
browser_view_->browser()->GetFeatures().side_panel_ui();
if (!side_panel_ui) {
return true;
}
return !side_panel_ui->IsSidePanelEntryShowing(
SidePanelEntryKey(SidePanelEntryId::kReadAnything));
}
void SidePanel::SetBackgroundRadii(const gfx::RoundedCornersF& radii) {
if (radii == background_radii_) {
return;
}
background_radii_ = radii;
if (border_view_) {
// Since the border_view paints the background, by adding rounded
// corners to border will paint a rounded background for the side panel.
border_view_->SetBorderRadii(background_radii_);
}
}
void SidePanel::UpdateWidthOnEntryChanged() {
SidePanelUI* side_panel_ui =
browser_view_->browser()->GetFeatures().side_panel_ui();
if (!side_panel_ui) {
return;
}
std::optional<SidePanelEntry::Id> current_entry =
side_panel_ui->GetCurrentEntryId(type_);
if (!current_entry) {
return;
}
PrefService* pref_service = browser_view_->browser()->profile()->GetPrefs();
const base::Value::Dict& dict =
pref_service->GetDict(prefs::kSidePanelIdToWidth);
std::string panel_id = SidePanelEntryIdToString(current_entry.value());
// Figure out for this side panel if we should:
// 1. Use the user's manually resized width
// 2. Use the side panels default width
// NOTE: If not specified, the side panel will default to
// SidePanelEntry::kSidePanelDefaultContentWidth which evaluates to the same
// value as GetMinimumSize().
if (std::optional<int> width_from_pref = dict.FindInt(panel_id)) {
SetPanelWidth(width_from_pref.value());
} else {
SetPanelWidth(side_panel_ui->GetCurrentEntryDefaultContentWidth(type_) +
GetBorderInsets().width());
}
}
void SidePanel::SetHorizontalAlignment(HorizontalAlignment alignment) {
horizontal_alignment_ = alignment;
}
SidePanel::HorizontalAlignment SidePanel::GetHorizontalAlignment() const {
return horizontal_alignment_;
}
bool SidePanel::IsRightAligned() const {
return GetHorizontalAlignment() == HorizontalAlignment::kRight;
}
gfx::Size SidePanel::GetMinimumSize() const {
const int min_height = 0;
return gfx::Size(
SidePanelEntry::kSidePanelDefaultContentWidth + GetBorderInsets().width(),
min_height);
}
bool SidePanel::IsClosing() {
return animation_.IsClosing();
}
void SidePanel::AddHeaderView(std::unique_ptr<views::View> view) {
// If a header view already exists make sure we remove it so that it is
// replaced.
if (header_view_) {
auto header_view = RemoveChildViewT(header_view_);
header_view_ = nullptr;
}
header_view_ = view.get();
AddChildView(std::move(view));
header_view_->DeprecatedLayoutImmediately();
if (border_view_) {
border_view_->HeaderViewChanged(header_view_);
}
// Update the border so that the insets include space for the header to be
// placed on top of the border area.
int top_inset = header_view_->height() - GetBorderInsets().top();
SetBorder(views::CreateEmptyBorder(GetBorderInsets() +
gfx::Insets::TLBR(top_inset, 0, 0, 0)));
}
void SidePanel::RemoveHeaderView() {
SetBorder(views::CreateEmptyBorder(GetBorderInsets().set_top(0)));
if (border_view_) {
border_view_->HeaderViewChanged(nullptr);
}
if (header_view_) {
auto header_view = RemoveChildViewT(header_view_);
header_view_ = nullptr;
}
}
void SidePanel::SetOutlineVisibility(bool visible) {
if (!border_view_) {
return;
}
border_view_->SetOutlineVisibilty(visible);
}
gfx::Size SidePanel::GetContentSizeUpperBound() const {
const int side_panel_width = width() > 0 ? width() : GetMinimumSize().width();
const int side_panel_height =
height() > 0 ? height() : browser_view_->height();
return gfx::Size(std::max(0, side_panel_width - GetBorderInsets().width()),
std::max(0, side_panel_height - GetBorderInsets().height()));
}
void SidePanel::OnBoundsChanged(const gfx::Rect& previous_bounds) {
if (previous_bounds.width() != width() && keyboard_resized_) {
keyboard_resized_ = false;
AnnounceResize();
}
}
double SidePanel::GetAnimationValue() const {
if (ShouldShowAnimation()) {
return animation_.GetCurrentValue();
} else {
return 1;
}
}
void SidePanel::OnChildViewAdded(View* observed_view, View* child) {
if (observed_view != this || child == border_view_ || child == resize_area_) {
return;
}
if (child != header_view_) {
content_view_observations_.AddObservation(child);
}
// Reorder `border_view_` to be last so that it gets painted on top, even if
// an added child also paints to a layer.
if (border_view_) {
ReorderChildView(border_view_, children().size());
}
// Reorder `header_view_` if it exists to get painted on top of the border
// view.
if (header_view_) {
ReorderChildView(header_view_, children().size());
}
// Reorder `resize_area_` to be last so that it gets painted on top of
// `border_view_`, for displaying the resize handle.
if (resize_area_) {
ReorderChildView(resize_area_, children().size());
}
if (header_view_) {
// The header view should come before all other side panel children except
// the resize area in focus order.
header_view_->InsertBeforeInFocusList(GetChildrenFocusList().front());
}
// The resize area should come before all other side panel children in focus
// order.
if (resize_area_) {
resize_area_->InsertBeforeInFocusList(GetChildrenFocusList().front());
}
}
void SidePanel::OnChildViewRemoved(View* observed_view, View* child) {
if (observed_view != this) {
return;
}
if (content_view_observations_.IsObservingSource(child)) {
content_view_observations_.RemoveObservation(child);
}
}
void SidePanel::AnimationProgressed(const gfx::Animation* animation) {
base::TimeDelta step_time =
base::TimeTicks::Now() - last_animation_step_timestamp_;
last_animation_step_timestamp_ = base::TimeTicks::Now();
if (!largest_animation_step_time_.has_value() ||
largest_animation_step_time_ < step_time) {
largest_animation_step_time_ = step_time;
}
InvalidateLayout();
}
void SidePanel::AnimationEnded(const gfx::Animation* animation) {
if (animation->GetCurrentValue() == 0) {
SetVisible(false);
state_ = State::kClosed;
} else {
state_ = State::kOpen;
}
if (largest_animation_step_time_.has_value()) {
SidePanelUtil::RecordSidePanelAnimationMetrics(
type_, largest_animation_step_time_.value());
}
InvalidateLayout();
}
void SidePanel::UpdateSidePanelWidthPref(const std::string& panel_id,
int width) {
PrefService* pref_service = browser_view_->browser()->profile()->GetPrefs();
ScopedDictPrefUpdate update(pref_service, prefs::kSidePanelIdToWidth);
base::Value::Dict& dict = update.Get();
// Update the dictionary with the new width for the specified panel_id.
dict.Set(panel_id, base::Value(width));
}
void SidePanel::OnResize(int resize_amount, bool done_resizing) {
if (starting_width_on_resize_ < 0) {
starting_width_on_resize_ = width();
}
int proposed_width = starting_width_on_resize_ +
((IsRightAligned() && !base::i18n::IsRTL()) ||
(!IsRightAligned() && base::i18n::IsRTL())
? -resize_amount
: resize_amount);
if (done_resizing) {
starting_width_on_resize_ = -1;
}
const int minimum_width = GetMinimumSize().width();
if (proposed_width < minimum_width) {
proposed_width = minimum_width;
}
if (width() != proposed_width) {
if (SidePanelUI* side_panel_ui =
browser_view_->browser()->GetFeatures().side_panel_ui()) {
if (std::optional<SidePanelEntry::Id> entry =
side_panel_ui->GetCurrentEntryId(type_)) {
std::string current_panel_id = SidePanelEntryIdToString(entry.value());
// Update the pref with the new width.
UpdateSidePanelWidthPref(current_panel_id, proposed_width);
}
}
SetPanelWidth(proposed_width);
did_resize_ = true;
}
}
void SidePanel::RecordMetricsIfResized() {
if (did_resize_) {
SidePanelUI* side_panel_ui =
browser_view_->browser()->GetFeatures().side_panel_ui();
if (!side_panel_ui) {
return;
}
std::optional<SidePanelEntry::Id> id =
side_panel_ui->GetCurrentEntryId(type_);
if (!id.has_value()) {
return;
}
int side_panel_contents_width = width() - GetBorderInsets().width();
int browser_window_width = browser_view_->width();
SidePanelUtil::RecordSidePanelResizeMetrics(
type_, id.value(), side_panel_contents_width, browser_window_width);
did_resize_ = false;
}
}
void SidePanel::Open(bool animated) {
UpdateVisibility(/*should_be_open=*/true, animated);
}
void SidePanel::Close(bool animated) {
UpdateVisibility(/*should_be_open=*/false, animated);
}
views::View* SidePanel::GetContentParentView() {
return content_parent_view_;
}
void SidePanel::UpdateVisibility(bool should_be_open, bool animate_transition) {
animate_transition &= ShouldShowAnimation();
if (should_be_open) {
state_ = animate_transition ? State::kOpening : State::kOpen;
} else {
state_ = animate_transition ? State::kClosing : State::kClosed;
}
std::vector<views::View*> views_to_hide;
// TODO(pbos): Iterate content instead. Requires moving the owned pointer out
// of owned contents before resetting it.
for (views::View* view : children()) {
if (view == border_view_ || view == resize_area_ || view == header_view_ ||
!view->GetVisible()) {
continue;
}
if (state_ == State::kClosing || state_ == State::kClosed) {
views_to_hide.push_back(view);
}
}
// Make sure the border visibility matches the side panel. Also dynamically
// create and destroy the layer to reclaim memory and avoid painting and
// compositing this border when it's not showing. See
// https://crbug.com/1269090.
// TODO(pbos): Should layer visibility/painting be automatically tied to
// parent visibility? I.e. the difference between GetVisible() and IsDrawn().
bool side_panel_open_or_closing = GetVisible() || should_be_open;
if (border_view_ &&
side_panel_open_or_closing != border_view_->GetVisible()) {
border_view_->SetVisible(side_panel_open_or_closing);
if (side_panel_open_or_closing) {
border_view_->SetPaintToLayer();
border_view_->layer()->SetFillsBoundsOpaquely(false);
if (header_view_ && header_view_->GetVisible()) {
border_view_->HeaderViewChanged(header_view_);
int top_inset = header_view_->height() - GetBorderInsets().top();
SetBorder(views::CreateEmptyBorder(
GetBorderInsets() + gfx::Insets::TLBR(top_inset, 0, 0, 0)));
}
} else {
border_view_->DestroyLayer();
}
}
if (animate_transition) {
if (should_be_open) {
// If the side panel should remain open but there are views to hide, hide
// them immediately.
for (auto* view : views_to_hide) {
view->SetVisible(false);
}
SetVisible(should_be_open);
largest_animation_step_time_.reset();
last_animation_step_timestamp_ = base::TimeTicks::Now();
animation_.Show();
} else if (GetVisible() && !IsClosing()) {
largest_animation_step_time_.reset();
last_animation_step_timestamp_ = base::TimeTicks::Now();
animation_.Hide();
}
} else {
// Set the animation value so that it accurately reflects what state the
// side panel should be in for layout.
animation_.Reset(should_be_open ? 1 : 0);
SetVisible(should_be_open);
}
}
bool SidePanel::ShouldShowAnimation() const {
return gfx::Animation::ShouldRenderRichAnimation() && !animations_disabled_;
}
void SidePanel::AnnounceResize() {
float side_panel_width = width();
float web_contents_width =
browser_view_->contents_container()->bounds().width();
float total_width = browser_view_->bounds().width();
int side_panel_percentage = (side_panel_width / total_width) * 100;
int web_contents_percentage = (web_contents_width / total_width) * 100;
if (side_panel_percentage + web_contents_percentage > 100) {
side_panel_percentage--;
}
bool side_panel_right_aligned = IsRightAligned();
std::u16string web_contents_side_text = l10n_util::GetStringUTF16(
side_panel_right_aligned
? IDS_SIDE_PANEL_RESIZE_LEFT_SIDE_ACCESSIBLE_ALERT
: IDS_SIDE_PANEL_RESIZE_RIGHT_SIDE_ACCESSIBLE_ALERT);
std::u16string side_panel_side_text = l10n_util::GetStringUTF16(
side_panel_right_aligned
? IDS_SIDE_PANEL_RESIZE_RIGHT_SIDE_ACCESSIBLE_ALERT
: IDS_SIDE_PANEL_RESIZE_LEFT_SIDE_ACCESSIBLE_ALERT);
GetViewAccessibility().AnnounceText(l10n_util::GetStringFUTF16(
IDS_SIDE_PANEL_RESIZE_ACCESSIBLE_ALERT, web_contents_side_text,
base::FormatPercent(web_contents_percentage), side_panel_side_text,
base::FormatPercent(side_panel_percentage)));
}
BEGIN_METADATA(SidePanel)
END_METADATA