chromium/ios/chrome/browser/shared/model/web_state_list/order_controller.mm

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/shared/model/web_state_list/order_controller.h"

#import <algorithm>
#import <cstdint>
#import <set>

#import "base/check_op.h"
#import "base/containers/contains.h"
#import "ios/chrome/browser/shared/model/web_state_list/order_controller_source.h"
#import "ios/chrome/browser/shared/model/web_state_list/removing_indexes.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"

namespace {

using Range = OrderController::Range;

// Finds the index of the `n`-th child of the WebState at `opener_index` that
// is not removed. If there are less than `n` such item, returns the index of
// the last one. If there are no items returns WebStateList::kInvalidIndex.
//
// If `check_navigation_index` is true, the opener-opened relationship is
// considered as broken if the opener has navigated away since the child has
// been opened.
int FindIndexOfNthWebStateOpenedBy(const RemovingIndexes& removing_indexes,
                                   const OrderControllerSource& source,
                                   bool check_navigation_index,
                                   int opener_index,
                                   int start_index,
                                   int n) {
  DCHECK_GT(n, 0);
  const int count = source.GetCount();

  int found_index = WebStateList::kInvalidIndex;
  for (int i = 1; i < count; ++i) {
    const int index = (start_index + i) % count;
    DCHECK_NE(index, start_index);

    // An item can't be its own opener.
    if (index == opener_index) {
      continue;
    }

    if (removing_indexes.Contains(index)) {
      continue;
    }

    if (!source.IsOpenerOfItemAt(index, opener_index, check_navigation_index)) {
      continue;
    }

    found_index = index;
    if (--n == 0) {
      break;
    }
  }

  return found_index;
}

// Returns the index of the closest item from `index` in `range` that is not
// scheduled for deletion, collapsed or WebStateList::kInvalidIndex. Prefer a
// WebState after `index` over one before.
int FindClosestWebStateInRange(
    const RemovingIndexes& removing_indexes,
    int index,
    Range range,
    const std::set<int>& collapsed_group_indexes = std::set<int>()) {
  for (int i = index + 1; i < range.end; ++i) {
    if (!removing_indexes.Contains(i) && !collapsed_group_indexes.contains(i)) {
      return i;
    }
  }

  for (int i = range.begin; i < index; ++i) {
    const int j = index - 1 - (i - range.begin);
    if (!removing_indexes.Contains(j) && !collapsed_group_indexes.contains(j)) {
      return j;
    }
  }

  return WebStateList::kInvalidIndex;
}

}  // anonymous namespace

// static
OrderController::InsertionParams OrderController::InsertionParams::Automatic(
    Range range) {
  return InsertionParams{
      .desired_index = WebStateList::kInvalidIndex,
      .opener_index = WebStateList::kInvalidIndex,
      .range = range,
  };
}

// static
OrderController::InsertionParams OrderController::InsertionParams::ForceIndex(
    int desired_index,
    Range range) {
  DCHECK_NE(desired_index, WebStateList::kInvalidIndex);
  return InsertionParams{
      .desired_index = desired_index,
      .opener_index = WebStateList::kInvalidIndex,
      .range = range,
  };
}

// static
OrderController::InsertionParams OrderController::InsertionParams::WithOpener(
    int opener_index,
    Range range) {
  DCHECK_NE(opener_index, WebStateList::kInvalidIndex);
  return InsertionParams{
      .desired_index = WebStateList::kInvalidIndex,
      .opener_index = opener_index,
      .range = range,
  };
}

OrderController::OrderController(const OrderControllerSource& source)
    : source_(source) {}

OrderController::~OrderController() = default;

int OrderController::DetermineInsertionIndex(InsertionParams params) const {
  DCHECK_GE(params.range.begin, 0);
  DCHECK_LE(params.range.begin, params.range.end);
  DCHECK_LE(params.range.end, source_->GetCount());

  int desired_index = WebStateList::kInvalidIndex;
  if (params.desired_index != WebStateList::kInvalidIndex) {
    // "Forced position" has the highest priority.
    desired_index = params.desired_index;
  } else if (params.opener_index != WebStateList::kInvalidIndex) {
    // "Relative to opener" means either after the last children of opener
    // or after the opener if there is no sibling.
    const int last_child_index = FindIndexOfNthWebStateOpenedBy(
        RemovingIndexes({}), source_.get(), /* check_navigation_index */ true,
        params.opener_index, params.opener_index, INT_MAX);

    if (last_child_index != WebStateList::kInvalidIndex) {
      desired_index = last_child_index + 1;
    } else {
      desired_index = params.opener_index + 1;
    }
  }

  // In all cases, ensure that the index is in the correct range.
  if (desired_index < params.range.begin || desired_index > params.range.end) {
    return params.range.end;
  }

  return desired_index;
}

int OrderController::DetermineNewActiveIndex(
    int active_index,
    const RemovingIndexes& removing_indexes) const {
  // If there is no active element, then there will be no new active element
  // after closing the element.
  if (active_index == WebStateList::kInvalidIndex) {
    return WebStateList::kInvalidIndex;
  }

  // Otherwise the index needs to be valid.
  const int count = source_->GetCount();
  DCHECK_GE(active_index, 0);
  DCHECK_LT(active_index, count);

  // If the elements removed are the the sole remaining elements in the
  // WebStateList, clear the selection (as the list will be empty).
  if (count == removing_indexes.count()) {
    return WebStateList::kInvalidIndex;
  }

  // If the active element is not removed, then the active element won't change
  // but its index may need to be tweaked if after some of the removed elements.
  if (!removing_indexes.Contains(active_index)) {
    return active_index;
  }

  // Check if any of the "child" of the active WebState can be selected to be
  // the new active element. Prefer childs located after the active element,
  // but this may end up selecting an element before it.
  const int child_index = FindIndexOfNthWebStateOpenedBy(
      removing_indexes, source_.get(), /* check_navigation_index */ false,
      active_index, active_index, 1);
  if (child_index != WebStateList::kInvalidIndex) {
    return child_index;
  }

  const int opener_index = source_->GetOpenerOfItemAt(active_index);
  if (opener_index != WebStateList::kInvalidIndex) {
    // Check if any of the "sibling" of the active WebState can be selected
    // to be the new active element. Prefer siblings located after the active
    // element, but this may end up selecting an element before it.
    const int sibling_index = FindIndexOfNthWebStateOpenedBy(
        removing_indexes, source_.get(), /* check_navigation_index */ false,
        opener_index, active_index, 1);
    if (sibling_index != WebStateList::kInvalidIndex) {
      return sibling_index;
    }

    // If the opener is not removed, select it as the next WebState.
    if (!removing_indexes.Contains(opener_index)) {
      return opener_index;
    }
  }

  const int pinned_count = source_->GetPinnedCount();
  const bool is_pinned = active_index < pinned_count;

  // The minimum range that contains all closed WebStates.
  RemovingIndexes::Range removing_indexes_span = removing_indexes.span();
  // Get the group range of the first and the last removed indexes.
  const TabGroupRange first_group_range =
      source_->GetGroupRangeOfItemAt(removing_indexes_span.start);
  const TabGroupRange last_group_range = source_->GetGroupRangeOfItemAt(
      removing_indexes_span.start + removing_indexes_span.count - 1);

  // If all the `removing_indexes` are in the same tab group, the closest
  // WebState in that group should become the next active cell.
  if (first_group_range != TabGroupRange::InvalidRange() &&
      first_group_range == last_group_range) {
    Range range = Range{.begin = first_group_range.range_begin(),
                        .end = first_group_range.range_end()};
    int closest_index =
        FindClosestWebStateInRange(removing_indexes, active_index, range);
    // `False` when all WebStates in the tab group are begging to be closed.
    if (closest_index != WebStateList::kInvalidIndex) {
      return closest_index;
    }
  }

  // Look for the closest non-removed and non-collapsed WebState (pinned or
  // regular WebStates).
  Range range = Range{.begin = is_pinned ? 0 : pinned_count,
                      .end = is_pinned ? pinned_count : count};
  int closest_index =
      FindClosestWebStateInRange(removing_indexes, active_index, range,
                                 source_->GetCollapsedGroupIndexes());
  if (closest_index != WebStateList::kInvalidIndex) {
    return closest_index;
  }

  // Look for the closest non-removed WebState (pinned or regular WebStates).
  closest_index =
      FindClosestWebStateInRange(removing_indexes, active_index, range);

  if (closest_index == WebStateList::kInvalidIndex) {
    // If all items in the same group are removed, look for the closest
    // WebState in the other group (pinned or regular WebStates).
    closest_index = FindClosestWebStateInRange(
        removing_indexes, active_index,
        Range{.begin = is_pinned ? pinned_count : 0,
              .end = is_pinned ? count : pinned_count});
  }

  DCHECK_NE(closest_index, WebStateList::kInvalidIndex);
  return closest_index;
}