blob: f904721f99f97a1dd3dda422ac142085572a1637 [file] [log] [blame]
// Copyright 2019 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/tabs/tab_group_header.h"
#include <memory>
#include <utility>
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tabs/tab_group_visual_data.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/views/tabs/tab_controller.h"
#include "chrome/browser/ui/views/tabs/tab_group_editor_bubble_view.h"
#include "chrome/browser/ui/views/tabs/tab_group_underline.h"
#include "chrome/browser/ui/views/tabs/tab_slot_view.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/tabs/tab_strip_controller.h"
#include "chrome/browser/ui/views/tabs/tab_strip_layout.h"
#include "chrome/browser/ui/views/tabs/tab_strip_types.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
TabGroupHeader::TabGroupHeader(TabStrip* tab_strip, TabGroupId group)
: tab_strip_(tab_strip) {
DCHECK(tab_strip);
set_group(group);
// The size and color of the chip are set in VisualsChanged().
title_chip_ = AddChildView(std::make_unique<views::View>());
// The text and color of the title are set in VisualsChanged().
title_ = title_chip_->AddChildView(std::make_unique<views::Label>());
title_->SetCollapseWhenHidden(true);
title_->SetAutoColorReadabilityEnabled(false);
title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
title_->SetElideBehavior(gfx::FADE_TAIL);
VisualsChanged();
}
bool TabGroupHeader::OnMousePressed(const ui::MouseEvent& event) {
// Ignore the click if the editor is already open. Do this so clicking
// on us again doesn't re-trigger the editor.
//
// Though the bubble is deactivated before we receive a mouse event,
// the actual widget destruction happens in a posted task. That task
// gets run after we receive the mouse event. If this sounds brittle,
// that's because it is!
if (editor_bubble_tracker_.is_open())
return false;
tab_strip_->MaybeStartDrag(this, event, tab_strip_->GetSelectionModel());
return true;
}
bool TabGroupHeader::OnMouseDragged(const ui::MouseEvent& event) {
tab_strip_->ContinueDrag(this, event);
return true;
}
void TabGroupHeader::OnMouseReleased(const ui::MouseEvent& event) {
if (!dragging()) {
editor_bubble_tracker_.Opened(
TabGroupEditorBubbleView::Show(this, tab_strip_, group().value()));
}
tab_strip_->EndDrag(END_DRAG_COMPLETE);
}
void TabGroupHeader::OnGestureEvent(ui::GestureEvent* event) {
switch (event->type()) {
case ui::ET_GESTURE_TAP_DOWN: {
if (!editor_bubble_tracker_.is_open()) {
tab_strip_->MaybeStartDrag(this, *event,
tab_strip_->GetSelectionModel());
}
break;
}
case ui::ET_GESTURE_SCROLL_UPDATE: {
tab_strip_->ContinueDrag(this, *event);
break;
}
case ui::ET_GESTURE_END: {
if (!dragging()) {
editor_bubble_tracker_.Opened(
TabGroupEditorBubbleView::Show(this, tab_strip_, group().value()));
}
tab_strip_->EndDrag(END_DRAG_COMPLETE);
break;
}
default:
break;
}
event->SetHandled();
}
TabSlotView::ViewType TabGroupHeader::GetTabSlotViewType() const {
return TabSlotView::ViewType::kTabGroupHeader;
}
TabSizeInfo TabGroupHeader::GetTabSizeInfo() const {
TabSizeInfo size_info;
// Group headers have a fixed width based on |title_|'s width.
const int width = CalculateWidth();
size_info.pinned_tab_width = width;
size_info.min_active_width = width;
size_info.min_inactive_width = width;
size_info.standard_width = width;
return size_info;
}
int TabGroupHeader::CalculateWidth() const {
// We don't want tabs to visually overlap group headers, so we add that space
// to the width to compensate. We don't want to actually remove the overlap
// during layout however; that would cause an the margin to be visually uneven
// when the header is in the first slot and thus wouldn't overlap anything to
// the left.
const int overlap_margin = TabStyle::GetTabOverlap() * 2;
// The empty and non-empty chips have different sizes and corner radii, but
// both should look nestled against the group stroke of the tab to the right.
// This requires a +/- 2px adjustment to the width, which causes the tab to
// the right to be positioned in the right spot.
const TabGroupVisualData* data =
tab_strip_->controller()->GetVisualDataForGroup(group().value());
const int right_adjust = data->title().empty() ? 2 : -2;
return overlap_margin + title_chip_->width() + right_adjust;
}
void TabGroupHeader::VisualsChanged() {
const TabGroupVisualData* data =
tab_strip_->controller()->GetVisualDataForGroup(group().value());
if (data->title().empty()) {
// If the title is empty, the chip is just a circle.
title_->SetVisible(false);
constexpr int kEmptyChipSize = 14;
const int y = (height() - kEmptyChipSize) / 2;
title_chip_->SetBounds(TabGroupUnderline::GetStrokeInset(), y,
kEmptyChipSize, kEmptyChipSize);
title_chip_->SetBackground(
views::CreateRoundedRectBackground(data->color(), kEmptyChipSize / 2));
} else {
// If the title is set, the chip is a rounded rect that matches the active
// tab shape, particularly the tab's corner radius.
title_->SetVisible(true);
title_->SetEnabledColor(
color_utils::GetColorWithMaxContrast(data->color()));
title_->SetText(data->title());
// Set the radius such that the chip nestles snugly against the tab corner
// radius, taking into account the group underline stroke.
const int corner_radius =
TabStyle::GetCornerRadius() - TabGroupUnderline::kStrokeThickness;
// Clamp the width to a maximum of half the standard tab width (not counting
// overlap).
const int max_width =
(TabStyle::GetStandardWidth() - TabStyle::GetTabOverlap()) / 2;
const int text_width =
std::min(title_->GetPreferredSize().width(), max_width);
const int text_height = title_->GetPreferredSize().height();
const int text_vertical_inset = 1;
const int text_horizontal_inset = corner_radius + text_vertical_inset;
const int y = (height() - text_height) / 2 - text_vertical_inset;
title_chip_->SetBounds(TabGroupUnderline::GetStrokeInset(), y,
text_width + 2 * text_horizontal_inset,
text_height + 2 * text_vertical_inset);
title_chip_->SetBackground(
views::CreateRoundedRectBackground(data->color(), corner_radius));
title_->SetBounds(text_horizontal_inset, text_vertical_inset, text_width,
text_height);
}
}
void TabGroupHeader::RemoveObserverFromWidget(views::Widget* widget) {
widget->RemoveObserver(&editor_bubble_tracker_);
}
void TabGroupHeader::EditorBubbleTracker::Opened(views::Widget* bubble_widget) {
DCHECK(bubble_widget);
DCHECK(!is_open_);
is_open_ = true;
bubble_widget->AddObserver(this);
}
void TabGroupHeader::EditorBubbleTracker::OnWidgetDestroyed(
views::Widget* widget) {
is_open_ = false;
}