#include "ui/views/controls/table/table_header.h"
#include <stddef.h>
#include <algorithm>
#include <memory>
#include <utility>
#include <vector>
#include "base/i18n/rtl.h"
#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkPathBuilder.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "ui/base/cursor/cursor.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/text_utils.h"
#include "ui/views/background.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/table/table_utils.h"
#include "ui/views/controls/table/table_view.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/view_utils.h"
namespace views {
namespace {
constexpr int kMinColumnWidth = 10;
constexpr int kResizeKeyboardAmount = 5;
constexpr int kCellVerticalPaddingDefault = 4;
constexpr int kCellHorizontalPaddingDefault = 7;
constexpr int kResizePadding = 5;
constexpr int kVerticalSeparatorPaddingDefault = 4;
constexpr int kHorizontalSeparatorPaddingDefault = 0;
constexpr int kSortIndicatorSize = 8;
}
class TableHeader::HighlightPathGenerator
: public views::HighlightPathGenerator {
public:
HighlightPathGenerator() = default;
HighlightPathGenerator(const HighlightPathGenerator&) = delete;
HighlightPathGenerator& operator=(const HighlightPathGenerator&) = delete;
~HighlightPathGenerator() override = default;
SkPath GetHighlightPath(const View* view) override {
const TableHeader* const header = static_cast<const TableHeader*>(view);
if (!header->HasFocusIndicator()) {
return SkPath();
}
const bool supports_cell_navigation =
PlatformStyle::kTableViewSupportsKeyboardNavigationByCell;
gfx::Rect bounds = supports_cell_navigation
? header->GetActiveHeaderCellBounds()
: header->GetLocalBounds();
bounds.set_x(header->GetMirroredXForRect(bounds));
const float default_radius = header->GetDefaultFocusRingRadius();
std::array<SkVector, 4> focus_ring_radii;
focus_ring_radii.fill({default_radius, default_radius});
if (const auto& columns = header->table_->visible_columns();
columns.size() != 0) {
const auto& active_column = header->table_->GetActiveVisibleColumnIndex();
if (active_column.has_value()) {
const float radius = header->GetFocusRingUpperRadius();
const bool is_first_column = active_column.value() == 0;
const bool is_last_column = active_column == columns.size() - 1;
const float upper_left = is_first_column || !supports_cell_navigation
? radius
: default_radius;
const float upper_right = is_last_column || !supports_cell_navigation
? radius
: default_radius;
const float lower_right = default_radius;
const float lower_left = default_radius;
focus_ring_radii = {SkVector{upper_left, upper_left},
SkVector{upper_right, upper_right},
SkVector{lower_right, lower_right},
SkVector{lower_left, lower_left}};
}
}
return SkPath::RRect(SkRRect::MakeRectRadii(gfx::RectToSkRect(bounds),
focus_ring_radii.data()));
}
};
using Columns = std::vector<TableView::VisibleColumn>;
TableHeader::TableHeader(base::WeakPtr<TableView> table)
: table_(std::move(table)),
font_list_(gfx::FontList().DeriveWithWeight(GetFontWeight())) {
HighlightPathGenerator::Install(
this, std::make_unique<TableHeader::HighlightPathGenerator>());
InstallFocusRing();
}
TableHeader::~TableHeader() = default;
void TableHeader::InstallFocusRing() {
if (views::FocusRing::Get(this)) {
views::FocusRing::Remove(this);
}
FocusRing::Install(this);
FocusRing* focus_ring = views::FocusRing::Get(this);
if (table_->table_style().inset_focus_ring) {
focus_ring->SetOutsetFocusRingDisabled(true);
focus_ring->SetHaloInset(0);
}
focus_ring->SetHasFocusPredicate(base::BindRepeating([](const View* view) {
const auto* v = views::AsViewClass<TableHeader>(view);
CHECK(v);
return v->GetHeaderRowHasFocus();
}));
}
void TableHeader::UpdateFocusState() {
views::FocusRing::Get(this)->SchedulePaint();
}
int TableHeader::GetCellVerticalPadding() const {
return table_->header_style().cell_vertical_padding.value_or(
kCellVerticalPaddingDefault);
}
int TableHeader::GetCellHorizontalPadding() const {
return table_->header_style().cell_horizontal_padding.value_or(
kCellHorizontalPaddingDefault);
}
int TableHeader::GetResizeBarVerticalPadding() const {
return table_->header_style().resize_bar_vertical_padding.value_or(
kVerticalSeparatorPaddingDefault);
}
int TableHeader::GetSeparatorHorizontalPadding() const {
return table_->header_style().separator_horizontal_padding.value_or(
kHorizontalSeparatorPaddingDefault);
}
ui::ColorId TableHeader::GetSeparatorHorizontalColorId() const {
return table_->header_style().separator_horizontal_color_id.value_or(
ui::kColorFocusableBorderUnfocused);
}
ui::ColorId TableHeader::GetSeparatorVerticalColorId() const {
return table_->header_style().separator_vertical_color_id.value_or(
ui::kColorTableHeaderSeparator);
}
ui::ColorId TableHeader::GetBackgroundColorId() const {
return table_->header_style().background_color_id.value_or(
ui::kColorTableHeaderBackground);
}
gfx::Font::Weight TableHeader::GetFontWeight() const {
return table_->header_style().font_weight.value_or(gfx::Font::Weight::NORMAL);
}
float TableHeader::GetFocusRingUpperRadius() const {
return table_->header_style().focus_ring_upper_corner_radius.value_or(
GetDefaultFocusRingRadius());
}
int TableHeader::GetSortIndicatorWidth() const {
return kSortIndicatorSize + kCellHorizontalPaddingDefault * 2;
}
void TableHeader::OnPaint(gfx::Canvas* canvas) {
ui::ColorProvider* color_provider = GetColorProvider();
const int vertical_padding = GetCellVerticalPadding();
const int horizontal_padding = GetCellHorizontalPadding();
const SkColor text_color =
color_provider->GetColor(ui::kColorTableHeaderForeground);
const SkColor separator_vertical_color =
color_provider->GetColor(GetSeparatorVerticalColorId());
const int resize_bar_vertical_padding = GetResizeBarVerticalPadding();
const int separator_horizontal_padding = GetSeparatorHorizontalPadding();
OnPaintBackground(canvas);
SkColor separator_horizontal_color =
color_provider->GetColor(GetSeparatorHorizontalColorId());
canvas->DrawSharpLine(
gfx::PointF(separator_horizontal_padding, height() - 1),
gfx::PointF(width() - separator_horizontal_padding, height() - 1),
separator_horizontal_color);
const Columns& columns = table_->visible_columns();
const int sorted_column_id = table_->sort_descriptors().empty()
? -1
: table_->sort_descriptors()[0].column_id;
const int sort_indicator_width = GetSortIndicatorWidth();
for (const auto& column : columns) {
if (column.width >= 2) {
const int separator_x = GetMirroredXInView(column.x + column.width - 1);
canvas->DrawSharpLine(
gfx::PointF(separator_x, resize_bar_vertical_padding),
gfx::PointF(separator_x, height() - resize_bar_vertical_padding),
separator_vertical_color);
}
const int x = column.x + horizontal_padding;
int width = column.width - horizontal_padding - horizontal_padding;
if (width <= 0) {
continue;
}
const int title_width =
gfx::GetStringWidth(column.column.title, font_list_);
const bool paint_sort_indicator =
(column.column.id == sorted_column_id &&
title_width + sort_indicator_width <= width);
if (paint_sort_indicator) {
width -= sort_indicator_width;
}
canvas->DrawStringRectWithFlags(
column.column.title, font_list_, text_color,
gfx::Rect(GetMirroredXWithWidthInView(x, width), vertical_padding,
width, height() - vertical_padding * 2),
TableColumnAlignmentToCanvasAlignment(
GetMirroredTableColumnAlignment(column.column.alignment)));
if (paint_sort_indicator) {
cc::PaintFlags flags;
flags.setColor(text_color);
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setAntiAlias(true);
int indicator_x = 0;
switch (column.column.alignment) {
case ui::TableColumn::LEFT:
indicator_x = x + title_width;
break;
case ui::TableColumn::CENTER:
indicator_x = x + width / 2 + title_width / 2;
break;
case ui::TableColumn::RIGHT:
indicator_x = x + width;
break;
}
const int scale = base::i18n::IsRTL() ? -1 : 1;
indicator_x += (sort_indicator_width - kSortIndicatorSize) / 2;
indicator_x = GetMirroredXInView(indicator_x);
int indicator_y = height() / 2 - kSortIndicatorSize / 2;
SkPathBuilder indicator_path;
if (table_->sort_descriptors()[0].ascending) {
indicator_path.moveTo(SkIntToScalar(indicator_x),
SkIntToScalar(indicator_y + kSortIndicatorSize));
indicator_path.lineTo(
SkIntToScalar(indicator_x + kSortIndicatorSize * scale),
SkIntToScalar(indicator_y + kSortIndicatorSize));
indicator_path.lineTo(
SkIntToScalar(indicator_x + kSortIndicatorSize / 2 * scale),
SkIntToScalar(indicator_y));
} else {
indicator_path.moveTo(SkIntToScalar(indicator_x),
SkIntToScalar(indicator_y));
indicator_path.lineTo(
SkIntToScalar(indicator_x + kSortIndicatorSize * scale),
SkIntToScalar(indicator_y));
indicator_path.lineTo(
SkIntToScalar(indicator_x + kSortIndicatorSize / 2 * scale),
SkIntToScalar(indicator_y + kSortIndicatorSize));
}
indicator_path.close();
canvas->DrawPath(indicator_path.detach(), flags);
}
}
}
gfx::Size TableHeader::CalculatePreferredSize(
const SizeBounds& ) const {
return gfx::Size(1, GetCellVerticalPadding() * 2 + font_list_.GetHeight());
}
bool TableHeader::GetNeedsNotificationWhenVisibleBoundsChange() const {
return true;
}
void TableHeader::OnVisibleBoundsChanged() {
table_->UpdateVirtualAccessibilityChildrenBounds();
}
void TableHeader::AddedToWidget() {
table_->UpdateVirtualAccessibilityChildrenBounds();
}
ui::Cursor TableHeader::GetCursor(const ui::MouseEvent& event) {
return GetResizeColumn(GetMirroredXInView(event.x())).has_value()
? ui::mojom::CursorType::kColumnResize
: View::GetCursor(event);
}
bool TableHeader::OnMousePressed(const ui::MouseEvent& event) {
if (event.IsOnlyLeftMouseButton()) {
StartResize(event);
return true;
}
return false;
}
bool TableHeader::OnMouseDragged(const ui::MouseEvent& event) {
ContinueResize(event);
return true;
}
void TableHeader::OnMouseReleased(const ui::MouseEvent& event) {
const bool was_resizing = resize_details_ != nullptr;
resize_details_.reset();
if (!was_resizing && event.IsOnlyLeftMouseButton()) {
ToggleSortOrder(event);
}
}
void TableHeader::OnMouseCaptureLost() {
if (is_resizing()) {
table_->SetVisibleColumnWidth(resize_details_->column_index,
resize_details_->initial_width);
}
resize_details_.reset();
}
void TableHeader::OnGestureEvent(ui::GestureEvent* event) {
switch (event->type()) {
case ui::EventType::kGestureTap:
if (!resize_details_.get()) {
ToggleSortOrder(*event);
}
break;
case ui::EventType::kGestureScrollBegin:
StartResize(*event);
break;
case ui::EventType::kGestureScrollUpdate:
ContinueResize(*event);
break;
case ui::EventType::kGestureScrollEnd:
resize_details_.reset();
break;
default:
return;
}
event->SetHandled();
}
void TableHeader::OnThemeChanged() {
View::OnThemeChanged();
SetBackground(CreateSolidBackground(
GetColorProvider()->GetColor(GetBackgroundColorId())));
}
void TableHeader::ResizeColumnViaKeyboard(
size_t index,
TableView::AdvanceDirection direction) {
const TableView::VisibleColumn& column = table_->GetVisibleColumn(index);
const int needed_for_title =
gfx::GetStringWidth(column.column.title, font_list_) +
2 * GetCellHorizontalPadding();
int new_width = column.width;
switch (direction) {
case TableView::AdvanceDirection::kIncrement:
new_width += kResizeKeyboardAmount;
break;
case TableView::AdvanceDirection::kDecrement:
new_width -= kResizeKeyboardAmount;
break;
}
table_->SetVisibleColumnWidth(
index, std::max({kMinColumnWidth, needed_for_title, new_width}));
}
bool TableHeader::GetHeaderRowHasFocus() const {
return table_->HasFocus() && table_->header_row_is_active();
}
gfx::Rect TableHeader::GetActiveHeaderCellBounds() const {
const std::optional<size_t> active_index =
table_->GetActiveVisibleColumnIndex();
DCHECK(active_index.has_value());
const TableView::VisibleColumn& column =
table_->GetVisibleColumn(active_index.value());
return gfx::Rect(column.x, 0, column.width, height());
}
bool TableHeader::HasFocusIndicator() const {
return table_->GetActiveVisibleColumnIndex().has_value();
}
float TableHeader::GetDefaultFocusRingRadius() const {
float thickness = FocusRing::kDefaultHaloThickness / 2.f;
if (const FocusRing* focus_ring = views::FocusRing::Get(this); focus_ring) {
thickness = focus_ring->GetHaloThickness() / 2.f;
}
return FocusRing::kDefaultCornerRadiusDp + thickness;
}
bool TableHeader::StartResize(const ui::LocatedEvent& event) {
if (is_resizing()) {
return false;
}
const std::optional<size_t> index =
GetResizeColumn(GetMirroredXInView(event.x()));
if (!index.has_value()) {
return false;
}
resize_details_ = std::make_unique<ColumnResizeDetails>();
resize_details_->column_index = index.value();
resize_details_->initial_x = event.root_location().x();
resize_details_->initial_width =
table_->GetVisibleColumn(index.value()).width;
return true;
}
void TableHeader::ContinueResize(const ui::LocatedEvent& event) {
if (!is_resizing()) {
return;
}
const int scale = base::i18n::IsRTL() ? -1 : 1;
const int delta =
scale * (event.root_location().x() - resize_details_->initial_x);
const TableView::VisibleColumn& column =
table_->GetVisibleColumn(resize_details_->column_index);
const int needed_for_title =
gfx::GetStringWidth(column.column.title, font_list_) +
2 * GetCellHorizontalPadding();
table_->SetVisibleColumnWidth(
resize_details_->column_index,
std::max({kMinColumnWidth, needed_for_title,
resize_details_->initial_width + delta}));
}
void TableHeader::ToggleSortOrder(const ui::LocatedEvent& event) {
if (table_->visible_columns().empty()) {
return;
}
const int x = GetMirroredXInView(event.x());
const std::optional<size_t> index = GetClosestVisibleColumnIndex(*table_, x);
if (!index.has_value()) {
return;
}
const TableView::VisibleColumn& column(
table_->GetVisibleColumn(index.value()));
if (x >= column.x && x < column.x + column.width && event.y() >= 0 &&
event.y() < height()) {
table_->ToggleSortOrder(index.value());
}
}
std::optional<size_t> TableHeader::GetResizeColumn(int x) const {
const Columns& columns(table_->visible_columns());
if (columns.empty()) {
return std::nullopt;
}
const std::optional<size_t> index = GetClosestVisibleColumnIndex(*table_, x);
DCHECK(index.has_value());
const TableView::VisibleColumn& column(
table_->GetVisibleColumn(index.value()));
if (index.value() > 0 && x >= column.x - kResizePadding &&
x <= column.x + kResizePadding) {
return index.value() - 1;
}
const int max_x = column.x + column.width;
return (x >= max_x - kResizePadding && x <= max_x + kResizePadding)
? std::make_optional(index.value())
: std::nullopt;
}
BEGIN_METADATA(TableHeader)
END_METADATA
}