With the news that Google would be phasing in a new version of Google Docs I thought we ought to get the current version working in Flow. Because of many bug fixes in Flow for other websites, Google Docs now seemed to load mostly fine, though it rendered without word wrap and you couldn’t actually type into it.
When I tried Google Docs many months ago some obscure exceptions were thrown to the console. Since then, some fixes we have done for other websites have meant these have almost entirely gone away. It’s not obvious which, but it’s always helpful – fix a bug for a test case or website and various other websites start working better without spending time specifically on them.
As with Gmail, I believe Flow is the only browser engine written after Google Docs that can run Google Docs. While still not quite perfect, it is definitely very usable – this blog has been created, written and edited entirely in Google Docs using Flow on a Raspberry Pi 400. The grammar checking and even the collaborative working features that are so useful work flawlessly (these all use XHR). Spell checking works but the underlines don’t appear as we don’t yet support SVGPatternElement.
Happily, the preview of the new canvas-based Google Docs also seems to render correctly in Flow, though their example page isn’t editable. When the full version has rolled out we’ll try it out and look to fix any bugs.
A detailed look
As I mentioned earlier, each paragraph of text was displayed as one long line without word wrap. There are many ways to calculate the width of letters or words using HTML, canvas or even SVG methods. I couldn’t immediately think of an API that we didn’t implement so assumed it was most likely a bug. With some debugging it was apparent that our implementation of
Element.getClientRects() ignored inline elements. Using Safari’s inspector, I could see that Google Docs appends the characters to a <span> element before calling that API, and Flow was always returning a zero-sized box.
Pleasingly, as well as fixing word wrapping, fixing
Element.getClientRects() also meant clicking the mouse would now position the caret in the correct position – keyboard input didn’t work, but it was beginning to look like a word processor.
Loading Google Docs popped up a couple of banner warnings about missing fonts and it being used on an unsupported browser. When dismissing the banners, the banner disappeared and then the whole page reloaded causing the popup banner to re-appear. Since text input didn’t work, I assumed these were getting in the way of the events. (It turns out that wasn’t true because of the complex way Google Docs handles focus.)
The banner has an HTMLAnchorElement, with an href of ‘#’, and activating that reloaded the current page. This turned out to be a bug in
History.setURL() not setting the cached document base, and while the URLs only differed in the query part, they didn’t match and so the navigate algorithm fetched the same document again.
The missing fonts popup banner was primarily due to our FontFaceSet interface (
documents.fonts) not being ‘set-like’. Thankfully an exception on the console pointed this out (‘
document.open() and if so, the fonts would fail to load.
Text input still didn’t work, which was because Google Docs usually keeps focus in a hidden iframe. This is in an unrelated part of the Document from where the text is displayed. To maintain the focus in this iframe, it listens to the focus events. Flow wasn’t sending focus events in the correct order, especially when moving between iframes. This was due to some remaining legacy code from Flow’s SVG heritage. The SVG 1.2 Tiny specification’s handling of focus is quite different to HTML’s. In particular, it states not to navigate to an element with
display:none, and there must always be a focused object defaulting to the Document. The focus handling in and out of iframes (important to Google Docs) wasn’t very clear to me. All browsers have slightly different states as focus changes from one to another. After a total overhaul of Flow’s focus code, keyboard input was largely now working, though a significant amount of work was needed on key mappings.
Flow was inserting the duplicate keys twice, once in the keypress event handler, and again in the beforeinput handler. All other browsers called
preventDefault() on the keypress Event, but Flow didn’t for these duplicated keys. Breakpointing
preventDefault() in Safari’s web inspector stopped on the line:
fad(g) || !(!hr || pr.lq && pr.Mh(“65”) || 112 > e || 123 < e || d && 45 == e || (s9c(this.V, a.preventDefault());
preventDefault() to be called every previous part of the line must be false, and Flow must have been returning early. Now, the variable ‘e’ contains the ascii character being inserted. The letter ‘p’ is 112 in ascii, and ‘z’ is 123. This range check was failing, so I needed to figure out why the range check was happening. It clearly wasn’t being called on other browsers. This means ‘
hr’ was true in Flow, and
pr.lq && pr.Mh(“65”) must be false. Both
pr are on the global, and it was easy to spot that
hr is true if Google Docs parses the user agent string and thinks it’s FireFox.
pr.Mh() seems to compare the Firefox version number with 65, so if it’s less than or equal to Firefox 65, the character matches that range and
preventDefault() is not called. I’ve no idea what the purpose of this is, but clearly I needed to stop it thinking Flow was older than Firefox 65.
A lot of debugging, and all it needed was our user agent to claim we’re like Firefox 68, not 62. Firefox 65’s change log does say it changes the way it handles keypress events, but I can’t imagine what the purpose of checking the letters p to z would be.
Other performance bugs included optimising animations to not trigger a render when they weren’t actually animating. The caret flash is a fade, but 2/3rds of the time is either fully on or off. There’s also an animation in the right-side panel that I’ve barely ever seen, when you explore a selection. Flow was continually animating this, despite its parent being hidden with
The resolution of the icons was also a problem because the Raspberry Pi has a maximum texture size of 2048×2048. The icons render to a 7208-pixel high image, which Flow was initially downscaling to 2048. This caused them to be rendered at very low resolution, appearing blurred. Larger images are now tiled with multiple textures if needed.
Another input bug that I spotted while typing this blog was that ctrl-b would insert a bold ‘b’. This was slightly curious, but I soon realised the keypress event was making the text bold. The following input event was inserting a ‘b’. The fix seems to be to suppress any input event if the keypress event was generated while the OS’s modifier key is held down.
Finally, the right mouse button didn’t bring up a menu. This was simply fixed by adding support for the contextmenu event.
Written in Google Docs, using Flow on a Raspberry Pi 400.
If you’d like to automatically receive our posts by email please join our mailing list.