blob: ea82fd08a29e0931c6d2e7693ed02e9927b0659f [file] [log] [blame]
// Copyright 2015 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_strip_layout.h"
#include <stddef.h>
#include <algorithm>
#include "base/logging.h"
#include "base/numerics/ranges.h"
#include "chrome/browser/ui/views/tabs/tab_animation_state.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/geometry/rect.h"
namespace {
// Inactive tabs have a smaller minimum width than the active tab. Layout has
// different behavior when inactive tabs are smaller than the active tab
// than it does when they are the same size.
enum class LayoutDomain {
// There is not enough space for inactive tabs to match the active tab's
// width.
kInactiveWidthBelowActiveWidth,
// There is enough space for inactive tabs to match the active tab's width.
kInactiveWidthEqualsActiveWidth
};
// Determines the size of each tab given information on the overall amount
// of space available relative to how much the tabs could use.
class TabSizer {
public:
TabSizer(TabSizeInfo tab_size_info,
LayoutDomain domain,
float space_fraction_available)
: tab_size_info_(tab_size_info),
domain_(domain),
space_fraction_available_(space_fraction_available) {}
int CalculateTabWidth(TabAnimationState tab) {
switch (domain_) {
case LayoutDomain::kInactiveWidthBelowActiveWidth:
return std::floor(gfx::Tween::FloatValueBetween(
space_fraction_available_, tab.GetMinimumWidth(tab_size_info_),
tab.GetLayoutCrossoverWidth(tab_size_info_)));
case LayoutDomain::kInactiveWidthEqualsActiveWidth:
return std::floor(gfx::Tween::FloatValueBetween(
space_fraction_available_,
tab.GetLayoutCrossoverWidth(tab_size_info_),
tab.GetPreferredWidth(tab_size_info_)));
}
}
// Returns true iff it's OK for this tab to be one pixel wider than
// CalculateTabWidth(|tab|).
bool TabAcceptsExtraSpace(TabAnimationState tab) {
if (space_fraction_available_ == 0.0f || space_fraction_available_ == 1.0f)
return false;
switch (domain_) {
case LayoutDomain::kInactiveWidthBelowActiveWidth:
return tab.GetMinimumWidth(tab_size_info_) <
tab.GetLayoutCrossoverWidth(tab_size_info_);
case LayoutDomain::kInactiveWidthEqualsActiveWidth:
return tab.GetLayoutCrossoverWidth(tab_size_info_) <
tab.GetPreferredWidth(tab_size_info_);
}
}
bool IsAlreadyPreferredWidth() {
return domain_ == LayoutDomain::kInactiveWidthEqualsActiveWidth &&
space_fraction_available_ == 1;
}
private:
const TabSizeInfo tab_size_info_;
const LayoutDomain domain_;
// The proportion of space requirements we can fulfill within the layout
// domain we're in.
const float space_fraction_available_;
};
// Solve layout constraints to determine how much space is available for tabs
// to use relative to how much they want to use.
TabSizer CalculateSpaceFractionAvailable(
const TabSizeInfo& tab_size_info,
const std::vector<TabAnimationState>& tabs,
int width) {
float minimum_width = 0;
float crossover_width = 0;
float preferred_width = 0;
for (TabAnimationState tab : tabs) {
// Add the tab's width, less the width of its trailing foot (which would
// be double counting).
minimum_width +=
tab.GetMinimumWidth(tab_size_info) - tab_size_info.tab_overlap;
crossover_width +=
tab.GetLayoutCrossoverWidth(tab_size_info) - tab_size_info.tab_overlap;
preferred_width +=
tab.GetPreferredWidth(tab_size_info) - tab_size_info.tab_overlap;
}
// Add back the width of the trailing foot of the last tab.
minimum_width += tab_size_info.tab_overlap;
crossover_width += tab_size_info.tab_overlap;
preferred_width += tab_size_info.tab_overlap;
LayoutDomain domain;
float space_fraction_available;
if (width < crossover_width) {
domain = LayoutDomain::kInactiveWidthBelowActiveWidth;
space_fraction_available =
(width - minimum_width) / (crossover_width - minimum_width);
} else {
domain = LayoutDomain::kInactiveWidthEqualsActiveWidth;
space_fraction_available =
preferred_width == crossover_width
? 1
: (width - crossover_width) / (preferred_width - crossover_width);
}
space_fraction_available =
base::ClampToRange(space_fraction_available, 0.0f, 1.0f);
return TabSizer(tab_size_info, domain, space_fraction_available);
}
// Because TabSizer::CalculateTabWidth() rounds down, the fractional part of tab
// widths go unused. Retroactively round up tab widths from left to right to
// use up that width.
void AllocateExtraSpace(std::vector<gfx::Rect>& bounds,
const std::vector<TabAnimationState>& tabs,
int width,
TabSizer tab_sizer) {
// Don't expand tabs if they are already at their preferred width.
if (tab_sizer.IsAlreadyPreferredWidth())
return;
const int extra_space = width - bounds.back().right();
int allocated_extra_space = 0;
for (size_t i = 0; i < tabs.size(); i++) {
TabAnimationState tab = tabs[i];
bounds[i].set_x(bounds[i].x() + allocated_extra_space);
if (allocated_extra_space < extra_space &&
tab_sizer.TabAcceptsExtraSpace(tab)) {
allocated_extra_space++;
bounds[i].set_width(bounds[i].width() + 1);
}
}
}
} // namespace
std::vector<gfx::Rect> CalculateTabBounds(
const TabSizeInfo& tab_size_info,
const std::vector<TabAnimationState>& tabs,
int width) {
DCHECK_LE(tab_size_info.min_inactive_width, tab_size_info.min_active_width);
if (tabs.empty())
return std::vector<gfx::Rect>();
TabSizer tab_sizer =
CalculateSpaceFractionAvailable(tab_size_info, tabs, width);
int next_x = 0;
std::vector<gfx::Rect> bounds;
for (TabAnimationState tab : tabs) {
const int tab_width = tab_sizer.CalculateTabWidth(tab);
bounds.push_back(
gfx::Rect(next_x, 0, tab_width, tab_size_info.standard_size.height()));
next_x += tab_width - tab_size_info.tab_overlap;
}
AllocateExtraSpace(bounds, tabs, width, tab_sizer);
return bounds;
}
std::vector<gfx::Rect> CalculatePinnedTabBounds(
const TabSizeInfo& tab_size_info,
const std::vector<TabAnimationState>& pinned_tabs) {
// Pinned tabs are always the same size regardless of the available width.
return CalculateTabBounds(tab_size_info, pinned_tabs, 0);
}