The Joy of the Hack: Gecko Focus Stealing Edition
I’ve been working on a prototype, on and off, over the past year. The idea is to do cool new stuff with the little popup menu that appears below the Firefox address bar as you type into it:
This prototype will be available to a beta testing audience as part of the new Test Pilot program at Mozilla, which is slated to launch in May. See the links section at the end of this article for more about Test Pilot.
There are lots of experiments we’d like to try with the urlbar, but the first one shows a server-generated recommendation above the existing history items and search suggestions. Because this is just a prototype, we want to get as much quality as we can with as little work as possible–which means we’re relying heavily on third-party services.
The recommendation is generated by a really simple algorithm:
- You type something in the urlbar, like ‘lol wut p’, and the keystrokes are sent to a server.
- The server looks up search suggestions for ‘lol wut p’, and finds that the top suggestion is ‘lol wut pear’.
- The server sends the suggestion, ‘lol wut pear’, to a search engine service; the top result is a page about that meme.
- The top result is sent back to the browser, where it’s shown in the urlbar popup:
Interacting with the popup
Keyboard interactions with the popup are straightforward. The list of results can be traversed using the up and down keys; a blue highlight tells the user which item is currently selected. Hitting ‘Enter’ loads the URL of the highlighted item.
If none of the results are highlighted when the user hits ‘Enter’, the browser either surfs to the URL in the urlbar, or searches the default search engine for the string in the urlbar. To clarify this search-or-navigate behavior, a top row was recently added to the urlbar, highlighted by default, which explains the options visually:
If the urlbar contents look like a URL, the top row explains that you’ll visit that URL by hitting Enter:
Otherwise, the top row explains that you’ll search for the typed string by hitting Enter:
Fitting the recommendation into the existing interface
Due to technical constraints, we’re inserting our recommendation above the existing list of results. Rather than continue to highlight the top results row, it seemed best to remove the highlight completely.
This initially seemed like a pretty simple task: the highlight is set by adjusting the selectedIndex
property on the panel, and setting it to -1
removes the highlight completely, so the
stealHighlight
function initially might look something like:
function stealHighlight() {
gURLBar.popup.selectedIndex = -1;
}
Sadly, this approach doesn’t work:
The results list is stealing the highlight back! What’s going on?
It turns out that the list of results isn’t rendered all at once. To keep the urlbar responsive to additional keystrokes or other user input, the results list is rendered a little at a time, in chunks of 5 rows, up to 6 times, for a total of 30 results in a scrollable list.
In addition, each time a new set of rows is inserted, the highlight is reapplied to
the selected item in the popup. This is why naively unsetting selectedIndex
didn’t
work: the recommendation finishes rendering before the results list, so the
highlight is quickly stolen back.
What to do?
There are two problems to be solved: detecting when new results are inserted, and stealing the highlight back quickly.
Detecting when results are inserted
The first problem, detecting when results are inserted, was a tough one. The UI controller code that fetches and inserts results is written in C++ (ugh), and there’s really no way to break into that code from an add-on. The C++ controller code is a huge file, so although Gecko’s XPCOM component system does allow components to be written in JS or C++, porting all of its behavior to JS would have required a rewrite–not a viable option. The existing code also doesn’t fire any signals that could be used to detect when results are about to be inserted.
The only option I could come up with was to use a MutationObserver
(MDN) to listen
for changes in the popup’s XUL DOM when the results were inserted. Even this required
some care: the results list recycles DOM nodes as a
performance enhancement, so listening for changes in the popup’s
childList
wasn’t enough. Happily, XUL elements store visible data in attributes,
as you can see in this example of a typical result:
<richlistitem
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
url="https://www.bing.com/search?q=lol+wut&pc=MOZI"
title="lol wut - Bing"
type="favicon"
text="lol wut p"
class="autocomplete-richlistitem"
image="moz-anno:favicon:https://www.bing.com/s/a/bing_p.ico"/>
When a result row is reused, its url
attribute is changed to reflect the
new result’s URL, and attribute changes are visible to MutationObservers.
Ultimately, listening for changes on the url
attribute, along with
changes in the number of child elements of the popup, led to fairly reliable signals.
Quickly stealing the highlight
The second problem, stealing the highlight quickly enough to prevent flickering, was a challenge.
We could try using setTimeout
ourselves, to steal the highlight on a later turn,
but this doesn’t quite work: depending on random timing issues, another set of
results might be inserted after the second time we steal the highlight.
Another problem with relying on setTimeout
is that rendering the results is slow,
so our delayed highlight stealing code can sometimes get stuck behind delayed result rendering code
in the timer queue–another nondeterministic race that adds up to
noticeable blue flickering, or the highlight again getting stolen back by the results list.
There’s another way to schedule future code execution in JavaScript: requestAnimationFrame
.
Where setTimeout(fn, n)
asks the browser to call fn
at least n
milliseconds in the future, requestAnimationFrame(fn)
asks the browser to call fn
on the very next frame, just before it flushes the rendered page to the screen.
rAF allows the highlight stealing code to cut in line, undoing highlight changes
at the last possible instant.
Using a single rAF wasn’t enough, for the same reasons that a single setTimeout
wasn’t enough. However, chaining rAF calls allows multiple successive frames to be
altered, and this is just aggressive enough to work fairly well.
The working highlight stealing code basically does this:
// In response to each observed mutation event,
// steal the highlight on the current and the next two frames.
function onMutation() {
stealHighlight();
requestAnimationFrame(() => {
stealHighlight();
requestAnimationFrame(() => {
stealHighlight();
});
});
}
It’s a bit of a weird hack, but it certainly works well enough for beta testing:
If you look carefully at the video, there’s some blue flicker that appears when I hit the space bar. I think this is because the existing code trims the whitespace, detects that the trimmed query hasn’t changed, and immediately re-renders, while the prototype code isn’t quite that smart yet. The few milliseconds’ delay translates into the highlighted top row getting rendered before the recommendation, and just for a moment, that blue flicker is visible.
Links
The working highlight management code is here, if you’d like to read it. It’s fairly complex: highlight management requires adjusting the highlight in response to key or mouse events. That said, it’s heavily commented, and might be interesting reading.
If you’d like to learn more about this experiment, its wiki page is a good place to start: you’ll find links to the github repos, UX artifacts, some producty discussion, and contact info for the team, if you’d like to get involved.
If you’re interested in the new Test Pilot, I’d suggest checking out the wiki, or Wil’s blog post about it.
Comments?
If you have questions or have spotted a typo, feel free to email or tweet at me.
If you’d like to offer longer comments or a response, I’d like to encourage you to post your own response on your own website, and share the link with me over email or twitter.