[css-font-load-events] Using Futures

The Font Load Events spec is currently event-based, with a few ad-hoc
callback-based APIs as well. It has an issue questioning whether it's
worthwhile to switch to DOM Futures.  I believe it is, and have a
suggested new API for doing so.

To start, I'll outline the three use-cases I think the spec is trying
to address.

1. Sites that are going to do some type of DOM measurement that will
depend on the loaded font (such as the size/position of an inline, or
the height of a block), would like to be able to check if all fonts
are loaded, and be notified when any currently-loading fonts are done.

2. Drawing text in <canvas> uses the CSS font mechanism, but doesn't
trigger autoloading of webfonts like text in DOM does. Apps that do
this would like some way to force font loads and be notified when
they're finished, so they can actually draw the text.

3. Sites which offer UI for the fonts that are available, like Google
Docs, would like to be able to tell when a given webfont is loading or
loaded, so they can indicate the status appropriately in the UI.

Right now, #1 is addressed through notifyWhenFontsReady(), which takes
a callback called when fonts are ready (in the next event-loop spin if
fonts are already done loading).  #2 is addressed by loadFont(), which
takes a 'font' declaration and optionally some text that it'll be
applied to, and kicks off font loads if there are any.  #3 is
addressed by the loading/loadingdone events while fire based on the
global state (whether any fonts are loading or not), and the
loadstart/load/error events which fire per-@font-face.

We can simplify this significantly by adding futures, and
rearchitecting somewhat.  Futures are a new concept defined in the DOM
spec, based on significant and long-running experiences in JS APIs,
where they have also been known as "Promises" or "Deferreds".  Futures
are designed to be the standardized way to listen to a *single* event.
It solves this use-case more simply than DOM Events (less baggage from
Event objects, for example), and address the important problem
listening for an event that may have already happened (no race
conditions!). Please check out Alex Russell's explanation and examples
at <http://gitbug.com/slightlyoff/DOMFuture> for more information.

Here's my proposed new API (explanations/justifications will follow):

partial interface document {
  readonly attribute FontList fonts;
}

interface FontList : EventTarget {
  /* whatever idl magic you need to make this an array-like filled
with Font objects */
  Future ready()
  boolean checkFont(DOMString font, optional DOMString text = " ");
  Future loadFont(DOMString font, optional DOMString text = " ");
  attribute EventHandler onloading;
  attribute EventHandler onloadingdone;
}

interface Font : EventTarget {
  Future ready()
  /* readonly versions of all the CSSFontFaceRule attributes, minus
ones that point into the stylesheet */
}

First of all, I'm replacing document.fontLoader with document.fonts,
an array-like of Font objects.  Each Font object is a readonly version
of a CSSFontFaceRule object, without any links into the CSSOM.  This
seems like a useful convenience API all by itself, but it's also
something that can be safely exposed to Workers, as there's no
DOM/CSSOM link to make it unsafe.  The FontList contains Font objects
for all the @font-face rules in all the stylesheets for the document,
in document order.

The FontList.ready() function returns a Future.  This is initially
unresolved, and resolves when the browser has loaded all stylesheets
and has finished loading all the fonts it chooses to initially load.
It must return the same future across multiple calls.  If, after
resolving the future, the browser beings loading more fonts, it must
create a new future to return from .ready(), and again resolve it when
the browser stops loading fonts.  This addresses use-case #1,
replacing notifyWhenFontsReady().

The loadFont() function has had its API reverted to be identical to
checkFont, and it just returns a future which is resolved as soon as
all the necessary requested fonts have loaded (which may be
immediately, if all necessary fonts are already loaded).  This
addresses use-case #2.

checkFont() is unchanged.

To address use-case #3, the API has two pieces.  If you care about the
loading status of individual fonts, look for the relevant Font object
and call its ready() function to obtain a Future which is resolved
when the font is done loading, or cancelled when the font load has an
error.  This replaces the loadstart/load/error events.  The
loading/loadingDone events are unchanged - they're useful for
providing UI indicating whether fonts are loading or not, rather than
a one-off "tell me when fonts are ready", which is what the .ready()
future is for.

I think this significantly simplifies several parts of the API, and
avoids potential race conditions with individual-font loads.  It also
unifies the APIs across several parts - right now you pass a callback
as an argument to one function, as part of an arg object to another,
and register event listeners for a third.  This is the reason de etre
for Futures, so  it appears to be doing its job. ^_^  Finally, this
addresses the use-case of handling fonts in Workers well (necessary
since we're adding <canvas> to Workers), while the existing API has
severe race-condition problems in that case.

Thoughts?

~TJ

Received on Thursday, 4 April 2013 05:39:24 UTC