Commit 26db7fa3 authored by Katie D's avatar Katie D Committed by Commit Bot

Caption bubble positioned according to spec.

The bubble is by default centered in its anchor view, and can
be moved anywhere within the anchor view as long as it is not
too close to the sides or bottom. If the bubble is moved within
the anchor view, it tries to stay at the same place with
respect to the anchor as the anchor changes size.

Bug: 1055150
Change-Id: Ic161a99e75bfcc7387b44ee14bed0bc996276e70
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2150691
Commit-Queue: Katie Dektar <katie@chromium.org>
Reviewed-by: default avatarAbigail Klein <abigailbklein@google.com>
Reviewed-by: default avatarElly Fong-Jones <ellyjones@chromium.org>
Cr-Commit-Position: refs/heads/master@{#760558}
parent 09014076
......@@ -30,16 +30,19 @@ namespace {
static constexpr int kLineHeightDip = 24;
static constexpr int kMaxHeightDip = kLineHeightDip * 2;
static constexpr int kCornerRadiusDip = 8;
static constexpr int kMaxWidthDip = 548;
static constexpr int kHorizontalMarginsDip = 6;
static constexpr int kVerticalMarginsDip = 8;
static constexpr double kPreferredAnchorWidthPercentage = 0.8;
static constexpr int kMaxWidthDip = 548;
static constexpr int kButtonPaddingDip = 48;
static constexpr int kSideMarginDip = 20;
// 90% opacity.
static constexpr int kCaptionBubbleAlpha = 230;
static constexpr char kPrimaryFont[] = "Roboto";
static constexpr char kSecondaryFont[] = "Arial";
static constexpr char kTertiaryFont[] = "sans-serif";
static constexpr int kFontSizePx = 16;
static constexpr double kDefaultRatioInParent = 0.5;
// CaptionBubble implementation of BubbleFrameView.
class CaptionBubbleFrameView : public views::BubbleFrameView {
......@@ -75,13 +78,88 @@ CaptionBubble::CaptionBubble(views::View* anchor,
: BubbleDialogDelegateView(anchor,
views::BubbleBorder::FLOAT,
views::BubbleBorder::Shadow::NO_SHADOW),
destroyed_callback_(std::move(destroyed_callback)) {
destroyed_callback_(std::move(destroyed_callback)),
ratio_in_parent_x_(kDefaultRatioInParent),
ratio_in_parent_y_(kDefaultRatioInParent) {
DialogDelegate::SetButtons(ui::DIALOG_BUTTON_NONE);
DialogDelegate::set_draggable(true);
}
CaptionBubble::~CaptionBubble() = default;
gfx::Rect CaptionBubble::GetBubbleBounds() {
// Get the height and width of the full bubble using the superclass method.
// This includes shadow and insets.
gfx::Rect original_bounds =
views::BubbleDialogDelegateView::GetBubbleBounds();
gfx::Rect anchor_rect = GetAnchorView()->GetBoundsInScreen();
// Calculate the desired width based on the original bubble's width (which is
// the max allowed per the spec).
int min_width = anchor_rect.width() - kSideMarginDip * 2;
int desired_width = anchor_rect.width() * kPreferredAnchorWidthPercentage;
int width = std::max(min_width, desired_width);
if (width > original_bounds.width())
width = original_bounds.width();
int height = original_bounds.height();
// The placement is based on the ratio between the center of the widget and
// the center of the anchor_rect.
int target_x =
anchor_rect.x() + anchor_rect.width() * ratio_in_parent_x_ - width / 2.0;
int target_y = anchor_rect.y() + anchor_rect.height() * ratio_in_parent_y_ -
height / 2.0;
latest_bounds_ = gfx::Rect(target_x, target_y, width, height);
latest_anchor_bounds_ = GetAnchorView()->GetBoundsInScreen();
anchor_rect.Inset(kSideMarginDip, 0, kSideMarginDip, kButtonPaddingDip);
if (!anchor_rect.Contains(latest_bounds_)) {
latest_bounds_.AdjustToFit(anchor_rect);
}
// If it still doesn't fit after being adjusted to fit, then it is too tall
// or too wide for the tiny window, and we need to simply hide it. Otherwise,
// ensure it is shown.
if (latest_bounds_.height() < height)
GetWidget()->Hide();
else if (!GetWidget()->IsVisible())
GetWidget()->Show();
return latest_bounds_;
}
void CaptionBubble::OnWidgetBoundsChanged(views::Widget* widget,
const gfx::Rect& new_bounds) {
gfx::Rect widget_bounds = GetWidget()->GetWindowBoundsInScreen();
gfx::Rect anchor_rect = GetAnchorView()->GetBoundsInScreen();
if (latest_bounds_ == widget_bounds && latest_anchor_bounds_ == anchor_rect) {
return;
}
if (latest_anchor_bounds_ != anchor_rect) {
// The window has moved. Reposition the widget within it.
SizeToContents();
return;
}
// The widget has moved within the window. Recalculate the desired ratio
// within the parent.
gfx::Rect bounds_rect = GetAnchorView()->GetBoundsInScreen();
bounds_rect.Inset(kSideMarginDip, 0, kSideMarginDip, kButtonPaddingDip);
bool out_of_bounds = false;
if (!bounds_rect.Contains(widget_bounds)) {
widget_bounds.AdjustToFit(bounds_rect);
out_of_bounds = true;
}
ratio_in_parent_x_ = (widget_bounds.CenterPoint().x() - anchor_rect.x()) /
(1.0 * anchor_rect.width());
ratio_in_parent_y_ = (widget_bounds.CenterPoint().y() - anchor_rect.y()) /
(1.0 * anchor_rect.height());
if (out_of_bounds)
SizeToContents();
}
void CaptionBubble::Init() {
auto* layout = SetLayoutManager(std::make_unique<views::FlexLayout>());
layout->SetOrientation(views::LayoutOrientation::kVertical);
......@@ -123,10 +201,7 @@ void CaptionBubble::Init() {
title->SetFontList(font_list);
title->SetText(l10n_util::GetStringUTF16(IDS_LIVE_CAPTION_BUBBLE_TITLE));
// TODO(crbug.com/1055150): Resize responsively with anchor size changes.
int min_width = GetAnchorView()->width() * kPreferredAnchorWidthPercentage;
int width = std::min(min_width, kMaxWidthDip);
SetPreferredSize(gfx::Size(width, kMaxHeightDip));
SetPreferredSize(gfx::Size(kMaxWidthDip, kMaxHeightDip));
set_margins(gfx::Insets(kHorizontalMarginsDip, kVerticalMarginsDip));
title_ = AddChildView(std::move(title));
......
......@@ -33,10 +33,14 @@ class CaptionBubble : public views::BubbleDialogDelegateView {
void SetText(const std::string& text);
protected:
// views::BubbleDialogDelegateView:
void Init() override;
bool ShouldShowCloseButton() const override;
views::NonClientFrameView* CreateNonClientFrameView(
views::Widget* widget) override;
gfx::Rect GetBubbleBounds() override;
void OnWidgetBoundsChanged(views::Widget* widget,
const gfx::Rect& new_bounds) override;
private:
friend class CaptionBubbleControllerViewsTest;
......@@ -46,6 +50,14 @@ class CaptionBubble : public views::BubbleDialogDelegateView {
views::Label* title_;
base::ScopedClosureRunner destroyed_callback_;
// The bubble tries to stay relatively positioned in its parent.
// ratio_in_parent_x_ represents the ratio along the parent width at which
// to display the center of the bubble, if possible.
double ratio_in_parent_x_;
double ratio_in_parent_y_;
gfx::Rect latest_bounds_;
gfx::Rect latest_anchor_bounds_;
};
} // namespace captions
......
......@@ -10,6 +10,7 @@
#include "base/macros.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/views/accessibility/caption_bubble.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "ui/views/controls/label.h"
#include "ui/views/widget/widget.h"
......@@ -51,6 +52,12 @@ class CaptionBubbleControllerViewsTest : public InProcessBrowserTest {
return controller_ ? controller_->caption_widget_ : nullptr;
}
// There may be some rounding errors. Check that points are almost the same.
void ExpectPointsApproximatelyEqual(gfx::Point first, gfx::Point second) {
EXPECT_LT(abs(first.x() - second.x()), 2);
EXPECT_LT(abs(first.y() - second.y()), 2);
}
private:
std::unique_ptr<CaptionBubbleControllerViews> controller_;
};
......@@ -110,4 +117,125 @@ IN_PROC_BROWSER_TEST_F(CaptionBubbleControllerViewsTest,
EXPECT_FALSE(GetTitle()->GetVisible());
}
IN_PROC_BROWSER_TEST_F(CaptionBubbleControllerViewsTest, BubblePositioning) {
views::View* contents_view =
BrowserView::GetBrowserViewForBrowser(browser())->GetContentsView();
browser()->window()->SetBounds(gfx::Rect(10, 10, 800, 600));
GetController()->OnCaptionReceived("Mantis shrimp have 12-16 photoreceptors");
ExpectPointsApproximatelyEqual(
contents_view->GetBoundsInScreen().CenterPoint(),
GetCaptionWidget()->GetClientAreaBoundsInScreen().CenterPoint());
EXPECT_EQ(GetBubble()->GetBoundsInScreen().width(), 548);
// Move the window and the widget should stay centered.
browser()->window()->SetBounds(gfx::Rect(50, 50, 800, 600));
ExpectPointsApproximatelyEqual(
contents_view->GetBoundsInScreen().CenterPoint(),
GetCaptionWidget()->GetClientAreaBoundsInScreen().CenterPoint());
EXPECT_EQ(GetBubble()->GetBoundsInScreen().width(), 548);
// Shrink the window's height.
browser()->window()->SetBounds(gfx::Rect(50, 50, 800, 300));
ExpectPointsApproximatelyEqual(
contents_view->GetBoundsInScreen().CenterPoint(),
GetCaptionWidget()->GetClientAreaBoundsInScreen().CenterPoint());
EXPECT_EQ(GetBubble()->GetBoundsInScreen().width(), 548);
// Grow the height again.
browser()->window()->SetBounds(gfx::Rect(50, 50, 800, 500));
ExpectPointsApproximatelyEqual(
contents_view->GetBoundsInScreen().CenterPoint(),
GetCaptionWidget()->GetClientAreaBoundsInScreen().CenterPoint());
EXPECT_EQ(GetBubble()->GetBoundsInScreen().width(), 548);
// Now shrink the width so that the caption bubble shrinks.
browser()->window()->SetBounds(gfx::Rect(50, 50, 500, 500));
gfx::Rect widget_bounds = GetCaptionWidget()->GetClientAreaBoundsInScreen();
gfx::Rect contents_bounds = contents_view->GetBoundsInScreen();
EXPECT_EQ(contents_bounds.CenterPoint(), widget_bounds.CenterPoint());
EXPECT_LT(GetBubble()->GetBoundsInScreen().width(), 548);
EXPECT_EQ(20, widget_bounds.x() - contents_bounds.x());
EXPECT_EQ(20, contents_bounds.right() - widget_bounds.right());
// Make it bigger again and ensure it's visible and wide again.
browser()->window()->SetBounds(gfx::Rect(10, 10, 800, 600));
ExpectPointsApproximatelyEqual(
contents_view->GetBoundsInScreen().CenterPoint(),
GetCaptionWidget()->GetClientAreaBoundsInScreen().CenterPoint());
EXPECT_EQ(GetBubble()->GetBoundsInScreen().width(), 548);
// Now move the widget within the window.
GetCaptionWidget()->SetBounds(
gfx::Rect(110, 210, GetCaptionWidget()->GetWindowBoundsInScreen().width(),
GetCaptionWidget()->GetWindowBoundsInScreen().height()));
// The bubble width should not have changed.
EXPECT_EQ(GetBubble()->GetBoundsInScreen().width(), 548);
// Move the window and the widget stays fixed with respect to the window.
browser()->window()->SetBounds(gfx::Rect(100, 100, 800, 600));
widget_bounds = GetCaptionWidget()->GetClientAreaBoundsInScreen();
EXPECT_EQ(200, widget_bounds.x());
EXPECT_EQ(300, widget_bounds.y());
EXPECT_EQ(GetBubble()->GetBoundsInScreen().width(), 548);
// Now put the window in the top corner for easier math.
browser()->window()->SetBounds(gfx::Rect(0, 0, 800, 600));
widget_bounds = GetCaptionWidget()->GetClientAreaBoundsInScreen();
EXPECT_EQ(100, widget_bounds.x());
EXPECT_EQ(200, widget_bounds.y());
contents_bounds = contents_view->GetBoundsInScreen();
double x_ratio = (widget_bounds.CenterPoint().x() - contents_bounds.x()) /
(1.0 * contents_bounds.width());
double y_ratio = (widget_bounds.CenterPoint().y() - contents_bounds.y()) /
(1.0 * contents_bounds.height());
// The center point ratio should not change as we resize the window, and the
// widget is repositioned.
browser()->window()->SetBounds(gfx::Rect(0, 0, 750, 550));
widget_bounds = GetCaptionWidget()->GetClientAreaBoundsInScreen();
contents_bounds = contents_view->GetBoundsInScreen();
double new_x_ratio = (widget_bounds.CenterPoint().x() - contents_bounds.x()) /
(1.0 * contents_bounds.width());
double new_y_ratio = (widget_bounds.CenterPoint().y() - contents_bounds.y()) /
(1.0 * contents_bounds.height());
EXPECT_NEAR(x_ratio, new_x_ratio, .005);
EXPECT_NEAR(y_ratio, new_y_ratio, .005);
browser()->window()->SetBounds(gfx::Rect(0, 0, 700, 500));
widget_bounds = GetCaptionWidget()->GetClientAreaBoundsInScreen();
contents_bounds = contents_view->GetBoundsInScreen();
new_x_ratio = (widget_bounds.CenterPoint().x() - contents_bounds.x()) /
(1.0 * contents_bounds.width());
new_y_ratio = (widget_bounds.CenterPoint().y() - contents_bounds.y()) /
(1.0 * contents_bounds.height());
EXPECT_NEAR(x_ratio, new_x_ratio, .005);
EXPECT_NEAR(y_ratio, new_y_ratio, .005);
// But if we make the window too small, the widget will stay within its
// bounds.
browser()->window()->SetBounds(gfx::Rect(0, 0, 500, 500));
widget_bounds = GetCaptionWidget()->GetClientAreaBoundsInScreen();
contents_bounds = contents_view->GetBoundsInScreen();
new_y_ratio = (widget_bounds.CenterPoint().y() - contents_bounds.y()) /
(1.0 * contents_bounds.height());
EXPECT_NEAR(y_ratio, new_y_ratio, .005);
EXPECT_TRUE(contents_bounds.Contains(widget_bounds));
// Making it big again resets the position to what it was before.
browser()->window()->SetBounds(gfx::Rect(0, 0, 800, 600));
widget_bounds = GetCaptionWidget()->GetClientAreaBoundsInScreen();
EXPECT_EQ(100, widget_bounds.x());
EXPECT_EQ(200, widget_bounds.y());
// Shrink it so small the caption bubble can't fit. Ensure it's hidden.
browser()->window()->SetBounds(gfx::Rect(0, 0, 200, 100));
EXPECT_FALSE(GetCaptionWidget()->IsVisible());
// Make it bigger again and ensure it's visible and wide again.
browser()->window()->SetBounds(gfx::Rect(0, 0, 800, 400));
EXPECT_TRUE(GetCaptionWidget()->IsVisible());
}
} // namespace captions
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment