blob: b1cea739d39719ce1d3b0c604ccc1b88e1b8e0a4 [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// 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 <optional>
#include <set>
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/views/tabs/tab_strip_layout_types.h"
#include "components/tabs/public/split_tab_id.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/geometry/rect.h"
namespace {
// 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 std::vector<TabWidthConstraints>& tabs,
std::optional<int> width) {
if (!width.has_value()) {
return TabSizer(LayoutDomain::kInactiveWidthEqualsActiveWidth, 1);
}
const int tab_overlap = TabStyle::Get()->GetTabOverlap();
float minimum_width = 0;
float crossover_width = 0;
float preferred_width = 0;
for (const TabWidthConstraints& tab : tabs) {
// Add the tab's width, less the width of its trailing foot (which would
// be double counting).
minimum_width += tab.GetMinimumWidth() - tab_overlap;
crossover_width += tab.GetLayoutCrossoverWidth() - tab_overlap;
preferred_width += tab.GetPreferredWidth() - tab_overlap;
}
// Add back the width of the trailing foot of the last tab.
minimum_width += tab_overlap;
crossover_width += tab_overlap;
preferred_width += tab_overlap;
LayoutDomain domain;
float space_fraction_available;
if (width < crossover_width) {
domain = LayoutDomain::kInactiveWidthBelowActiveWidth;
// `minimum_width` may equal `crossover_width` when there is only one tab,
// that tab is active, and the tabstrip width is smaller than that width,
// which will generally happen during startup of a new window. In this case
// the layout will always be replaced before we paint, so our return value
// is irrelevant.
space_fraction_available = minimum_width == crossover_width
? 1
: (width.value() - minimum_width) /
(crossover_width - minimum_width);
} else {
domain = LayoutDomain::kInactiveWidthEqualsActiveWidth;
// `preferred_width` may equal `crossover_width` when all tabs are pinned.
// In this case tabs will have the same width regardless of the space
// available to them, so our return value is irrelevant.
space_fraction_available = preferred_width == crossover_width
? 1
: (width.value() - crossover_width) /
(preferred_width - crossover_width);
}
space_fraction_available = std::clamp(space_fraction_available, 0.0f, 1.0f);
return TabSizer(domain, space_fraction_available);
}
} // namespace
TabSizer::TabSizer(LayoutDomain domain, float space_fraction_available)
: domain_(domain), space_fraction_available_(space_fraction_available) {}
int TabSizer::CalculateTabWidth(const TabWidthConstraints& tab) const {
switch (domain_) {
case LayoutDomain::kInactiveWidthBelowActiveWidth:
return std::floor(gfx::Tween::FloatValueBetween(
space_fraction_available_, tab.GetMinimumWidth(),
tab.GetLayoutCrossoverWidth()));
case LayoutDomain::kInactiveWidthEqualsActiveWidth:
return std::floor(gfx::Tween::FloatValueBetween(
space_fraction_available_, tab.GetLayoutCrossoverWidth(),
tab.GetPreferredWidth()));
}
}
bool TabSizer::TabAcceptsExtraSpace(const TabWidthConstraints& tab) const {
if (space_fraction_available_ == 0.0f || space_fraction_available_ == 1.0f) {
return false;
}
// To avoid the two halves of a split being different widths and having
// inconsistent rendering of favicons, don't accept extra space.
if (tab.get_state().split().has_value()) {
return false;
}
switch (domain_) {
case LayoutDomain::kInactiveWidthBelowActiveWidth:
return tab.GetMinimumWidth() < tab.GetLayoutCrossoverWidth();
case LayoutDomain::kInactiveWidthEqualsActiveWidth:
return tab.GetLayoutCrossoverWidth() < tab.GetPreferredWidth();
}
}
bool TabSizer::IsAlreadyPreferredWidth() const {
return domain_ == LayoutDomain::kInactiveWidthEqualsActiveWidth &&
space_fraction_available_ == 1;
}
// 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<TabWidthConstraints>& tabs,
std::optional<int> extra_space,
TabSizer tab_sizer) {
// Don't expand tabs if they are already at their preferred width.
if (tab_sizer.IsAlreadyPreferredWidth() || !extra_space.has_value() ||
extra_space.value() <= 0) {
return;
}
int allocated_extra_space = 0;
for (size_t i = 0; i < tabs.size(); i++) {
const TabWidthConstraints& tab = tabs[i];
bounds->at(i).set_x(bounds->at(i).x() + allocated_extra_space);
if (allocated_extra_space < extra_space &&
tab_sizer.TabAcceptsExtraSpace(tab)) {
allocated_extra_space++;
bounds->at(i).set_width(bounds->at(i).width() + 1);
}
}
}
std::pair<std::vector<gfx::Rect>, LayoutDomain> CalculateTabBounds(
const std::vector<TabWidthConstraints>& tabs,
std::optional<int> width) {
if (tabs.empty()) {
return {std::vector<gfx::Rect>(),
LayoutDomain::kInactiveWidthEqualsActiveWidth};
}
TabSizer tab_sizer = CalculateSpaceFractionAvailable(tabs, width);
int next_x = 0;
std::vector<gfx::Rect> bounds;
for (const TabWidthConstraints& tab : tabs) {
const int tab_width = tab_sizer.CalculateTabWidth(tab);
bounds.emplace_back(next_x, 0, tab_width,
TabStyle::Get()->GetStandardHeight());
next_x += tab_width - TabStyle::Get()->GetTabOverlap();
}
const std::optional<int> calculated_extra_space =
width.has_value()
? std::make_optional(width.value() - bounds.back().right())
: std::nullopt;
const std::optional<int> extra_space = calculated_extra_space;
AllocateExtraSpace(&bounds, tabs, extra_space, tab_sizer);
return {bounds, tab_sizer.domain()};
}