Files
brave-core/components/tabs/impl/tree_tab_node.cc
T
Sangwoo Ko ce0f439a19 [Tree Tabs] Support tab groups in tree tab strip (#34826)
* Support tab groups in tree tab strip

Integrate tab groups with the tree tab strip so that groups are represented
as a single tree node wrapping the group (not one tree node per tab). Tabs
inside a group remain direct children of the group collection.

Collection and delegate:
- BraveTreeTabStripCollectionDelegate: When adding a tab with new_group_id,
  add to collection without wrapping in a tree node; the group wraps it.
  MoveTabsRecursive: handle empty tab_indices (no-op when moving within same
  group), and route move-out-of-group and move-into-group to new helpers.
- MoveTabsIntoGroup: unwrap tabs from tree nodes (or detach from other
  groups), add to target group; when group is detached (new group), wrap
  group in a TreeTabNode and attach at the correct tree position.
- MoveTabsOutOfGroup: move tabs from TabGroupTabCollection back into the
  tree by wrapping each in a tree node at the destination index.
- BraveTabStripCollection: add PopDetachedGroupCollectionForDelegate and
  GetTreeTabNodeIdForGroup; chromium_src TabStripCollection gains virtual
  GetTreeTabNodeIdForGroup. BraveTabStripCollectionDelegate implements
  GetTreeTabNodeIdForGroup.

TreeTabNodeTabCollection:
- BuildTreeTabs: wrap entire groups in one tree node (processed_groups set);
  grouped tabs stay as direct children of the group.
- New constructor that wraps a TabGroupTabCollection (for creating a group
  in tree mode). Single-tab constructor allows null for GetEmptyTreeTabNode
  when a tab was moved into a group and the view still holds the old node id.

Model, controller, and UI:
- BraveTabStripModel::GetTreeTabNodeIdForGroup returns the tree node id for
  a group (or nullptr if tree tabs off). Wired through controller and
  BraveTabStripCollection.
- BraveBrowserTabStripController::GetTreeTabNode returns GetEmptyTreeTabNode()
  when the node is null (e.g. tab just moved into group, before
  TabGroupedStateChanged/AddTabToGroup updates the view).
- OnTreeTabChanged: handle tab index kNoTab and detached state during group
  creation; avoid double-clearing when AddTabToGroup will clear the node id.
- BraveTabStrip::AddTabToGroup override sets the tab's tree_tab_node from
  GetTreeTabNodeIdForGroup when adding to a group in tree mode.
2026-03-24 11:18:48 +01:00

208 lines
7.0 KiB
C++

// Copyright (c) 2025 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
#include "brave/components/tabs/public/tree_tab_node.h"
#include <variant>
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/weak_ptr.h"
#include "brave/components/tabs/public/tree_tab_node_id.h"
#include "brave/components/tabs/public/tree_tab_node_tab_collection.h"
#include "components/tabs/public/split_tab_data.h"
#include "components/tabs/public/tab_interface.h"
namespace tabs {
const TreeTabNode& TreeTabNode::GetEmptyTreeTabNode() {
static base::NoDestructor<TreeTabNodeTabCollection>
empty_tree_tab_node_tab_collection(
tree_tab::TreeTabNodeId::GenerateNew(),
base::WrapUnique<TabInterface>(nullptr), base::DoNothing(),
base::DoNothing());
return empty_tree_tab_node_tab_collection->node();
}
TreeTabNode::TreeTabNode(TreeTabNodeTabCollection& collection,
const tree_tab::TreeTabNodeId& id)
: collection_(collection), id_(id) {}
int TreeTabNode::GetTreeHeight() const {
return collection_->GetTopLevelAncestor()->node().height();
}
std::vector<const TabInterface*> TreeTabNode::GetTabs() const {
const auto& value = collection_->current_value();
if (!value.has_value()) {
return {};
}
if (const base::WeakPtr<TabInterface>* tab_ptr =
std::get_if<base::WeakPtr<TabInterface>>(&*value)) {
if (const TabInterface* tab = tab_ptr->get()) {
return {tab};
}
return {};
}
if (const raw_ptr<SplitTabCollection>* split_ptr =
std::get_if<raw_ptr<SplitTabCollection>>(&*value)) {
SplitTabCollection* split = split_ptr->get();
if (split && split->data()) {
std::vector<const TabInterface*> result;
for (TabInterface* tab : split->data()->ListTabs()) {
result.push_back(tab);
}
return result;
}
return {};
}
if (const raw_ptr<TabGroupTabCollection>* group_ptr =
std::get_if<raw_ptr<TabGroupTabCollection>>(&*value)) {
TabGroupTabCollection* group = group_ptr->get();
if (group) {
std::vector<const TabInterface*> result;
for (TabInterface* tab : group->GetTabsRecursive()) {
result.push_back(tab);
}
return result;
}
return {};
}
return {};
}
std::optional<tree_tab::TreeTabNodeId>
TreeTabNode::GetClosestCollapsedAncestorId() const {
const TabCollection* current = collection_->GetParentCollection();
while (current && current->type() == TabCollection::Type::TREE_NODE) {
const auto* parent_tree =
static_cast<const TreeTabNodeTabCollection*>(current);
if (parent_tree->node().collapsed()) {
return parent_tree->node().id();
}
current = current->GetParentCollection();
}
return std::nullopt;
}
void TreeTabNode::CollectDescendantIds(
std::vector<tree_tab::TreeTabNodeId>& out) {
for (const auto& child : collection_->GetTreeNodeChildren()) {
if (std::holds_alternative<tabs::TabCollection*>(child)) {
TabCollection* collection = std::get<tabs::TabCollection*>(child);
if (collection->type() != TabCollection::Type::TREE_NODE) {
continue;
}
auto* child_tree = static_cast<TreeTabNodeTabCollection*>(collection);
const tree_tab::TreeTabNodeId& child_id = child_tree->node().id();
out.push_back(child_id);
child_tree->node().CollectDescendantIds(out);
}
}
}
void TreeTabNode::CollectUncollapsedDescendantIds(
std::vector<tree_tab::TreeTabNodeId>& out) {
for (const auto& child : collection_->GetTreeNodeChildren()) {
if (std::holds_alternative<tabs::TabCollection*>(child)) {
TabCollection* collection = std::get<tabs::TabCollection*>(child);
if (collection->type() != TabCollection::Type::TREE_NODE) {
continue;
}
auto* child_tree = static_cast<TreeTabNodeTabCollection*>(collection);
TreeTabNode& child_node = child_tree->node();
out.push_back(child_node.id());
if (!child_node.collapsed()) {
child_node.CollectUncollapsedDescendantIds(out);
}
}
}
}
int TreeTabNode::CalculateLevelAndHeightRecursively(
base::PassKey<TreeTabNodeTabCollection> pass_key) {
return CalculateLevelAndHeightRecursivelyImpl();
}
void TreeTabNode::OnChildHeightChanged(
base::PassKey<TreeTabNodeTabCollection> pass_key) {
OnChildHeightChangedImpl();
}
int TreeTabNode::CalculateLevelAndHeightRecursivelyImpl() {
auto* parent_collection = collection_->GetParentCollection();
if (!parent_collection ||
parent_collection->type() != TabCollection::Type::TREE_NODE) {
// If there's no parent or the parent is not a tree node, this is the root.
level_ = 0;
} else {
auto* parent_tree_node =
static_cast<TreeTabNodeTabCollection*>(parent_collection);
level_ = parent_tree_node->node().level_ + 1;
}
int max_height = std::numeric_limits<int>::min();
for (const auto& child : collection_->GetTreeNodeChildren()) {
if (std::holds_alternative<tabs::TabCollection*>(child)) {
auto* collection = std::get<tabs::TabCollection*>(child);
if (collection->type() != TabCollection::Type::TREE_NODE) {
// If non-tree node child, e.g. split or group, height would be 0 for
// this node as it should not have descendants.
continue;
}
max_height = std::max(max_height,
static_cast<TreeTabNodeTabCollection*>(collection)
->node()
.CalculateLevelAndHeightRecursivelyImpl() +
1);
}
}
height_ = (max_height == std::numeric_limits<int>::min()) ? 0 : max_height;
return height_;
}
void TreeTabNode::OnChildHeightChangedImpl() {
// Update height of this node.
int max_height = std::numeric_limits<int>::min();
for (const auto& child : collection_->GetTreeNodeChildren()) {
if (std::holds_alternative<tabs::TabCollection*>(child)) {
auto* collection = std::get<tabs::TabCollection*>(child);
if (collection->type() != TabCollection::Type::TREE_NODE) {
// If non-tree node child, height would be 1 for this node
max_height = std::max(max_height, 1);
continue;
}
max_height = std::max(
max_height,
static_cast<TreeTabNodeTabCollection*>(collection)->node().height_ +
1);
}
}
auto new_height =
(max_height == std::numeric_limits<int>::min()) ? 0 : max_height;
if (new_height == height_) {
return;
}
height_ = new_height;
if (auto* parent_collection = collection_->GetParentCollection();
parent_collection &&
parent_collection->type() == TabCollection::Type::TREE_NODE) {
static_cast<TreeTabNodeTabCollection*>(parent_collection)
->node()
.OnChildHeightChangedImpl();
}
}
} // namespace tabs