// 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