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

[Local NTP][Realbox] Adds support for entity suggestions in the NTP realbox

before: https://screenshot.googleplex.com/M5ENgtHmDrF
after: https://screenshot.googleplex.com/tgrmVBKYGjO

Bug: 1030911
Change-Id: Ida41a52d1b2e305ae467a1f83452cfd084b26c1a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1979148
Commit-Queue: Moe Ahmadi <mahmadi@chromium.org>
Reviewed-by: default avatarDan Beam <dbeam@chromium.org>
Reviewed-by: default avatarWill Harris <wfh@chromium.org>
Cr-Commit-Position: refs/heads/master@{#728288}
parent 031aa7ea
...@@ -436,6 +436,8 @@ let ACMatchClassification; ...@@ -436,6 +436,8 @@ let ACMatchClassification;
* inlineAutocompletion: string, * inlineAutocompletion: string,
* isSearchType: boolean, * isSearchType: boolean,
* fillIntoEdit: string, * fillIntoEdit: string,
* imageDominantColor: string,
* imageUrl: string,
* supportsDeletion: boolean, * supportsDeletion: boolean,
* swapContentsAndDescription: boolean, * swapContentsAndDescription: boolean,
* type: string, * type: string,
...@@ -454,6 +456,9 @@ let AutocompleteResult; ...@@ -454,6 +456,9 @@ let AutocompleteResult;
/** @type {function(!AutocompleteResult):void} */ /** @type {function(!AutocompleteResult):void} */
window.chrome.embeddedSearch.searchBox.autocompleteresultchanged; window.chrome.embeddedSearch.searchBox.autocompleteresultchanged;
/** @type {function(number, string, string):void} */
window.chrome.embeddedSearch.searchBox.autocompletematchimageavailable;
/** /**
* @param {number} line * @param {number} line
* @param {string} url * @param {string} url
......
...@@ -264,7 +264,7 @@ body.hide-fakebox #fakebox { ...@@ -264,7 +264,7 @@ body.hide-fakebox #fakebox {
overflow: hidden; overflow: hidden;
padding-bottom: 8px; padding-bottom: 8px;
padding-inline-end: 16px; padding-inline-end: 16px;
padding-inline-start: 48px; padding-inline-start: 52px;
padding-top: 8px; padding-top: 8px;
position: relative; position: relative;
text-decoration: none; text-decoration: none;
...@@ -276,6 +276,24 @@ body.hide-fakebox #fakebox { ...@@ -276,6 +276,24 @@ body.hide-fakebox #fakebox {
background-position-x: calc(100% - 16px); background-position-x: calc(100% - 16px);
} }
#realbox-matches a.has-image {
display: flex;
flex-direction: column;
height: 32px;
justify-content: center;
padding-bottom: 6px;
padding-top: 6px;
}
#realbox-matches a.has-image > span {
margin-bottom: 2px;
margin-top: 2px;
}
#realbox-matches a.has-image .description {
font-size: 14px;
}
#realbox-matches.removable a { #realbox-matches.removable a {
padding-inline-end: 48px; padding-inline-end: 48px;
} }
...@@ -284,16 +302,22 @@ body.hide-fakebox #fakebox { ...@@ -284,16 +302,22 @@ body.hide-fakebox #fakebox {
margin-bottom: 8px; /* Last result is tight with border-radius. */ 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, .clock-icon,
.search-icon { .search-icon {
-webkit-mask-position: center; -webkit-mask-position: center;
-webkit-mask-repeat: no-repeat; -webkit-mask-repeat: no-repeat;
-webkit-mask-size: 16px; -webkit-mask-size: 16px;
background-color: var(--search-box-icon, rgb(117, 117, 117)); background-color: var(--search-box-icon, rgb(117, 117, 117));
bottom: 0;
left: 16px; left: 16px;
position: absolute;
top: 0;
width: 24px; width: 24px;
} }
...@@ -311,9 +335,31 @@ body.hide-fakebox #fakebox { ...@@ -311,9 +335,31 @@ body.hide-fakebox #fakebox {
} }
html[dir=rtl] :-webkit-any(.clock-icon, .search-icon) { html[dir=rtl] :-webkit-any(.clock-icon, .search-icon) {
left: auto;
right: 16px; right: 16px;
} }
.image-container {
align-items: center;
border-radius: 8px;
display: flex;
height: 32px;
justify-content: center;
left: 12px;
width: 32px;
}
html[dir=rtl] .image-container {
left: auto;
right: 12px;
}
.match-image {
border-radius: 8px;
max-height: 32px;
max-width: 32px;
}
#realbox-matches a:hover { #realbox-matches a:hover {
background-color: var(--search-box-results-bg-hovered, rgb(232, 232, 233)); background-color: var(--search-box-results-bg-hovered, rgb(232, 232, 233));
} }
...@@ -416,7 +462,7 @@ html[dir=rtl] #fakebox-text { ...@@ -416,7 +462,7 @@ html[dir=rtl] #fakebox-text {
#fakebox-text, #fakebox-text,
#realbox { #realbox {
padding-inline-start: 48px; padding-inline-start: 52px;
} }
#fakebox-cursor { #fakebox-cursor {
......
...@@ -67,6 +67,8 @@ const CLASSES = { ...@@ -67,6 +67,8 @@ const CLASSES = {
// Applies styles to dialogs used in customization. // Applies styles to dialogs used in customization.
CUSTOMIZE_DIALOG: 'customize-dialog', CUSTOMIZE_DIALOG: 'customize-dialog',
DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide', DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide',
DESCRIPTION: 'description',
DIM: 'dim',
DISMISSABLE: 'dismissable', DISMISSABLE: 'dismissable',
DISMISS_ICON: 'dismiss-icon', DISMISS_ICON: 'dismiss-icon',
DISMISS_PROMO: 'dismiss-promo', DISMISS_PROMO: 'dismiss-promo',
...@@ -78,12 +80,18 @@ const CLASSES = { ...@@ -78,12 +80,18 @@ const CLASSES = {
FLOAT_UP: 'float-up', FLOAT_UP: 'float-up',
// Applies drag focus style to the fakebox // Applies drag focus style to the fakebox
FAKEBOX_DRAG_FOCUS: 'fakebox-drag-focused', FAKEBOX_DRAG_FOCUS: 'fakebox-drag-focused',
HAS_IMAGE: 'has-image', // A realbox match with an image.
// Applies a different style to the error notification if a link is present. // Applies a different style to the error notification if a link is present.
HAS_LINK: 'has-link', HAS_LINK: 'has-link',
HIDE_FAKEBOX: 'hide-fakebox', HIDE_FAKEBOX: 'hide-fakebox',
HIDE_NOTIFICATION: 'notice-hide', HIDE_NOTIFICATION: 'notice-hide',
// Contains the image next to a realbox match. Displays a placeholder color
// while the realbox match image is loading.
IMAGE_CONTAINER: 'image-container',
INITED: 'inited', // Reveals the <body> once init() is done. INITED: 'inited', // Reveals the <body> once init() is done.
LEFT_ALIGN_ATTRIBUTION: 'left-align-attribution', LEFT_ALIGN_ATTRIBUTION: 'left-align-attribution',
// The image next to a realbox match.
MATCH_IMAGE: 'match-image',
// Vertically centers the most visited section for a non-Google provided page. // Vertically centers the most visited section for a non-Google provided page.
NON_GOOGLE_PAGE: 'non-google-page', NON_GOOGLE_PAGE: 'non-google-page',
REMOVABLE: 'removable', REMOVABLE: 'removable',
...@@ -261,6 +269,13 @@ let delayedHideNotification = null; ...@@ -261,6 +269,13 @@ let delayedHideNotification = null;
*/ */
let enterWasPressed = false; let enterWasPressed = false;
/**
* A cache from image URL to image content in the form of data URL in order to
* reuse match image data that have been loaded before and to avoid flickering.
* @type {!Object<string>}
*/
const imageUrlToDataUrlCache = {};
/** /**
* True if dark mode is enabled. * True if dark mode is enabled.
* @type {boolean} * @type {boolean}
...@@ -353,6 +368,36 @@ function autocompleteResultChanged(result) { ...@@ -353,6 +368,36 @@ function autocompleteResultChanged(result) {
} }
} }
/**
* @param {number} matchIndex
* @param {string} imageUrl
* @param {string} dataUrl
*/
function autocompleteMatchImageAvailable(matchIndex, imageUrl, dataUrl) {
if (!autocompleteResult || !autocompleteResult.matches[matchIndex] ||
autocompleteResult.matches[matchIndex].imageUrl !== imageUrl) {
return;
}
// Ignore images that have previously been loaded. Those are rendered already.
if (imageUrlToDataUrlCache[imageUrl]) {
return;
}
imageUrlToDataUrlCache[imageUrl] = dataUrl;
const realboxMatchesEl = $(IDS.REALBOX_MATCHES);
const matchEls = Array.from(realboxMatchesEl.children);
assert(autocompleteResult.matches.length === matchEls.length);
const imageContainerEl = assert(
matchEls[matchIndex].getElementsByClassName(CLASSES.IMAGE_CONTAINER)[0]);
const imageEl = document.createElement('img');
imageEl.classList.add(CLASSES.MATCH_IMAGE);
imageEl.src = dataUrl;
imageContainerEl.appendChild(imageEl);
imageContainerEl.style.backgroundColor = 'transparent';
}
/** /**
* @param {number} style * @param {number} style
* @return {!Array<string>} * @return {!Array<string>}
...@@ -878,6 +923,8 @@ function init() { ...@@ -878,6 +923,8 @@ function init() {
realboxWrapper.addEventListener('keydown', onRealboxWrapperKeydown); realboxWrapper.addEventListener('keydown', onRealboxWrapperKeydown);
searchboxApiHandle.autocompleteresultchanged = autocompleteResultChanged; searchboxApiHandle.autocompleteresultchanged = autocompleteResultChanged;
searchboxApiHandle.autocompletematchimageavailable =
autocompleteMatchImageAvailable;
if (!iframesAndVoiceSearchDisabledForTesting) { if (!iframesAndVoiceSearchDisabledForTesting) {
speech.init( speech.init(
...@@ -1630,28 +1677,52 @@ function renderAutocompleteMatches(matches) { ...@@ -1630,28 +1677,52 @@ function renderAutocompleteMatches(matches) {
} }
}; };
const hasImage = !!match.imageUrl;
if (hasImage) {
matchEl.classList.add(CLASSES.HAS_IMAGE);
}
if (match.isSearchType) { if (match.isSearchType) {
const icon = document.createElement('div'); const icon = document.createElement('div');
const isSearchHistory = SEARCH_HISTORY_MATCH_TYPES.includes(match.type); if (hasImage) {
icon.classList.add( icon.classList.add(CLASSES.IMAGE_CONTAINER);
isSearchHistory ? CLASSES.CLOCK_ICON : CLASSES.SEARCH_ICON);
if (imageUrlToDataUrlCache[match.imageUrl]) {
const imageEl = document.createElement('img');
imageEl.classList.add(CLASSES.MATCH_IMAGE);
imageEl.src = imageUrlToDataUrlCache[match.imageUrl];
icon.appendChild(imageEl);
} else if (match.imageDominantColor) {
// .25 Opacity matching c/b/u/views/omnibox/omnibox_match_cell_view.cc
icon.style.backgroundColor = match.imageDominantColor + '40';
}
} else {
const isSearchHistory = SEARCH_HISTORY_MATCH_TYPES.includes(match.type);
icon.classList.add(
isSearchHistory ? CLASSES.CLOCK_ICON : CLASSES.SEARCH_ICON);
}
matchEl.appendChild(icon); matchEl.appendChild(icon);
} else { } else {
const iconUrl = getIconUrl(match.destinationUrl); const iconUrl = getIconUrl(match.destinationUrl);
matchEl.style.backgroundImage = `url(${iconUrl})`; matchEl.style.backgroundImage = `url(${iconUrl})`;
} }
const contentsEls = const contentsEl =
renderMatchClassifications(match.contents, match.contentsClass); renderMatchClassifications(match.contents, match.contentsClass);
const descriptionEls = []; let descriptionEl;
const separatorEls = []; let separatorEl;
let separatorText = ''; let separatorText = '';
if (match.description) { if (match.description) {
descriptionEls.push(...renderMatchClassifications( descriptionEl =
match.description, match.descriptionClass)); renderMatchClassifications(match.description, match.descriptionClass);
separatorText = configData.translatedStrings.realboxSeparator; descriptionEl.classList.add(CLASSES.DESCRIPTION);
separatorEls.push(document.createTextNode(separatorText)); if (hasImage) {
descriptionEl.classList.add(CLASSES.DIM);
} else {
separatorText = configData.translatedStrings.realboxSeparator;
separatorEl = document.createTextNode(separatorText);
}
} }
const ariaLabel = match.swapContentsAndDescription ? const ariaLabel = match.swapContentsAndDescription ?
...@@ -1660,11 +1731,13 @@ function renderAutocompleteMatches(matches) { ...@@ -1660,11 +1731,13 @@ function renderAutocompleteMatches(matches) {
matchEl.setAttribute('aria-label', ariaLabel); matchEl.setAttribute('aria-label', ariaLabel);
const layout = match.swapContentsAndDescription ? const layout = match.swapContentsAndDescription ?
[descriptionEls, separatorEls, contentsEls] : [descriptionEl, separatorEl, contentsEl] :
[contentsEls, separatorEls, descriptionEls]; [contentsEl, separatorEl, descriptionEl];
for (const col of layout) { for (const colEl of layout) {
col.forEach(colEl => matchEl.appendChild(colEl)); if (colEl) {
matchEl.appendChild(colEl);
}
} }
if (match.supportsDeletion && configData.suggestionTransparencyEnabled) { if (match.supportsDeletion && configData.suggestionTransparencyEnabled) {
...@@ -1718,16 +1791,22 @@ function renderAutocompleteMatches(matches) { ...@@ -1718,16 +1791,22 @@ function renderAutocompleteMatches(matches) {
/** /**
* @param {string} text * @param {string} text
* @param {!Array<!ACMatchClassification>} classifications * @param {!Array<!ACMatchClassification>} classifications
* @return {!Array<!Element>} * @return {!Element}
*/ */
function renderMatchClassifications(text, classifications) { function renderMatchClassifications(text, classifications) {
return classifications.map((classification, i) => { return classifications
const classes = classificationStyleToClasses(classification.style); .map((classification, i) => {
const next = classifications[i + 1] || {offset: text.length}; const classes = classificationStyleToClasses(classification.style);
const classifiedText = text.substring(classification.offset, next.offset); const next = classifications[i + 1] || {offset: text.length};
return classes.length ? spanWithClasses(classifiedText, classes) : const classifiedText =
document.createTextNode(classifiedText); text.substring(classification.offset, next.offset);
}); return classes.length ? spanWithClasses(classifiedText, classes) :
document.createTextNode(classifiedText);
})
.reduce((container, currentElement) => {
container.appendChild(currentElement);
return container;
}, document.createElement('span'));
} }
/** /**
......
...@@ -120,6 +120,19 @@ void SearchIPCRouter::AutocompleteResultChanged( ...@@ -120,6 +120,19 @@ void SearchIPCRouter::AutocompleteResultChanged(
embedded_search_client()->AutocompleteResultChanged(std::move(result)); embedded_search_client()->AutocompleteResultChanged(std::move(result));
} }
void SearchIPCRouter::AutocompleteMatchImageAvailable(
uint32_t match_index,
const std::string& image_url,
const std::string& data_url) {
if (!policy_->ShouldProcessAutocompleteMatchImageAvailable(is_active_tab_) ||
!embedded_search_client()) {
return;
}
embedded_search_client()->AutocompleteMatchImageAvailable(
match_index, image_url, data_url);
}
void SearchIPCRouter::OnNavigationEntryCommitted() { void SearchIPCRouter::OnNavigationEntryCommitted() {
++commit_counter_; ++commit_counter_;
if (!embedded_search_client()) if (!embedded_search_client())
......
...@@ -219,6 +219,8 @@ class SearchIPCRouter : public content::WebContentsObserver, ...@@ -219,6 +219,8 @@ class SearchIPCRouter : public content::WebContentsObserver,
virtual bool ShouldProcessOptOutOfSearchSuggestions() = 0; virtual bool ShouldProcessOptOutOfSearchSuggestions() = 0;
virtual bool ShouldProcessThemeChangeMessages() = 0; virtual bool ShouldProcessThemeChangeMessages() = 0;
virtual bool ShouldProcessAutocompleteResultChanged(bool is_active_tab) = 0; virtual bool ShouldProcessAutocompleteResultChanged(bool is_active_tab) = 0;
virtual bool ShouldProcessAutocompleteMatchImageAvailable(
bool is_active_tab) = 0;
virtual bool ShouldProcessQueryAutocomplete(bool is_active_tab) = 0; virtual bool ShouldProcessQueryAutocomplete(bool is_active_tab) = 0;
virtual bool ShouldProcessStopAutocomplete() = 0; virtual bool ShouldProcessStopAutocomplete() = 0;
virtual bool ShouldProcessBlocklistPromo() = 0; virtual bool ShouldProcessBlocklistPromo() = 0;
...@@ -248,6 +250,11 @@ class SearchIPCRouter : public content::WebContentsObserver, ...@@ -248,6 +250,11 @@ class SearchIPCRouter : public content::WebContentsObserver,
// Updates the renderer with the autocomplete results. // Updates the renderer with the autocomplete results.
void AutocompleteResultChanged(chrome::mojom::AutocompleteResultPtr result); void AutocompleteResultChanged(chrome::mojom::AutocompleteResultPtr result);
// Updates the renderer with the given autocomplete match's image data.
void AutocompleteMatchImageAvailable(uint32_t match_index,
const std::string& image_url,
const std::string& data_url);
// Tells the SearchIPCRouter that a new page in an Instant process committed. // Tells the SearchIPCRouter that a new page in an Instant process committed.
void OnNavigationEntryCommitted(); void OnNavigationEntryCommitted();
......
...@@ -137,6 +137,11 @@ bool SearchIPCRouterPolicyImpl::ShouldProcessAutocompleteResultChanged( ...@@ -137,6 +137,11 @@ bool SearchIPCRouterPolicyImpl::ShouldProcessAutocompleteResultChanged(
return is_active_tab && !is_incognito_ && search::IsInstantNTP(web_contents_); return is_active_tab && !is_incognito_ && search::IsInstantNTP(web_contents_);
} }
bool SearchIPCRouterPolicyImpl::ShouldProcessAutocompleteMatchImageAvailable(
bool is_active_tab) {
return is_active_tab && !is_incognito_ && search::IsInstantNTP(web_contents_);
}
bool SearchIPCRouterPolicyImpl::ShouldProcessQueryAutocomplete( bool SearchIPCRouterPolicyImpl::ShouldProcessQueryAutocomplete(
bool is_active_tab) { bool is_active_tab) {
return is_active_tab && !is_incognito_ && search::IsInstantNTP(web_contents_); return is_active_tab && !is_incognito_ && search::IsInstantNTP(web_contents_);
......
...@@ -55,6 +55,8 @@ class SearchIPCRouterPolicyImpl : public SearchIPCRouter::Policy { ...@@ -55,6 +55,8 @@ class SearchIPCRouterPolicyImpl : public SearchIPCRouter::Policy {
bool ShouldProcessOptOutOfSearchSuggestions() override; bool ShouldProcessOptOutOfSearchSuggestions() override;
bool ShouldProcessThemeChangeMessages() override; bool ShouldProcessThemeChangeMessages() override;
bool ShouldProcessAutocompleteResultChanged(bool is_active_tab) override; bool ShouldProcessAutocompleteResultChanged(bool is_active_tab) override;
bool ShouldProcessAutocompleteMatchImageAvailable(
bool is_active_tab) override;
bool ShouldProcessQueryAutocomplete(bool is_active_tab) override; bool ShouldProcessQueryAutocomplete(bool is_active_tab) override;
bool ShouldProcessStopAutocomplete() override; bool ShouldProcessStopAutocomplete() override;
bool ShouldProcessBlocklistPromo() override; bool ShouldProcessBlocklistPromo() override;
......
...@@ -160,6 +160,7 @@ class MockSearchIPCRouterPolicy : public SearchIPCRouter::Policy { ...@@ -160,6 +160,7 @@ class MockSearchIPCRouterPolicy : public SearchIPCRouter::Policy {
MOCK_METHOD0(ShouldSendLocalBackgroundSelected, bool()); MOCK_METHOD0(ShouldSendLocalBackgroundSelected, bool());
MOCK_METHOD0(ShouldProcessThemeChangeMessages, bool()); MOCK_METHOD0(ShouldProcessThemeChangeMessages, bool());
MOCK_METHOD1(ShouldProcessAutocompleteResultChanged, bool(bool)); MOCK_METHOD1(ShouldProcessAutocompleteResultChanged, bool(bool));
MOCK_METHOD1(ShouldProcessAutocompleteMatchImageAvailable, bool(bool));
MOCK_METHOD1(ShouldProcessQueryAutocomplete, bool(bool)); MOCK_METHOD1(ShouldProcessQueryAutocomplete, bool(bool));
MOCK_METHOD0(ShouldProcessStopAutocomplete, bool()); MOCK_METHOD0(ShouldProcessStopAutocomplete, bool());
MOCK_METHOD0(ShouldProcessBlocklistPromo, bool()); MOCK_METHOD0(ShouldProcessBlocklistPromo, bool());
...@@ -1120,6 +1121,36 @@ TEST_F(SearchIPCRouterTest, IgnoreAutocompleteResultChanged) { ...@@ -1120,6 +1121,36 @@ TEST_F(SearchIPCRouterTest, IgnoreAutocompleteResultChanged) {
std::vector<chrome::mojom::AutocompleteMatchPtr>())); std::vector<chrome::mojom::AutocompleteMatchPtr>()));
} }
TEST_F(SearchIPCRouterTest, SendAutocompleteMatchImageAvailable) {
NavigateAndCommitActiveTab(GURL("chrome-search://foo/bar"));
SetupMockDelegateAndPolicy();
MockSearchIPCRouterPolicy* policy = GetSearchIPCRouterPolicy();
EXPECT_CALL(*policy, ShouldProcessAutocompleteMatchImageAvailable(_))
.Times(1)
.WillOnce(Return(true));
EXPECT_CALL(*mock_embedded_search_client(),
AutocompleteMatchImageAvailable(_, _, _))
.Times(1);
GetSearchIPCRouter().AutocompleteMatchImageAvailable(0, std::string(),
std::string());
}
TEST_F(SearchIPCRouterTest, IgnoreAutocompleteMatchImageAvailable) {
NavigateAndCommitActiveTab(GURL("chrome-search://foo/bar"));
SetupMockDelegateAndPolicy();
MockSearchIPCRouterPolicy* policy = GetSearchIPCRouterPolicy();
EXPECT_CALL(*policy, ShouldProcessAutocompleteMatchImageAvailable(_))
.Times(1)
.WillOnce(Return(false));
EXPECT_CALL(*mock_embedded_search_client(),
AutocompleteMatchImageAvailable(_, _, _))
.Times(0);
GetSearchIPCRouter().AutocompleteMatchImageAvailable(0, std::string(),
std::string());
}
TEST_F(SearchIPCRouterTest, IgnoreQueryAutocomplete) { TEST_F(SearchIPCRouterTest, IgnoreQueryAutocomplete) {
NavigateAndCommitActiveTab(GURL("chrome-search://foo/bar")); NavigateAndCommitActiveTab(GURL("chrome-search://foo/bar"));
SetupMockDelegateAndPolicy(); SetupMockDelegateAndPolicy();
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
#include <memory> #include <memory>
#include "base/base64.h"
#include "base/metrics/histogram_macros.h" #include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h" #include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h" #include "base/metrics/user_metrics_action.h"
...@@ -103,6 +104,8 @@ std::vector<chrome::mojom::AutocompleteMatchPtr> CreateAutocompleteMatches( ...@@ -103,6 +104,8 @@ std::vector<chrome::mojom::AutocompleteMatchPtr> CreateAutocompleteMatches(
description_class.style)); description_class.style));
} }
mojom_match->destination_url = match.destination_url.spec(); mojom_match->destination_url = match.destination_url.spec();
mojom_match->image_dominant_color = match.image_dominant_color;
mojom_match->image_url = match.image_url;
mojom_match->fill_into_edit = match.fill_into_edit; mojom_match->fill_into_edit = match.fill_into_edit;
mojom_match->inline_autocompletion = match.inline_autocompletion; mojom_match->inline_autocompletion = match.inline_autocompletion;
mojom_match->is_search_type = AutocompleteMatch::IsSearchType(match.type); mojom_match->is_search_type = AutocompleteMatch::IsSearchType(match.type);
...@@ -512,6 +515,34 @@ void SearchTabHelper::OnResultChanged(bool default_result_changed) { ...@@ -512,6 +515,34 @@ void SearchTabHelper::OnResultChanged(bool default_result_changed) {
ipc_router_.AutocompleteResultChanged(chrome::mojom::AutocompleteResult::New( ipc_router_.AutocompleteResultChanged(chrome::mojom::AutocompleteResult::New(
autocomplete_controller_->input().text(), autocomplete_controller_->input().text(),
CreateAutocompleteMatches(autocomplete_controller_->result()))); CreateAutocompleteMatches(autocomplete_controller_->result())));
// Create new bitmap requests.
BitmapFetcherHelper bitmap_fetcher_helper(profile());
int match_index = -1;
for (const auto& match : autocomplete_controller_->result()) {
match_index++;
if (match.ImageUrl().is_empty()) {
continue;
}
bitmap_fetcher_helper.RequestImage(
match.ImageUrl(),
base::BindRepeating(&SearchTabHelper::OnBitmapFetched,
weak_factory_.GetWeakPtr(), match_index,
match.ImageUrl().spec()));
}
}
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();
ipc_router_.AutocompleteMatchImageAvailable(match_index, image_url, data_url);
} }
void SearchTabHelper::OnSelectLocalBackgroundImage() { void SearchTabHelper::OnSelectLocalBackgroundImage() {
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
#define CHROME_BROWSER_UI_SEARCH_SEARCH_TAB_HELPER_H_ #define CHROME_BROWSER_UI_SEARCH_SEARCH_TAB_HELPER_H_
#include <memory> #include <memory>
#include <string>
#include <vector> #include <vector>
#include "base/gtest_prod_util.h" #include "base/gtest_prod_util.h"
...@@ -44,6 +45,7 @@ class OmniboxView; ...@@ -44,6 +45,7 @@ class OmniboxView;
class Profile; class Profile;
class SearchIPCRouterTest; class SearchIPCRouterTest;
class SearchSuggestService; class SearchSuggestService;
class SkBitmap;
// This is the browser-side, per-tab implementation of the embeddedSearch API // This is the browser-side, per-tab implementation of the embeddedSearch API
// (see https://www.chromium.org/embeddedsearch). // (see https://www.chromium.org/embeddedsearch).
...@@ -172,6 +174,10 @@ class SearchTabHelper : public content::WebContentsObserver, ...@@ -172,6 +174,10 @@ class SearchTabHelper : public content::WebContentsObserver,
// Overridden from AutocompleteControllerDelegate: // Overridden from AutocompleteControllerDelegate:
void OnResultChanged(bool default_match_changed) override; void OnResultChanged(bool default_match_changed) override;
void OnBitmapFetched(int match_index,
const std::string& image_url,
const SkBitmap& bitmap);
OmniboxView* GetOmniboxView(); OmniboxView* GetOmniboxView();
const OmniboxView* GetOmniboxView() const; const OmniboxView* GetOmniboxView() const;
......
...@@ -47,6 +47,8 @@ struct AutocompleteMatch { ...@@ -47,6 +47,8 @@ struct AutocompleteMatch {
string destination_url; string destination_url;
mojo_base.mojom.String16 inline_autocompletion; mojo_base.mojom.String16 inline_autocompletion;
mojo_base.mojom.String16 fill_into_edit; mojo_base.mojom.String16 fill_into_edit;
string image_dominant_color;
string image_url;
bool is_search_type; // Result of AutocompleteMatch::IsSearchType(). bool is_search_type; // Result of AutocompleteMatch::IsSearchType().
string type; // Result of AutocompleteMatchType::ToString(). string type; // Result of AutocompleteMatchType::ToString().
bool swap_contents_and_description; bool swap_contents_and_description;
...@@ -225,6 +227,11 @@ interface EmbeddedSearchClient { ...@@ -225,6 +227,11 @@ interface EmbeddedSearchClient {
// Updates the NTP realbox with the autocomplete results. // Updates the NTP realbox with the autocomplete results.
AutocompleteResultChanged(AutocompleteResult result); AutocompleteResultChanged(AutocompleteResult result);
// Updates the NTP realbox with the given autocomplete match's image data.
AutocompleteMatchImageAvailable(uint32 match_index,
string image_url,
string data_url);
// Update the page sequence number for the page. // Update the page sequence number for the page.
SetPageSequenceNumber(int32 page_seq_no); SetPageSequenceNumber(int32 page_seq_no);
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
#ifndef CHROME_COMMON_SEARCH_MOCK_EMBEDDED_SEARCH_CLIENT_H_ #ifndef CHROME_COMMON_SEARCH_MOCK_EMBEDDED_SEARCH_CLIENT_H_
#define CHROME_COMMON_SEARCH_MOCK_EMBEDDED_SEARCH_CLIENT_H_ #define CHROME_COMMON_SEARCH_MOCK_EMBEDDED_SEARCH_CLIENT_H_
#include <string>
#include "chrome/common/search.mojom.h" #include "chrome/common/search.mojom.h"
#include "chrome/common/search/instant_types.h" #include "chrome/common/search/instant_types.h"
#include "testing/gmock/include/gmock/gmock.h" #include "testing/gmock/include/gmock/gmock.h"
...@@ -16,6 +18,8 @@ class MockEmbeddedSearchClient : public chrome::mojom::EmbeddedSearchClient { ...@@ -16,6 +18,8 @@ class MockEmbeddedSearchClient : public chrome::mojom::EmbeddedSearchClient {
MOCK_METHOD1(AutocompleteResultChanged, MOCK_METHOD1(AutocompleteResultChanged,
void(chrome::mojom::AutocompleteResultPtr result)); void(chrome::mojom::AutocompleteResultPtr result));
MOCK_METHOD3(AutocompleteMatchImageAvailable,
void(uint32_t, const std::string&, const std::string&));
MOCK_METHOD1(SetPageSequenceNumber, void(int)); MOCK_METHOD1(SetPageSequenceNumber, void(int));
MOCK_METHOD2(FocusChanged, void(OmniboxFocusState, OmniboxFocusChangeReason)); MOCK_METHOD2(FocusChanged, void(OmniboxFocusState, OmniboxFocusChangeReason));
MOCK_METHOD1(MostVisitedInfoChanged, void(const InstantMostVisitedInfo&)); MOCK_METHOD1(MostVisitedInfoChanged, void(const InstantMostVisitedInfo&));
......
...@@ -541,6 +541,15 @@ void SearchBox::AutocompleteResultChanged( ...@@ -541,6 +541,15 @@ void SearchBox::AutocompleteResultChanged(
} }
} }
void SearchBox::AutocompleteMatchImageAvailable(uint32_t match_index,
const std::string& image_url,
const std::string& data_url) {
if (can_run_js_in_renderframe_) {
SearchBoxExtension::DispatchAutocompleteMatchImageAvailable(
render_frame()->GetWebFrame(), match_index, image_url, data_url);
}
}
void SearchBox::MostVisitedInfoChanged( void SearchBox::MostVisitedInfoChanged(
const InstantMostVisitedInfo& most_visited_info) { const InstantMostVisitedInfo& most_visited_info) {
has_received_most_visited_ = true; has_received_most_visited_ = true;
......
...@@ -240,6 +240,9 @@ class SearchBox : public content::RenderFrameObserver, ...@@ -240,6 +240,9 @@ class SearchBox : public content::RenderFrameObserver,
// Overridden from chrome::mojom::EmbeddedSearchClient: // Overridden from chrome::mojom::EmbeddedSearchClient:
void AutocompleteResultChanged( void AutocompleteResultChanged(
chrome::mojom::AutocompleteResultPtr result) override; chrome::mojom::AutocompleteResultPtr result) override;
void AutocompleteMatchImageAvailable(uint32_t match_index,
const std::string& image_url,
const std::string& data_url) override;
void SetPageSequenceNumber(int page_seq_no) override; void SetPageSequenceNumber(int page_seq_no) override;
void FocusChanged(OmniboxFocusState new_focus_state, void FocusChanged(OmniboxFocusState new_focus_state,
OmniboxFocusChangeReason reason) override; OmniboxFocusChangeReason reason) override;
......
...@@ -444,6 +444,8 @@ base::Value CreateAutocompleteMatches( ...@@ -444,6 +444,8 @@ base::Value CreateAutocompleteMatches(
dict.SetStringKey("inlineAutocompletion", match->inline_autocompletion); dict.SetStringKey("inlineAutocompletion", match->inline_autocompletion);
dict.SetBoolKey("isSearchType", match->is_search_type); dict.SetBoolKey("isSearchType", match->is_search_type);
dict.SetStringKey("fillIntoEdit", match->fill_into_edit); dict.SetStringKey("fillIntoEdit", match->fill_into_edit);
dict.SetStringKey("imageDominantColor", match->image_dominant_color);
dict.SetStringKey("imageUrl", match->image_url);
dict.SetBoolKey("swapContentsAndDescription", dict.SetBoolKey("swapContentsAndDescription",
match->swap_contents_and_description); match->swap_contents_and_description);
dict.SetStringKey("type", match->type); dict.SetStringKey("type", match->type);
...@@ -497,6 +499,19 @@ static const char kDispatchAutocompleteResultChanged[] = ...@@ -497,6 +499,19 @@ static const char kDispatchAutocompleteResultChanged[] =
" true;" " true;"
"}"; "}";
static const char kDispatchAutocompleteMatchImageAvailable[] =
"if (window.chrome &&"
" window.chrome.embeddedSearch &&"
" window.chrome.embeddedSearch.searchBox &&"
" "
"window.chrome.embeddedSearch.searchBox.autocompletematchimageavailable &&"
" typeof window.chrome.embeddedSearch.searchBox"
" .autocompletematchimageavailable === 'function') {"
" window.chrome.embeddedSearch.searchBox"
" .autocompletematchimageavailable(%d, '%s', '%s');"
" true;"
"}";
static const char kDispatchDeleteCustomLinkResult[] = static const char kDispatchDeleteCustomLinkResult[] =
"if (window.chrome &&" "if (window.chrome &&"
" window.chrome.embeddedSearch &&" " window.chrome.embeddedSearch &&"
...@@ -1457,6 +1472,7 @@ void SearchBoxExtension::DispatchDeleteCustomLinkResult( ...@@ -1457,6 +1472,7 @@ void SearchBoxExtension::DispatchDeleteCustomLinkResult(
Dispatch(frame, script); Dispatch(frame, script);
} }
// static
void SearchBoxExtension::DispatchAutocompleteResultChanged( void SearchBoxExtension::DispatchAutocompleteResultChanged(
blink::WebLocalFrame* frame, blink::WebLocalFrame* frame,
chrome::mojom::AutocompleteResultPtr result) { chrome::mojom::AutocompleteResultPtr result) {
...@@ -1470,6 +1486,18 @@ void SearchBoxExtension::DispatchAutocompleteResultChanged( ...@@ -1470,6 +1486,18 @@ void SearchBoxExtension::DispatchAutocompleteResultChanged(
kDispatchAutocompleteResultChanged, json.c_str()))); kDispatchAutocompleteResultChanged, json.c_str())));
} }
// static
void SearchBoxExtension::DispatchAutocompleteMatchImageAvailable(
blink::WebLocalFrame* frame,
uint32_t match_index,
const std::string& image_url,
const std::string& data_url) {
blink::WebString script(blink::WebString::FromUTF8(
base::StringPrintf(kDispatchAutocompleteMatchImageAvailable, match_index,
image_url.c_str(), data_url.c_str())));
Dispatch(frame, script);
}
// static // static
void SearchBoxExtension::DispatchInputCancel(blink::WebLocalFrame* frame) { void SearchBoxExtension::DispatchInputCancel(blink::WebLocalFrame* frame) {
Dispatch(frame, kDispatchInputCancelScript); Dispatch(frame, kDispatchInputCancelScript);
......
...@@ -40,6 +40,11 @@ class SearchBoxExtension { ...@@ -40,6 +40,11 @@ class SearchBoxExtension {
static void DispatchAutocompleteResultChanged( static void DispatchAutocompleteResultChanged(
blink::WebLocalFrame* frame, blink::WebLocalFrame* frame,
chrome::mojom::AutocompleteResultPtr result); chrome::mojom::AutocompleteResultPtr result);
static void DispatchAutocompleteMatchImageAvailable(
blink::WebLocalFrame* frame,
uint32_t match_index,
const std::string& image_url,
const std::string& data_url);
static void DispatchInputCancel(blink::WebLocalFrame* frame); static void DispatchInputCancel(blink::WebLocalFrame* frame);
static void DispatchInputStart(blink::WebLocalFrame* frame); static void DispatchInputStart(blink::WebLocalFrame* frame);
static void DispatchKeyCaptureChange(blink::WebLocalFrame* frame); static void DispatchKeyCaptureChange(blink::WebLocalFrame* frame);
......
...@@ -27,6 +27,9 @@ test.realbox.IDS = { ...@@ -27,6 +27,9 @@ test.realbox.IDS = {
* @const * @const
*/ */
test.realbox.CLASSES = { test.realbox.CLASSES = {
HAS_IMAGE: 'has-image',
IMAGE_CONTAINER: 'image-container',
MATCH_IMAGE: 'match-image',
REMOVABLE: 'removable', REMOVABLE: 'removable',
REMOVE_ICON: 'remove-icon', REMOVE_ICON: 'remove-icon',
SELECTED: 'selected', SELECTED: 'selected',
...@@ -1300,3 +1303,64 @@ test.realbox2.testRealboxIconPrefixSearch = function() { ...@@ -1300,3 +1303,64 @@ test.realbox2.testRealboxIconPrefixSearch = function() {
assertFalse(test.realbox.areMatchesShowing()); assertFalse(test.realbox.areMatchesShowing());
assertFalse(!!realboxIcon.style.backgroundImage); assertFalse(!!realboxIcon.style.backgroundImage);
}; };
test.realbox2.testEntityMatchImage = function() {
const imageUrl = 'http://example.com/star.png';
const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC=';
const realboxIcon = $(test.realbox.IDS.REALBOX_ICON);
assertFalse(!!realboxIcon.style.backgroundImage);
test.realbox.realboxEl.value = 'star';
test.realbox.realboxEl.dispatchEvent(new CustomEvent('input'));
chrome.embeddedSearch.searchBox.autocompleteresultchanged({
input: test.realbox.realboxEl.value,
matches: [
test.realbox.getSearchMatch({
allowedToBeDefaultMatch: true,
imageUrl,
imageDominantColor: '#757575'
}),
test.realbox.getSearchMatch(),
],
});
assertTrue(test.realbox.areMatchesShowing());
// The first match is selected but it doesn't change the realbox icon.
const matchEls = $(test.realbox.IDS.REALBOX_MATCHES).children;
assertEquals(2, matchEls.length);
assertTrue(matchEls[0].classList.contains(test.realbox.CLASSES.SELECTED));
assertFalse(!!realboxIcon.style.backgroundImage);
// The first match is showing a placeholder color until the image loads.
assertTrue(matchEls[0].classList.contains(test.realbox.CLASSES.HAS_IMAGE));
const imageContainerEl = matchEls[0].getElementsByClassName(
test.realbox.CLASSES.IMAGE_CONTAINER)[0];
assertEquals(
'rgba(117, 117, 117, 0.25)', imageContainerEl.style.backgroundColor);
// The URL of the loaded image must match that of the autocomplete result at
// the given index.
chrome.embeddedSearch.searchBox.autocompletematchimageavailable(
1, imageUrl, dataUrl);
assertEquals(0, imageContainerEl.children.length);
assertEquals(
'rgba(117, 117, 117, 0.25)', imageContainerEl.style.backgroundColor);
chrome.embeddedSearch.searchBox.autocompletematchimageavailable(
0, 'http://example.com/moon.png', dataUrl);
assertEquals(0, imageContainerEl.children.length);
assertEquals(
'rgba(117, 117, 117, 0.25)', imageContainerEl.style.backgroundColor);
// Once the image is successfully loaded it replaces the placeholder color.
chrome.embeddedSearch.searchBox.autocompletematchimageavailable(
0, imageUrl, dataUrl);
assertEquals(
dataUrl,
imageContainerEl
.getElementsByClassName(test.realbox.CLASSES.MATCH_IMAGE)[0]
.src);
assertEquals('transparent', imageContainerEl.style.backgroundColor);
};
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