| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/views/omnibox/omnibox_match_cell_view.h" |
| |
| #include <algorithm> |
| #include <optional> |
| |
| #include "base/metrics/field_trial_params.h" |
| #include "chrome/browser/ui/color/chrome_color_id.h" |
| #include "chrome/browser/ui/layout_constants.h" |
| #include "chrome/browser/ui/omnibox/omnibox_theme.h" |
| #include "chrome/browser/ui/views/chrome_layout_provider.h" |
| #include "chrome/browser/ui/views/chrome_typography.h" |
| #include "chrome/browser/ui/views/omnibox/omnibox_popup_view_views.h" |
| #include "chrome/browser/ui/views/omnibox/omnibox_text_view.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chrome/grit/theme_resources.h" |
| #include "components/omnibox/browser/actions/omnibox_action.h" |
| #include "components/omnibox/browser/autocomplete_match.h" |
| #include "components/omnibox/browser/autocomplete_match_type.h" |
| #include "components/omnibox/browser/autocomplete_provider.h" |
| #include "components/omnibox/browser/omnibox_field_trial.h" |
| #include "components/omnibox/browser/suggestion_answer.h" |
| #include "components/omnibox/browser/vector_icons.h" |
| #include "components/omnibox/common/omnibox_feature_configs.h" |
| #include "components/omnibox/common/omnibox_features.h" |
| #include "content/public/common/color_parser.h" |
| #include "skia/ext/image_operations.h" |
| #include "third_party/omnibox_proto/answer_type.pb.h" |
| #include "third_party/omnibox_proto/rich_answer_template.pb.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/metadata/metadata_header_macros.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/pointer/touch_ui_controller.h" |
| #include "ui/color/color_provider.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/geometry/size_f.h" |
| #include "ui/gfx/geometry/skia_conversions.h" |
| #include "ui/gfx/image/canvas_image_source.h" |
| #include "ui/gfx/image/image_skia_operations.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/gfx/render_text.h" |
| #include "ui/views/controls/image_view.h" |
| #include "ui/views/controls/link.h" |
| #include "ui/views/layout/layout_provider.h" |
| #include "ui/views/layout/layout_types.h" |
| #include "ui/views/style/typography.h" |
| |
| namespace { |
| |
| // The edge length of the favicon, answer icon, and entity backgrounds. |
| static constexpr int kUniformRowHeightIconSize = 28; |
| |
| // The gap between the left|right edge of the IPH background to the left|right |
| // edge of the text bounds. Does not apply to the left side of IPHs with icons, |
| // since the text will have to be further right to accommodate the icons. |
| static constexpr int kIphTextIndent = 14; |
| |
| // The extra space added to the left-side inset of the toolbelt suggestion. |
| static constexpr int kToolbeltTextInsetLeft = 12; |
| |
| // The right-side inset of the toolbelt suggestion. |
| static constexpr int kToolbeltTextInsetRight = 8; |
| |
| // The radius of the rounded square backgrounds of icons, answers, and entities. |
| static constexpr int kIconAndImageCornerRadius = 4; |
| |
| // Size of weather icon with a round square background. |
| static constexpr int kWeatherImageSize = 24; |
| |
| // Size of the weather's round square background. |
| static constexpr int kWeatherBackgroundSize = 28; |
| |
| // The vertical gap between the contents and descriptions for multiline answers. |
| static constexpr int kHistoryEmbeddingAnswerGap = 3; |
| |
| // The vertical padding above and below the description for multiline answers. |
| // Chosen so that the vertical distance between the bottom of the answer text |
| // and the bottom of the hover fill is the same as for other, 1-line matches. |
| static constexpr int kHistoryEmbeddingAnswerBottomPadding = 8; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // PlaceholderImageSource: |
| |
| class PlaceholderImageSource : public gfx::CanvasImageSource { |
| public: |
| PlaceholderImageSource(const gfx::Size& canvas_size, SkColor color); |
| PlaceholderImageSource(const PlaceholderImageSource&) = delete; |
| PlaceholderImageSource& operator=(const PlaceholderImageSource&) = delete; |
| ~PlaceholderImageSource() override = default; |
| |
| // gfx::CanvasImageSource: |
| void Draw(gfx::Canvas* canvas) override; |
| |
| private: |
| const SkColor color_; |
| }; |
| |
| PlaceholderImageSource::PlaceholderImageSource(const gfx::Size& canvas_size, |
| SkColor color) |
| : gfx::CanvasImageSource(canvas_size), color_(color) {} |
| |
| void PlaceholderImageSource::Draw(gfx::Canvas* canvas) { |
| cc::PaintFlags flags; |
| flags.setAntiAlias(true); |
| flags.setStyle(cc::PaintFlags::kFill_Style); |
| flags.setColor(color_); |
| const int corner_radius = views::LayoutProvider::Get()->GetCornerRadiusMetric( |
| views::Emphasis::kMedium); |
| canvas->sk_canvas()->drawRoundRect(gfx::RectToSkRect(gfx::Rect(size())), |
| corner_radius, corner_radius, flags); |
| } |
| |
| } // namespace |
| |
| // Produces the largest centered square gfx::Rect that fits within a rectangle |
| // from origin to `size`. |
| gfx::Rect FullCenteredSquare(const gfx::Size& size) { |
| int side = std::min(size.width(), size.height()); |
| return gfx::Rect((size.width() - side) / 2, (size.height() - side) / 2, side, |
| side); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // OmniboxMatchCellView: |
| |
| // static |
| void OmniboxMatchCellView::ComputeMatchMaxWidths(int contents_width, |
| int separator_width, |
| int description_width, |
| int iph_link_width, |
| int available_width, |
| bool allow_shrinking_contents, |
| int* contents_max_width, |
| int* description_max_width, |
| int* iph_link_max_width) { |
| available_width = std::max(available_width, 0); |
| |
| // The IPH link is top priority. |
| *iph_link_max_width = std::min(iph_link_width, available_width); |
| available_width = std::max(available_width - iph_link_width, 0); |
| |
| *contents_max_width = std::min(contents_width, available_width); |
| *description_max_width = std::min(description_width, available_width); |
| |
| // If the description is empty, contents can get the full available width. |
| if (!description_width) { |
| return; |
| } |
| |
| // If we want to display the description, we need to reserve enough space for |
| // the separator. |
| available_width -= separator_width; |
| if (available_width < 0) { |
| *description_max_width = 0; |
| return; |
| } |
| |
| if (contents_width + description_width > available_width) { |
| if (allow_shrinking_contents) { |
| // Try to split the available space fairly between contents and |
| // description (if one wants less than half, give it all it wants and |
| // give the other the remaining space; otherwise, give each half). |
| // However, if this makes the contents too narrow to show a significant |
| // amount of information, give the contents more space. |
| *contents_max_width = std::max((available_width + 1) / 2, |
| available_width - description_width); |
| |
| constexpr int kMinimumContentsWidth = 300; |
| *contents_max_width = std::min( |
| std::min(std::max(*contents_max_width, kMinimumContentsWidth), |
| contents_width), |
| available_width); |
| } |
| |
| // Give the description the remaining space, unless this makes it too small |
| // to display anything meaningful, in which case just hide the description |
| // and let the contents take up the whole width. However, when action chips |
| // are inlined, we don't hide the description view (in order to match the |
| // behavior of the realbox). |
| *description_max_width = |
| std::min(description_width, available_width - *contents_max_width); |
| if (*description_max_width == 0) { |
| // If we're not going to display the description, the contents can have |
| // the space we reserved for the separator. |
| available_width += separator_width; |
| *contents_max_width = std::min(contents_width, available_width); |
| } |
| } |
| } |
| |
| OmniboxMatchCellView::OmniboxMatchCellView(OmniboxResultView* result_view) { |
| icon_view_ = AddChildView(std::make_unique<views::ImageView>()); |
| answer_image_view_ = AddChildView(std::make_unique<views::ImageView>()); |
| answer_image_view_->SetCanProcessEventsWithinSubtree(false); |
| answer_image_view_->SetCornerRadius( |
| views::LayoutProvider::Get()->GetCornerRadiusMetric( |
| views::Emphasis::kMedium)); |
| tail_suggest_ellipse_view_ = |
| AddChildView(std::make_unique<OmniboxTextView>(result_view)); |
| tail_suggest_ellipse_view_->SetText(AutocompleteMatch::kEllipsis); |
| content_view_ = AddChildView(std::make_unique<OmniboxTextView>(result_view)); |
| description_view_ = |
| AddChildView(std::make_unique<OmniboxTextView>(result_view)); |
| separator_view_ = |
| AddChildView(std::make_unique<OmniboxTextView>(result_view)); |
| separator_view_->SetText( |
| l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR)); |
| iph_link_view_ = AddChildView(std::make_unique<views::Link>( |
| u"", ChromeTextContext::CONTEXT_OMNIBOX_POPUP, views::style::STYLE_LINK)); |
| } |
| |
| OmniboxMatchCellView::~OmniboxMatchCellView() = default; |
| |
| // static |
| bool OmniboxMatchCellView::ShouldDisplayImage(const AutocompleteMatch& match) { |
| // Extension suggestions in unscoped mode can have an `image_url` specified, |
| // but they should be displayed as icon view instead of an image view (i.e. |
| // following the default icon view size instead the larger image view size). |
| return match.answer_type != omnibox::ANSWER_TYPE_UNSPECIFIED || |
| match.type == AutocompleteMatchType::CALCULATOR || |
| (!match.image_url.is_empty() && |
| match.provider->type() != |
| AutocompleteProvider::TYPE_UNSCOPED_EXTENSION) || |
| (match.HasTakeoverAction( |
| OmniboxActionId::CONTEXTUAL_SEARCH_OPEN_LENS) && |
| omnibox_feature_configs::ContextualSearch::Get() |
| .open_lens_action_uses_thumbnail); |
| } |
| |
| void OmniboxMatchCellView::OnMatchUpdate(const OmniboxResultView* result_view, |
| const AutocompleteMatch& match) { |
| if (ShouldDisplayImage(match)) { |
| // Enterprise search aggregator people suggestions may display an image. |
| layout_style_ = LayoutStyle::SEARCH_SUGGESTION_WITH_IMAGE; |
| } else if (AutocompleteMatch::IsSearchType(match.type)) { |
| layout_style_ = LayoutStyle::SEARCH_SUGGESTION; |
| } else if (match.IsToolbelt()) { |
| layout_style_ = LayoutStyle::TOOLBELT; |
| } else if (match.IsIphSuggestion()) { |
| layout_style_ = LayoutStyle::IPH_SUGGESTION; |
| } else if (match.type == |
| AutocompleteMatchType::Type::HISTORY_EMBEDDINGS_ANSWER) { |
| layout_style_ = LayoutStyle::HISTORY_EMBEDDING_ANSWER; |
| } else { |
| layout_style_ = LayoutStyle::DEFAULT_NON_SEARCH_SUGGESTION; |
| } |
| |
| tail_suggest_ellipse_view_->SetVisible( |
| !match.tail_suggest_common_prefix.empty()); |
| tail_suggest_ellipse_view_->ApplyTextColor( |
| result_view->GetThemeState() == OmniboxPartState::SELECTED |
| ? kColorOmniboxResultsTextSelected |
| : kColorOmniboxText); |
| |
| // Set up the separator. |
| separator_view_->SetSize(layout_style_ == |
| LayoutStyle::HISTORY_EMBEDDING_ANSWER || |
| match.description.empty() |
| ? gfx::Size() |
| : separator_view_->GetPreferredSize()); |
| |
| // Set up the IPH link following the main IPH text. |
| iph_link_view_->SetText(match.iph_link_text); |
| iph_link_view_->SetVisible(layout_style_ == LayoutStyle::IPH_SUGGESTION); |
| |
| // Set up the small icon. |
| icon_view_->SetSize(layout_style_ == LayoutStyle::SEARCH_SUGGESTION_WITH_IMAGE |
| ? gfx::Size() |
| : icon_view_->GetPreferredSize()); |
| |
| // Used for non-weather answer images (e.g. calc answers). |
| const auto apply_vector_icon = [=, this](const gfx::VectorIcon& vector_icon) { |
| const auto* color_provider = GetColorProvider(); |
| const auto& icon = gfx::CreateVectorIcon( |
| vector_icon, |
| color_provider->GetColor(kColorOmniboxAnswerIconGM3Foreground)); |
| answer_image_view_->SetImageSize( |
| gfx::Size(kUniformRowHeightIconSize, kUniformRowHeightIconSize)); |
| answer_image_view_->SetImage(ui::ImageModel::FromImageSkia( |
| gfx::ImageSkiaOperations::CreateImageWithRoundRectBackground( |
| gfx::SizeF(kUniformRowHeightIconSize, kUniformRowHeightIconSize), |
| kIconAndImageCornerRadius, |
| color_provider->GetColor(kColorOmniboxAnswerIconGM3Background), |
| icon))); |
| }; |
| if (match.type == AutocompleteMatchType::CALCULATOR) { |
| apply_vector_icon(omnibox::kAnswerCalculatorIcon); |
| separator_view_->SetSize(gfx::Size()); |
| } else if (layout_style_ != LayoutStyle::SEARCH_SUGGESTION_WITH_IMAGE) { |
| answer_image_view_->SetImage(ui::ImageModel()); |
| answer_image_view_->SetSize(gfx::Size()); |
| } else { |
| // Determine if we have a local icon (or else it will be downloaded). |
| if (match.answer_type == omnibox::ANSWER_TYPE_WEATHER) { |
| // Weather icons are downloaded. We just need to set the correct size. |
| answer_image_view_->SetImageSize( |
| gfx::Size(kUniformRowHeightIconSize, kUniformRowHeightIconSize)); |
| } else if (match.answer_type != omnibox::ANSWER_TYPE_UNSPECIFIED) { |
| apply_vector_icon( |
| AutocompleteMatch::AnswerTypeToAnswerIcon(match.answer_type)); |
| } else { |
| // Use the hovered background color as the default placeholder color. |
| SkColor color = GetColorProvider()->GetColor( |
| GetOmniboxBackgroundColorId(OmniboxPartState::HOVERED)); |
| // If `image_dominant_color` is provided, override the default. |
| if (!match.image_dominant_color.empty()) { |
| content::ParseHexColorString(match.image_dominant_color, &color); |
| color = SkColorSetA(color, 0x40); // 25% transparency (arbitrary). |
| } |
| |
| gfx::Size size(kUniformRowHeightIconSize, kUniformRowHeightIconSize); |
| answer_image_view_->SetImageSize(size); |
| answer_image_view_->SetImage(ui::ImageModel::FromImageSkia( |
| gfx::CanvasImageSource::MakeImageSkia<PlaceholderImageSource>( |
| size, color))); |
| } |
| } |
| SetTailSuggestCommonPrefixWidth( |
| (match.type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL) |
| ? match.tail_suggest_common_prefix // Used for indent calculation. |
| : std::u16string()); |
| |
| // Set content & description texts. |
| if (match.answer_template.has_value()) { |
| content_view_->SetTextWithStyling(match.contents, match.contents_class); |
| omnibox::AnswerData answer_data = match.answer_template->answers(0); |
| content_view_->AppendAndStyleAnswerText( |
| /*formatted_string=*/answer_data.headline(), /*fragment_index=*/1u, |
| /*answer_type=*/match.answer_type, /*is_headline=*/true); |
| // The subhead text may be multiline. |
| description_view_->SetMultilineAnswerText( |
| /*formatted_string=*/answer_data.subhead(), |
| /*answer_type=*/match.answer_type); |
| } else if (layout_style_ == LayoutStyle::HISTORY_EMBEDDING_ANSWER) { |
| content_view_->SetMultilineText(match.contents); |
| description_view_->SetTextWithStyling(match.description, |
| match.description_class); |
| } else { |
| content_view_->SetTextWithStyling(match.contents, match.contents_class); |
| description_view_->SetTextWithStyling(match.description, |
| match.description_class); |
| } |
| } |
| |
| void OmniboxMatchCellView::SetIcon(const gfx::ImageSkia& image, |
| const AutocompleteMatch& match) { |
| const bool is_pedal_suggestion_row = |
| match.type == AutocompleteMatchType::PEDAL; |
| const bool is_journeys_suggestion_row = |
| match.type == AutocompleteMatchType::HISTORY_CLUSTER; |
| const bool is_instant_keyword_row = |
| AutocompleteMatch::IsFeaturedSearchType(match.type); |
| bool should_draw_icon_background = is_pedal_suggestion_row || |
| is_journeys_suggestion_row || |
| is_instant_keyword_row; |
| |
| // Do not apply the distinctive background color to the open lens action when |
| // the UI tweaks are enabled. |
| if (match.HasTakeoverAction(OmniboxActionId::CONTEXTUAL_SEARCH_OPEN_LENS) && |
| omnibox_feature_configs::ContextualSearch::Get().open_lens_action_ui_tweaks) { |
| should_draw_icon_background = false; |
| } |
| |
| if (should_draw_icon_background) { |
| // When a PEDAL suggestion has been split out to its own row, apply a square |
| // background with a distinctive color to the respective icon. Journeys |
| // suggestion rows should also receive the same treatment. |
| const auto background_color = is_pedal_suggestion_row || |
| is_journeys_suggestion_row || |
| is_instant_keyword_row |
| ? kColorOmniboxAnswerIconGM3Background |
| : kColorOmniboxResultsIconGM3Background; |
| icon_view_->SetImage(ui::ImageModel::FromImageSkia( |
| gfx::ImageSkiaOperations::CreateImageWithRoundRectBackground( |
| gfx::SizeF(kUniformRowHeightIconSize, kUniformRowHeightIconSize), |
| kIconAndImageCornerRadius, |
| GetColorProvider()->GetColor(background_color), image))); |
| } else { |
| icon_view_->SetImage(ui::ImageModel::FromImageSkia(image)); |
| } |
| } |
| |
| void OmniboxMatchCellView::ClearIcon() { |
| icon_view_->SetImage(ui::ImageModel()); |
| } |
| |
| void OmniboxMatchCellView::SetImage(const gfx::ImageSkia& image, |
| const AutocompleteMatch& match) { |
| // Weather icons are also sourced remotely and therefore fall into this flow. |
| // Other answers don't. |
| const bool is_weather_answer = |
| match.answer_type == omnibox::ANSWER_TYPE_WEATHER; |
| |
| int width = image.width(); |
| int height = image.height(); |
| |
| // Weather icon square background should be the same color as the pop-up |
| // background. The experimental thumbnail is treated similar to the |
| // weather icon and is likewise resized to reduce downscaling artifacts. |
| // However, thumbnails are usually rectangular and should preserve aspect |
| // ratio by cropping edges and scaling to fill, not by shrinking to fit. |
| const bool is_thumbnail = |
| match.HasTakeoverAction(OmniboxActionId::CONTEXTUAL_SEARCH_OPEN_LENS) && |
| omnibox_feature_configs::ContextualSearch::Get() |
| .open_lens_action_uses_thumbnail; |
| if (is_weather_answer || is_thumbnail) { |
| // Explicitly resize to avoid pixelation. Note the thumbnail uses best |
| // quality because it makes a noticeable difference for so much downscaling. |
| // UX also suggested a 5% opacity black overlay to reduce white on white |
| // edgeless thumbnail effect. Reducing HSL lightness does this efficiently. |
| gfx::ImageSkia resized_image = |
| is_thumbnail ? gfx::ImageSkiaOperations::CreateHSLShiftedImage( |
| gfx::ImageSkiaOperations::CreateResizedImage( |
| gfx::ImageSkiaOperations::ExtractSubset( |
| image, FullCenteredSquare(image.size())), |
| skia::ImageOperations::RESIZE_BEST, |
| gfx::Size(kWeatherImageSize, kWeatherImageSize)), |
| {-1, -1, 0.5 - (0.05 / 2)}) |
| : gfx::ImageSkiaOperations::CreateResizedImage( |
| image, skia::ImageOperations::RESIZE_GOOD, |
| gfx::Size(kWeatherImageSize, kWeatherImageSize)); |
| answer_image_view_->SetImage(ui::ImageModel::FromImageSkia( |
| gfx::ImageSkiaOperations::CreateImageWithRoundRectBackground( |
| gfx::SizeF(kWeatherBackgroundSize, kWeatherBackgroundSize), |
| kIconAndImageCornerRadius, |
| GetColorProvider()->GetColor(kColorOmniboxResultsBackground), |
| resized_image))); |
| } else { |
| answer_image_view_->SetImage(ui::ImageModel::FromImageSkia(image)); |
| |
| // Usually, answer images are square. But if that's not the case, setting |
| // answer_image_view_ size proportional to the image size preserves |
| // the aspect ratio. |
| if (width == height) { |
| return; |
| } |
| const int max = std::max(width, height); |
| width = kUniformRowHeightIconSize * width / max; |
| height = kUniformRowHeightIconSize * height / max; |
| answer_image_view_->SetImageSize(gfx::Size(width, height)); |
| } |
| } |
| |
| gfx::Insets OmniboxMatchCellView::GetInsets() const { |
| const int vertical_margin = 0; |
| // Toolbelt text bounds are set to match the UX spec. IPH text bounds should |
| // be centered within the IPH background when there's no IPH icon. So make |
| // their `right_margin` equal to their text's x position. |
| const int right_margin = |
| layout_style_ == LayoutStyle::TOOLBELT |
| ? kToolbeltTextInsetRight |
| : layout_style_ == LayoutStyle::IPH_SUGGESTION |
| ? OmniboxMatchCellView::kMarginLeft + kIphTextIndent |
| : 7; |
| return gfx::Insets::TLBR(vertical_margin, OmniboxMatchCellView::kMarginLeft, |
| vertical_margin, right_margin); |
| } |
| |
| void OmniboxMatchCellView::Layout(PassKey) { |
| LayoutSuperclass<views::View>(this); |
| |
| const gfx::Rect child_area = GetContentsBounds(); |
| int x = child_area.x(); |
| const int y = child_area.y(); |
| |
| const int row_height = child_area.height(); |
| |
| const int image_x = GetImageIndent(); |
| views::ImageView* const image_view = |
| layout_style_ == LayoutStyle::SEARCH_SUGGESTION_WITH_IMAGE |
| ? answer_image_view_.get() |
| : icon_view_.get(); |
| image_view->SetBounds(image_x, y, kImageBoundsWidth, row_height); |
| |
| const int text_indent = GetTextIndent() + tail_suggest_common_prefix_width_; |
| x += text_indent; |
| const int text_width = child_area.width() - text_indent; |
| |
| if (layout_style_ == LayoutStyle::HISTORY_EMBEDDING_ANSWER) { |
| if (description_view_->GetText().empty()) { |
| content_view_->SetBounds(x, y, text_width, |
| content_view_->GetHeightForWidth(text_width)); |
| return; |
| } |
| |
| // Position contents above description. Leave `kHistoryEmbeddingAnswerGap` |
| // between them; and `kHistoryEmbeddingAnswerBottomPadding` between the |
| // bottom of description and the bottom of this `OmniboxMatchCellView`. |
| int needed_content_height = |
| content_view_->GetText().empty() |
| ? 0 |
| : content_view_->GetHeightForWidth(text_width); |
| int needed_description_height = description_view_->GetText().empty() |
| ? 0 |
| : description_view_->GetLineHeight(); |
| int top_padding = row_height - needed_content_height - |
| needed_description_height - kHistoryEmbeddingAnswerGap - |
| kHistoryEmbeddingAnswerBottomPadding; |
| content_view_->SetBounds(x, y + top_padding, text_width, |
| needed_content_height); |
| description_view_->SetBounds( |
| x, content_view_->bounds().bottom() + kHistoryEmbeddingAnswerGap, |
| text_width, needed_description_height); |
| return; |
| } |
| |
| int content_width = content_view_->GetPreferredSize().width(); |
| int description_width = description_view_->GetPreferredSize().width(); |
| const gfx::Size separator_size = separator_view_->GetPreferredSize(); |
| int iph_link_width = iph_link_view_->GetPreferredSize().width(); |
| ComputeMatchMaxWidths( |
| content_width, separator_size.width(), description_width, iph_link_width, |
| /*available_width=*/text_width, |
| /*allow_shrinking_contents=*/ |
| layout_style_ != LayoutStyle::SEARCH_SUGGESTION && |
| layout_style_ != LayoutStyle::SEARCH_SUGGESTION_WITH_IMAGE, |
| &content_width, &description_width, &iph_link_width); |
| if (tail_suggest_ellipse_view_->GetVisible()) { |
| const int tail_suggest_ellipse_width = |
| tail_suggest_ellipse_view_->GetPreferredSize().width(); |
| tail_suggest_ellipse_view_->SetBounds(x - tail_suggest_ellipse_width, y, |
| tail_suggest_ellipse_width, |
| row_height); |
| } |
| content_view_->SetBounds(x, y, content_width, row_height); |
| x += content_view_->width(); |
| if (description_width) { |
| separator_view_->SetSize(separator_size); |
| separator_view_->SetBounds(x, y, separator_view_->width(), row_height); |
| x += separator_view_->width(); |
| description_view_->SetBounds(x, y, description_width, row_height); |
| x += description_view_->width(); |
| } else { |
| separator_view_->SetSize(gfx::Size()); |
| description_view_->SetSize(gfx::Size()); |
| } |
| iph_link_view_->SetBounds(x, y, iph_link_width, row_height); |
| } |
| |
| gfx::Size OmniboxMatchCellView::CalculatePreferredSize( |
| const views::SizeBounds& available_size) const { |
| // Compute width before height because height for multiline suggestions (e.g., |
| // `HISTORY_EMBEDDING_ANSWER`) will depend on how many lines they require, |
| // which will in turn depend on available width. |
| int width; |
| if (layout_style_ == LayoutStyle::HISTORY_EMBEDDING_ANSWER) { |
| // Use `parent()` width instead of `width()` or any other size-methods on |
| // self because `CalculatePreferredSize()` is called before self has been |
| // sized; `width()` and all size-methods on self will return stale sizes for |
| // new & changed matches and for old matches when the browser is resized. |
| width = parent()->width(); |
| } else { |
| // 1-line suggestions can't naively consume the entire parent width because |
| // that would shift the history embedding chip, tab switch button, and |
| // keyword button to the right. |
| width = GetInsets().width() + GetTextIndent() + |
| tail_suggest_common_prefix_width_ + |
| content_view_->GetPreferredSize().width() + |
| iph_link_view_->GetPreferredSize().width(); |
| const int description_width = description_view_->GetPreferredSize().width(); |
| if (description_width > 0) { |
| width += separator_view_->GetPreferredSize().width() + description_width; |
| } |
| } |
| |
| int height; |
| if (layout_style_ == LayoutStyle::HISTORY_EMBEDDING_ANSWER) { |
| if (content_view_->GetText().empty() && |
| description_view_->GetText().empty()) { |
| // Answers can hide `OmniboxMatchCellView`, only displaying their |
| // `OmniboxLocalAnswerHeaderView`. |
| height = 0; |
| } else if (description_view_->GetText().empty()) { |
| // In the error cases, answers can display only contents. |
| height = 28; |
| } else { |
| // Enough room to display the contents, description, a gap between them, |
| // and padding above and below them. |
| int available_width_for_text = |
| width - GetTextIndent() - GetInsets().width(); |
| height = content_view_->GetHeightForWidth(available_width_for_text) + |
| description_view_->GetLineHeight() + kHistoryEmbeddingAnswerGap + |
| kHistoryEmbeddingAnswerBottomPadding; |
| } |
| } else if (layout_style_ == LayoutStyle::IPH_SUGGESTION || |
| layout_style_ == LayoutStyle::TOOLBELT) { |
| // IPH and toolbelt suggestions have extra height. |
| height = kRowHeight + 4; |
| } else { |
| // The height for traditional 1-line matches. |
| height = kRowHeight; |
| } |
| |
| return gfx::Size(width, height); |
| } |
| |
| gfx::Size OmniboxMatchCellView::GetMinimumSize() const { |
| return gfx::Size(GetTextIndent(), GetPreferredSize().height()); |
| } |
| |
| int OmniboxMatchCellView::GetImageIndent() const { |
| // Image indent ignores the `OmniboxMatchCellView::GetInsets()`. |
| |
| // This number is independent of other layout numbers; i.e., it's not meant to |
| // align with any other UI; it's just arbitrarily chosen by UX. Hence, it's |
| // not derived from other matches' `indent` below. |
| if (layout_style_ == LayoutStyle::IPH_SUGGESTION || |
| layout_style_ == LayoutStyle::TOOLBELT) { |
| return 2; |
| } |
| |
| // The entity, answer, and icon images are horizontally centered within their |
| // bounds. So their center-line will be at `image_x+kImageBoundsWidth/2`. This |
| // means their left x coordinate will depend on their actual sizes. This code |
| // guarantees: |
| // a) Entities' left x coordinate is 16. |
| // b) Entities, answers, and icons continue to be center-aligned. |
| // c) Regardless of the state of those other features and their widths. |
| // This applies to both touch-UI and non-touch-UI. |
| int indent = 16 + kUniformRowHeightIconSize / 2 - kImageBoundsWidth / 2; |
| |
| indent += omnibox_feature_configs::AdjustOmniboxIndent() |
| .Get() |
| .match_icon_indent_offset; |
| |
| return indent; |
| } |
| |
| int OmniboxMatchCellView::GetTextIndent() const { |
| // Text indent is added to the `OmniboxMatchCellView::GetInsets()`. It is not |
| // added to the image position & size. |
| |
| // Toolbelt layout is similar to IPH and has a custom indent. |
| if (layout_style_ == LayoutStyle::TOOLBELT) { |
| return kToolbeltTextInsetLeft; |
| } |
| |
| // Some IPH matches have no icons. They should be moved further left so the |
| // gap between the IPH background and the start of the IPH text isn't jarring. |
| // Non-IPH matches without icons (e.g. the 'no results found' tab match) don't |
| // want to apply this left shift because their text needs to align with the |
| // other matches' and the omnibox's texts. This number is independent of other |
| // layout numbers; i.e., it's not meant to align with other UI; it's just |
| // arbitrarily chosen by UX. Hence, it's not derived from other matches' |
| // `indent` below. |
| if (layout_style_ == LayoutStyle::IPH_SUGGESTION && |
| icon_view_->GetPreferredSize() == gfx::Size{}) { |
| return kIphTextIndent; |
| } |
| |
| // Answers don't have an icon, and their text needs to line up with the icons |
| // of other suggestions, so they need a smaller indent. |
| if (layout_style_ == LayoutStyle::HISTORY_EMBEDDING_ANSWER) { |
| return 18; |
| } |
| |
| // For normal matches, the gap between the left edge of this view and the |
| // left edge of its favicon or answer image. |
| int indent = 52; |
| |
| indent += omnibox_feature_configs::AdjustOmniboxIndent() |
| .Get() |
| .match_text_indent_offset; |
| |
| if (layout_style_ == LayoutStyle::IPH_SUGGESTION) { |
| // The IPH row left inset is +`kIphOffset` from other suggestions, so the |
| // text indent should be -`kIphOffset` to keep the text aligned. IPH matches |
| // seem to have inner padding, so the gap between the left edge of this |
| // `OmniboxMatchCellView` and the IPH icon/text is actually larger than |
| // `indent`. |
| indent -= kIphOffset; |
| } |
| |
| return indent; |
| } |
| |
| void OmniboxMatchCellView::SetTailSuggestCommonPrefixWidth( |
| const std::u16string& common_prefix) { |
| InvalidateLayout(); |
| if (common_prefix.empty()) { |
| tail_suggest_common_prefix_width_ = 0; |
| return; |
| } |
| std::unique_ptr<gfx::RenderText> render_text = |
| content_view_->CreateRenderText(common_prefix); |
| tail_suggest_common_prefix_width_ = render_text->GetStringSize().width(); |
| } |
| |
| BEGIN_METADATA(OmniboxMatchCellView) |
| END_METADATA |