blob: 6edfe9748bfa140cb270e9733aafd00b001d8f70 [file] [log] [blame]
// 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/ui/bubble/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"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Bubble's maximum width, preserves readability, ensuring that the bubble does
// not span across wide screens.
const CGFloat kBubbleMaxWidth = 375.0f;
// 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
// `anchorPoint`, 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 `anchorPoint`,
// which should be the coordinate system in which the bubble is drawn.
CGFloat LeadingDistance(CGPoint anchorPoint,
CGFloat bubbleAlignmentOffset,
BubbleAlignment alignment,
CGFloat bubbleWidth,
CGFloat boundingWidth,
bool isRTL) {
// Find `leadingOffset`, the distance from the bubble's leading edge to the
// anchor point. This depends on alignment and bubble width.
CGFloat leadingOffset;
switch (alignment) {
case BubbleAlignmentLeading:
leadingOffset = bubbleAlignmentOffset;
break;
case BubbleAlignmentCenter:
leadingOffset = bubbleWidth / 2.0f;
break;
case BubbleAlignmentTrailing:
leadingOffset = bubbleWidth - bubbleAlignmentOffset;
break;
default:
NOTREACHED() << "Invalid bubble alignment " << alignment;
break;
}
CGFloat leadingDistance;
if (isRTL) {
leadingDistance = boundingWidth - (anchorPoint.x + leadingOffset);
} else {
leadingDistance = anchorPoint.x - leadingOffset;
}
// Round the leading distance.
return round(leadingDistance);
}
// Calculate the y-coordinate of the bubble's origin based on `anchorPoint`, the
// point of the UI element the bubble is anchored at, and the bubble's arrow
// direction and size. The returned float is in the same coordinate system as
// `anchorPoint`, which should be the coordinate system in which the bubble is
// drawn.
CGFloat OriginY(CGPoint anchorPoint,
BubbleArrowDirection arrowDirection,
CGFloat bubbleHeight) {
CGFloat originY;
if (arrowDirection == BubbleArrowDirectionUp) {
originY = anchorPoint.y;
} else {
DCHECK_EQ(arrowDirection, BubbleArrowDirectionDown);
originY = anchorPoint.y - bubbleHeight;
}
// Round down the origin Y.
return floor(originY);
}
// Calculate the maximum width of the bubble such that it stays within its
// bounding coordinate space. `anchorPointX` 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. `bubbleAlignmentOffset` 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, `boundingWidth` is the width
// of the coordinate space in which the bubble is drawn, and `isRTL` is true if
// the language is RTL and `false` otherwise.
CGFloat BubbleMaxWidth(CGFloat anchorPointX,
CGFloat bubbleAlignmentOffset,
BubbleAlignment alignment,
CGFloat boundingWidth,
bool isRTL) {
CGFloat maxWidth;
switch (alignment) {
case BubbleAlignmentLeading:
if (isRTL) {
// The bubble is aligned right, and can use space to the left of the
// anchor point and within `BubbleAlignmentOffset()` from the right.
maxWidth = anchorPointX + bubbleAlignmentOffset;
} else {
// The bubble is aligned left, and can use space to the right of the
// anchor point and within `BubbleAlignmentOffset()` from the left.
maxWidth = boundingWidth - anchorPointX + bubbleAlignmentOffset;
}
break;
case BubbleAlignmentCenter:
// The width of half the bubble cannot exceed the distance from the anchor
// point to the closest edge of the superview.
maxWidth = MIN(anchorPointX, boundingWidth - anchorPointX) * 2.0f;
break;
case BubbleAlignmentTrailing:
if (isRTL) {
// The bubble is aligned left, and can use space to the right of the
// anchor point and within `BubbleAlignmentOffset()` from the left.
maxWidth = boundingWidth - anchorPointX + bubbleAlignmentOffset;
} else {
// The bubble is aligned right, and can use space to the left of the
// anchor point and within `BubbleAlignmentOffset()` from the right.
maxWidth = anchorPointX + bubbleAlignmentOffset;
}
break;
default:
NOTREACHED() << "Invalid bubble alignment " << alignment;
break;
}
// Round up the width.
return ceil(MIN(maxWidth, kBubbleMaxWidth));
}
// Calculate the maximum height of the bubble such that it stays within its
// bounding coordinate space. `anchorPointY` 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. `boundingHeight` is the height of the coordinate space in which
// the bubble is drawn.
CGFloat BubbleMaxHeight(CGFloat anchorPointY,
BubbleArrowDirection direction,
CGFloat boundingHeight) {
CGFloat maxHeight;
switch (direction) {
case BubbleArrowDirectionUp:
maxHeight = boundingHeight - anchorPointY;
break;
case BubbleArrowDirectionDown:
maxHeight = anchorPointY;
break;
default:
NOTREACHED() << "Invalid bubble direction " << direction;
break;
}
// Round up the height.
return ceil(maxHeight);
}
} // 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 targetFrame, BubbleArrowDirection arrowDirection) {
CGPoint anchorPoint;
anchorPoint.x = CGRectGetMidX(targetFrame);
if (arrowDirection == BubbleArrowDirectionUp) {
anchorPoint.y = CGRectGetMaxY(targetFrame);
return anchorPoint;
}
DCHECK_EQ(arrowDirection, BubbleArrowDirectionDown);
anchorPoint.y = CGRectGetMinY(targetFrame);
return anchorPoint;
}
// 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. `anchorPoint` is the point on the targetĀ UI element the
// bubble is anchored at in the bubble's superview's coordinate system.
// `bubbleAlignmentOffset` 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. `boundingSize` is the size
// of the superview. `isRTL` 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 anchorPoint,
CGFloat bubbleAlignmentOffset,
BubbleArrowDirection direction,
BubbleAlignment alignment,
CGSize boundingSize,
bool isRTL) {
CGFloat maxWidth = BubbleMaxWidth(anchorPoint.x, bubbleAlignmentOffset,
alignment, boundingSize.width, isRTL);
CGFloat maxHeight =
BubbleMaxHeight(anchorPoint.y, direction, boundingSize.height);
return CGSizeMake(maxWidth, maxHeight);
}
CGSize BubbleMaxSize(CGPoint anchorPoint,
CGFloat bubbleAlignmentOffset,
BubbleArrowDirection direction,
BubbleAlignment alignment,
CGSize boundingSize) {
bool isRTL = base::i18n::IsRTL();
return BubbleMaxSize(anchorPoint, bubbleAlignmentOffset, direction, alignment,
boundingSize, isRTL);
}
// Calculate the bubble's frame. `anchorPoint` is the point on the UI element
// the bubble is pointing to. `bubbleAlignmentOffset` 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). `boundingWidth` is the width of the bubble's
// superview. `isRTL` 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 anchorPoint,
CGFloat bubbleAlignmentOffset,
CGSize size,
BubbleArrowDirection direction,
BubbleAlignment alignment,
CGFloat boundingWidth,
bool isRTL) {
CGFloat leading =
LeadingDistance(anchorPoint, bubbleAlignmentOffset, alignment, size.width,
boundingWidth, isRTL);
CGFloat originY = OriginY(anchorPoint, direction, size.height);
// Use a `LayoutRect` to ensure that the bubble is mirrored in RTL contexts.
base::i18n::TextDirection textDirection =
isRTL ? base::i18n::RIGHT_TO_LEFT : base::i18n::LEFT_TO_RIGHT;
CGRect bubbleFrame = LayoutRectGetRectUsingDirection(
LayoutRectMake(leading, boundingWidth, originY, size.width, size.height),
textDirection);
return bubbleFrame;
}
CGRect BubbleFrame(CGPoint anchorPoint,
CGFloat bubbleAlignmentOffset,
CGSize size,
BubbleArrowDirection direction,
BubbleAlignment alignment,
CGFloat boundingWidth) {
bool isRTL = base::i18n::IsRTL();
return BubbleFrame(anchorPoint, bubbleAlignmentOffset, size, direction,
alignment, boundingWidth, isRTL);
}
CGFloat FloatingArrowAlignmentOffset(CGFloat boundingWidth,
CGPoint anchorPoint,
BubbleAlignment alignment) {
CGFloat alignmentOffset;
BOOL isRTL = base::i18n::IsRTL();
switch (alignment) {
case BubbleAlignmentLeading:
alignmentOffset = isRTL ? boundingWidth - anchorPoint.x : anchorPoint.x;
break;
case BubbleAlignmentCenter:
alignmentOffset = 0.0f; // value is ignored when laying out the arrow.
break;
case BubbleAlignmentTrailing:
alignmentOffset = isRTL ? anchorPoint.x : boundingWidth - anchorPoint.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, alignmentOffset),
BubbleDefaultAlignmentOffset());
}
} // namespace bubble_util