blob: 7133e06d071742e83381368e79c839ca9afcf855 [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/omnibox/ui/omnibox_container_view.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/omnibox/model/omnibox_metrics_recorder.h"
#import "ios/chrome/browser/omnibox/public/omnibox_constants.h"
#import "ios/chrome/browser/omnibox/public/omnibox_ui_features.h"
#import "ios/chrome/browser/omnibox/ui/omnibox_text_field_ios.h"
#import "ios/chrome/browser/omnibox/ui/omnibox_text_input.h"
#import "ios/chrome/browser/omnibox/ui/omnibox_text_view_ios.h"
#import "ios/chrome/browser/omnibox/ui/omnibox_thumbnail_button.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/util/animation_util.h"
#import "ios/chrome/browser/shared/ui/util/layout_guide_names.h"
#import "ios/chrome/browser/shared/ui/util/rtl_geometry.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/shared/ui/util/util_swift.h"
#import "ios/chrome/common/material_timing.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/image_util.h"
#import "ios/chrome/common/ui/util/pointer_interaction_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/grit/ios_theme_resources.h"
#import "skia/ext/skia_utils_ios.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/gfx/color_palette.h"
#import "ui/gfx/image/image.h"
namespace {
/// Width of the thumbnail.
const CGFloat kThumbnailWidth = 48;
/// Height of the thumbnail.
const CGFloat kThumbnailHeight = 40;
/// Space between the thumbnail image and the omnibox text.
const CGFloat kThumbnailImageTrailingMargin = 10;
/// Space between the leading icon and the thumbnail image.
const CGFloat kThumbnailImageLeadingMargin = 9;
// The leading image margins when presented in the lens overlay.
const CGFloat kLeadingImageLeadingMarginLensOverlay = 12;
const CGFloat kLeadingImageTrailingMarginLensOverlay = 9;
/// Size of the leading image when presented in composebox.
const CGFloat kLeadingImageSizeAIM = 22;
// The leading image margins when presented in composebox.
const CGFloat kLeadingImageLeadingMarginAIM = 7;
const CGFloat kLeadingImageTrailingMarginAIM = 11;
/// Space between the clear button and the edge of the omnibox.
const CGFloat kTextInputViewClearButtonTrailingOffset = 4;
/// The maximum number of lines for the text view before it starts scrolling.
const int kMaxLines = 3;
/// Clear button inset on all sides.
const CGFloat kClearButtonInset = 4.0f;
/// Clear button image size.
const CGFloat kClearButtonImageSize = 17.0f;
const CGFloat kClearButtonSize = 28.0f;
/// Whether the omnibox is using the text view instead of the text field.
bool UseTextView(OmniboxPresentationContext presentation_context) {
if (presentation_context == OmniboxPresentationContext::kLocationBar) {
return IsMultilineBrowserOmniboxEnabled();
} else if (presentation_context == OmniboxPresentationContext::kComposebox) {
return YES;
}
return NO;
}
/// Creates and configures the leading image view.
UIImageView* CreateLeadingImageView(
UIColor* icon_tint,
OmniboxPresentationContext presentation_context) {
UIImageView* leading_image_view = [[UIImageView alloc] init];
leading_image_view.translatesAutoresizingMaskIntoConstraints = NO;
leading_image_view.tintColor = icon_tint;
if (presentation_context == OmniboxPresentationContext::kComposebox) {
leading_image_view.contentMode = UIViewContentModeScaleAspectFit;
[NSLayoutConstraint activateConstraints:@[
[leading_image_view.widthAnchor
constraintEqualToConstant:kLeadingImageSizeAIM],
[leading_image_view.heightAnchor
constraintEqualToConstant:kLeadingImageSizeAIM],
]];
} else {
leading_image_view.contentMode = UIViewContentModeCenter;
[NSLayoutConstraint activateConstraints:@[
[leading_image_view.widthAnchor
constraintEqualToConstant:kOmniboxLeadingImageSize],
[leading_image_view.heightAnchor
constraintEqualToConstant:kOmniboxLeadingImageSize],
]];
}
return leading_image_view;
}
/// Creates and configures the thumbnail button.
OmniboxThumbnailButton* CreateThumbnailButton() {
OmniboxThumbnailButton* thumbnail_button =
[[OmniboxThumbnailButton alloc] init];
thumbnail_button.translatesAutoresizingMaskIntoConstraints = NO;
thumbnail_button.accessibilityLabel =
l10n_util::GetNSString(IDS_IOS_OMNIBOX_REMOVE_THUMBNAIL_LABEL);
thumbnail_button.accessibilityHint =
l10n_util::GetNSString(IDS_IOS_OMNIBOX_REMOVE_THUMBNAIL_HINT);
thumbnail_button.hidden = YES;
[NSLayoutConstraint activateConstraints:@[
[thumbnail_button.widthAnchor constraintEqualToConstant:kThumbnailWidth],
[thumbnail_button.heightAnchor constraintEqualToConstant:kThumbnailHeight],
]];
return thumbnail_button;
}
/// Creates and configures the clear button.
UIButton* CreateClearButton() {
UIButtonConfiguration* conf =
[UIButtonConfiguration plainButtonConfiguration];
conf.image =
DefaultSymbolWithPointSize(kXMarkCircleFillSymbol, kClearButtonImageSize);
conf.contentInsets =
NSDirectionalEdgeInsetsMake(kClearButtonInset, kClearButtonInset,
kClearButtonInset, kClearButtonInset);
UIButton* clear_button = [UIButton buttonWithType:UIButtonTypeSystem];
clear_button.translatesAutoresizingMaskIntoConstraints = NO;
clear_button.configuration = conf;
clear_button.tintColor = [UIColor colorNamed:kTextfieldPlaceholderColor];
SetA11yLabelAndUiAutomationName(clear_button, IDS_IOS_ACCNAME_CLEAR_TEXT,
@"Clear Text");
clear_button.pointerInteractionEnabled = YES;
clear_button.pointerStyleProvider =
CreateLiftEffectCirclePointerStyleProvider();
[NSLayoutConstraint activateConstraints:@[
[clear_button.widthAnchor constraintEqualToConstant:kClearButtonSize],
[clear_button.heightAnchor constraintEqualToConstant:kClearButtonSize],
]];
return clear_button;
}
} // namespace
#pragma mark - OmniboxContainerView
@interface OmniboxContainerView () <OmniboxTextViewHeightDelegate>
// Redefined as readwrite.
@property(nonatomic, strong) UIButton* clearButton;
@end
@implementation OmniboxContainerView {
/// The leading image view. Used for autocomplete icons.
UIImageView* _leadingImageView;
// The image thumnail button.
OmniboxThumbnailButton* _thumbnailButton;
// The text input view.
UIView<OmniboxTextInput>* _textInputView;
// Stores the text view for height adjustment.
OmniboxTextViewIOS* _textView;
/// The context in which the omnibox is presented.
OmniboxPresentationContext _presentationContext;
// The constraint for the textfield's leading anchor when the thumbnail is
// visible.
NSLayoutConstraint* _textInputViewLeadingToThumbnailConstraint;
// The constraint for the textfield's leading anchor when the thumbnail is
// hidden.
NSLayoutConstraint* _textInputViewLeadingToIconConstraint;
// Text input height constraint.
NSLayoutConstraint* _textInputHeightConstraint;
// The last known width of the text view, used to avoid redundant height
// calculations.
CGFloat _lastKnownTextViewWidth;
}
@synthesize heightDelegate = _heightDelegate;
#pragma mark - Public
- (instancetype)initWithFrame:(CGRect)frame
textColor:(UIColor*)textColor
textInputTint:(UIColor*)textInputTint
iconTint:(UIColor*)iconTint
presentationContext:(OmniboxPresentationContext)presentationContext {
self = [super initWithFrame:frame];
if (self) {
_presentationContext = presentationContext;
_leadingImageView = CreateLeadingImageView(iconTint, presentationContext);
self.clearButton = CreateClearButton();
[self createAndAddTextInputViewWithTextColor:textColor
textInputTint:textInputTint];
[self addSubview:_leadingImageView];
[self addSubview:self.clearButton];
// Constraints.
[_textInputView
setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
forAxis:
UILayoutConstraintAxisHorizontal];
[_textInputView setContentHuggingPriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisHorizontal];
NSLayoutAnchor* referenceCenterYAnchor =
_textInputView.viewForVerticalAlignment.centerYAnchor;
CGFloat leadingImageLeadingOffset = kOmniboxLeadingImageViewEdgeOffset;
if (_presentationContext == OmniboxPresentationContext::kLensOverlay) {
leadingImageLeadingOffset = kLeadingImageLeadingMarginLensOverlay;
} else if (_presentationContext ==
OmniboxPresentationContext::kComposebox) {
leadingImageLeadingOffset = kLeadingImageLeadingMarginAIM;
}
[NSLayoutConstraint activateConstraints:@[
[_leadingImageView.leadingAnchor
constraintEqualToAnchor:self.leadingAnchor
constant:leadingImageLeadingOffset],
[_leadingImageView.centerYAnchor
constraintEqualToAnchor:referenceCenterYAnchor],
[_textInputView.topAnchor constraintEqualToAnchor:self.topAnchor],
[_textInputView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[self.clearButton.centerYAnchor
constraintEqualToAnchor:referenceCenterYAnchor],
[self.clearButton.trailingAnchor
constraintEqualToAnchor:self.trailingAnchor
constant:-kTextInputViewClearButtonTrailingOffset],
[_textInputView.trailingAnchor
constraintEqualToAnchor:self.clearButton.leadingAnchor],
]];
// Thumbnail image view.
if (base::FeatureList::IsEnabled(kEnableLensOverlay)) {
_thumbnailButton = CreateThumbnailButton();
[self addSubview:_thumbnailButton];
[NSLayoutConstraint activateConstraints:@[
[_thumbnailButton.leadingAnchor
constraintEqualToAnchor:_leadingImageView.trailingAnchor
constant:kThumbnailImageLeadingMargin],
[_thumbnailButton.centerYAnchor
constraintEqualToAnchor:referenceCenterYAnchor]
]];
// The textInputView can be anchored to the thumbnail (if visible) or the
// leading icon (if thumbnail is hidden).
_textInputViewLeadingToThumbnailConstraint = [_textInputView.leadingAnchor
constraintEqualToAnchor:_thumbnailButton.trailingAnchor
constant:kThumbnailImageTrailingMargin];
}
CGFloat textInputViewLeadingOffset = kOmniboxTextFieldLeadingOffsetImage;
if (_presentationContext == OmniboxPresentationContext::kLensOverlay) {
textInputViewLeadingOffset = kLeadingImageTrailingMarginLensOverlay;
} else if (_presentationContext ==
OmniboxPresentationContext::kComposebox) {
textInputViewLeadingOffset = kLeadingImageTrailingMarginAIM;
}
_textInputViewLeadingToIconConstraint = [_textInputView.leadingAnchor
constraintEqualToAnchor:_leadingImageView.trailingAnchor
constant:textInputViewLeadingOffset];
// By default, the thumbnail is hidden, so the text field is anchored to the
// leading icon.
_textInputViewLeadingToIconConstraint.active = YES;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (_textView.bounds.size.width != _lastKnownTextViewWidth) {
_lastKnownTextViewWidth = _textView.bounds.size.width;
[self updateTextViewHeight];
}
}
- (void)setLeadingImage:(UIImage*)image
withAccessibilityIdentifier:(NSString*)accessibilityIdentifier {
_leadingImageView.image = image;
_leadingImageView.accessibilityIdentifier = accessibilityIdentifier;
}
- (void)setLeadingImageScale:(CGFloat)scaleValue {
_leadingImageView.transform =
CGAffineTransformMakeScale(scaleValue, scaleValue);
}
- (UIButton*)thumbnailButton {
return _thumbnailButton;
}
- (UIImage*)thumbnailImage {
return _thumbnailButton.thumbnailImage;
}
- (void)setThumbnailImage:(UIImage*)image {
[_thumbnailButton setThumbnailImage:image];
[_thumbnailButton setHidden:!image];
BOOL thumbnailVisible = ![_thumbnailButton isHidden];
if (_textInputViewLeadingToThumbnailConstraint) {
_textInputViewLeadingToThumbnailConstraint.active = thumbnailVisible;
}
_textInputViewLeadingToIconConstraint.active = !thumbnailVisible;
}
- (void)setLayoutGuideCenter:(LayoutGuideCenter*)layoutGuideCenter {
_layoutGuideCenter = layoutGuideCenter;
[_layoutGuideCenter referenceView:_leadingImageView
underName:kOmniboxLeadingImageGuide];
[_layoutGuideCenter referenceView:_textInputView
underName:kOmniboxTextFieldGuide];
}
- (void)setClearButtonHidden:(BOOL)isHidden {
self.clearButton.hidden = isHidden;
}
- (id<OmniboxTextInput>)textInput {
return _textInputView;
}
- (void)updateTextViewHeight {
if (!_textView) {
return;
}
// Recalculate textView height and update it to clip and scroll if necessary.
CGFloat verticalPadding =
_textView.textContainerInset.top + _textView.textContainerInset.bottom;
CGFloat singleLineHeight = [self singleLineHeight];
CGFloat maxHeight = (singleLineHeight * kMaxLines) + verticalPadding;
// Calculate the height of the user text.
NSAttributedString* userText = _textView.attributedUserText;
if (_textView.isPreEditing) {
userText = [[NSAttributedString alloc] initWithString:@""];
}
// Calculate the precise drawing width.
UIEdgeInsets textContainerInsets = _textView.textContainerInset;
CGFloat lineFragmentPadding = _textView.textContainer.lineFragmentPadding;
CGFloat drawingWidth = _textView.bounds.size.width -
textContainerInsets.left - textContainerInsets.right -
lineFragmentPadding * 2.0;
if (drawingWidth < 0) {
drawingWidth = 0;
}
CGFloat userTextHeight = 0;
if (userText.length > 0) {
userTextHeight = [self heightForAttributedText:userText
withDrawingWidth:drawingWidth];
}
if (!userTextHeight) {
userTextHeight = singleLineHeight;
}
CGFloat newHeight = ceilf(userTextHeight + verticalPadding);
NSInteger numberOfLines = round(userTextHeight / singleLineHeight);
[self.metricsRecorder setNumberOfLines:numberOfLines];
_textView.textContainer.maximumNumberOfLines = numberOfLines;
NSLineBreakMode defaultLineBreakMode =
[self lineBreakModeForUserText:userText];
[self updateLastLineClipping:defaultLineBreakMode];
newHeight = MIN(newHeight, maxHeight);
if (!_textInputHeightConstraint) {
_textInputHeightConstraint =
[_textView.heightAnchor constraintEqualToConstant:newHeight];
_textInputHeightConstraint.active = YES;
} else {
_textInputHeightConstraint.constant = newHeight;
}
_textView.scrollEnabled = userTextHeight > maxHeight;
[self.heightDelegate textFieldViewContaining:self didChangeHeight:newHeight];
}
/// Updates the paragraph style to clip the last line.
- (void)updateLastLineClipping:(NSLineBreakMode)defaultLineBreakMode {
NSTextStorage* textStorage = _textView.textStorage;
NSRange fullRange = NSMakeRange(0, textStorage.length);
[self applyLineBreakMode:defaultLineBreakMode
toMutableAttributedString:textStorage
inRange:fullRange];
NSLayoutManager* layoutManager = _textView.layoutManager;
NSUInteger numberOfGlyphs = [layoutManager numberOfGlyphs];
NSUInteger maxLines = _textView.textContainer.maximumNumberOfLines;
if (numberOfGlyphs == 0 || maxLines == 0) {
return;
}
// Determine the actual number of lines.
NSUInteger lineCount = 0;
NSRange lineRange;
for (NSUInteger glyphIndex = 0; glyphIndex < numberOfGlyphs; lineCount++) {
[layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex
effectiveRange:&lineRange];
glyphIndex = NSMaxRange(lineRange);
}
if (lineCount >= maxLines) {
// Find the glyph index at the start of the line to be clipped.
NSUInteger clipStartGlyphIndex = 0;
for (NSUInteger i = 0; i < maxLines - 1; i++) {
if (clipStartGlyphIndex >= numberOfGlyphs) {
break;
}
[layoutManager lineFragmentRectForGlyphAtIndex:clipStartGlyphIndex
effectiveRange:&lineRange];
clipStartGlyphIndex = NSMaxRange(lineRange);
}
// Convert the glyph index to a character index.
NSUInteger clipStartCharIndex =
[layoutManager characterIndexForGlyphAtIndex:clipStartGlyphIndex];
// Apply clipping to the last line and beyond.
if (clipStartCharIndex < textStorage.length) {
NSRange clipRange = NSMakeRange(clipStartCharIndex,
textStorage.length - clipStartCharIndex);
[self applyLineBreakMode:NSLineBreakByClipping
toMutableAttributedString:textStorage
inRange:clipRange];
}
}
}
#pragma mark - TextFieldViewContaining
- (UIView*)textFieldView {
return _textInputView;
}
#pragma mark - Private
/// Creates the text input view and adds it to the view hierarchy.
- (void)createAndAddTextInputViewWithTextColor:(UIColor*)textColor
textInputTint:(UIColor*)textInputTint {
if (UseTextView(_presentationContext)) {
OmniboxTextViewIOS* textView =
[[OmniboxTextViewIOS alloc] initWithFrame:CGRectZero
textColor:textColor
tintColor:textInputTint
presentationContext:_presentationContext];
textView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:textView];
// The placeholder must be added as a sibling to the textview. Constraints
// are handled internally in the text view.
UILabel* placeholderLabel = [[UILabel alloc] init];
placeholderLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:placeholderLabel];
textView.placeholderLabel = placeholderLabel;
textView.heightDelegate = self;
_textView = textView;
_textInputView = textView;
[self updateTextViewHeight];
} else {
OmniboxTextFieldIOS* textField =
[[OmniboxTextFieldIOS alloc] initWithFrame:CGRectZero
textColor:textColor
tintColor:textInputTint
presentationContext:_presentationContext];
// Do not use the system clear button. Use a custom view instead.
textField.clearButtonMode = UITextFieldViewModeNever;
textField.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:textField];
_textInputView = textField;
}
}
#pragma mark - OmniboxTextViewHeightDelegate
- (void)textViewContentChanged:(OmniboxTextViewIOS*)textView {
[self updateTextViewHeight];
}
#pragma mark - Private
/// Returns the height of a single line of text with the current font.
- (CGFloat)singleLineHeight {
UIFont* font = _textView.font ?: _textView.currentFont;
// Create a sample attributed string for one line.
NSAttributedString* singleLineSampler =
[[NSAttributedString alloc] initWithString:@"T"
attributes:@{NSFontAttributeName : font}];
CGSize singleLineConstraint = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX);
NSStringDrawingOptions options =
NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading;
CGRect singleLineBoundingRect =
[singleLineSampler boundingRectWithSize:singleLineConstraint
options:options
context:nil];
CGFloat measuredSingleLineHeight = ceilf(singleLineBoundingRect.size.height);
// If for some reason measurement fails, fall back to font.lineHeight.
if (measuredSingleLineHeight <= 0) {
measuredSingleLineHeight = font.lineHeight;
}
return measuredSingleLineHeight;
}
/// Computes the height needed to layout `attributedText` with `drawingWidth`.
/// The height is computed with `lineBreakModeForUserText`.
- (CGFloat)heightForAttributedText:(NSAttributedString*)attributedText
withDrawingWidth:(CGFloat)drawingWidth {
if (attributedText.length == 0) {
return 0;
}
NSMutableAttributedString* mutableString = [[NSMutableAttributedString alloc]
initWithAttributedString:attributedText];
NSLineBreakMode lineBreakMode =
[self lineBreakModeForUserText:attributedText];
NSRange fullRange = NSMakeRange(0, mutableString.length);
[self applyLineBreakMode:lineBreakMode
toMutableAttributedString:mutableString
inRange:fullRange];
CGSize constraintSize = CGSizeMake(drawingWidth, CGFLOAT_MAX);
NSStringDrawingOptions options =
NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading;
CGRect boundingRect = [mutableString boundingRectWithSize:constraintSize
options:options
context:nil];
return ceilf(boundingRect.size.height);
}
/// Returns the line break mode to apply for the text.
- (NSLineBreakMode)lineBreakModeForUserText:(NSAttributedString*)text {
BOOL containsWhitespace =
[text.string
rangeOfCharacterFromSet:[NSCharacterSet
whitespaceAndNewlineCharacterSet]]
.location != NSNotFound;
NSLineBreakMode defaultLineBreakMode = containsWhitespace
? NSLineBreakByWordWrapping
: NSLineBreakByCharWrapping;
return defaultLineBreakMode;
}
/// Applies a line break mode to an attributed string within a specific range,
/// preserving all other paragraph style properties.
///
/// @param mutableAttributedString The mutable attributed string to modify.
/// @param lineBreakMode The new NSLineBreakMode to apply.
/// @param range The range within the attributed string to apply the line break
/// mode.
- (void)applyLineBreakMode:(NSLineBreakMode)lineBreakMode
toMutableAttributedString:
(NSMutableAttributedString*)mutableAttributedString
inRange:(NSRange)range {
[mutableAttributedString
enumerateAttribute:NSParagraphStyleAttributeName
inRange:range
options:0
usingBlock:^(id value, NSRange currentRange, BOOL* stop) {
NSParagraphStyle* existingStyle = (NSParagraphStyle*)value;
// If a paragraph style exists, copy it. Otherwise, create a new
// default one.
NSMutableParagraphStyle* newParagraphStyle =
existingStyle ? [existingStyle mutableCopy]
: [[NSMutableParagraphStyle alloc] init];
// Set the desired line break mode.
newParagraphStyle.lineBreakMode = lineBreakMode;
// Re-apply the modified style to the original range.
[mutableAttributedString
addAttribute:NSParagraphStyleAttributeName
value:newParagraphStyle
range:currentRange];
}];
}
@end