blob: 8f21aa7edbb3b0409c68e0d850541573710f993c [file] [log] [blame]
// Copyright 2018 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_style.h"
#include <algorithm>
#include <utility>
#include "base/numerics/ranges.h"
#include "cc/paint/paint_record.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/tabs/glow_hover_controller.h"
#include "chrome/browser/ui/views/tabs/tab.h"
#include "chrome/browser/ui/views/tabs/tab_close_button.h"
#include "chrome/browser/ui/views/tabs/tab_controller.h"
#include "chrome/grit/theme_resources.h"
#include "third_party/skia/include/core/SkScalar.h"
#include "third_party/skia/include/pathops/SkPathOps.h"
#include "ui/base/material_design/material_design_controller.h"
#include "ui/base/theme_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/widget/widget.h"
namespace {
// Cache of pre-painted backgrounds for tabs.
class BackgroundCache {
public:
BackgroundCache() = default;
~BackgroundCache() = default;
// Updates the cache key with the new values.
// Returns true if any of the values changed.
bool UpdateCacheKey(float scale,
const gfx::Size& size,
SkColor active_color,
SkColor inactive_color,
SkColor stroke_color,
float stroke_thickness);
const sk_sp<cc::PaintRecord>& fill_record() const { return fill_record_; }
void set_fill_record(sk_sp<cc::PaintRecord>&& record) {
fill_record_ = record;
}
const sk_sp<cc::PaintRecord>& stroke_record() const { return stroke_record_; }
void set_stroke_record(sk_sp<cc::PaintRecord>&& record) {
stroke_record_ = record;
}
private:
// Parameters used to construct the PaintRecords.
float scale_ = 0.f;
gfx::Size size_;
SkColor active_color_ = 0;
SkColor inactive_color_ = 0;
SkColor stroke_color_ = 0;
float stroke_thickness_ = 0.f;
sk_sp<cc::PaintRecord> fill_record_;
sk_sp<cc::PaintRecord> stroke_record_;
DISALLOW_COPY_AND_ASSIGN(BackgroundCache);
};
// Tab style implementation for the GM2 refresh (Chrome 69).
class GM2TabStyle : public TabStyle {
public:
explicit GM2TabStyle(const Tab* tab);
protected:
// TabStyle:
gfx::Path GetPath(
PathType path_type,
float scale,
bool force_active = false,
RenderUnits render_units = RenderUnits::kPixels) const override;
SeparatorBounds GetSeparatorBounds(float scale) const override;
gfx::Insets GetContentsInsets() const override;
int GetStrokeThickness(bool should_paint_as_active = false) const override;
SeparatorOpacities GetSeparatorOpacities(bool for_layout) const override;
void PaintTab(gfx::Canvas* canvas, const gfx::Path& clip) const override;
private:
// Returns whether we shoould extend the hit test region for Fitts' Law.
bool ShouldExtendHitTest() const;
// Painting helper functions:
void PaintInactiveTabBackground(gfx::Canvas* canvas,
const gfx::Path& clip) const;
void PaintTabBackground(gfx::Canvas* canvas,
bool active,
int fill_id,
int y_inset,
const gfx::Path* clip) const;
void PaintTabBackgroundFill(gfx::Canvas* canvas,
bool active,
bool paint_hover_effect,
SkColor active_color,
SkColor inactive_color,
int fill_id,
int y_inset) const;
void PaintBackgroundStroke(gfx::Canvas* canvas,
bool active,
SkColor stroke_color) const;
void PaintSeparators(gfx::Canvas* canvas) const;
// Given a tab of width |width|, returns the radius to use for the corners.
static float GetTopCornerRadiusForWidth(int width);
// Scales |bounds| by scale and aligns so that adjacent tabs meet up exactly
// during painting.
static gfx::RectF ScaleAndAlignBounds(const gfx::Rect& bounds,
float scale,
int stroke_thickness);
const Tab* const tab_;
// Cache of the paint output for tab backgrounds.
mutable BackgroundCache background_active_cache_;
mutable BackgroundCache background_inactive_cache_;
};
// Thickness in DIPs of the separator painted on the left and right edges of
// the tab.
constexpr int kSeparatorThickness = 1;
// Returns the radius of the outer corners of the tab shape.
int GetCornerRadius() {
return ChromeLayoutProvider::Get()->GetCornerRadiusMetric(
views::EMPHASIS_HIGH);
}
// Returns how far from the leading and trailing edges of a tab the contents
// should actually be laid out.
int GetContentsHorizontalInsetSize() {
return GetCornerRadius() * 2;
}
// Returns the height of the separator between tabs.
int GetSeparatorHeight() {
return ui::MaterialDesignController::touch_ui() ? 24 : 20;
}
void DrawHighlight(gfx::Canvas* canvas,
const SkPoint& p,
SkScalar radius,
SkColor color) {
const SkColor colors[2] = {color, SkColorSetA(color, 0)};
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setShader(cc::PaintShader::MakeRadialGradient(
p, radius, colors, nullptr, 2, SkShader::kClamp_TileMode));
canvas->sk_canvas()->drawRect(
SkRect::MakeXYWH(p.x() - radius, p.y() - radius, radius * 2, radius * 2),
flags);
}
// Updates a target value, returning true if it changed.
template <class T>
bool UpdateValue(T* dest, const T& src) {
if (*dest == src)
return false;
*dest = src;
return true;
}
// BackgroundCache -------------------------------------------------------------
bool BackgroundCache::UpdateCacheKey(float scale,
const gfx::Size& size,
SkColor active_color,
SkColor inactive_color,
SkColor stroke_color,
float stroke_thickness) {
// Use | instead of || to prevent lazy evaluation.
return UpdateValue(&scale_, scale) | UpdateValue(&size_, size) |
UpdateValue(&active_color_, active_color) |
UpdateValue(&inactive_color_, inactive_color) |
UpdateValue(&stroke_color_, stroke_color) |
UpdateValue(&stroke_thickness_, stroke_thickness);
}
// GM2TabStyle -----------------------------------------------------------------
GM2TabStyle::GM2TabStyle(const Tab* tab) : tab_(tab) {}
gfx::Path GM2TabStyle::GetPath(PathType path_type,
float scale,
bool force_active,
RenderUnits render_units) const {
const int stroke_thickness = GetStrokeThickness(force_active);
// We'll do the entire path calculation in aligned pixels.
// TODO(dfried): determine if we actually want to use |stroke_thickness| as
// the inset in this case.
gfx::RectF aligned_bounds =
ScaleAndAlignBounds(tab_->bounds(), scale, stroke_thickness);
if (path_type == PathType::kInteriorClip) {
// When there is a separator, animate the clip to account for it, in sync
// with the separator's fading.
// TODO(pkasting): Consider crossfading the favicon instead of animating
// the clip, especially if other children get crossfaded.
const auto opacities = GetSeparatorOpacities(true);
constexpr float kChildClipPadding = 2.5f;
aligned_bounds.Inset(gfx::InsetsF(0.0f, kChildClipPadding + opacities.left,
0.0f,
kChildClipPadding + opacities.right));
}
// Calculate the corner radii. Note that corner radius is based on original
// tab width (in DIP), not our new, scaled-and-aligned bounds.
const float radius = GetTopCornerRadiusForWidth(tab_->width()) * scale;
float top_radius = radius;
float bottom_radius = radius;
// Compute |extension| as the width outside the separators. This is a fixed
// value equal to the normal corner radius.
const float extension = GetCornerRadius() * scale;
// Calculate the bounds of the actual path.
const float left = aligned_bounds.x();
const float right = aligned_bounds.right();
float tab_top = aligned_bounds.y();
float tab_left = left + extension;
float tab_right = right - extension;
// Overlap the toolbar below us so that gaps don't occur when rendering at
// non-integral display scale factors.
const float extended_bottom = aligned_bounds.bottom();
const float bottom_extension =
GetLayoutConstant(TABSTRIP_TOOLBAR_OVERLAP) * scale;
float tab_bottom = extended_bottom - bottom_extension;
// Path-specific adjustments:
const float stroke_adjustment = stroke_thickness * scale;
if (path_type == PathType::kInteriorClip) {
// Inside of the border runs |stroke_thickness| inside the outer edge.
tab_left += stroke_adjustment;
tab_right -= stroke_adjustment;
tab_top += stroke_adjustment;
top_radius -= stroke_adjustment;
} else if (path_type == PathType::kFill || path_type == PathType::kBorder) {
tab_left += 0.5f * stroke_adjustment;
tab_right -= 0.5f * stroke_adjustment;
tab_top += 0.5f * stroke_adjustment;
top_radius -= 0.5f * stroke_adjustment;
tab_bottom -= 0.5f * stroke_adjustment;
bottom_radius -= 0.5f * stroke_adjustment;
} else if (path_type == PathType::kHitTest ||
path_type == PathType::kExteriorClip) {
// Outside border needs to draw its bottom line a stroke width above the
// bottom of the tab, to line up with the stroke that runs across the rest
// of the bottom of the tab bar (when strokes are enabled).
tab_bottom -= stroke_adjustment;
bottom_radius -= stroke_adjustment;
}
const bool extend_to_top =
(path_type == PathType::kHitTest) && ShouldExtendHitTest();
// When the radius shrinks, it leaves a gap between the bottom corners and the
// edge of the tab. Make sure we account for this - and for any adjustment we
// may have made to the location of the tab!
const float corner_gap = (right - tab_right) - bottom_radius;
gfx::Path path;
if (path_type == PathType::kInteriorClip) {
// Clip path is a simple rectangle.
path.addRect(tab_left, tab_top, tab_right, tab_bottom);
} else if (path_type == PathType::kHighlight) {
// The path is a round rect inset by the focus ring thickness. The
// radius is also adjusted by the inset.
const float inset = views::PlatformStyle::kFocusHaloThickness +
views::PlatformStyle::kFocusHaloInset;
SkRRect rrect = SkRRect::MakeRectXY(
SkRect::MakeLTRB(tab_left + inset, tab_top + inset, tab_right - inset,
tab_bottom - inset),
radius - inset, radius - inset);
path.addRRect(rrect);
} else {
// We will go clockwise from the lower left. We start in the overlap region,
// preventing a gap between toolbar and tabstrip.
// TODO(dfried): verify that the we actually want to start the stroke for
// the exterior path outside the region; we might end up rendering an
// extraneous descending pixel on displays with odd scaling and nonzero
// stroke width.
// Start with the left side of the shape.
// Draw everything left of the bottom-left corner of the tab.
// ╭─────────╮
// │ Content │
// ┏━╯ ╰─┐
path.moveTo(left, extended_bottom);
path.lineTo(left, tab_bottom);
path.lineTo(left + corner_gap, tab_bottom);
// Draw the bottom-left arc.
// ╭─────────╮
// │ Content │
// ┌─╝ ╰─┐
path.arcTo(bottom_radius, bottom_radius, 0, SkPath::kSmall_ArcSize,
SkPath::kCCW_Direction, tab_left, tab_bottom - bottom_radius);
// Draw the ascender and top arc, if present.
if (extend_to_top) {
// ┎─────────╮
// ┃ Content │
// ┌─╯ ╰─┐
path.lineTo(tab_left, tab_top);
} else {
// ╔─────────╮
// ┃ Content │
// ┌─╯ ╰─┐
path.lineTo(tab_left, tab_top + top_radius);
path.arcTo(top_radius, top_radius, 0, SkPath::kSmall_ArcSize,
SkPath::kCW_Direction, tab_left + top_radius, tab_top);
}
// Draw the top crossbar and top-right curve, if present.
if (extend_to_top) {
// ┌━━━━━━━━━┑
// │ Content │
// ┌─╯ ╰─┐
path.lineTo(tab_right, tab_top);
} else {
// ╭━━━━━━━━━╗
// │ Content │
// ┌─╯ ╰─┐
path.lineTo(tab_right - top_radius, tab_top);
path.arcTo(top_radius, top_radius, 0, SkPath::kSmall_ArcSize,
SkPath::kCW_Direction, tab_right, tab_top + top_radius);
}
// Draw the descender and bottom-right arc.
// ╭─────────╮
// │ Content ┃
// ┌─╯ ╚─┐
path.lineTo(tab_right, tab_bottom - bottom_radius);
path.arcTo(bottom_radius, bottom_radius, 0, SkPath::kSmall_ArcSize,
SkPath::kCCW_Direction, right - corner_gap, tab_bottom);
// Draw everything right of the bottom-right corner of the tab.
// ╭─────────╮
// │ Content │
// ┌─╯ ╰━┓
path.lineTo(right, tab_bottom);
path.lineTo(right, extended_bottom);
if (path_type != PathType::kBorder)
path.close();
}
// Convert path to be relative to the tab origin.
gfx::PointF origin(tab_->origin());
origin.Scale(scale);
path.offset(-origin.x(), -origin.y());
// Possibly convert back to DIPs.
if (render_units == RenderUnits::kDips && scale != 1.0f)
path.transform(SkMatrix::MakeScale(1.f / scale));
return path;
}
TabStyle::SeparatorBounds GM2TabStyle::GetSeparatorBounds(float scale) const {
const gfx::RectF aligned_bounds =
ScaleAndAlignBounds(tab_->bounds(), scale, GetStrokeThickness());
const int corner_radius = GetCornerRadius() * scale;
gfx::SizeF separator_size(GetSeparatorSize());
separator_size.Scale(scale);
SeparatorBounds separator_bounds;
separator_bounds.leading =
gfx::RectF(aligned_bounds.x() + corner_radius,
aligned_bounds.y() +
(aligned_bounds.height() - separator_size.height()) / 2,
separator_size.width(), separator_size.height());
separator_bounds.trailing = separator_bounds.leading;
separator_bounds.trailing.set_x(aligned_bounds.right() -
(corner_radius + separator_size.width()));
gfx::PointF origin(tab_->bounds().origin());
origin.Scale(scale);
separator_bounds.leading.Offset(-origin.x(), -origin.y());
separator_bounds.trailing.Offset(-origin.x(), -origin.y());
return separator_bounds;
}
gfx::Insets GM2TabStyle::GetContentsInsets() const {
const int stroke_thickness = GetStrokeThickness();
const int horizontal_inset = GetContentsHorizontalInsetSize();
return gfx::Insets(
stroke_thickness, horizontal_inset,
stroke_thickness + GetLayoutConstant(TABSTRIP_TOOLBAR_OVERLAP),
horizontal_inset);
}
int GM2TabStyle::GetStrokeThickness(bool should_paint_as_active) const {
return (tab_->IsActive() || should_paint_as_active)
? tab_->controller()->GetStrokeThickness()
: 0;
}
TabStyle::SeparatorOpacities GM2TabStyle::GetSeparatorOpacities(
bool for_layout) const {
// Something should visually separate tabs from each other and any adjacent
// new tab button. Normally, active and hovered tabs draw distinct shapes
// (via different background colors) and thus need no separators, while
// background tabs need separators between them. In single-tab mode, the
// active tab has no visible shape and thus needs separators on any side with
// an adjacent new tab button. (The other sides will be faded out below.)
float leading_opacity, trailing_opacity;
if (tab_->controller()->SingleTabMode()) {
leading_opacity = trailing_opacity = 1.f;
} else if (tab_->IsActive()) {
leading_opacity = trailing_opacity = 0;
} else {
// Fade out the trailing separator while this tab or the subsequent tab is
// hovered. If the subsequent tab is active, don't consider its hover
// animation value, lest the trailing separator on this tab disappear while
// the subsequent tab is being dragged.
const float hover_value = tab_->hover_controller()->GetAnimationValue();
const Tab* subsequent_tab = tab_->controller()->GetAdjacentTab(tab_, 1);
const float subsequent_hover =
!for_layout && subsequent_tab && !subsequent_tab->IsActive()
? float{subsequent_tab->hover_controller()->GetAnimationValue()}
: 0;
trailing_opacity = 1.f - std::max(hover_value, subsequent_hover);
// The leading separator need not consider the previous tab's hover value,
// since if there is a previous tab that's hovered and not being dragged,
// it will draw atop this tab.
leading_opacity = 1.f - hover_value;
const Tab* previous_tab = tab_->controller()->GetAdjacentTab(tab_, -1);
if (tab_->IsSelected()) {
// Since this tab is selected, its shape will be visible against adjacent
// unselected tabs, so remove the separator in those cases.
if (previous_tab && !previous_tab->IsSelected())
leading_opacity = 0;
if (subsequent_tab && !subsequent_tab->IsSelected())
trailing_opacity = 0;
} else if (tab_->controller()->HasVisibleBackgroundTabShapes()) {
// Since this tab is unselected, adjacent selected tabs will normally
// paint atop it, covering the separator. But if the user drags those
// selected tabs away, the exposed region looks like the window frame; and
// since background tab shapes are visible, there should be no separator.
// TODO(pkasting): https://crbug.com/876599 When a tab is animating
// into this gap, we should adjust its separator opacities as well.
if (previous_tab && previous_tab->IsSelected())
leading_opacity = 0;
if (subsequent_tab && subsequent_tab->IsSelected())
trailing_opacity = 0;
}
}
// For the first or last tab in the strip, fade the leading or trailing
// separator based on the NTB position and how close to the target bounds this
// tab is. In the steady state, this hides separators on the opposite end of
// the strip from the NTB; it fades out the separators as tabs animate into
// these positions, after they pass by the other tabs; and it snaps the
// separators to full visibility immediately when animating away from these
// positions, which seems desirable.
const NewTabButtonPosition ntb_position =
tab_->controller()->GetNewTabButtonPosition();
const gfx::Rect target_bounds =
tab_->controller()->GetTabAnimationTargetBounds(tab_);
const int tab_width = std::max(tab_->width(), target_bounds.width());
const float target_opacity =
float{std::min(std::abs(tab_->x() - target_bounds.x()), tab_width)} /
tab_width;
// If the tab shapes are visible, never draw end separators.
const bool always_hide_separators_on_ends =
tab_->controller()->HasVisibleBackgroundTabShapes();
if (tab_->controller()->IsFirstVisibleTab(tab_) &&
(ntb_position != LEADING || always_hide_separators_on_ends))
leading_opacity = target_opacity;
if (tab_->controller()->IsLastVisibleTab(tab_) &&
(ntb_position != AFTER_TABS || always_hide_separators_on_ends))
trailing_opacity = target_opacity;
// Return the opacities in physical order, rather than logical.
if (base::i18n::IsRTL())
std::swap(leading_opacity, trailing_opacity);
return {leading_opacity, trailing_opacity};
}
void GM2TabStyle::PaintTab(gfx::Canvas* canvas, const gfx::Path& clip) const {
int active_tab_fill_id = 0;
int active_tab_y_inset = 0;
if (tab_->GetThemeProvider()->HasCustomImage(IDR_THEME_TOOLBAR)) {
active_tab_fill_id = IDR_THEME_TOOLBAR;
active_tab_y_inset = GetStrokeThickness(true);
}
if (tab_->IsActive()) {
PaintTabBackground(canvas, true /* active */, active_tab_fill_id,
active_tab_y_inset, nullptr /* clip */);
} else {
PaintInactiveTabBackground(canvas, clip);
const float throb_value = tab_->GetThrobValue();
if (throb_value > 0) {
canvas->SaveLayerAlpha(gfx::ToRoundedInt(throb_value * 0xff),
tab_->GetLocalBounds());
PaintTabBackground(canvas, true /* active */, active_tab_fill_id,
active_tab_y_inset, nullptr /* clip */);
canvas->Restore();
}
}
}
bool GM2TabStyle::ShouldExtendHitTest() const {
const views::Widget* widget = tab_->GetWidget();
return widget->IsMaximized() || widget->IsFullscreen();
}
void GM2TabStyle::PaintInactiveTabBackground(gfx::Canvas* canvas,
const gfx::Path& clip) const {
bool has_custom_image;
int fill_id = tab_->controller()->GetBackgroundResourceId(&has_custom_image);
if (!has_custom_image)
fill_id = 0;
PaintTabBackground(canvas, false /* active */, fill_id, 0,
tab_->controller()->MaySetClip() ? &clip : nullptr);
}
void GM2TabStyle::PaintTabBackground(gfx::Canvas* canvas,
bool active,
int fill_id,
int y_inset,
const gfx::Path* clip) const {
// |y_inset| is only set when |fill_id| is being used.
DCHECK(!y_inset || fill_id);
const SkColor active_color =
tab_->controller()->GetTabBackgroundColor(TAB_ACTIVE);
const SkColor inactive_color =
tab_->GetThemeProvider()->GetDisplayProperty(
ThemeProperties::SHOULD_FILL_BACKGROUND_TAB_COLOR)
? tab_->controller()->GetTabBackgroundColor(TAB_INACTIVE)
: SK_ColorTRANSPARENT;
const SkColor stroke_color =
tab_->controller()->GetToolbarTopSeparatorColor();
const bool paint_hover_effect =
!active && tab_->hover_controller()->ShouldDraw();
const float scale = canvas->image_scale();
const float stroke_thickness = GetStrokeThickness(active);
// If there is a |fill_id| we don't try to cache. This could be improved but
// would require knowing then the image from the ThemeProvider had been
// changed, and invalidating when the tab's x-coordinate or background_offset_
// changed.
//
// If |paint_hover_effect|, we don't try to cache since hover effects change
// on every invalidation and we would need to invalidate the cache based on
// the hover states.
//
// Finally, we don't cache for non-integral scale factors, since tabs draw
// with slightly different offsets so as to pixel-align the layout rect (see
// ScaleAndAlignBounds()).
if (fill_id || paint_hover_effect || (std::trunc(scale) != scale)) {
PaintTabBackgroundFill(canvas, active, paint_hover_effect, active_color,
inactive_color, fill_id, y_inset);
if (stroke_thickness > 0) {
gfx::ScopedCanvas scoped_canvas(clip ? canvas : nullptr);
if (clip)
canvas->sk_canvas()->clipPath(*clip, SkClipOp::kDifference, true);
PaintBackgroundStroke(canvas, active, stroke_color);
}
} else {
const gfx::Size& size = tab_->size();
BackgroundCache& cache =
active ? background_active_cache_ : background_inactive_cache_;
// If any of the cache key values have changed, update the cached records.
if (cache.UpdateCacheKey(scale, size, active_color, inactive_color,
stroke_color, stroke_thickness)) {
cc::PaintRecorder recorder;
{
gfx::Canvas cache_canvas(
recorder.beginRecording(size.width(), size.height()), scale);
PaintTabBackgroundFill(&cache_canvas, active, paint_hover_effect,
active_color, inactive_color, fill_id, y_inset);
cache.set_fill_record(recorder.finishRecordingAsPicture());
}
if (stroke_thickness > 0) {
gfx::Canvas cache_canvas(
recorder.beginRecording(size.width(), size.height()), scale);
PaintBackgroundStroke(&cache_canvas, active, stroke_color);
cache.set_stroke_record(recorder.finishRecordingAsPicture());
}
}
canvas->sk_canvas()->drawPicture(cache.fill_record());
if (stroke_thickness > 0) {
gfx::ScopedCanvas scoped_canvas(clip ? canvas : nullptr);
if (clip)
canvas->sk_canvas()->clipPath(*clip, SkClipOp::kDifference, true);
canvas->sk_canvas()->drawPicture(cache.stroke_record());
}
}
PaintSeparators(canvas);
}
void GM2TabStyle::PaintTabBackgroundFill(gfx::Canvas* canvas,
bool active,
bool paint_hover_effect,
SkColor active_color,
SkColor inactive_color,
int fill_id,
int y_inset) const {
const gfx::Path fill_path =
GetPath(PathType::kFill, canvas->image_scale(), active);
gfx::ScopedCanvas scoped_canvas(canvas);
const float scale = canvas->UndoDeviceScaleFactor();
canvas->ClipPath(fill_path, true);
// In the active case, always fill the tab with its bg color first in case the
// image is transparent. In the inactive case, the image is guaranteed to be
// opaque, so it's only necessary to fill the color when there's no image.
if (active || !fill_id) {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(active ? active_color : inactive_color);
canvas->DrawRect(gfx::ScaleToEnclosingRect(tab_->GetLocalBounds(), scale),
flags);
}
if (fill_id) {
gfx::ScopedCanvas scale_scoper(canvas);
canvas->sk_canvas()->scale(scale, scale);
canvas->TileImageInt(*tab_->GetThemeProvider()->GetImageSkiaNamed(fill_id),
tab_->GetMirroredX() + tab_->background_offset(), 0, 0,
y_inset, tab_->width(), tab_->height());
}
if (paint_hover_effect) {
SkPoint hover_location(
gfx::PointToSkPoint(tab_->hover_controller()->location()));
hover_location.scale(SkFloatToScalar(scale));
const SkScalar kMinHoverRadius = 16;
const SkScalar radius =
std::max(SkFloatToScalar(tab_->width() / 4.f), kMinHoverRadius);
DrawHighlight(
canvas, hover_location, radius * scale,
SkColorSetA(active_color, tab_->hover_controller()->GetAlpha()));
}
}
void GM2TabStyle::PaintBackgroundStroke(gfx::Canvas* canvas,
bool active,
SkColor stroke_color) const {
gfx::Path outer_path =
GetPath(TabStyle::PathType::kBorder, canvas->image_scale(), active);
gfx::ScopedCanvas scoped_canvas(canvas);
float scale = canvas->UndoDeviceScaleFactor();
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(stroke_color);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(GetStrokeThickness(active) * scale);
canvas->DrawPath(outer_path, flags);
}
void GM2TabStyle::PaintSeparators(gfx::Canvas* canvas) const {
const auto separator_opacities = GetSeparatorOpacities(false);
if (!separator_opacities.left && !separator_opacities.right)
return;
gfx::ScopedCanvas scoped_canvas(canvas);
const float scale = canvas->UndoDeviceScaleFactor();
TabStyle::SeparatorBounds separator_bounds = GetSeparatorBounds(scale);
const SkColor separator_base_color =
tab_->controller()->GetTabSeparatorColor();
const auto separator_color = [separator_base_color](float opacity) {
return SkColorSetA(separator_base_color,
gfx::Tween::IntValueBetween(opacity, SK_AlphaTRANSPARENT,
SK_AlphaOPAQUE));
};
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(separator_color(separator_opacities.left));
canvas->DrawRect(separator_bounds.leading, flags);
flags.setColor(separator_color(separator_opacities.right));
canvas->DrawRect(separator_bounds.trailing, flags);
}
// static
float GM2TabStyle::GetTopCornerRadiusForWidth(int width) {
// Get the width of the top of the tab by subtracting the width of the outer
// corners.
const int ideal_radius = GetCornerRadius();
const int top_width = width - ideal_radius * 2;
// To maintain a round-rect appearance, ensure at least one third of the top
// of the tab is flat.
const float radius = top_width / 3.f;
return base::ClampToRange<float>(radius, 0, ideal_radius);
}
// static
gfx::RectF GM2TabStyle::ScaleAndAlignBounds(const gfx::Rect& bounds,
float scale,
int stroke_thickness) {
// Convert to layout bounds. We must inset the width such that the right edge
// of one tab's layout bounds is the same as the left edge of the next tab's;
// this way the two tabs' separators will be drawn at the same coordinate.
gfx::RectF aligned_bounds(bounds);
const int corner_radius = GetCornerRadius();
// Note: This intentionally doesn't subtract TABSTRIP_TOOLBAR_OVERLAP from the
// bottom inset, because we want to pixel-align the bottom of the stroke, not
// the bottom of the overlap.
gfx::InsetsF layout_insets(stroke_thickness, corner_radius, stroke_thickness,
corner_radius + kSeparatorThickness);
aligned_bounds.Inset(layout_insets);
// Scale layout bounds from DIP to px.
aligned_bounds.Scale(scale);
// Snap layout bounds to nearest pixels so we get clean lines.
const float x = std::round(aligned_bounds.x());
const float y = std::round(aligned_bounds.y());
// It's important to round the right edge and not the width, since rounding
// both x and width would mean the right edge would accumulate error.
const float right = std::round(aligned_bounds.right());
const float bottom = std::round(aligned_bounds.bottom());
aligned_bounds = gfx::RectF(x, y, right - x, bottom - y);
// Convert back to full bounds. It's OK that the outer corners of the curves
// around the separator may not be snapped to the pixel grid as a result.
aligned_bounds.Inset(-layout_insets.Scale(scale));
return aligned_bounds;
}
} // namespace
// TabStyle --------------------------------------------------------------------
TabStyle::~TabStyle() = default;
// static
TabStyle* TabStyle::CreateForTab(const Tab* tab) {
return new GM2TabStyle(tab);
}
// static
int TabStyle::GetMinimumActiveWidth() {
return TabCloseButton::GetWidth() + GetContentsHorizontalInsetSize() * 2;
}
// static
int TabStyle::GetMinimumInactiveWidth() {
// Allow tabs to shrink until they appear to be 16 DIP wide excluding
// outer corners.
constexpr int kInteriorWidth = 16;
// The overlap contains the trailing separator that is part of the interior
// width; avoid double-counting it.
return kInteriorWidth - kSeparatorThickness + GetTabOverlap();
}
// static
int TabStyle::GetStandardWidth() {
// The standard tab width is 240 DIP including both separators.
constexpr int kTabWidth = 240;
// The overlap includes one separator, so subtract it here.
return kTabWidth + GetTabOverlap() - kSeparatorThickness;
}
// static
int TabStyle::GetPinnedWidth() {
constexpr int kTabPinnedContentWidth = 23;
return kTabPinnedContentWidth + GetContentsHorizontalInsetSize() * 2;
}
// static
int TabStyle::GetTabOverlap() {
return GetCornerRadius() * 2 + kSeparatorThickness;
}
// static
int TabStyle::GetDragHandleExtension(int height) {
return (height - GetSeparatorHeight()) / 2 - 1;
}
// static
gfx::Insets TabStyle::GetTabInternalPadding() {
return gfx::Insets(0, GetCornerRadius());
}
// static
gfx::Size TabStyle::GetSeparatorSize() {
return gfx::Size(kSeparatorThickness, GetSeparatorHeight());
}