chromium/ios/chrome/browser/bubble/ui_bundled/bubble_util.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/bubble/ui_bundled/bubble_util.h"

#import <ostream>

#import "base/check_op.h"
#import "base/i18n/rtl.h"
#import "base/notreached.h"
#import "ios/chrome/browser/shared/ui/util/rtl_geometry.h"
#import "ios/chrome/browser/bubble/ui_bundled/bubble_constants.h"

namespace {

// Bubble's maximum width, preserves readability, ensuring that the bubble does
// not span across wide screens.
const CGFloat kBubbleMaxWidth = 375.0f;

// Whether bubble with arrow direction `direction` is pointing left.
BOOL IsArrowPointingLeft(BubbleArrowDirection direction, bool is_rtl) {
  return direction ==
         (is_rtl ? BubbleArrowDirectionTrailing : BubbleArrowDirectionLeading);
}

// Calculate the distance from the bubble's leading edge to the leading edge of
// its bounding coordinate system. In LTR contexts, the returned float is the
// x-coordinate of the bubble's origin. This calculation is based on
// `anchor_point`, which is the point of the target UI element the bubble is
// anchored at, and the bubble's alignment offset, alignment, direction, and
// size. The returned float is in the same coordinate system as `anchor_point`,
// which should be the coordinate system in which the bubble is drawn.
CGFloat LeadingDistance(CGPoint anchor_point,
                        BubbleArrowDirection arrow_direction,
                        CGFloat bubble_alignment_offset,
                        BubbleAlignment alignment,
                        CGFloat bubble_width,
                        CGFloat bounding_width,
                        bool is_rtl) {
  // Find `leading_offset`, the distance from the bubble's leading edge to the
  // anchor point. This depends on alignment and bubble width.
  CGFloat leading_offset;
  switch (arrow_direction) {
    case BubbleArrowDirectionUp:
    case BubbleArrowDirectionDown:
      switch (alignment) {
        case BubbleAlignmentTopOrLeading:
          leading_offset = bubble_alignment_offset;
          break;
        case BubbleAlignmentCenter:
          leading_offset = bubble_width / 2.0f;
          break;
        case BubbleAlignmentBottomOrTrailing:
          leading_offset = bubble_width - bubble_alignment_offset;
          break;
        default:
          NOTREACHED_IN_MIGRATION() << "Invalid bubble alignment " << alignment;
          break;
      }
      break;
    case BubbleArrowDirectionLeading:
      leading_offset = 0;
      break;
    case BubbleArrowDirectionTrailing:
      leading_offset = bubble_width;
      break;
  }
  CGFloat leading_distance;
  if (is_rtl) {
    leading_distance = bounding_width - (anchor_point.x + leading_offset);
  } else {
    leading_distance = anchor_point.x - leading_offset;
  }
  // Round the leading distance.
  return round(leading_distance);
}

// Calculate the y-coordinate of the bubble's origin based on `anchor_point`,
// the point of the UI element the bubble is anchored at, and the bubble's
// alignment offset, alignment, direction and size. The returned float is in the
// same coordinate system as `anchor_point`, which should be the coordinate
// system in which the bubble is drawn.
CGFloat OriginY(CGPoint anchor_point,
                BubbleArrowDirection arrow_direction,
                CGFloat bubble_alignment_offset,
                BubbleAlignment alignment,
                CGFloat bubble_height) {
  CGFloat origin_y;
  switch (arrow_direction) {
    case BubbleArrowDirectionUp:
      origin_y = anchor_point.y;
      break;
    case BubbleArrowDirectionDown:
      origin_y = anchor_point.y - bubble_height;
      break;
    case BubbleArrowDirectionLeading:
    case BubbleArrowDirectionTrailing:
      switch (alignment) {
        case BubbleAlignmentTopOrLeading:
          origin_y = anchor_point.y - bubble_alignment_offset;
          break;
        case BubbleAlignmentCenter:
          origin_y = anchor_point.y - bubble_height / 2;
          break;
        case BubbleAlignmentBottomOrTrailing:
          origin_y = anchor_point.y - (bubble_height - bubble_alignment_offset);
          break;
      }
      break;
  }
  // Round down the origin Y.
  return floor(origin_y);
}

// Calculate the maximum width of the bubble such that it stays within its
// bounding coordinate space. `anchor_point_x` is the x-coordinate of the point
// on the target UI element the bubble is anchored at. It is in the coordinate
// system in which the bubble is drawn. `direction` is the direction the bubble
// arrow points to. `bubble_alignment_offset` is the distance from the leading
// edge of the bubble to the anchor point if leading aligned, and from the
// trailing edge of the bubble to the anchor point if trailing aligned.
// `alignment` is the bubble's alignment, `bounding_width` is the width of the
// coordinate space in which the bubble is drawn, and `is_rtl` is true if the
// language is RTL and `false` otherwise.
CGFloat BubbleMaxWidth(CGFloat anchor_point_x,
                       BubbleArrowDirection direction,
                       CGFloat bubble_alignment_offset,
                       BubbleAlignment alignment,
                       CGFloat bounding_width,
                       bool is_rtl) {
  CGFloat max_width;
  // Space on the left of the anchor point.
  CGFloat distance_to_left_edge = anchor_point_x;
  // Space on the right of the anchor point.
  CGFloat distance_to_right_edge = bounding_width - anchor_point_x;
  switch (direction) {
    case BubbleArrowDirectionUp:
    case BubbleArrowDirectionDown:
      switch (alignment) {
        case BubbleAlignmentTopOrLeading:
          // The bubble can use the space from the anchor point to the trailing
          // edge.
          max_width =
              (is_rtl ? distance_to_left_edge : distance_to_right_edge) +
              bubble_alignment_offset;
          break;
        case BubbleAlignmentCenter:
          // The width of half the bubble cannot exceed the distance from the
          // anchor point to the closest edge of the superview.
          max_width = MIN(distance_to_left_edge, distance_to_right_edge) * 2.0f;
          break;
        case BubbleAlignmentBottomOrTrailing:
          // The bubble can use the space from the anchor point to the leading
          // edge.
          max_width =
              (is_rtl ? distance_to_right_edge : distance_to_left_edge) +
              bubble_alignment_offset;
          break;
        default:
          NOTREACHED_IN_MIGRATION() << "Invalid bubble alignment " << alignment;
          break;
      }
      break;
    case BubbleArrowDirectionLeading:
    case BubbleArrowDirectionTrailing:
      if (IsArrowPointingLeft(direction, is_rtl)) {
        max_width = distance_to_right_edge;
      } else {
        max_width = distance_to_left_edge;
      }
      break;
  }
  // Round up the width.
  return ceil(MIN(max_width, kBubbleMaxWidth));
}

// Calculate the maximum height of the bubble such that it stays within its
// bounding coordinate space. `anchor_point_y` is the y-coordinate of the point
// on the target UI element the bubble is anchored at. It is in the coordinate
// system in which the bubble is drawn. `direction` is the direction the arrow
// is pointing. `bubble_alignment_offset` is the distance from the leading or
// top edge of the bubble to the anchor point if leading aligned, and from the
// bottom or trailing edge of the bubble to the anchor point if trailing
// aligned. `alignment` is the bubble's alignment, `bounding_height` is the
// height of the coordinate space in which the bubble is drawn.
CGFloat BubbleMaxHeight(CGFloat anchor_point_y,
                        BubbleArrowDirection direction,
                        CGFloat bubble_alignment_offset,
                        BubbleAlignment alignment,
                        CGFloat bounding_height) {
  CGFloat max_height;
  // Space on the top of the anchor point.
  CGFloat distance_to_top_edge = anchor_point_y;
  // Space on the bottom of the anchor point.
  CGFloat distance_to_bottom_edge = bounding_height - anchor_point_y;
  switch (direction) {
    case BubbleArrowDirectionUp:
      max_height = distance_to_bottom_edge;
      break;
    case BubbleArrowDirectionDown:
      max_height = distance_to_top_edge;
      break;
    case BubbleArrowDirectionLeading:
    case BubbleArrowDirectionTrailing:
      switch (alignment) {
        case BubbleAlignmentTopOrLeading:
          // The bubble can use the space from the anchor point to the bottom
          // edge.
          max_height = distance_to_bottom_edge + bubble_alignment_offset;
          break;
        case BubbleAlignmentCenter:
          // The height of half the bubble cannot exceed the distance from the
          // anchor point to the closest edge of the superview.
          max_height =
              MIN(distance_to_top_edge, distance_to_bottom_edge) * 2.0f;
          break;
        case BubbleAlignmentBottomOrTrailing:
          // The bubble can use the space from the anchor point to the top
          // edge.
          max_height = distance_to_top_edge + bubble_alignment_offset;
          break;
      }
      break;
  }
  // Round up the height.
  return ceil(max_height);
}

}  // namespace

namespace bubble_util {

CGFloat BubbleDefaultAlignmentOffset() {
  // This is used to replace a constant that would change based on the flag.
  return 29;
}

CGPoint AnchorPoint(CGRect target_frame, BubbleArrowDirection arrow_direction) {
  CGPoint anchor_point;
  bool is_rtl = base::i18n::IsRTL();
  switch (arrow_direction) {
    case BubbleArrowDirectionUp:
      anchor_point.x = CGRectGetMidX(target_frame);
      anchor_point.y = CGRectGetMaxY(target_frame);
      break;
    case BubbleArrowDirectionDown:
      anchor_point.x = CGRectGetMidX(target_frame);
      anchor_point.y = CGRectGetMinY(target_frame);
      break;
    case BubbleArrowDirectionLeading:
    case BubbleArrowDirectionTrailing:
      anchor_point.x = IsArrowPointingLeft(arrow_direction, is_rtl)
                           ? CGRectGetMaxX(target_frame)
                           : CGRectGetMinX(target_frame);
      anchor_point.y = CGRectGetMidY(target_frame);
      break;
  }
  return anchor_point;
}

// Calculate the maximum size of the bubble such that it stays within its
// superview's bounding coordinate space and does not overlap the other side of
// the anchor point. `anchor_point` is the point on the targetĀ UI element the
// bubble is anchored at in the bubble's superview's coordinate system.
// `bubble_alignment_offset` is the distance from the leading edge of the bubble
// to the anchor point if leading aligned, and from the trailing edge of the
// bubble to the anchor point if trailing aligned. `direction` is the bubble's
// direction. `alignment` is the bubble's alignment. `bounding_size` is the size
// of the superview. `is_rtl` is `true` if the coordinates are in right-to-left
// language coordinates and `false` otherwise. This method is unit tested so it
// cannot be in the above anonymous namespace.
CGSize BubbleMaxSize(CGPoint anchor_point,
                     CGFloat bubble_alignment_offset,
                     BubbleArrowDirection direction,
                     BubbleAlignment alignment,
                     CGSize bounding_size,
                     bool is_rtl) {
  CGFloat max_width =
      BubbleMaxWidth(anchor_point.x, direction, bubble_alignment_offset,
                     alignment, bounding_size.width, is_rtl);
  CGFloat max_height =
      BubbleMaxHeight(anchor_point.y, direction, bubble_alignment_offset,
                      alignment, bounding_size.height);
  return CGSizeMake(max_width, max_height);
}

CGSize BubbleMaxSize(CGPoint anchor_point,
                     CGFloat bubble_alignment_offset,
                     BubbleArrowDirection direction,
                     BubbleAlignment alignment,
                     CGSize bounding_size) {
  bool is_rtl = base::i18n::IsRTL();
  return BubbleMaxSize(anchor_point, bubble_alignment_offset, direction,
                       alignment, bounding_size, is_rtl);
}

// Calculate the bubble's frame. `anchor_point` is the point on the UI element
// the bubble is pointing to. `bubble_alignment_offset` is the distance from the
// leading edge of the bubble to the anchor point if leading aligned, and from
// the trailing edge of the bubble to the anchor point if trailing aligned.
// `size` is the size of the bubble. `direction` is the direction the bubble's
// arrow is pointing. `alignment` is the alignment of the anchor (either
// leading, centered, or trailing). `bounding_width` is the width of the
// bubble's superview. `is_rtl` is `true` if the coordinates are in
// right-to-left language coordinates and `false` otherwise. This method is unit
// tested so it cannot be in the above anonymous namespace.
CGRect BubbleFrame(CGPoint anchor_point,
                   CGFloat bubble_alignment_offset,
                   CGSize size,
                   BubbleArrowDirection direction,
                   BubbleAlignment alignment,
                   CGFloat bounding_width,
                   bool is_rtl) {
  CGFloat leading =
      LeadingDistance(anchor_point, direction, bubble_alignment_offset,
                      alignment, size.width, bounding_width, is_rtl);
  CGFloat origin_y = OriginY(anchor_point, direction, bubble_alignment_offset,
                             alignment, size.height);
  // Use a `LayoutRect` to ensure that the bubble is mirrored in RTL contexts.
  base::i18n::TextDirection textDirection =
      is_rtl ? base::i18n::RIGHT_TO_LEFT : base::i18n::LEFT_TO_RIGHT;
  CGRect bubble_frame = LayoutRectGetRectUsingDirection(
      LayoutRectMake(leading, bounding_width, origin_y, size.width,
                     size.height),
      textDirection);
  return bubble_frame;
}

CGRect BubbleFrame(CGPoint anchor_point,
                   CGFloat bubble_alignment_offset,
                   CGSize size,
                   BubbleArrowDirection direction,
                   BubbleAlignment alignment,
                   CGFloat bounding_width) {
  bool is_rtl = base::i18n::IsRTL();
  return BubbleFrame(anchor_point, bubble_alignment_offset, size, direction,
                     alignment, bounding_width, is_rtl);
}

CGFloat FloatingArrowAlignmentOffset(CGFloat bounding_width,
                                     CGPoint anchor_point,
                                     BubbleAlignment alignment) {
  CGFloat alignment_offset;
  BOOL is_rtl = base::i18n::IsRTL();
  // TODO(crbug.com/40276959): Leading and trailing direction.
  switch (alignment) {
    case BubbleAlignmentTopOrLeading:
      alignment_offset =
          is_rtl ? bounding_width - anchor_point.x : anchor_point.x;
      break;
    case BubbleAlignmentCenter:
      alignment_offset = 0.0f;  // value is ignored when laying out the arrow.
      break;
    case BubbleAlignmentBottomOrTrailing:
      alignment_offset =
          is_rtl ? anchor_point.x : bounding_width - anchor_point.x;
      break;
  }
  // Alignment offset must be greater than `BubbleDefaultAlignmentOffset` to
  // make sure the arrow is in the frame of the background of the bubble. The
  // maximum is set to the middle of the bubble so the arrow stays close to the
  // leading edge when using a leading alignment.
  return MAX(MIN(kBubbleMaxWidth / 2, alignment_offset),
             BubbleDefaultAlignmentOffset());
}

}  // namespace bubble_util