Commit eb1d461e authored by Moe Ahmadi's avatar Moe Ahmadi Committed by Commit Bot

[NTP][Realbox] Load realbox match icons/favicons dynamically (C++ -> JS)

NTP realbox matches support different icons for different match types:
"loupe" for search suggestions, "clock" for historical search suggestions,
"page" for navigation suggestions, "drive icons" for Google Drive, etc.
Currently only the "loupe" and "clock" are supported by hardcoding in CSS.
This CL changes that by sending the appropriate icon information along with
the matches form C++ to JS allowing for a 1:1 mapping between the icons in
the NTP realbox and the Omnibox. A followup CL will complete this mapping
and add tests to avoid regression in the future.

NTP realbox input has a "loupe" icon by default. When a match with a
different icon or favicon is selected, the realbox input also gets that
icon/favicon. This CL refactors the way these icons/favicons are displayed
for the realbox input as well as the matches: Icons are rendered via
webkit-mask-image and background-color and favicons are rendered via
background-image set on the element's style property in JS.

Also instead of requesting favicons for *every* navigation match which
results in display the default "page" favicon for matches without one,
favicons are now requested in C++ and sent to JS only if one is available;
taking advantage of high res SVG icon for matches without a favicon as well
as Google Drive matches, calculator matches, etc.

Bug: 1039357,1061221
Change-Id: I90e17aa573ba7ef211e3f1423fa7de511cb7e053
Tbr: dcheng@chromium.org
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2106655
Commit-Queue: Moe Ahmadi <mahmadi@chromium.org>
Reviewed-by: default avatarDaniel Cheng <dcheng@chromium.org>
Reviewed-by: default avatarTommy Li <tommycli@chromium.org>
Reviewed-by: default avatarEsmael Elmoslimany <aee@chromium.org>
Cr-Commit-Position: refs/heads/master@{#753126}
parent 394cc845
......@@ -109,6 +109,11 @@
<include name="IDR_OFFLINE_INTERNALS_BROWSER_PROXY_JS" file="resources\offline_pages\offline_internals_browser_proxy.js" type="BINDATA" compress="gzip" />
</if>
<if expr="not is_android">
<!-- New Tab Page -->
<part file="resources/local_ntp/icons.grdp" />
</if>
<if expr="enable_supervised_users">
<part file="resources/supervised_user_error_page_resources.grdp" />
</if>
......
......@@ -439,11 +439,12 @@ let ACMatchClassification;
* description: string,
* descriptionClass: !Array<!ACMatchClassification>,
* destinationUrl: string,
* inlineAutocompletion: string,
* isSearchType: boolean,
* fillIntoEdit: string,
* iconUrl: string,
* imageDominantColor: string,
* imageUrl: string,
* inlineAutocompletion: string,
* isSearchType: boolean,
* supportsDeletion: boolean,
* swapContentsAndDescription: boolean,
* type: string,
......
<?xml version="1.0" encoding="utf-8"?>
<grit-part>
<include name="IDR_LOCAL_NTP_ICONS_CLOCK"
file="resources/local_ntp/icons/clock.svg" type="BINDATA" compress="gzip" />
<include name="IDR_LOCAL_NTP_ICONS_PAGE"
file="resources/local_ntp/icons/page.svg" type="BINDATA" compress="gzip" />
</grit-part>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="gray" d="M12 2c5.52 0 10 4.48 10 10s-4.48 10-10 10S2 17.52 2 12 6.48 2 12 2zM4 12h4.4c3.41.02 4.92 1.73 4.54 5.13H9.49v2.47a8 8 0 0 0 10.5-8.08C19.33 12.5 18.33 13 17 13c-2.14 0-3.21-.92-3.21-2.75h-3.75c-.27-2.73.68-4.09 2.87-4.09 0-.97.33-1.6.81-1.97A8 8 0 0 0 4 12z"/></svg>
\ No newline at end of file
......@@ -153,24 +153,6 @@ body.hide-fakebox #fakebox {
max-width: 584px;
}
.google-g-icon {
background-image:
url(../../../../ui/webui/resources/images/200-logo_googleg.png);
background-position: center;
background-repeat: no-repeat;
background-size: 14px;
bottom: 0;
left: 16px;
position: absolute;
top: 0;
width: 24px;
}
[dir=rtl] .google-g-icon {
left: auto;
right: 16px;
}
#fakebox {
background: white;
cursor: text;
......@@ -219,14 +201,22 @@ body.hide-fakebox #fakebox {
display: none;
}
/* The realbox is a "loupe" (.search-icon) by default, but when the selection
* ends up on match with a different icon, the realbox also gets that icon.
* Example: typing the start of a URL with a favicon or the user pressing
* up/down to select matches sets a background-image here and disables the
* -webkit-mask applied by .search-icon. */
#realbox-icon {
/* The realbox has a "loupe" or "search" icon by default. When a match with a
* different icon ("clock" for historical searches or "page" for URLs) or a
* favicon is selected, the realbox also gets that icon/favicon. For both the
* realbox as well as the dropdown, icons are rendered via webkit-mask-image and
* background-color and favicons are rendered via background-
image set on the
* element's style property in JS. */
#realbox-icon,
.match-icon {
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: 16px;
background-color: var(--search-box-icon, rgb(117, 117, 117));
background-position: center center;
background-repeat: no-repeat;
background-size: 16px;
bottom: 0;
left: 16px;
margin: auto;
......@@ -235,13 +225,17 @@ body.hide-fakebox #fakebox {
width: 24px;
}
[dir=rtl] #realbox-icon {
[dir=rtl] :-webkit-any(#realbox-icon, .match-icon) {
left: auto;
right: 16px;
}
#realbox-icon.load-favicon {
background-size: 24px;
#realbox-icon[data-icon='google_g'] {
background-size: 12px;
}
#realbox-icon[data-icon='search'] {
-webkit-mask-size: 20px; /* Loupe in realbox is bigger than in matches. */
}
#realbox-matches {
......@@ -269,9 +263,6 @@ body.hide-fakebox #fakebox {
}
#realbox-matches a {
background-position: 16px center;
background-repeat: no-repeat;
background-size: 24px;
font-size: 16px;
line-height: 1;
outline: none;
......@@ -316,55 +307,21 @@ body.hide-fakebox #fakebox {
margin-bottom: 8px; /* Last result is tight with border-radius. */
}
.clock-icon,
.search-icon,
.image-container {
bottom: 0;
margin: auto;
position: absolute;
top: 0;
}
.clock-icon,
.search-icon {
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: 16px;
background-color: var(--search-box-icon, rgb(117, 117, 117));
left: 16px;
width: 24px;
}
#realbox-matches a:-webkit-any(:focus-within, .selected)
:-webkit-any(.clock-icon, .search-icon) {
#realbox-matches a:-webkit-any(:focus-within, .selected) .match-icon {
background-color: var(--search-box-icon-selected, rgb(117, 117, 117));
}
.clock-icon {
-webkit-mask-image: url(icons/clock.svg);
}
.search-icon {
-webkit-mask-image:
url(../../../../ui/webui/resources/images/icon_search.svg);
}
#realbox-input-wrapper > .search-icon {
-webkit-mask-size: 20px; /* Loupe in realbox is bigger than in matches. */
}
html[dir=rtl] :-webkit-any(.clock-icon, .search-icon) {
left: auto;
right: 16px;
}
.image-container {
align-items: center;
border-radius: 8px;
bottom: 0;
display: flex;
height: 32px;
justify-content: center;
left: 12px;
margin: auto;
position: absolute;
top: 0;
width: 32px;
}
......@@ -473,7 +430,22 @@ html[dir=rtl] #fakebox > input {
}
#fakebox .search-icon {
-webkit-mask-image: url(../../../../ui/webui/resources/images/icon_search.svg);
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: 20px;
background-color: rgb(117, 117, 117);
bottom: 0;
left: 16px;
margin: auto;
position: absolute;
top: 0;
width: 24px;
}
html[dir=rtl] #fakebox .search-icon {
left: auto;
right: 16px;
}
#fakebox-text {
......
......@@ -78,8 +78,8 @@
<div id="realbox-container" $i18n{hiddenIfRealboxDisabled}>
<div id="realbox-input-wrapper">
<div id="realbox-icon" class="$i18n{realboxIconClass}"
data-realbox-icon-class="$i18n{realboxIconClass}"></div>
<div id="realbox-icon" data-default-icon="$i18n{realboxDefaultIcon}">
</div>
<input id="realbox" type="search" autocomplete="off" spellcheck="false"
aria-live="polite" autofocus>
<button id="realbox-microphone" class="microphone-icon" hidden></button>
......
......@@ -84,6 +84,11 @@ using search_provider_logos::LogoCallbackReason;
using search_provider_logos::LogoMetadata;
using search_provider_logos::LogoService;
const char kClockIconResourceName[] = "clock";
const char kGoogleGIconResourceName[] = "google_g";
const char kSearchIconResourceName[] = "search";
const char kPageIconResourceName[] = "page";
namespace {
// Language code used to check features run in English in the US.
......@@ -138,6 +143,10 @@ const struct Resource{
// added complexity.
{chrome::kChromeSearchLocalNtpBackgroundFilename, kLocalResource,
"image/jpg"},
{kClockIconResourceName, IDR_LOCAL_NTP_ICONS_CLOCK, "image/svg+xml"},
{kGoogleGIconResourceName, IDR_WEBUI_IMAGES_200_LOGO_GOOGLE_G, "image/png"},
{kPageIconResourceName, IDR_LOCAL_NTP_ICONS_PAGE, "image/svg+xml"},
{kSearchIconResourceName, IDR_WEBUI_IMAGES_ICON_SEARCH, "image/svg+xml"},
};
// This enum must match the numbering for NTPSearchSuggestionsRequestStatusi in
......@@ -621,6 +630,9 @@ class LocalNtpSource::SearchConfigurationProvider
"suggestionTransparencyEnabled",
base::FeatureList::IsEnabled(
omnibox::kOmniboxSuggestionTransparencyOptions));
config_data.SetBoolean(
"useGoogleGIcon",
base::FeatureList::IsEnabled(ntp_features::kRealboxUseGoogleGIcon));
}
config_data.SetBoolean(
......@@ -1040,8 +1052,8 @@ void LocalNtpSource::StartDataRequest(
bool use_google_g_icon =
base::FeatureList::IsEnabled(ntp_features::kRealboxUseGoogleGIcon);
replacements["realboxIconClass"] =
use_google_g_icon ? "google-g-icon" : "search-icon";
replacements["realboxDefaultIcon"] =
use_google_g_icon ? kGoogleGIconResourceName : kSearchIconResourceName;
ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
base::StringPiece html = bundle.GetRawDataResource(IDR_LOCAL_NTP_HTML);
......
......@@ -38,6 +38,11 @@ namespace search_provider_logos {
class LogoService;
} // namespace search_provider_logos
extern const char kClockIconResourceName[];
extern const char kGoogleGIconResourceName[];
extern const char kSearchIconResourceName[];
extern const char kPageIconResourceName[];
// Serves HTML and resources for the local New Tab page, i.e.
// chrome-search://local-ntp/local-ntp.html.
// WARNING: Due to the threading model of URLDataSource, some methods of this
......
......@@ -20,6 +20,8 @@
#include "chrome/browser/bitmap_fetcher/bitmap_fetcher_service_factory.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/extensions/extension_checkup.h"
#include "chrome/browser/favicon/favicon_service_factory.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/predictors/autocomplete_action_predictor.h"
#include "chrome/browser/predictors/autocomplete_action_predictor_factory.h"
#include "chrome/browser/profiles/profile.h"
......@@ -27,6 +29,7 @@
#include "chrome/browser/search/chrome_colors/chrome_colors_factory.h"
#include "chrome/browser/search/instant_service.h"
#include "chrome/browser/search/instant_service_factory.h"
#include "chrome/browser/search/local_ntp_source.h"
#include "chrome/browser/search/ntp_features.h"
#include "chrome/browser/search/promos/promo_service.h"
#include "chrome/browser/search/promos/promo_service_factory.h"
......@@ -53,6 +56,7 @@
#include "chrome/common/url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/favicon/core/favicon_service.h"
#include "components/google/core/common/google_util.h"
#include "components/navigation_metrics/navigation_metrics.h"
#include "components/omnibox/browser/autocomplete_classifier.h"
......@@ -66,6 +70,7 @@
#include "components/omnibox/browser/omnibox_popup_model.h"
#include "components/omnibox/browser/omnibox_view.h"
#include "components/omnibox/browser/suggestion_answer.h"
#include "components/omnibox/browser/vector_icons.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/search/search.h"
#include "components/search_engines/template_url_service.h"
......@@ -75,6 +80,7 @@
#include "components/sync/base/user_selectable_type.h"
#include "components/sync/driver/sync_service.h"
#include "components/sync/driver/sync_user_settings.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
......@@ -86,10 +92,26 @@
#include "google_apis/gaia/gaia_auth_util.h"
#include "third_party/metrics_proto/omnibox_event.pb.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/vector_icon_types.h"
#include "url/gurl.h"
namespace {
// TODO(mahmadi): Make sure all the vector icons returned by
// AutocompleteMatch::GetVectorIcon have an equivalent SVG resource.
std::string AutocompleteMatchVectorIconToResourceName(
const gfx::VectorIcon& icon) {
if (icon.name == omnibox::kClockIcon.name) {
return kClockIconResourceName;
} else if (icon.name == omnibox::kPageIcon.name) {
return kPageIconResourceName;
} else if (icon.name == vector_icons::kSearchIcon.name) {
return kSearchIconResourceName;
} else {
return "";
}
}
std::vector<chrome::mojom::AutocompleteMatchPtr> CreateAutocompleteMatches(
const AutocompleteResult& result) {
std::vector<chrome::mojom::AutocompleteMatchPtr> matches;
......@@ -111,6 +133,8 @@ std::vector<chrome::mojom::AutocompleteMatchPtr> CreateAutocompleteMatches(
description_class.style));
}
mojom_match->destination_url = match.destination_url.spec();
mojom_match->icon_url =
AutocompleteMatchVectorIconToResourceName(match.GetVectorIcon(false));
mojom_match->image_dominant_color = match.image_dominant_color;
mojom_match->image_url = match.image_url.spec();
mojom_match->fill_into_edit = match.fill_into_edit;
......@@ -125,6 +149,13 @@ std::vector<chrome::mojom::AutocompleteMatchPtr> CreateAutocompleteMatches(
return matches;
}
// Converts an in-memory bitmap data to a base64 data url.
std::string GetBitmapDataUrl(const char* data, size_t size) {
std::string base_64;
base::Base64Encode(base::StringPiece(data, size), &base_64);
return "data:image/png;base64," + base_64;
}
bool IsCacheableNTP(content::WebContents* contents) {
content::NavigationEntry* entry =
contents->GetController().GetLastCommittedEntry();
......@@ -177,7 +208,13 @@ SearchTabHelper::SearchTabHelper(content::WebContents* web_contents)
ipc_router_(web_contents,
this,
std::make_unique<SearchIPCRouterPolicyImpl>(web_contents)),
instant_service_(nullptr) {
instant_service_(nullptr),
favicon_cache_(FaviconServiceFactory::GetForProfile(
profile(),
ServiceAccessType::EXPLICIT_ACCESS),
HistoryServiceFactory::GetForProfile(
profile(),
ServiceAccessType::EXPLICIT_ACCESS)) {
DCHECK(search::IsInstantExtendedAPIEnabled());
instant_service_ = InstantServiceFactory::GetForProfile(profile());
......@@ -467,20 +504,33 @@ void SearchTabHelper::OnResultChanged(AutocompleteController* controller,
autocomplete_controller_->input().text(),
CreateAutocompleteMatches(autocomplete_controller_->result())));
// Create new bitmap requests.
BitmapFetcherService* bitmap_fetcher_service =
BitmapFetcherServiceFactory::GetForBrowserContext(profile());
int match_index = -1;
for (const auto& match : autocomplete_controller_->result()) {
match_index++;
if (match.ImageUrl().is_empty()) {
continue;
}
// Create new bitmap requests.
if (!match.image_url.is_empty()) {
bitmap_fetcher_service->RequestImage(
match.ImageUrl(), base::BindOnce(&SearchTabHelper::OnBitmapFetched,
match.image_url, base::BindOnce(&SearchTabHelper::OnBitmapFetched,
weak_factory_.GetWeakPtr(),
match_index, match.ImageUrl().spec()));
match_index, match.image_url.spec()));
}
// Request favicons for navigational matches.
if (!AutocompleteMatch::IsSearchType(match.type) &&
match.type != AutocompleteMatchType::DOCUMENT_SUGGESTION) {
gfx::Image favicon = favicon_cache_.GetLargestFaviconForPageUrl(
match.destination_url,
base::BindOnce(&SearchTabHelper::OnFaviconFetched,
weak_factory_.GetWeakPtr(), match_index,
match.destination_url.spec()));
if (!favicon.IsEmpty()) {
OnFaviconFetched(match_index, match.destination_url.spec(), favicon);
}
}
}
}
......@@ -503,15 +553,21 @@ void SearchTabHelper::OnBitmapFetched(int match_index,
const std::string& image_url,
const SkBitmap& bitmap) {
auto data = gfx::Image::CreateFrom1xBitmap(bitmap).As1xPNGBytes();
std::string base_64;
base::Base64Encode(base::StringPiece(data->front_as<char>(), data->size()),
&base_64);
const char kDataUrlPrefix[] = "data:image/png;base64,";
std::string data_url = GURL(kDataUrlPrefix + base_64).spec();
std::string data_url = GetBitmapDataUrl(data->front_as<char>(), data->size());
ipc_router_.AutocompleteMatchImageAvailable(match_index, image_url, data_url);
}
void SearchTabHelper::OnFaviconFetched(int match_index,
const std::string& page_url,
const gfx::Image& favicon) {
DCHECK(!favicon.IsEmpty());
auto data = favicon.As1xPNGBytes();
std::string data_url = GetBitmapDataUrl(data->front_as<char>(), data->size());
ipc_router_.AutocompleteMatchImageAvailable(match_index, page_url, data_url);
}
void SearchTabHelper::OnSelectLocalBackgroundImage() {
if (select_file_dialog_)
return;
......
......@@ -13,6 +13,7 @@
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string16.h"
#include "base/task/cancelable_task_tracker.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/search/chrome_colors/chrome_colors_service.h"
......@@ -24,6 +25,7 @@
#include "chrome/common/search/ntp_logging_events.h"
#include "components/ntp_tiles/ntp_tile_impression.h"
#include "components/omnibox/browser/autocomplete_controller.h"
#include "components/omnibox/browser/favicon_cache.h"
#include "components/omnibox/common/omnibox_focus_state.h"
#include "content/public/browser/reload_type.h"
#include "content/public/browser/web_contents_observer.h"
......@@ -39,6 +41,10 @@ class WebContents;
struct LoadCommittedDetails;
}
namespace gfx {
class Image;
}
class AutocompleteController;
class GURL;
class InstantService;
......@@ -177,6 +183,10 @@ class SearchTabHelper : public content::WebContentsObserver,
const std::string& image_url,
const SkBitmap& bitmap);
void OnFaviconFetched(int match_index,
const std::string& page_url,
const gfx::Image& favicon);
Profile* profile() const;
// Returns whether input is in progress, i.e. if the omnibox has focus and the
......@@ -207,6 +217,10 @@ class SearchTabHelper : public content::WebContentsObserver,
std::unique_ptr<AutocompleteController> autocomplete_controller_;
base::TimeTicks time_of_first_autocomplete_query_;
base::CancelableTaskTracker cancelable_task_tracker_;
FaviconCache favicon_cache_;
WEB_CONTENTS_USER_DATA_KEY_DECL();
base::WeakPtrFactory<SearchTabHelper> weak_factory_{this};
......
......@@ -44,10 +44,24 @@ struct AutocompleteMatch {
array<ACMatchClassification> contents_class;
mojo_base.mojom.String16 description;
array<ACMatchClassification> description_class;
// if a favicon is available for |destination_url| it is fetched in C++ and
// the resulting data URL is sent to JS via AutocompleteMatchImageAvailable
// along with the |destination_url| and the match index which are used to
// identify the appropriate match.
string destination_url;
mojo_base.mojom.String16 inline_autocompletion;
mojo_base.mojom.String16 fill_into_edit;
// The url for the suggestion icon. This is a relative url pointing to a
// bundled resource and is used directly in CSS to show the icon.
string icon_url;
// Used to paint a placeholder while fetching |image_url|. These two fields
// are valid for entity suggestions only. Entity suggestions have a |type| of
// 'search-suggest-entity'.
string image_dominant_color;
// The image url for entity suggestions. |image_url| is an external url and
// therefore is fetched in C++ and the resulting data URL is sent to JS via
// AutocompleteMatchImageAvailable along with the |image_url| and the match
// index which are used to identify the appropriate match.
string image_url;
bool is_search_type; // Result of AutocompleteMatch::IsSearchType().
string type; // Result of AutocompleteMatchType::ToString().
......@@ -231,9 +245,11 @@ interface EmbeddedSearchClient {
// Updates the NTP realbox with the autocomplete results.
AutocompleteResultChanged(AutocompleteResult result);
// Updates the NTP realbox with the given autocomplete match's image data.
// Updates the NTP realbox popup with the image or favicon data URL for the
// given |match_index| and |url| where |url| is an AutocompleteMatch image_url
// or destination_url for an entity or a navigation suggestion respectively.
AutocompleteMatchImageAvailable(uint32 match_index,
string image_url,
string url,
string data_url);
// Update the page sequence number for the page.
......
......@@ -455,6 +455,7 @@ base::Value CreateAutocompleteMatches(
dict.SetStringKey("inlineAutocompletion", match->inline_autocompletion);
dict.SetBoolKey("isSearchType", match->is_search_type);
dict.SetStringKey("fillIntoEdit", match->fill_into_edit);
dict.SetStringKey("iconUrl", match->icon_url);
dict.SetStringKey("imageDominantColor", match->image_dominant_color);
dict.SetStringKey("imageUrl", match->image_url);
dict.SetBoolKey("swapContentsAndDescription",
......
......@@ -70,8 +70,7 @@
<div id="realbox-container">
<div id="realbox-input-wrapper">
<div id="realbox-icon" class="search-icon"
data-realbox-icon-class="search-icon"></div>
<div id="realbox-icon" data-default-icon="search"></div>
<input id="realbox" type="search" autocomplete="off"
spellcheck="false" autofocus>
<button id="realbox-microphone" class="microphone-icon" hidden>
......
......@@ -3,6 +3,8 @@
<!-- TODO(dschuyler): Many of these may be included for the unit tests which
don't appear to flatten the html. We may be able to avoid including
these if the browser_tests would not try to load them. -->
<include name="IDR_WEBUI_IMAGES_200_LOGO_GOOGLE_G"
file="images/200-logo_googleg.png" type="BINDATA" compress="gzip" />
<include name="IDR_WEBUI_IMAGES_ARROW_DOWN"
file="images/arrow_down.svg" type="BINDATA" compress="gzip" />
<include name="IDR_WEBUI_IMAGES_ARROW_RIGHT"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment