0

Add RefreshBannerView for Mahi Panel.

Bug: b:319731624

Change-Id: I60ab57ca1dca1590d0872416a13701182cde4f9f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5336604
Commit-Queue: Ahmed Mehfooz <amehfooz@chromium.org>
Reviewed-by: Andre Le <leandre@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1268822}
This commit is contained in:
Ahmed Mehfooz
2024-03-06 01:02:35 +00:00
committed by Chromium LUCI CQ
parent 2dd86ccb39
commit 6995e7f697
10 changed files with 307 additions and 11 deletions

@ -1777,6 +1777,8 @@ component("ash") {
"system/mahi/mahi_panel_view.h",
"system/mahi/mahi_panel_widget.cc",
"system/mahi/mahi_panel_widget.h",
"system/mahi/refresh_banner_view.cc",
"system/mahi/refresh_banner_view.h",
"system/mahi/summary_outlines_section.cc",
"system/mahi/summary_outlines_section.h",
"system/media/media_color_theme.cc",

@ -5132,6 +5132,9 @@ Some features are limited to increase battery life.
<message name="IDS_ASH_MAHI_LEARN_MORE_LINK_LABEL_TEXT" desc="Text for the learn more link that user can navigate to learn more about the project information">
Learn more
</message>
<message name="IDS_ASH_MAHI_REFRESH_BANNER_LABEL_TEXT" desc="Text for the refresh banner shown above the mahi panel.">
Refresh <ph name="CONTENT_TITLE">$1<ex>test content title</ex></ph>
</message>
<!-- Multi-profiles intro dialog -->
<message name="IDS_ASH_MULTIPROFILES_INTRO_HEADLINE" desc="Describes which feature multi-profiles intro dialog presents.">

@ -0,0 +1 @@
c2c9d1ddb31f999d972f73a2a31939b30725348f

@ -18,6 +18,8 @@ enum ViewId {
kThumbsUpButton,
kThumbsDownButton,
kLearnMoreLink,
kRefreshView,
kMahiPanelView,
};
// The size of the icon that appears in the panel's source row.
@ -28,6 +30,9 @@ inline constexpr char kMahiFeedbackHistogramName[] = "Ash.Mahi.Feedback";
// TODO(b/319264190): Replace the string here with the correct URL.
inline constexpr char kLearnMorePage[] = "https://google.com";
inline constexpr int kRefreshBannerStackDepth = 25;
inline constexpr int kPanelCornerRadius = 16;
} // namespace ash::mahi_constants
#endif // ASH_SYSTEM_MAHI_MAHI_CONSTANTS_H_

@ -58,7 +58,6 @@ namespace ash {
namespace {
constexpr SkScalar kContentScrollViewCornerRadius = 16;
constexpr int kPanelCornerRadius = 16;
constexpr gfx::Insets kPanelPadding = gfx::Insets(16);
constexpr int kPanelChildSpacing = 8;
constexpr int kHeaderRowSpacing = 8;
@ -116,19 +115,22 @@ MahiPanelView::MahiPanelView() {
SetDefault(views::kMarginsKey, gfx::Insets::VH(kPanelChildSpacing, 0));
SetIgnoreDefaultMainAxisMargins(true);
SetCollapseMargins(true);
SetID(mahi_constants::ViewId::kMahiPanelView);
SetBackground(views::CreateThemedRoundedRectBackground(
cros_tokens::kCrosSysSystemBaseElevated, kPanelCornerRadius));
cros_tokens::kCrosSysSystemBaseElevated,
mahi_constants::kPanelCornerRadius));
// Create a layer for the view for background blur and rounded corners.
SetPaintToLayer();
layer()->SetRoundedCornerRadius(gfx::RoundedCornersF{kPanelCornerRadius});
layer()->SetRoundedCornerRadius(
gfx::RoundedCornersF{mahi_constants::kPanelCornerRadius});
layer()->SetFillsBoundsOpaquely(false);
layer()->SetIsFastRoundedCorner(true);
layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma);
layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality);
SetBorder(std::make_unique<views::HighlightBorder>(
kPanelCornerRadius,
mahi_constants::kPanelCornerRadius,
views::HighlightBorder::Type::kHighlightBorderOnShadow,
/*insets_type=*/views::HighlightBorder::InsetsType::kHalfInsets));

@ -9,10 +9,13 @@
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/shell.h"
#include "ash/system/mahi/mahi_constants.h"
#include "ash/system/mahi/mahi_panel_view.h"
#include "ash/system/mahi/refresh_banner_view.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer_type.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/widget/unique_widget_ptr.h"
#include "ui/views/widget/widget.h"
@ -22,25 +25,44 @@ namespace {
constexpr int kPanelDefaultWidth = 340;
constexpr int kPanelDefaultHeight = 450;
constexpr int kPanelHeightWithRefreshBanner = 482;
constexpr int kPanelBoundsPadding = 8;
gfx::Rect CalculateWidgetBounds(aura::Window* root_window) {
gfx::Rect CalculateWidgetBounds(aura::Window* root_window,
bool refresh_banner_shown = false) {
auto display =
display::Screen::GetScreen()->GetDisplayNearestWindow(root_window);
auto bottom_right = display.work_area().bottom_right();
int height = refresh_banner_shown ? kPanelHeightWithRefreshBanner
: kPanelDefaultHeight;
// The panel is positioned at the bottom right corner of the screen.
// TODO(b/319476980): Make sure Mahi main panel bounds work when shelf
// alignment changes.
return gfx::Rect(bottom_right.x() - kPanelDefaultWidth - kPanelBoundsPadding,
bottom_right.y() - kPanelDefaultHeight - kPanelBoundsPadding,
kPanelDefaultWidth, kPanelDefaultHeight);
bottom_right.y() - height - kPanelBoundsPadding,
kPanelDefaultWidth, height);
}
} // namespace
MahiPanelWidget::MahiPanelWidget(InitParams params)
: views::Widget(std::move(params)) {}
: views::Widget(std::move(params)) {
auto* contents_view = SetContentsView(
views::Builder<views::BoxLayoutView>()
// We need to set a negative value for between child spacing here
// because we need the `RefreshBannerView` to overlap with the
// `MahiPanelView`.
.SetBetweenChildSpacing(-mahi_constants::kRefreshBannerStackDepth)
.SetOrientation(views::BoxLayout::Orientation::kVertical)
.Build());
refresh_view_ =
contents_view->AddChildView(std::make_unique<RefreshBannerView>());
refresh_view_observation_.Observe(refresh_view_);
contents_view->AddChildView(std::make_unique<MahiPanelView>());
}
MahiPanelWidget::~MahiPanelWidget() = default;
@ -61,13 +83,35 @@ views::UniqueWidgetPtr MahiPanelWidget::CreatePanelWidget(int64_t display_id) {
views::UniqueWidgetPtr widget =
std::make_unique<MahiPanelWidget>(std::move(params));
widget->SetContentsView(std::make_unique<MahiPanelView>());
widget->SetBounds(CalculateWidgetBounds(root_window));
return widget;
}
void MahiPanelWidget::SetRefreshViewVisible(bool visible) {
// TODO(b/319731624): Finish this function.
if (!refresh_view_) {
return;
}
if (refresh_view_->GetVisible() == visible) {
return;
}
visible ? refresh_view_->Show() : refresh_view_->Hide();
}
void MahiPanelWidget::OnViewVisibilityChanged(views::View* observed_view,
views::View* starting_view) {
CHECK_EQ(observed_view, refresh_view_);
SetBounds(
CalculateWidgetBounds(GetNativeWindow(), observed_view->GetVisible()));
}
void MahiPanelWidget::OnViewIsDeleting(views::View* observed_view) {
CHECK_EQ(observed_view, refresh_view_);
refresh_view_observation_.Reset();
refresh_view_ = nullptr;
}
} // namespace ash

@ -6,15 +6,18 @@
#define ASH_SYSTEM_MAHI_MAHI_PANEL_WIDGET_H_
#include "ash/ash_export.h"
#include "ui/views/view_observer.h"
#include "ui/views/widget/unique_widget_ptr.h"
#include "ui/views/widget/widget.h"
namespace ash {
class RefreshBannerView;
// The widget that contains the Mahi panel.
// TODO(b/319329379): Use this class in `CreatePanelWidget()` when resizing and
// closing capability is added.
class ASH_EXPORT MahiPanelWidget : public views::Widget {
class ASH_EXPORT MahiPanelWidget : public views::Widget, views::ViewObserver {
public:
explicit MahiPanelWidget(InitParams params);
@ -28,6 +31,18 @@ class ASH_EXPORT MahiPanelWidget : public views::Widget {
// Shows/hides the refresh UI in the panel.
void SetRefreshViewVisible(bool visible);
private:
// views::ViewObserver:
void OnViewVisibilityChanged(views::View* observed_view,
views::View* starting_view) override;
void OnViewIsDeleting(views::View* observed_view) override;
// Owned by views hierarchy.
raw_ptr<RefreshBannerView> refresh_view_ = nullptr;
base::ScopedObservation<views::View, views::ViewObserver>
refresh_view_observation_{this};
};
} // namespace ash

@ -6,6 +6,7 @@
#include "ash/public/cpp/shelf_config.h"
#include "ash/system/mahi/fake_mahi_manager.h"
#include "ash/system/mahi/mahi_constants.h"
#include "ash/test/ash_test_base.h"
#include "chromeos/components/mahi/public/cpp/mahi_manager.h"
#include "ui/aura/window.h"
@ -58,5 +59,31 @@ TEST_F(MahiPanelWidgetTest, WidgetBounds) {
widget->GetRestoredBounds());
}
TEST_F(MahiPanelWidgetTest, WidgetBoundsWithRefreshBanner) {
auto widget = MahiPanelWidget::CreatePanelWidget(GetPrimaryDisplay().id());
auto* panel_view = widget->GetContentsView()->GetViewByID(
mahi_constants::ViewId::kMahiPanelView);
auto* refresh_view = widget->GetContentsView()->GetViewByID(
mahi_constants::ViewId::kRefreshView);
auto panel_view_bounds = panel_view->GetBoundsInScreen();
auto widget_bounds = widget->GetRestoredBounds();
refresh_view->SetVisible(true);
// Make sure the `MahiPanelView` has the exact same location on the screen
// after the `RefreshBannerView` changes visibility.
EXPECT_EQ(panel_view_bounds, panel_view->GetBoundsInScreen());
// The widget's height should increase by the height of the
// `RefreshBannerView` subtracted by `kRefreshBannerStackDepth`.
int height_delta =
widget->GetRestoredBounds().height() - widget_bounds.height();
EXPECT_EQ(height_delta, refresh_view->GetBoundsInScreen().height() -
mahi_constants::kRefreshBannerStackDepth);
}
} // namespace
} // namespace ash

@ -0,0 +1,158 @@
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/system/mahi/refresh_banner_view.h"
#include <string>
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/icon_button.h"
#include "ash/style/typography.h"
#include "ash/system/mahi/mahi_constants.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "chromeos/components/mahi/public/cpp/mahi_manager.h"
#include "components/vector_icons/vector_icons.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkPathBuilder.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/background.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/layout/flex_layout_view.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/view_class_properties.h"
namespace ash {
namespace {
constexpr int kRefreshBannerCornerRadius = 20;
constexpr base::TimeDelta kRefreshBannerAnimationDurationMs =
base::Milliseconds(100);
constexpr gfx::Insets kRefreshBannerInteriorMargin =
gfx::Insets::TLBR(4, 28, mahi_constants::kRefreshBannerStackDepth + 4, 28);
constexpr gfx::Insets kTitleLabelMargin = gfx::Insets::TLBR(0, 0, 0, 8);
SkPath GetClipPath(gfx::Size size) {
int width = size.width();
int height = size.height();
auto top_left = SkPoint::Make(0, 0);
auto top_right = SkPoint::Make(width, 0);
auto bottom_left = SkPoint::Make(0, height);
auto bottom_right = SkPoint::Make(width, height);
int radius = kRefreshBannerCornerRadius;
int bottom_radius = mahi_constants::kPanelCornerRadius;
const auto horizontal_offset = SkPoint::Make(radius, 0.f);
const auto vertical_offset = SkPoint::Make(0.f, radius);
const auto bottom_vertical_offset =
SkPoint::Make(0.f, mahi_constants::kRefreshBannerStackDepth - 1);
const auto bottom_horizontal_offset = SkPoint::Make(bottom_radius, 0.f);
return SkPathBuilder()
// Start just before the curve of the top-left corner.
.moveTo(radius, 0.f)
// Draw the top-left rounded corner.
.arcTo(top_left, top_left + vertical_offset, radius)
// Draw the bottom-left rounded corner and the vertical line
// connecting it to the top-left corner.
.lineTo(bottom_left)
.arcTo(bottom_left - bottom_vertical_offset,
bottom_left - bottom_vertical_offset + bottom_horizontal_offset,
radius)
// Draw the bottom-right rounded corner and the horizontal line
// connecting it to the bottom-left corner.
.arcTo(bottom_right - bottom_vertical_offset, bottom_right, bottom_radius)
.lineTo(bottom_right)
.arcTo(top_right, top_right - horizontal_offset, bottom_radius)
.lineTo(radius, 0.f)
.close()
.detach();
}
} // namespace
RefreshBannerView::RefreshBannerView() {
auto* manager = chromeos::MahiManager::Get();
SetBackground(views::CreateThemedRoundedRectBackground(
cros_tokens::kCrosSysSystemPrimaryContainer, /*radius=*/0));
SetOrientation(views::LayoutOrientation::kHorizontal);
SetMainAxisAlignment(views::LayoutAlignment::kCenter);
SetInteriorMargin(kRefreshBannerInteriorMargin);
SetID(mahi_constants::ViewId::kRefreshView);
// We need to paint this view to a layer for animations.
SetPaintToLayer();
SetVisible(false);
AddChildView(
views::Builder<views::Label>()
.SetText(l10n_util::GetStringFUTF16(
IDS_ASH_MAHI_REFRESH_BANNER_LABEL_TEXT,
manager ? manager->GetContentTitle() : base::EmptyString16()))
.SetEnabledColorId(cros_tokens::kCrosSysOnPrimaryContainer)
.SetFontList(TypographyProvider::Get()->ResolveTypographyToken(
TypographyToken::kCrosAnnotation2))
.SetProperty(views::kMarginsKey, kTitleLabelMargin)
.Build());
auto* icon_button =
AddChildView(IconButton::Builder()
.SetVectorIcon(&vector_icons::kReloadChromeRefreshIcon)
.SetType(IconButton::Type::kSmallProminentFloating)
.Build());
icon_button->SetIconColor(cros_tokens::kCrosSysPrimary);
}
RefreshBannerView::~RefreshBannerView() = default;
void RefreshBannerView::Show() {
SetVisible(true);
gfx::Transform transform;
transform.Translate(
gfx::Vector2d(0, mahi_constants::kRefreshBannerStackDepth));
views::AnimationBuilder()
.Once()
.SetDuration(base::TimeDelta())
.SetOpacity(this, 0)
.SetTransform(this, transform)
.At(base::Milliseconds(0))
.SetDuration(kRefreshBannerAnimationDurationMs)
.SetOpacity(this, 1)
.SetTransform(this, gfx::Transform());
}
void RefreshBannerView::Hide() {
views::AnimationBuilder()
.OnEnded(base::BindOnce(
[](base::WeakPtr<views::View> view) {
if (view) {
view->SetVisible(false);
}
},
weak_ptr_factory_.GetWeakPtr()))
.Once()
.SetDuration(kRefreshBannerAnimationDurationMs)
.SetOpacity(this, 0.0);
}
void RefreshBannerView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
SetClipPath(GetClipPath(GetContentsBounds().size()));
// Make sure the refresh banner is always shown on top.
if (layer() && layer()->parent()) {
layer()->parent()->StackAtTop(layer());
}
}
BEGIN_METADATA(RefreshBannerView)
END_METADATA
} // namespace ash

@ -0,0 +1,39 @@
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ASH_SYSTEM_MAHI_REFRESH_BANNER_VIEW_H_
#define ASH_SYSTEM_MAHI_REFRESH_BANNER_VIEW_H_
#include "ash/ash_export.h"
#include "base/memory/weak_ptr.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/views/layout/flex_layout_view.h"
namespace ash {
class ASH_EXPORT RefreshBannerView : public views::FlexLayoutView {
METADATA_HEADER(RefreshBannerView, views::FlexLayoutView)
public:
RefreshBannerView();
RefreshBannerView(const RefreshBannerView&) = delete;
RefreshBannerView& operator=(const RefreshBannerView&) = delete;
~RefreshBannerView() override;
// Shows the refresh banner on top of the Mahi panel by animating it.
void Show();
// Hides the refresh banner by animating it out.
void Hide();
private:
// views::FlexLayoutView:
void OnBoundsChanged(const gfx::Rect& previous_bounds) override;
base::WeakPtrFactory<RefreshBannerView> weak_ptr_factory_{this};
};
} // namespace ash
#endif // ASH_SYSTEM_MAHI_REFRESH_BANNER_VIEW_H_