From http://www.jwz.org/xscreensaver/xscreensaver-5.37.tar.gz
[xscreensaver] / hacks / fontglide.c
index c1e4bdd343623857a64e3696620194851132a6e9..263393a282ea0fd6ab1c6ba681172a0e6d943808 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2003, 2005 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright (c) 2003-2017 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
  * Requires a system with scalable fonts.  (X's font handing sucks.  A lot.)
  */
 
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif /* HAVE_CONFIG_H */
+
+
+/* If you turn on DEBUG, this program also masquerades as a tool for
+   debugging font metrics issues, which is probably only if interest
+   if you are doing active development on libjwxyz.a itself.
+ */
+/* #define DEBUG */
+
 #include <math.h>
+#include <time.h>
+
+#ifndef HAVE_JWXYZ
+# include <X11/Intrinsic.h>
+#endif
+
+#ifdef HAVE_UNISTD_H
+# include <unistd.h>
+#endif
+
 #include "screenhack.h"
-#include <X11/Intrinsic.h>
+#include "textclient.h"
+#include "xft.h"
+#include "utf8wc.h"
 
 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
 #include "xdbe.h"
 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
 
-extern XtAppContext app;
-
-
 typedef struct {
   char *text;
-  int x, y, width, height;
-  int ascent, lbearing, rbearing;
+
+  int x, y;            /* Position of origin of first character in word */
+
+                       /* These have the same meanings as in XCharStruct: */
+  int lbearing;                /* origin to leftmost pixel */
+  int rbearing;                /* origin to rightmost pixel */
+  int ascent;          /* origin to topmost pixel */
+  int descent;         /* origin to bottommost pixel */
+  int width;           /* origin to next word's origin */
 
   int nticks, tick;
   int start_x,  start_y;
@@ -38,16 +65,14 @@ typedef struct {
 
 typedef struct {
   int id;
-  XColor fg;
-  XColor bg;
   Bool dark_p;
   Bool move_chars_p;
   int width;
 
   char *font_name;
-  XFontStruct *font;
-
   GC fg_gc;
+  XftFont *xftfont;
+  XftColor xftcolor_fg, xftcolor_bg;
 
   int nwords;
   word **words;
@@ -69,41 +94,104 @@ typedef struct {
 
 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
   XdbeBackBuffer backb;
+  Bool dbeclear_p;
 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
 
   Bool dbuf;            /* Whether we're using double buffering. */
-  Bool dbeclear_p;      /* ? */
 
   int border_width;     /* size of the font outline */
   char *charset;        /* registry and encoding for font lookups */
   double speed;                /* frame rate multiplier */
   double linger;       /* multiplier for how long to leave words on screen */
   Bool trails_p;
-  Bool debug_p;
-  enum { PAGE, SCROLL } mode;
+  enum { PAGE, SCROLL, CHARS } mode;
 
   char *font_override;  /* if -font was specified on the cmd line */
 
-  FILE *pipe;
-  XtInputId pipe_id;
-  Time subproc_relaunch_delay;
-  Bool input_available_p;
-
   char buf [40];       /* this only needs to be as big as one "word". */
   int buf_tail;
+  Bool early_p;
+  time_t start_time;
 
   int nsentences;
   sentence **sentences;
   Bool spawn_p;                /* whether it is time to create a new sentence */
   int latest_sentence;
+  unsigned long frame_delay;
+  int id_tick;
+  text_data *tc;
+
+# ifdef DEBUG
+  Bool debug_p;
+  unsigned long debug_metrics_p;
+  int debug_metrics_antialiasing_p;
+  int debug_scale;
+  unsigned entering_unicode_p; /* 0 = No, 1 = Just started, 2 = in progress */
+  XFontStruct *metrics_font1;
+  XFontStruct *metrics_font2;
+  XftFont *metrics_xftfont;
+  GC label_gc;
+  char *prev_font_name;
+  char *next_font_name;
+# endif /* DEBUG */
 
 } state;
 
 
-static void launch_text_generator (state *);
 static void drain_input (state *s);
 
 
+static int
+pick_font_size (state *s)
+{
+  double scale = s->xgwa.height / 1024.0;  /* shrink for small windows */
+  int min, max, r, pixel;
+
+  min = scale * 24;
+  max = scale * 260;
+
+  if (min < 10) min = 10;
+  if (max < 30) max = 30;
+
+  r = ((max-min)/3)+1;
+
+  pixel = min + ((random() % r) + (random() % r) + (random() % r));
+
+  if (s->mode == SCROLL)  /* scroll mode likes bigger fonts */
+    pixel *= 1.5;
+
+  return pixel;
+}
+
+
+#ifdef HAVE_JWXYZ
+
+static char *
+append_font_name(Display *dpy, char *dest, const XFontStruct *font)
+{
+  int i;
+  for (i = 0; i != font->n_properties; ++i) {
+    if (font->properties[i].name == XA_FONT) {
+      const char *suffix = XGetAtomName (dpy, font->properties[i].card32);
+      strcpy(dest, suffix);
+      return dest + strlen(suffix);
+    }
+  }
+
+  dest[0] = '?';
+  dest[1] = 0;
+  return dest + 1;
+
+/*
+  float s;
+  const char *n = jwxyz_nativeFontName (font->fid, &s);
+  return dest + sprintf (dest, "%s %.1f", n, s);
+ */
+}
+
+#endif
+
+
 /* Finds the set of scalable fonts on the system; picks one;
    and loads that font in a random pixel size.
    Returns False if something went wrong.
@@ -111,19 +199,27 @@ static void drain_input (state *s);
 static Bool
 pick_font_1 (state *s, sentence *se)
 {
+  Bool ok = False;
   char pattern[1024];
+  char pattern2[1024];
+
+#ifndef HAVE_JWXYZ /* real Xlib */
   char **names = 0;
   char **names2 = 0;
   XFontStruct *info = 0;
   int count = 0, count2 = 0;
   int i;
-  Bool ok = False;
 
-  if (se->font)
+  if (se->xftfont)
     {
-      XFreeFont (s->dpy, se->font);
+      XftFontClose (s->dpy, se->xftfont);
+      XftColorFree (s->dpy, s->xgwa.visual, s->xgwa.colormap,
+                    &se->xftcolor_fg);
+      XftColorFree (s->dpy, s->xgwa.visual, s->xgwa.colormap,
+                    &se->xftcolor_bg);
+
       free (se->font_name);
-      se->font = 0;
+      se->xftfont = 0;
       se->font_name = 0;
     }
 
@@ -162,9 +258,12 @@ pick_font_1 (state *s, sentence *se)
   names2 = XListFontsWithInfo (s->dpy, names[i], 1000, &count2, &info);
   if (count2 <= 0)
     {
-      fprintf (stderr, "%s: pattern %s\n"
-                "     gave unusable %s\n\n",
-               progname, pattern, names[i]);
+# ifdef DEBUG
+      if (s->debug_p)
+        fprintf (stderr, "%s: pattern %s\n"
+                 "     gave unusable %s\n\n",
+                 progname, pattern, names[i]);
+# endif /* DEBUG */
       goto FAIL;
     }
 
@@ -212,23 +311,7 @@ pick_font_1 (state *s, sentence *se)
 #undef INT
 #undef STR
 
-    {
-      double scale = s->xgwa.height / 1024.0;  /* shrink for small windows */
-      int min, max, r;
-
-      min = scale * 24;
-      max = scale * 260;
-
-      if (min < 10) min = 10;
-      if (max < 30) max = 30;
-
-      r = ((max-min)/3)+1;
-
-      pixel = min + ((random() % r) + (random() % r) + (random() % r));
-
-      if (s->mode == SCROLL)  /* scroll mode likes bigger fonts */
-        pixel *= 1.5;
-    }
+    pixel = pick_font_size (s);
 
 #if 0
     /* Occasionally change the aspect ratio of the font, by increasing
@@ -279,39 +362,144 @@ pick_font_1 (state *s, sentence *se)
   XFreeFontInfo (names2, info, count2);
   XFreeFontNames (names);
 
-  if (! ok) return False;
+# else  /* HAVE_JWXYZ */
 
-  se->font = XLoadQueryFont (s->dpy, pattern);
-  if (! se->font)
+  if (s->font_override)
+    sprintf (pattern, "%.200s", s->font_override);
+  else
     {
-      if (s->debug_p)
-        fprintf (stderr, "%s: unable to load font %s\n",
-                 progname, pattern);
-      return False;
+      const char *family = "random";
+      const char *weight = ((random() % 2)  ? "regular" : "bold");
+      const char *slant  = ((random() % 2)  ? "o" : "r");
+      int size = 10 * pick_font_size (s);
+      sprintf (pattern, "*-%s-%s-%s-*-*-*-%d-*", family, weight, slant, size);
     }
+  ok = True;
+# endif /* HAVE_JWXYZ */
+
+  if (! ok) return False;
 
-  if (se->font->min_bounds.width == se->font->max_bounds.width &&
-      !s->font_override)
+  se->xftfont = XftFontOpenXlfd (s->dpy, screen_number (s->xgwa.screen),
+                                 pattern);
+
+  if (! se->xftfont)
     {
-      /* This is to weed out
-         "-urw-nimbus mono l-medium-o-normal--*-*-*-*-p-*-iso8859-1" and
-         "-urw-courier-medium-r-normal--*-*-*-*-p-*-iso8859-1".
-         We asked for only proportional fonts, but this fixed-width font
-         shows up anyway -- but it has goofy metrics (see below) so it
-         looks terrible anyway.
-       */
+# ifdef DEBUG
       if (s->debug_p)
-        fprintf (stderr,
-                 "%s: skipping bogus monospace non-charcell font: %s\n",
+        fprintf (stderr, "%s: unable to load font %s\n",
                  progname, pattern);
+#endif
       return False;
     }
 
+  strcpy (pattern2, pattern);
+# ifdef HAVE_JWXYZ
+  {
+    char *out = pattern2 + strlen(pattern2);
+    out[0] = ' ';
+    out[1] = '(';
+    out = append_font_name (s->dpy, out + 2, se->xftfont->xfont);
+    out[0] = ')';
+    out[1] = 0;
+  }
+# endif
+
+# ifdef DEBUG
+  if (s->prev_font_name) free (s->prev_font_name);
+  s->prev_font_name = s->next_font_name;
+  s->next_font_name = strdup (pattern2);
+# endif
+
+  /* Sometimes we get fonts with screwed up metrics.  For example:
+     -b&h-lucida-medium-r-normal-sans-40-289-100-100-p-0-iso8859-1
+
+     When using XDrawString, XTextExtents and XTextExtents16, it is rendered
+     as a scaled-up bitmap font.  The character M has rbearing 70, ascent 68
+     and width 78, which is correct for the glyph as rendered.
+
+     But when using XftDrawStringUtf8 and XftTextExtentsUtf8, it is rendered
+     at the original, smaller, un-scaled size, with rbearing 26, ascent 25
+     and... width 77!
+
+     So it's taking the *size* from the unscaled font, the *advancement* from
+     the scaled-up version, and then *not* actually scaling it up.  Awesome.
+
+     So, after loading the font, measure the M, and if its advancement is more
+     than 20% larger than its rbearing, reject the font.
+
+     ------------------------------------------------------------------------
+
+     Some observations on this nonsense from Dave Odell:
+
+     1. -*-lucidatypewriter-bold-r-normal-*-*-480-*-*-*-*-iso8859-1 normally
+        resolves to /usr/share/fonts/X11/100dpi/lutBS24-ISO8859-1.pcf.gz.
+
+        -*-lucidatypewriter-* is from the 'xfonts-100dpi' package in
+        Debian/Ubuntu. It's usually (54.46% of systems), but not always,
+        installed whenever an X.org server (57.96% of systems) is.  It might
+        be a good idea for this and xfonts-75dpi to be recommended
+        dependencies of XScreenSaver in Debian, but that's neither here nor
+        there.  https://qa.debian.org/popcon.php?package=xorg
+        https://qa.debian.org/popcon.php?package=xfonts-100dpi
+
+     2. It normally resolves to the PCF font... but not always.
+
+        Fontconfig has /etc/fonts/conf.d/ (it's /opt/local/etc/fonts/conf.d/
+        with MacPorts) containing symlinks to configuration files. And both
+        Debian and Ubuntu normally has a 70-no-bitmaps.conf, installed as part
+        of the 'fontconfig-config' package. And the 70-no-bitmaps.conf
+        symlink... disables bitmap fonts.
+
+        Without bitmap fonts, I get DejaVu Sans.
+
+     3. There's another symlink of interest here:
+        /etc/fonts/conf.d/10-scale-bitmap-fonts.conf. This adds space to the
+        right of glyphs of bitmap fonts when the requested size of the font is
+        larger than the actual bitmap font. Ubuntu and MacPorts has this one.
+
+        This specifically is causing text to have excessive character spacing.
+
+        (jwz asks: WHY WOULD ANYONE EVER WANT THIS BEHAVIOR?)
+
+     4. Notice that I'm only talking about Debian and Ubuntu. Other distros
+        will probably have different symlinks in /etc/fonts/conf.d/. So yes,
+        this can be an issue on Linux as well as MacOS.
+   */
+  {
+    XGlyphInfo extents;
+    int rbearing, width;
+    float ratio;
+    float min = 0.8;
+
+    XftTextExtentsUtf8 (s->dpy, se->xftfont, (FcChar8 *) "M", 1, &extents);
+    rbearing = extents.width - extents.x;
+    width = extents.xOff;
+    ratio = rbearing / (float) width;
+
+# ifdef DEBUG
+    if (s->debug_p)
+      fprintf (stderr, "%s: M ratio %.2f (%d %d): %s\n", progname,
+               ratio, rbearing, width, pattern2);
+# endif
+
+    if (ratio < min && !s->font_override)
+      {
+# ifdef DEBUG
+        if (s->debug_p)
+          fprintf (stderr, "%s: skipping font with broken metrics: %s\n",
+                   progname, pattern2);
+# endif
+        return False;
+      }
+  }
+
+
+# ifdef DEBUG
   if (s->debug_p) 
-    fprintf(stderr, "%s: %s\n", progname, pattern);
+    fprintf(stderr, "%s: %s\n", progname, pattern2);
+# endif /* DEBUG */
 
   se->font_name = strdup (pattern);
-  XSetFont (s->dpy, se->fg_gc, se->font->fid);
   return True;
 }
 
@@ -323,10 +511,11 @@ static void
 pick_font (state *s, sentence *se)
 {
   int i;
-  for (i = 0; i < 20; i++)
+  for (i = 0; i < 50; i++)
     if (pick_font_1 (s, se))
       return;
-  fprintf (stderr, "%s: too many font-loading failures: giving up!\n", progname);
+  fprintf (stderr, "%s: too many font-loading failures: giving up!\n",
+           progname);
   exit (1);
 }
 
@@ -339,16 +528,33 @@ static char *unread_word_text = 0;
 static char *
 get_word_text (state *s)
 {
-  char *start = s->buf;
-  char *end;
+  const char *start = s->buf;
+  const char *end;
   char *result = 0;
   int lfs = 0;
 
+  drain_input (s);
+
+  /* If we just launched, and haven't had any text yet, and it has been
+     more than 2 seconds since we launched, then push out "Loading..."
+     as our first text.  So if the text source is speedy, just use that.
+     But if we'd display a blank screen for a while, give 'em something
+     to see.
+   */
+  if (s->early_p &&
+      !*s->buf &&
+      !unread_word_text &&
+      s->start_time < ((time ((time_t *) 0) - 2)))
+    {
+      unread_word_text = "Loading...";
+      s->early_p = False;
+    }
+
   if (unread_word_text)
     {
-      char *s = unread_word_text;
+      result = unread_word_text;
       unread_word_text = 0;
-      return s;
+      return strdup (result);
     }
 
   /* Skip over whitespace at the beginning of the buffer,
@@ -399,55 +605,71 @@ get_word_text (state *s)
       s->buf_tail -= n;
     }
 
-  /* See if there is more to be read, now that there's room in the buffer. */
-  drain_input (s);
-
   return result;
 }
 
 
+/* Returns a 1-bit pixmap of the same size as the drawable,
+   with a 0 wherever the drawable is black.
+ */
+static Pixmap
+make_mask (Screen *screen, Visual *visual, Drawable drawable)
+{
+  Display *dpy = DisplayOfScreen (screen);
+  unsigned long black = BlackPixelOfScreen (screen);
+  Window r;
+  int x, y;
+  unsigned int w, h, bw, d;
+  XImage *out, *in;
+  Pixmap mask;
+  GC gc;
+
+  XGetGeometry (dpy, drawable, &r, &x, &y, &w, &h, &bw, &d);
+  in = XGetImage (dpy, drawable, 0, 0, w, h, ~0L, ZPixmap);
+  out = XCreateImage (dpy, visual, 1, XYPixmap, 0, 0, w, h, 8, 0);
+  out->data = (char *) malloc (h * out->bytes_per_line);
+  for (y = 0; y < h; y++)
+    for (x = 0; x < w; x++)
+      XPutPixel (out, x, y, (black != XGetPixel (in, x, y)));
+  mask = XCreatePixmap (dpy, drawable, w, h, 1L);
+  gc = XCreateGC (dpy, mask, 0, 0);
+  XPutImage (dpy, mask, gc, out, 0, 0, 0, 0, w, h);
+  XFreeGC (dpy, gc);
+  free (in->data);
+  free (out->data);
+  in->data = out->data = 0;
+  XDestroyImage (in);
+  XDestroyImage (out);
+  return mask;
+}
+
+
 /* Gets some random text, and creates a "word" object from it.
  */
 static word *
-new_word (state *s, sentence *se, char *txt, Bool alloc_p)
+new_word (state *s, sentence *se, const char *txt, Bool alloc_p)
 {
   word *w;
-  XCharStruct overall;
-  int dir, ascent, descent;
+  XGlyphInfo extents;
   int bw = s->border_width;
 
   if (!txt)
     return 0;
 
   w = (word *) calloc (1, sizeof(*w));
-  XTextExtents (se->font, txt, strlen(txt), &dir, &ascent, &descent, &overall);
-
-  w->width    = overall.rbearing - overall.lbearing + bw + bw;
-  w->height   = overall.ascent   + overall.descent  + bw + bw;
-  w->ascent   = overall.ascent   + bw;
-  w->lbearing = overall.lbearing - bw;
-  w->rbearing = overall.width    + bw;
-
-# if 0
-  /* The metrics on some fonts are strange -- e.g.,
-     "-urw-nimbus mono l-medium-o-normal--*-*-*-*-p-*-iso8859-1" and
-     "-urw-courier-medium-r-normal--*-*-*-*-p-*-iso8859-1" both have
-     an rbearing so wide that it looks like there are two spaces after
-     each letter.  If this character says it has an rbearing that is to
-     the right of its ink, ignore that.
-
-     #### Of course, this hack only helps when we're in `move_chars_p' mode
-          and drawing a char at a time -- when we draw the whole word at once,
-          XDrawString believes the bogus metrics and spaces the font out
-          crazily anyway.
-
-     Sigh, this causes some text to mis-render in, e.g.,
-     "-adobe-utopia-medium-i-normal--114-*-100-100-p-*-iso8859-1"
-     (in "ux", we need the rbearing on "r" or we get too much overlap.)
-   */
-  if (w->rbearing > w->width)
-    w->rbearing = w->width;
-# endif /* 0 */
+  XftTextExtentsUtf8 (s->dpy, se->xftfont, (FcChar8 *) txt, strlen(txt),
+                      &extents);
+
+  w->lbearing = -extents.x;
+  w->rbearing = extents.width - extents.x;
+  w->ascent   = extents.y;
+  w->descent  = extents.height - extents.y;
+  w->width    = extents.xOff;
+
+  w->lbearing -= bw;
+  w->rbearing += bw;
+  w->descent  += bw;
+  w->ascent   += bw;
 
   if (s->mode == SCROLL && !alloc_p) abort();
 
@@ -455,83 +677,87 @@ new_word (state *s, sentence *se, char *txt, Bool alloc_p)
     {
       int i, j;
       XGCValues gcv;
-      GC gc0, gc1;
+      GC gc_fg, gc_bg, gc_black;
+      XftDraw *xftdraw;
+      int width  = w->rbearing - w->lbearing;
+      int height = w->ascent + w->descent;
+
+      if (width <= 0)  width  = 1;
+      if (height <= 0) height = 1;
+
+      w->pixmap = XCreatePixmap (s->dpy, s->b, width, height, s->xgwa.depth);
+      xftdraw = XftDrawCreate (s->dpy, w->pixmap, s->xgwa.visual,
+                               s->xgwa.colormap);
 
-      w->pixmap = XCreatePixmap (s->dpy, s->b, w->width, w->height, 1L);
-      w->mask   = XCreatePixmap (s->dpy, s->b, w->width, w->height, 1L);
+      gcv.foreground = se->xftcolor_fg.pixel;
+      gc_fg = XCreateGC (s->dpy, w->pixmap, GCForeground, &gcv);
 
-      gcv.font = se->font->fid;
-      gcv.foreground = 0L;
-      gcv.background = 1L;
-      gc0 = XCreateGC (s->dpy, w->pixmap, GCFont|GCForeground|GCBackground,
-                       &gcv);
-      gcv.foreground = 1L;
-      gcv.background = 0L;
-      gc1 = XCreateGC (s->dpy, w->pixmap, GCFont|GCForeground|GCBackground,
-                       &gcv);
+      gcv.foreground = se->xftcolor_bg.pixel;
+      gc_bg = XCreateGC (s->dpy, w->pixmap, GCForeground, &gcv);
 
-      XFillRectangle (s->dpy, w->mask,   gc0, 0, 0, w->width, w->height);
-      XFillRectangle (s->dpy, w->pixmap, gc0, 0, 0, w->width, w->height);
+      gcv.foreground = BlackPixelOfScreen (s->xgwa.screen);
+      gc_black = XCreateGC (s->dpy, w->pixmap, GCForeground, &gcv);
 
+      XFillRectangle (s->dpy, w->pixmap, gc_black, 0, 0, width, height);
+
+# ifdef DEBUG
       if (s->debug_p)
         {
           /* bounding box (behind the characters) */
-          XDrawRectangle (s->dpy, w->pixmap, (se->dark_p ? gc0 : gc1),
-                          0, 0, w->width-1, w->height-1);
-          XDrawRectangle (s->dpy, w->mask,   gc1,
-                          0, 0, w->width-1, w->height-1);
+          XDrawRectangle (s->dpy, w->pixmap, (se->dark_p ? gc_bg : gc_fg),
+                          0, 0, width-1, height-1);
         }
+# endif /* DEBUG */
 
-      /* Draw foreground text */
-      XDrawString (s->dpy, w->pixmap, gc1, -w->lbearing, w->ascent,
-                   txt, strlen(txt));
-
-      /* Cheesy hack to draw a border */
-      /* (I should be able to do this in i*2 time instead of i*i time,
-         but I can't get it right, so fuck it.) */
-      XSetFunction (s->dpy, gc1, GXor);
+      /* Draw background text for border */
       for (i = -bw; i <= bw; i++)
         for (j = -bw; j <= bw; j++)
-          XCopyArea (s->dpy, w->pixmap, w->mask, gc1,
-                     0, 0, w->width, w->height,
-                     i, j);
+          XftDrawStringUtf8 (xftdraw, &se->xftcolor_bg, se->xftfont,
+                             -w->lbearing + i, w->ascent + j,
+                             (FcChar8 *) txt, strlen(txt));
+
+      /* Draw foreground text */
+      XftDrawStringUtf8 (xftdraw, &se->xftcolor_fg, se->xftfont,
+                         -w->lbearing, w->ascent,
+                         (FcChar8 *) txt, strlen(txt));
 
+# ifdef DEBUG
       if (s->debug_p)
         {
-          XSetFunction (s->dpy, gc1, GXset);
-          if (w->ascent != w->height)
+          if (w->ascent != height)
             {
               /* baseline (on top of the characters) */
-              XDrawLine (s->dpy, w->pixmap, (se->dark_p ? gc0 : gc1),
-                         0, w->ascent, w->width-1, w->ascent);
-              XDrawLine (s->dpy, w->mask,   gc1,
-                         0, w->ascent, w->width-1, w->ascent);
+              XDrawLine (s->dpy, w->pixmap, (se->dark_p ? gc_bg : gc_fg),
+                         0, w->ascent, width-1, w->ascent);
             }
 
-          if (w->lbearing != 0)
+          if (w->lbearing < 0)
             {
               /* left edge of charcell */
-              XDrawLine (s->dpy, w->pixmap, (se->dark_p ? gc0 : gc1),
-                         w->lbearing, 0, w->lbearing, w->height-1);
-              XDrawLine (s->dpy, w->mask,   gc1,
-                         w->lbearing, 0, w->lbearing, w->height-1);
+              XDrawLine (s->dpy, w->pixmap, (se->dark_p ? gc_bg : gc_fg),
+                         -w->lbearing, 0,
+                         -w->lbearing, height-1);
             }
 
           if (w->rbearing != w->width)
             {
               /* right edge of charcell */
-              XDrawLine (s->dpy, w->pixmap, (se->dark_p ? gc0 : gc1),
-                         w->rbearing, 0, w->rbearing, w->height-1);
-              XDrawLine (s->dpy, w->mask,   gc1,
-                         w->rbearing, 0, w->rbearing, w->height-1);
+              XDrawLine (s->dpy, w->pixmap, (se->dark_p ? gc_bg : gc_fg),
+                         w->width - w->lbearing, 0,
+                         w->width - w->lbearing, height-1);
             }
         }
+# endif /* DEBUG */
+
+      w->mask = make_mask (s->xgwa.screen, s->xgwa.visual, w->pixmap);
 
-      XFreeGC (s->dpy, gc0);
-      XFreeGC (s->dpy, gc1);
+      XftDrawDestroy (xftdraw);
+      XFreeGC (s->dpy, gc_fg);
+      XFreeGC (s->dpy, gc_bg);
+      XFreeGC (s->dpy, gc_black);
     }
 
-  w->text = txt;
+  w->text = strdup (txt);
   return w;
 }
 
@@ -546,14 +772,13 @@ free_word (state *s, word *w)
 
 
 static sentence *
-new_sentence (state *s)
+new_sentence (state *st, state *s)
 {
-  static int id = 0;
   XGCValues gcv;
   sentence *se = (sentence *) calloc (1, sizeof (*se));
   se->fg_gc = XCreateGC (s->dpy, s->b, 0, &gcv);
   se->anim_state = IN;
-  se->id = ++id;
+  se->id = ++st->id_tick;
   return se;
 }
 
@@ -564,16 +789,21 @@ free_sentence (state *s, sentence *se)
   int i;
   for (i = 0; i < se->nwords; i++)
     free_word (s, se->words[i]);
-  if (se->words) free (se->words);
-
-  if (se->fg.flags)
-    XFreeColors (s->dpy, s->xgwa.colormap, &se->fg.pixel, 1, 0);
-  if (se->bg.flags)
-    XFreeColors (s->dpy, s->xgwa.colormap, &se->bg.pixel, 1, 0);
+  if (se->words)
+    free (se->words);
+  if (se->font_name)
+    free (se->font_name);
+  if (se->fg_gc)
+    XFreeGC (s->dpy, se->fg_gc);
 
-  if (se->font_name) free (se->font_name);
-  if (se->font) XFreeFont (s->dpy, se->font);
-  if (se->fg_gc) XFreeGC (s->dpy, se->fg_gc);
+  if (se->xftfont)
+    {
+      XftFontClose (s->dpy, se->xftfont);
+      XftColorFree (s->dpy, s->xgwa.visual, s->xgwa.colormap,
+                    &se->xftcolor_fg);
+      XftColorFree (s->dpy, s->xgwa.visual, s->xgwa.colormap,
+                    &se->xftcolor_bg);
+    }
 
   free (se);
 }
@@ -601,53 +831,56 @@ split_words (state *s, sentence *se)
   word **words2;
   int nwords2 = 0;
   int i, j;
+
+  char ***word_chars = (char ***) malloc (se->nwords * sizeof(*word_chars));
   for (i = 0; i < se->nwords; i++)
-    nwords2 += strlen (se->words[i]->text);
+    {
+      int L;
+      word *ow = se->words[i];
+      word_chars[i] = utf8_split (ow->text, &L);
+      nwords2 += L;
+    }
 
   words2 = (word **) calloc (nwords2, sizeof(*words2));
 
   for (i = 0, j = 0; i < se->nwords; i++)
     {
-      word *ow = se->words[i];
-      int L = strlen (ow->text);
+      char **chars = word_chars[i];
+      word *parent = se->words[i];
+      int x  = parent->x;
+      int y  = parent->y;
+      int sx = parent->start_x;
+      int sy = parent->start_y;
+      int tx = parent->target_x;
+      int ty = parent->target_y;
       int k;
 
-      int x  = ow->x;
-      int y  = ow->y;
-      int sx = ow->start_x;
-      int sy = ow->start_y;
-      int tx = ow->target_x;
-      int ty = ow->target_y;
-
-      for (k = 0; k < L; k++)
+      for (k = 0; chars[k]; k++)
         {
-          char *t2 = malloc (2);
-          word *w2;
-          int xoff, yoff;
-
-          t2[0] = ow->text[k];
-          t2[1] = 0;
-          w2 = new_word (s, se, t2, True);
+          char *t2 = chars[k];
+          word *w2 = new_word (s, se, t2, True);
           words2[j++] = w2;
 
-          xoff = (w2->lbearing - ow->lbearing);
-          yoff = (ow->ascent - w2->ascent);
+          w2->x = x;
+          w2->y = y;
+          w2->start_x = sx;
+          w2->start_y = sy;
+          w2->target_x = tx;
+          w2->target_y = ty;
 
-          w2->x        = x  + xoff;
-          w2->y        = y  + yoff;
-          w2->start_x  = sx + xoff;
-          w2->start_y  = sy + yoff;
-          w2->target_x = tx + xoff;
-          w2->target_y = ty + yoff;
-
-          x  += w2->rbearing;
-          sx += w2->rbearing;
-          tx += w2->rbearing;
+          x  += w2->width;
+          sx += w2->width;
+          tx += w2->width;
         }
 
-      free_word (s, ow);
-      se->words[i] = 0;
+      /* This is not invariant when kerning is involved! */
+      /* if (x != parent->x + parent->width) abort(); */
+
+      free (chars);  /* but we retain its contents */
+      free_word (s, parent);
     }
+  if (j != nwords2) abort();
+  free (word_chars);
   free (se->words);
 
   se->words = words2;
@@ -662,7 +895,7 @@ static void
 scatter_sentence (state *s, sentence *se)
 {
   int i = 0;
-  int off = 100;
+  int off = s->border_width * 4 + 2;
 
   int flock_p = ((random() % 4) == 0);
   int mode = (flock_p ? (random() % 12) : 0);
@@ -672,69 +905,32 @@ scatter_sentence (state *s, sentence *se)
       word *w = se->words[i];
       int x, y;
       int r = (flock_p ? mode : (random() % 4));
-      switch (r)
-        {
-          /* random positions on the edges */
-
-        case 0:
-          x = -off - w->width;
-          y = random() % s->xgwa.height;
-          break;
-        case 1:
-          x = off + s->xgwa.width;
-          y = random() % s->xgwa.height;
-          break;
-        case 2:
-          x = random() % s->xgwa.width;
-          y = -off - w->height;
-          break;
-        case 3:
-          x = random() % s->xgwa.width;
-          y = off + s->xgwa.height;
-          break;
-
-          /* straight towards the edges */
-
-        case 4:
-          x = -off - w->width;
-          y = w->target_y;
-          break;
-        case 5:
-          x = off + s->xgwa.width;
-          y = w->target_y;
-          break;
-        case 6:
-          x = w->target_x;
-          y = -off - w->height;
-          break;
-        case 7:
-          x = w->target_x;
-          y = off + s->xgwa.height;
-          break;
-
-          /* corners */
-
-        case 8:
-          x = -off - w->width;
-          y = -off - w->height;
-          break;
-        case 9:
-          x = -off - w->width;
-          y =  off + s->xgwa.height;
-          break;
-        case 10:
-          x =  off + s->xgwa.width;
-          y =  off + s->xgwa.height;
-          break;
-        case 11:
-          x =  off + s->xgwa.width;
-          y = -off - w->height;
-          break;
-
-        default:
-          abort();
-          break;
-        }
+      int left   = -(off + w->rbearing);
+      int top    = -(off + w->descent);
+      int right  = off - w->lbearing + s->xgwa.width;
+      int bottom = off + w->ascent + s->xgwa.height;
+
+      switch (r) {
+      /* random positions on the edges */
+      case 0:  x = left;  y = random() % s->xgwa.height; break;
+      case 1:  x = right; y = random() % s->xgwa.height; break;
+      case 2:  x = random() % s->xgwa.width; y = top;    break;
+      case 3:  x = random() % s->xgwa.width; y = bottom; break;
+
+      /* straight towards the edges */
+      case 4:  x = left;  y = w->target_y;  break;
+      case 5:  x = right; y = w->target_y;  break;
+      case 6:  x = w->target_x; y = top;    break;
+      case 7:  x = w->target_x; y = bottom; break;
+
+      /* corners */
+      case 8:  x = left;  y = top;    break;
+      case 9:  x = left;  y = bottom; break;
+      case 10: x = right; y = top;    break;
+      case 11: x = right; y = bottom; break;
+
+      default: abort(); break;
+      }
 
       if (se->anim_state == IN)
         {
@@ -794,7 +990,7 @@ aim_sentence (state *s, sentence *se)
     }
 
   nticks = ((se->words[0]->start_x - se->words[0]->target_x)
-            / (s->speed * 10));
+            / (s->speed * 7));
   nticks *= (frand(0.9) + frand(0.9) + frand(0.9));
 
   if (nticks < 2)
@@ -850,47 +1046,36 @@ sort_sentences (state *s)
 static void
 recolor (state *s, sentence *se)
 {
-  if (se->fg.flags)
-    XFreeColors (s->dpy, s->xgwa.colormap, &se->fg.pixel, 1, 0);
-  if (se->bg.flags)
-    XFreeColors (s->dpy, s->xgwa.colormap, &se->bg.pixel, 1, 0);
-
-  se->fg.flags  = DoRed|DoGreen|DoBlue;
-  se->bg.flags  = DoRed|DoGreen|DoBlue;
-
-  switch (random() % 2)
+  XRenderColor fg, bg;
+
+  fg.red   = (random() % 0x5555) + 0xAAAA;
+  fg.green = (random() % 0x5555) + 0xAAAA;
+  fg.blue  = (random() % 0x5555) + 0xAAAA;
+  fg.alpha = 0xFFFF;
+  bg.red   = (random() % 0x5555);
+  bg.green = (random() % 0x5555);
+  bg.blue  = (random() % 0x5555);
+  bg.alpha = 0xFFFF;
+  se->dark_p = False;
+
+  if (random() & 1)
     {
-    case 0:   /* bright fg, dim bg */
-      se->fg.red    = (random() % 0x8888) + 0x8888;
-      se->fg.green  = (random() % 0x8888) + 0x8888;
-      se->fg.blue   = (random() % 0x8888) + 0x8888;
-      se->bg.red    = (random() % 0x5555);
-      se->bg.green  = (random() % 0x5555);
-      se->bg.blue   = (random() % 0x5555);
-      break;
-
-    case 1:   /* bright bg, dim fg */
-      se->fg.red    = (random() % 0x4444);
-      se->fg.green  = (random() % 0x4444);
-      se->fg.blue   = (random() % 0x4444);
-      se->bg.red    = (random() % 0x4444) + 0xCCCC;
-      se->bg.green  = (random() % 0x4444) + 0xCCCC;
-      se->bg.blue   = (random() % 0x4444) + 0xCCCC;
-      break;
-
-    default:
-      abort();
-      break;
+      XRenderColor swap = fg; fg = bg; bg = swap;
+      se->dark_p = True;
     }
 
-  if (s->debug_p)
-    se->dark_p = (se->fg.red*2 + se->fg.green*3 + se->fg.blue <
-                  se->bg.red*2 + se->bg.green*3 + se->bg.blue);
+  if (se->xftfont)
+    {
+      XftColorFree (s->dpy, s->xgwa.visual, s->xgwa.colormap,
+                    &se->xftcolor_fg);
+      XftColorFree (s->dpy, s->xgwa.visual, s->xgwa.colormap,
+                    &se->xftcolor_bg);
+    }
 
-  if (XAllocColor (s->dpy, s->xgwa.colormap, &se->fg))
-    XSetForeground (s->dpy, se->fg_gc, se->fg.pixel);
-  if (XAllocColor (s->dpy, s->xgwa.colormap, &se->bg))
-    XSetBackground (s->dpy, se->fg_gc, se->bg.pixel);
+  XftColorAllocValue (s->dpy, s->xgwa.visual, s->xgwa.colormap, &fg,
+                     &se->xftcolor_fg);
+  XftColorAllocValue (s->dpy, s->xgwa.visual, s->xgwa.colormap, &bg,
+                     &se->xftcolor_bg);
 }
 
 
@@ -928,7 +1113,8 @@ populate_sentence (state *s, sentence *se)
 
   int array_size = 100;
 
-  se->move_chars_p = (s->mode == SCROLL ? False :
+  se->move_chars_p = (s->mode == CHARS ? True :
+                      s->mode == SCROLL ? False :
                       (random() % 3) ? False : True);
   se->alignment = (random() % 3);
 
@@ -947,6 +1133,7 @@ populate_sentence (state *s, sentence *se)
   switch (s->mode)
     {
     case PAGE:
+    case CHARS:
       left  = random() % (s->xgwa.width / 3);
       right = s->xgwa.width - (random() % (s->xgwa.width / 3));
       top = random() % (s->xgwa.height * 2 / 3);
@@ -976,15 +1163,23 @@ populate_sentence (state *s, sentence *se)
             break;             /* If EOF after some words, end of sentence. */
         }
 
-      if (! se->font)           /* Got a word: need a font now */
+      if (! se->xftfont)           /* Got a word: need a font now */
         {
+          XGlyphInfo extents;
           pick_font (s, se);
-          if (y < se->font->ascent)
-            y += se->font->ascent;
-          space = XTextWidth (se->font, " ", 1);
+          if (y < se->xftfont->ascent)
+            y += se->xftfont->ascent;
+
+          /* Measure the space character to figure out how much room to
+             leave between words (since we don't actually render that.) */
+          XftTextExtentsUtf8 (s->dpy, se->xftfont, (FcChar8 *) " ", 1,
+                              &extents);
+          space = extents.xOff;
         }
 
       w = new_word (s, se, txt, !se->move_chars_p);
+      free (txt);
+      txt = 0;
 
       /* If we have a few words, let punctuation terminate the sentence:
          stop gathering more words if the last word ends in a period, etc. */
@@ -1007,39 +1202,40 @@ populate_sentence (state *s, sentence *se)
       if (se->nwords >= 25)  /* ok that's just about enough out of you */
         done = True;
 
-      if (s->mode == PAGE &&
+      if ((s->mode == PAGE || s->mode == CHARS) &&
           x + w->rbearing > right)                     /* wrap line */
         {
           align_line (s, se, line_start, x, right);
           line_start = se->nwords;
 
           x = left;
-          y += se->font->ascent;
+          y += se->xftfont->ascent + se->xftfont->descent;
 
           /* If we're close to the bottom of the screen, stop, and 
              unread the current word.  (But not if this is the first
              word, otherwise we might just get stuck on it.)
            */
           if (se->nwords > 0 &&
-              y + se->font->ascent > s->xgwa.height)
+              y + se->xftfont->ascent + se->xftfont->descent > s->xgwa.height)
             {
               unread_word (s, w);
-              done = True;
+              free (w);
+              /* done = True; */
               break;
             }
         }
 
-      w->target_x = x + w->lbearing;
-      w->target_y = y - w->ascent;
+      w->target_x = x;
+      w->target_y = y;
 
-      x += w->rbearing + space;
+      x += w->width + space;
       se->width = x;
 
       if (se->nwords >= (array_size - 1))
         {
           array_size += 100;
-          se->words = (word **) realloc (se->words,
-                                         array_size * sizeof(*se->words));
+          se->words = (word **)
+            realloc (se->words, array_size * sizeof(*se->words));
           if (!se->words)
             {
               fprintf (stderr, "%s: out of memory (%d words)\n",
@@ -1056,6 +1252,7 @@ populate_sentence (state *s, sentence *se)
   switch (s->mode)
     {
     case PAGE:
+    case CHARS:
       align_line (s, se, line_start, x, right);
       if (se->move_chars_p)
         split_words (s, se);
@@ -1078,29 +1275,33 @@ populate_sentence (state *s, sentence *se)
         fprintf (stderr, " %s", se->words[i]->text);
       fprintf (stderr, "\n");
     }
-# endif
+# endif /* DEBUG */
 }
 
 
 /* Render a single word object to the screen.
  */
 static void
-draw_word (state *s, sentence *se, word *w)
+draw_word (state *s, sentence *se, word *word)
 {
-  if (! w->pixmap) return;
-
-  if (w->x + w->width < 0 ||
-      w->y + w->height < 0 ||
-      w->x > s->xgwa.width ||
-      w->y > s->xgwa.height)
+  int x, y, w, h;
+  if (! word->pixmap) return;
+
+  x = word->x + word->lbearing;
+  y = word->y - word->ascent;
+  w = word->rbearing - word->lbearing;
+  h = word->ascent + word->descent;
+
+  if (x + w < 0 ||
+      y + h < 0 ||
+      x > s->xgwa.width ||
+      y > s->xgwa.height)
     return;
 
-  XSetClipMask (s->dpy, se->fg_gc, w->mask);
-  XSetClipOrigin (s->dpy, se->fg_gc, w->x, w->y);
-  XCopyPlane (s->dpy, w->pixmap, s->b, se->fg_gc,
-              0, 0, w->width, w->height,
-              w->x, w->y,
-              1L);
+  XSetClipMask (s->dpy, se->fg_gc, word->mask);
+  XSetClipOrigin (s->dpy, se->fg_gc, x, y);
+  XCopyArea (s->dpy, word->pixmap, s->b, se->fg_gc,
+             0, 0, w, h, x, y);
 }
 
 
@@ -1116,7 +1317,7 @@ more_sentences (state *s)
       sentence *se = s->sentences[i];
       if (! se)
         {
-          se = new_sentence (s);
+          se = new_sentence (s, s);
           populate_sentence (s, se);
           if (se->nwords > 0)
             s->spawn_p = False, any = True;
@@ -1153,6 +1354,7 @@ draw_sentence (state *s, sentence *se)
       switch (s->mode)
         {
         case PAGE:
+        case CHARS:
           if (se->anim_state != PAUSE &&
               w->tick <= w->nticks)
             {
@@ -1163,7 +1365,8 @@ draw_sentence (state *s, sentence *se)
               w->y = w->start_y + (dy * r);
 
               w->tick++;
-              if (se->anim_state == OUT && s->mode == PAGE)
+              if (se->anim_state == OUT &&
+                  (s->mode == PAGE || s->mode == CHARS))
                 w->tick++;  /* go out faster */
               moved = True;
             }
@@ -1201,7 +1404,7 @@ draw_sentence (state *s, sentence *se)
                                progname, se->id,
                                se->words[0]->x + se->width,
                                rand_p ? " randomly" : "");
-# endif
+# endif /* DEBUG */
                   }
               }
           }
@@ -1231,7 +1434,7 @@ draw_sentence (state *s, sentence *se)
 # ifdef DEBUG
           if (s->debug_p)
             fprintf (stderr, "%s: PAUSE %d\n", progname, se->id);
-# endif
+# endif /* DEBUG */
           break;
         case PAUSE:
           if (--se->pause_tick <= 0)
@@ -1241,14 +1444,14 @@ draw_sentence (state *s, sentence *se)
 # ifdef DEBUG
               if (s->debug_p)
                 fprintf (stderr, "%s: OUT   %d\n", progname, se->id);
-# endif
+# endif /* DEBUG */
             }
           break;
         case OUT:
 # ifdef DEBUG
           if (s->debug_p)
             fprintf (stderr, "%s: DEAD  %d\n", progname, se->id);
-# endif
+# endif /* DEBUG */
           {
             int j;
             for (j = 0; j < s->nsentences; j++)
@@ -1265,221 +1468,794 @@ draw_sentence (state *s, sentence *se)
 }
 
 
-/* Render all the words to the screen, and run the animation one step.
-   Clear screen first, swap buffers after.
- */
-static void
-draw_fontglide (state *s)
-{
-  int i;
+#ifdef DEBUG    /* All of this stuff is for -debug-metrics mode. */
 
-  if (s->spawn_p)
-    more_sentences (s);
-
-  if (!s->trails_p)
-    XFillRectangle (s->dpy, s->b, s->bg_gc,
-                    0, 0, s->xgwa.width, s->xgwa.height);
 
-  for (i = 0; i < s->nsentences; i++)
-    draw_sentence (s, s->sentences[i]);
+static Pixmap
+scale_ximage (Screen *screen, Window window, XImage *img, int scale,
+              int margin)
+{
+  Display *dpy = DisplayOfScreen (screen);
+  int x, y;
+  unsigned width = img->width, height = img->height;
+  Pixmap p2;
+  GC gc;
+
+  p2 = XCreatePixmap (dpy, window, width*scale, height*scale, img->depth);
+  gc = XCreateGC (dpy, p2, 0, 0);
+
+  XSetForeground (dpy, gc, BlackPixelOfScreen (screen));
+  XFillRectangle (dpy, p2, gc, 0, 0, width*scale, height*scale);
+  for (y = 0; y < height; y++)
+    for (x = 0; x < width; x++)
+      {
+        XSetForeground (dpy, gc, XGetPixel (img, x, y));
+        XFillRectangle (dpy, p2, gc, x*scale, y*scale, scale, scale);
+      }
 
-#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
-  if (s->backb)
+  if (scale > 2)
     {
-      XdbeSwapInfo info[1];
-      info[0].swap_window = s->window;
-      info[0].swap_action = (s->dbeclear_p ? XdbeBackground : XdbeUndefined);
-      XdbeSwapBuffers (s->dpy, info, 1);
+      XWindowAttributes xgwa;
+      XColor c;
+      c.red = c.green = c.blue = 0x4444;
+      c.flags = DoRed|DoGreen|DoBlue;
+      XGetWindowAttributes (dpy, window, &xgwa);
+      if (! XAllocColor (dpy, xgwa.colormap, &c)) abort();
+      XSetForeground (dpy, gc, c.pixel);
+      XDrawRectangle (dpy, p2, gc, 0, 0, width*scale-1, height*scale-1);
+      XDrawRectangle (dpy, p2, gc, margin*scale, margin*scale,
+                      width*scale-1, height*scale-1);
+      for (y = 0; y <= height - 2*margin; y++)
+        XDrawLine (dpy, p2, gc,
+                   margin*scale,          (y+margin)*scale-1,
+                   (width-margin)*scale,  (y+margin)*scale-1);
+      for (x = 0; x <= width - 2*margin; x++)
+        XDrawLine (dpy, p2, gc,
+                   (x+margin)*scale-1, margin*scale,
+                   (x+margin)*scale-1, (height-margin)*scale);
+      XFreeColors (dpy, xgwa.colormap, &c.pixel, 1, 0);
     }
-  else
-#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
-  if (s->dbuf)
+
+  XFreeGC (dpy, gc);
+  return p2;
+}
+
+
+static int check_edge (Display *dpy, Drawable p, GC gc,
+                       unsigned msg_x, unsigned msg_y, const char *msg,
+                       XImage *img,  
+                       unsigned x, unsigned y, unsigned dim, unsigned end)
+{
+  unsigned pt[2];
+  pt[0] = x;
+  pt[1] = y;
+  end += pt[dim];
+  
+  for (;;)
     {
-      XCopyArea (s->dpy, s->b, s->window, s->bg_gc,
-                0, 0, s->xgwa.width, s->xgwa.height, 0, 0);
+      if (pt[dim] == end)
+        {
+          XDrawString (dpy, p, gc, msg_x, msg_y, msg, strlen (msg));
+          return 1;
+        }
+      
+      if (XGetPixel(img, pt[0], pt[1]) & 0xffffff)
+        break;
+      
+      ++pt[dim];
     }
+  
+  return 0;
 }
 
 
-static void
-handle_events (state *s)
+static unsigned long
+fontglide_draw_metrics (state *s)
 {
-  while (XPending (s->dpy))
+  unsigned int margin = (s->debug_metrics_antialiasing_p ? 2 : 0);
+
+  char txt[2], utxt[3], txt2[80];
+  XChar2b *txt3 = 0;
+  const char *fn = (s->font_override ? s->font_override : "fixed");
+  char fn2[1024];
+  XCharStruct c, overall, fake_c;
+  int dir, ascent, descent;
+  int x, y;
+  XGlyphInfo extents;
+  XftColor xftcolor;
+  XftDraw *xftdraw;
+  int sc = s->debug_scale;
+  GC gc;
+  unsigned long red    = 0xFFFF0000;  /* so shoot me */
+  unsigned long green  = 0xFF00FF00;
+  unsigned long blue   = 0xFF6666FF;
+  unsigned long yellow = 0xFFFFFF00;
+  unsigned long cyan   = 0xFF004040;
+  int i, j;
+  Drawable dest = s->b ? s->b : s->window;
+
+  if (sc < 1) sc = 1;
+
+  /* Self-test these macros to make sure they're symmetrical. */
+  for (i = 0; i < 1000; i++)
+    {
+      XGlyphInfo g, g2;
+      XRectangle r;
+      c.lbearing = (random()%50)-100;
+      c.rbearing = (random()%50)-100;
+      c.ascent   = (random()%50)-100;
+      c.descent  = (random()%50)-100;
+      c.width    = (random()%50)-100;
+      XCharStruct_to_XGlyphInfo (c, g);
+      XGlyphInfo_to_XCharStruct (g, overall);
+      if (c.lbearing != overall.lbearing) abort();
+      if (c.rbearing != overall.rbearing) abort();
+      if (c.ascent   != overall.ascent)   abort();
+      if (c.descent  != overall.descent)  abort();
+      if (c.width    != overall.width)    abort();
+      XCharStruct_to_XGlyphInfo (overall, g2);
+      if (g.x      != g2.x)      abort();
+      if (g.y      != g2.y)      abort();
+      if (g.xOff   != g2.xOff)   abort();
+      if (g.yOff   != g2.yOff)   abort();
+      if (g.width  != g2.width)  abort();
+      if (g.height != g2.height) abort();
+      XCharStruct_to_XmbRectangle (overall, r);
+      XmbRectangle_to_XCharStruct (r, c, c.width);
+      if (c.lbearing != overall.lbearing) abort();
+      if (c.rbearing != overall.rbearing) abort();
+      if (c.ascent   != overall.ascent)   abort();
+      if (c.descent  != overall.descent)  abort();
+      if (c.width    != overall.width)    abort();
+    }
+
+  txt[0] = s->debug_metrics_p;
+  txt[1] = 0;
+
+  /* Convert Unicode code point to UTF-8. */
+  utxt[utf8_encode(s->debug_metrics_p, utxt, 4)] = 0;
+
+  txt3 = utf8_to_XChar2b (utxt, 0);
+
+  if (! s->metrics_font1)
+    s->metrics_font1 = XLoadQueryFont (s->dpy, fn);
+  if (! s->metrics_font2)
+    s->metrics_font2 = XLoadQueryFont (s->dpy, "fixed");
+  if (! s->metrics_font1)
+    s->metrics_font1 = s->metrics_font2;
+
+  gc  = XCreateGC (s->dpy, dest, 0, 0);
+  XSetFont (s->dpy, gc,  s->metrics_font1->fid);
+
+# if defined(HAVE_JWXYZ)
+  jwxyz_XSetAntiAliasing (s->dpy, gc, False);
+# endif
+
+  if (! s->metrics_xftfont)
     {
-      XEvent event;
-      XNextEvent (s->dpy, &event);
+      s->metrics_xftfont =
+        XftFontOpenXlfd (s->dpy, screen_number(s->xgwa.screen), fn);
+      if (! s->metrics_xftfont)
+        {
+          const char *fn2 = "fixed";
+          s->metrics_xftfont =
+            XftFontOpenName (s->dpy, screen_number(s->xgwa.screen), fn2);
+          if (s->metrics_xftfont)
+            fn = fn2;
+          else
+            {
+              fprintf (stderr, "%s: XftFontOpen failed on \"%s\" and \"%s\"\n",
+                       progname, fn, fn2);
+              exit (1);
+            }
+        }
+    }
+
+  strcpy (fn2, fn);
+# ifdef HAVE_JWXYZ
+  append_font_name (s->dpy, fn2, s->metrics_xftfont->xfont);
+# endif
+
+  xftdraw = XftDrawCreate (s->dpy, dest, s->xgwa.visual,
+                           s->xgwa.colormap);
+  XftColorAllocName (s->dpy, s->xgwa.visual, s->xgwa.colormap, "white",
+                     &xftcolor);
+  XftTextExtentsUtf8 (s->dpy, s->metrics_xftfont,
+                      (FcChar8 *) utxt, strlen(utxt),
+                      &extents);
+
+
+  XTextExtents (s->metrics_font1, txt, strlen(txt), 
+                &dir, &ascent, &descent, &overall);
+  c = ((s->debug_metrics_p >= s->metrics_font1->min_char_or_byte2 &&
+        s->debug_metrics_p <= s->metrics_font1->max_char_or_byte2)
+        ? s->metrics_font1->per_char[s->debug_metrics_p -
+                                     s->metrics_font1->min_char_or_byte2]
+        : overall);
+
+  XSetForeground (s->dpy, gc, BlackPixelOfScreen (s->xgwa.screen));
+  XFillRectangle (s->dpy, dest, gc, 0, 0, s->xgwa.width, s->xgwa.height);
+
+  XSetForeground (s->dpy, gc, WhitePixelOfScreen (s->xgwa.screen));
+  XSetFont (s->dpy, gc, s->metrics_font2->fid);
+  XDrawString (s->dpy, dest, gc, 
+               s->xgwa.width / 2,
+               s->xgwa.height - 5,
+               fn2, strlen(fn2));
+
+# ifdef HAVE_JWXYZ
+  {
+    char *name = jwxyz_unicode_character_name (
+      s->dpy, s->metrics_font1->fid, s->debug_metrics_p);
+    if (!name || !*name) name = strdup("unknown character name");
+    XDrawString (s->dpy, dest, gc, 
+                 10,
+                 10 + 2 * (s->metrics_font2->ascent +
+                           s->metrics_font2->descent),
+                 name, strlen(name));
+    free (name);
+  }
+# endif
+
+  /* i 0, j 0: top left,  XDrawString,       char metrics
+     i 1, j 0: bot left,  XDrawString,       overall metrics, ink escape
+     i 0, j 1: top right, XftDrawStringUtf8, utf8 metrics
+     i 1, j 1: bot right, XDrawString16,     16 metrics, ink escape
+   */
+  for (j = 0; j < 2; j++) {
+    Bool xft_p = (j != 0);
+    int ww = s->xgwa.width / 2 - 20;
+    int xoff = (j == 0 ? 0 : ww + 20);
+
+    /* XDrawString only does 8-bit characters, so skip it outside Latin-1. */
+    if (s->debug_metrics_p >= 256)
+      {
+        if (!xft_p)
+          continue;
+        xoff = 0;
+        ww = s->xgwa.width;
+      }
+
+    x = (ww - overall.width) / 2;
+    
+   for (i = 0; i < 2; i++)
+    {
+      XCharStruct cc;
+      int x1 = xoff + ww * 0.18;
+      int x2 = xoff + ww * 0.82;
+      int x3 = xoff + ww;
+      int pixw, pixh;
+      Pixmap p;
+
+      y = 80;
+      {
+        int h = sc * (ascent + descent);
+        int min = (ascent + descent) * 4;
+        if (h < min) h = min;
+        if (i == 1) h *= 3;
+        y += h;
+      }
+
+      memset (&fake_c, 0, sizeof(fake_c));
+
+      if (!xft_p && i == 0)
+        cc = c;
+      else if (!xft_p && i == 1)
+        cc = overall;
+      else if (xft_p && i == 0)
+        {
+          /* Measure the glyph in the Xft way */
+          XGlyphInfo extents;
+          XftTextExtentsUtf8 (s->dpy, 
+                              s->metrics_xftfont,
+                              (FcChar8 *) utxt, strlen(utxt),
+                              &extents);
+          XGlyphInfo_to_XCharStruct (extents, fake_c);
+          cc = fake_c;
+        }
+      else if (xft_p)
+        {
+          /* Measure the glyph in the 16-bit way */
+          int dir, ascent, descent;
+          XTextExtents16 (s->metrics_font1, txt3, 1, &dir, &ascent, &descent,
+                          &fake_c);
+          cc = fake_c;
+        }
 
-      if (event.xany.type == ConfigureNotify)
+      pixw = margin * 2 + cc.rbearing - cc.lbearing;
+      pixh = margin * 2 + cc.ascent + cc.descent;
+      p = (pixw > 0 && pixh > 0
+           ? XCreatePixmap (s->dpy, dest, pixw, pixh, s->xgwa.depth)
+           : 0);
+
+      if (p)
         {
-          XGetWindowAttributes (s->dpy, s->window, &s->xgwa);
+          Pixmap p2;
+          GC gc2 = XCreateGC (s->dpy, p, 0, 0);
+# ifdef HAVE_JWXYZ
+          jwxyz_XSetAntiAliasing (s->dpy, gc2, False);
+# endif
+          XSetFont (s->dpy, gc2, s->metrics_font1->fid);
+          XSetForeground (s->dpy, gc2, BlackPixelOfScreen (s->xgwa.screen));
+          XFillRectangle (s->dpy, p, gc2, 0, 0, pixw, pixh);
+          XSetForeground (s->dpy, gc,  WhitePixelOfScreen (s->xgwa.screen));
+          XSetForeground (s->dpy, gc2, WhitePixelOfScreen (s->xgwa.screen));
+# ifdef HAVE_JWXYZ
+          jwxyz_XSetAntiAliasing (s->dpy, gc2,
+                                  s->debug_metrics_antialiasing_p);
+# endif
 
-          if (s->dbuf && (s->ba))
+          if (xft_p && i == 0)
             {
-              XFreePixmap (s->dpy, s->ba);
-              s->ba = XCreatePixmap (s->dpy, s->window, 
-                                    s->xgwa.width, s->xgwa.height,
-                                    s->xgwa.depth);
-              XFillRectangle (s->dpy, s->ba, s->bg_gc, 0, 0, 
-                              s->xgwa.width, s->xgwa.height);
-              s->b = s->ba;
+              XftDraw *xftdraw2 = XftDrawCreate (s->dpy, p, s->xgwa.visual,
+                                                 s->xgwa.colormap);
+              XftDrawStringUtf8 (xftdraw2, &xftcolor, 
+                                 s->metrics_xftfont,
+                                 -cc.lbearing + margin,
+                                 cc.ascent + margin,
+                                 (FcChar8 *) utxt, strlen(utxt));
+              XftDrawDestroy (xftdraw2);
             }
+          else if (xft_p)
+            XDrawString16 (s->dpy, p, gc2,
+                           -cc.lbearing + margin,
+                           cc.ascent + margin,
+                           txt3, 1);
+          else
+            XDrawString (s->dpy, p, gc2,
+                         -cc.lbearing + margin,
+                         cc.ascent + margin,
+                         txt, strlen(txt));
+
+          {
+            unsigned x2, y2;
+            XImage *img = XGetImage (s->dpy, p, 0, 0, pixw, pixh,
+                                     ~0L, ZPixmap);
+            XImage *img2;
+
+            if (i == 1)
+              {
+                unsigned w = pixw - margin * 2, h = pixh - margin * 2;
+
+                if (margin > 0)
+                  {
+                    /* Check for ink escape. */
+                    unsigned long ink = 0;
+                    for (y2 = 0; y2 != pixh; ++y2)
+                      for (x2 = 0; x2 != pixw; ++x2)
+                        {
+                          /* Sloppy... */
+                          if (! (x2 >= margin &&
+                                 x2 < pixw - margin &&
+                                 y2 >= margin &&
+                                 y2 < pixh - margin))
+                            ink |= XGetPixel (img, x2, y2);
+                        }
+
+                      if (ink & 0xFFFFFF)
+                      {
+                        XSetFont (s->dpy, gc, s->metrics_font2->fid);
+                        XDrawString (s->dpy, dest, gc,
+                                     xoff + 10, 40,
+                                     "Ink escape!", 11);
+                      }
+                  }
+
+                /* ...And wasted space. */
+                if (w && h)
+                  {
+                    if (check_edge (s->dpy, dest, gc, 120, 60, "left",
+                                    img, margin, margin, 1, h) |
+                        check_edge (s->dpy, dest, gc, 160, 60, "right",
+                                    img, margin + w - 1, margin, 1, h) |
+                        check_edge (s->dpy, dest, gc, 200, 60, "top",
+                                    img, margin, margin, 0, w) |
+                        check_edge (s->dpy, dest, gc, 240, 60, "bottom",
+                                    img, margin, margin + h - 1, 0, w))
+                      {
+                        XSetFont (s->dpy, gc, s->metrics_font2->fid);
+                        XDrawString (s->dpy, dest, gc, 
+                                     xoff + 10, 60,
+                                     "Wasted space: ", 14);
+                      }
+                  }
+              }
+
+            if (s->debug_metrics_antialiasing_p)
+              {
+                /* Draw a dark cyan boundary around antialiased glyphs */
+                img2 = XCreateImage (s->dpy, s->xgwa.visual, img->depth,
+                                     ZPixmap, 0, NULL,
+                                     img->width, img->height,
+                                     img->bitmap_pad, 0);
+                img2->data = malloc (img->bytes_per_line * img->height);
+
+                for (y2 = 0; y2 != pixh; ++y2)
+                  for (x2 = 0; x2 != pixw; ++x2) 
+                    {
+                      unsigned long px = XGetPixel (img, x2, y2);
+                      if ((px & 0xffffff) == 0)
+                        {
+                          unsigned long neighbors = 0;
+                          if (x2)
+                            neighbors |= XGetPixel (img, x2 - 1, y2);
+                          if (x2 != pixw - 1)
+                            neighbors |= XGetPixel (img, x2 + 1, y2);
+                          if (y2)
+                            neighbors |= XGetPixel (img, x2, y2 - 1);
+                          if (y2 != pixh - 1)
+                            neighbors |= XGetPixel (img, x2, y2 + 1);
+                          XPutPixel (img2, x2, y2,
+                                     (neighbors & 0xffffff
+                                      ? cyan
+                                      : BlackPixelOfScreen (s->xgwa.screen)));
+                        }
+                      else
+                        {
+                          XPutPixel (img2, x2, y2, px);
+                        }
+                    }
+              }
+            else
+              {
+                img2 = img;
+                img = NULL;
+              }
+
+            p2 = scale_ximage (s->xgwa.screen, s->window, img2, sc, margin);
+            if (img)
+              XDestroyImage (img);
+            XDestroyImage (img2);
+          }
+
+          XCopyArea (s->dpy, p2, dest, gc,
+                     0, 0, sc*pixw, sc*pixh,
+                     xoff + x + sc * (cc.lbearing - margin),
+                     y - sc * (cc.ascent + margin));
+          XFreePixmap (s->dpy, p);
+          XFreePixmap (s->dpy, p2);
+          XFreeGC (s->dpy, gc2);
         }
 
-      screenhack_handle_event (s->dpy, &event);
+      if (i == 0)
+        {
+          XSetFont (s->dpy, gc, s->metrics_font1->fid);
+          XSetForeground (s->dpy, gc,  WhitePixelOfScreen (s->xgwa.screen));
+# ifdef HAVE_JWXYZ
+          jwxyz_XSetAntiAliasing (s->dpy, gc, s->debug_metrics_antialiasing_p);
+# endif
+          sprintf (txt2, "%s        [XX%sXX]    [%s%s%s%s]",
+                   (xft_p ? utxt : txt),
+                   (xft_p ? utxt : txt),
+                   (xft_p ? utxt : txt),
+                   (xft_p ? utxt : txt),
+                   (xft_p ? utxt : txt),
+                   (xft_p ? utxt : txt));
+
+          if (xft_p)
+            XftDrawStringUtf8 (xftdraw, &xftcolor,
+                               s->metrics_xftfont,
+                               xoff + x/2 + (sc*cc.rbearing/2) - cc.rbearing/2
+                               + 40,
+                               ascent + 10,
+                             (FcChar8 *) txt2, strlen(txt2));
+          else
+            XDrawString (s->dpy, dest, gc,
+                         xoff + x/2 + (sc*cc.rbearing/2) - cc.rbearing/2,
+                         ascent + 10,
+                         txt2, strlen(txt2));
+# ifdef HAVE_JWXYZ
+          jwxyz_XSetAntiAliasing (s->dpy, gc, False);
+# endif
+          XSetFont (s->dpy, gc, s->metrics_font2->fid);
+          if (xft_p)
+            {
+              char *uptr;
+              char *tptr = txt2 + sprintf(txt2, "U+%04lX", s->debug_metrics_p);
+              *tptr++ = " ?_"[s->entering_unicode_p];
+              *tptr++ = ' ';
+
+              uptr = utxt;
+              while (*uptr)
+                {
+                  tptr += sprintf (tptr, "0%03o ", (unsigned char) *uptr);
+                  ++uptr;
+                }
+              *tptr++ = ' ';
+              uptr = utxt;
+              while (*uptr)
+                {
+                  tptr += sprintf (tptr, "%02x ", (unsigned char) *uptr);
+                  ++uptr;
+                }
+            }
+          else
+            sprintf (txt2, "%c %3ld 0%03lo 0x%02lx%s",
+                     (char)s->debug_metrics_p, s->debug_metrics_p,
+                     s->debug_metrics_p, s->debug_metrics_p,
+                     (txt[0] < s->metrics_font1->min_char_or_byte2
+                      ? " *" : ""));
+          XDrawString (s->dpy, dest, gc,
+                       xoff + 10, 20,
+                       txt2, strlen(txt2));
+        }
+
+# ifdef HAVE_JWXYZ
+      jwxyz_XSetAntiAliasing (s->dpy, gc, True);
+# endif
+
+      {
+        const char *ss = (j == 0
+                          ? (i == 0 ? "char" : "overall")
+                          : (i == 0 ? "utf8" : "16 bit"));
+        XSetFont (s->dpy, gc, s->metrics_font2->fid);
+
+        XSetForeground (s->dpy, gc, red);
+
+        sprintf (txt2, "%s ascent %d", ss, ascent);
+        XDrawString (s->dpy, dest, gc, 
+                     xoff + 10,
+                     y - sc*ascent - 2,
+                     txt2, strlen(txt2));
+        XDrawLine (s->dpy, dest, gc,
+                   xoff, y - sc*ascent,
+                   x3,   y - sc*ascent);
+
+        sprintf (txt2, "%s descent %d", ss, descent);
+        XDrawString (s->dpy, dest, gc, 
+                     xoff + 10,
+                     y + sc*descent - 2,
+                     txt2, strlen(txt2));
+        XDrawLine (s->dpy, dest, gc, 
+                   xoff, y + sc*descent,
+                   x3,   y + sc*descent);
+      }
+
+
+      /* ascent, descent, baseline */
+
+      XSetForeground (s->dpy, gc, green);
+
+      sprintf (txt2, "ascent %d", cc.ascent);
+      if (cc.ascent != 0)
+        XDrawString (s->dpy, dest, gc,
+                     x1, y - sc*cc.ascent - 2,
+                     txt2, strlen(txt2));
+      XDrawLine (s->dpy, dest, gc,
+                 x1, y - sc*cc.ascent,
+                 x2, y - sc*cc.ascent);
+
+      sprintf (txt2, "descent %d", cc.descent);
+      if (cc.descent != 0)
+        XDrawString (s->dpy, dest, gc,
+                     x1, y + sc*cc.descent - 2,
+                     txt2, strlen(txt2));
+      XDrawLine (s->dpy, dest, gc,
+                 x1, y + sc*cc.descent,
+                 x2, y + sc*cc.descent);
+
+      XSetForeground (s->dpy, gc, yellow);
+      strcpy (txt2, "baseline");
+      XDrawString (s->dpy, dest, gc,
+                   x1, y - 2,
+                   txt2, strlen(txt2));
+      XDrawLine (s->dpy, dest, gc, x1, y, x2, y);
+
+
+      /* origin, width */
+
+      XSetForeground (s->dpy, gc, blue);
+
+      strcpy (txt2, "origin");
+      XDrawString (s->dpy, dest, gc,
+                   xoff + x + 2,
+                   y + sc*descent + 50,
+                   txt2, strlen(txt2));
+      XDrawLine (s->dpy, dest, gc,
+                 xoff + x, y - sc*(ascent  - 10),
+                 xoff + x, y + sc*(descent + 10));
+
+      sprintf (txt2, "width %d", cc.width);
+      XDrawString (s->dpy, dest, gc,
+                   xoff + x + sc*cc.width + 2,
+                   y + sc*descent + 60, 
+                   txt2, strlen(txt2));
+      XDrawLine (s->dpy, dest, gc,
+                 xoff + x + sc*cc.width, y - sc*(ascent  - 10),
+                 xoff + x + sc*cc.width, y + sc*(descent + 10));
+
+
+      /* lbearing, rbearing */
+
+      XSetForeground (s->dpy, gc, green);
+
+      sprintf (txt2, "lbearing %d", cc.lbearing);
+      XDrawString (s->dpy, dest, gc,
+                   xoff + x + sc*cc.lbearing + 2,
+                   y + sc * descent + 30, 
+                   txt2, strlen(txt2));
+      XDrawLine (s->dpy, dest, gc,
+                 xoff + x + sc*cc.lbearing, y - sc*ascent,
+                 xoff + x + sc*cc.lbearing, y + sc*descent + 20);
+
+      sprintf (txt2, "rbearing %d", cc.rbearing);
+      XDrawString (s->dpy, dest, gc,
+                   xoff + x + sc*cc.rbearing + 2,
+                   y + sc * descent + 40, 
+                   txt2, strlen(txt2));
+      XDrawLine (s->dpy, dest, gc,
+                 xoff + x + sc*cc.rbearing, y - sc*ascent,
+                 xoff + x + sc*cc.rbearing, y + sc*descent + 40);
+
+      /* y += sc * (ascent + descent) * 2; */
     }
+  }
+
+  if (dest != s->window)
+    XCopyArea (s->dpy, dest, s->window, s->bg_gc,
+               0, 0, s->xgwa.width, s->xgwa.height, 0, 0);
+
+  XFreeGC (s->dpy, gc);
+  XftColorFree (s->dpy, s->xgwa.visual, s->xgwa.colormap, &xftcolor);
+  XftDrawDestroy (xftdraw);
+  free (txt3);
+
+  return s->frame_delay;
 }
 
+# endif /* DEBUG */
 
-\f
-/* Subprocess.
-   (This bit mostly cribbed from phosphor.c)
- */
 
-static void
-subproc_cb (XtPointer closure, int *source, XtInputId *id)
+/* Render all the words to the screen, and run the animation one step.
+   Clear screen first, swap buffers after.
+ */
+static unsigned long
+fontglide_draw (Display *dpy, Window window, void *closure)
 {
   state *s = (state *) closure;
-  s->input_available_p = True;
-}
+  int i;
+
+# ifdef DEBUG
+  if (s->debug_metrics_p)
+    return fontglide_draw_metrics (closure);
+# endif /* DEBUG */
 
+  if (s->spawn_p)
+    more_sentences (s);
 
-static void
-launch_text_generator (state *s)
-{
-  char *oprogram = get_string_resource ("program", "Program");
-  char *program = (char *) malloc (strlen (oprogram) + 10);
-  strcpy (program, "( ");
-  strcat (program, oprogram);
-  strcat (program, " ) 2>&1");
+  if (!s->trails_p)
+    XFillRectangle (s->dpy, s->b, s->bg_gc,
+                    0, 0, s->xgwa.width, s->xgwa.height);
 
-  if (s->debug_p)
-    fprintf (stderr, "%s: forking: %s\n", progname, program);
+  for (i = 0; i < s->nsentences; i++)
+    draw_sentence (s, s->sentences[i]);
 
-  if ((s->pipe = popen (program, "r")))
+# ifdef DEBUG
+  if (s->debug_p && (s->prev_font_name || s->next_font_name))
     {
-      s->pipe_id =
-        XtAppAddInput (app, fileno (s->pipe),
-                       (XtPointer) (XtInputReadMask | XtInputExceptMask),
-                       subproc_cb, (XtPointer) s);
+      if (! s->label_gc)
+        {
+          if (! s->metrics_font2)
+            s->metrics_font2 = XLoadQueryFont (s->dpy, "fixed");
+          s->label_gc = XCreateGC (dpy, s->b, 0, 0);
+          XSetFont (s->dpy, s->label_gc, s->metrics_font2->fid);
+        }
+      if (s->prev_font_name)
+        XDrawString (s->dpy, s->b, s->label_gc,
+                     10, 10 + s->metrics_font2->ascent,
+                     s->prev_font_name, strlen(s->prev_font_name));
+      if (s->next_font_name)
+        XDrawString (s->dpy, s->b, s->label_gc,
+                     10, 10 + s->metrics_font2->ascent * 2,
+                     s->next_font_name, strlen(s->next_font_name));
+    }
+# endif /* DEBUG */
+
+#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
+  if (s->backb)
+    {
+      XdbeSwapInfo info[1];
+      info[0].swap_window = s->window;
+      info[0].swap_action = (s->dbeclear_p ? XdbeBackground : XdbeUndefined);
+      XdbeSwapBuffers (s->dpy, info, 1);
     }
   else
+#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
+  if (s->dbuf)
     {
-      perror (program);
+      XCopyArea (s->dpy, s->b, s->window, s->bg_gc,
+                0, 0, s->xgwa.width, s->xgwa.height, 0, 0);
     }
-}
-
 
-static void
-relaunch_generator_timer (XtPointer closure, XtIntervalId *id)
-{
-  state *s = (state *) closure;
-  launch_text_generator (s);
+  return s->frame_delay;
 }
 
 
+
 /* When the subprocess has generated some output, this reads as much as it
    can into s->buf at s->buf_tail.
  */
 static void
 drain_input (state *s)
 {
-  /* allow subproc_cb() to run, if the select() down in Xt says that
-     input is available.  This tells us whether we can read() without
-     blocking. */
-  if (! s->input_available_p)
-    if (XtAppPending (app) & (XtIMTimer|XtIMAlternateInput))
-      XtAppProcessEvent (app, XtIMTimer|XtIMAlternateInput);
-
-  if (! s->pipe) return;
-  if (! s->input_available_p) return;
-  s->input_available_p = False;
-
-  if (s->buf_tail < sizeof(s->buf) - 2)
+  while (s->buf_tail < sizeof(s->buf) - 2)
     {
-      int target = sizeof(s->buf) - s->buf_tail - 2;
-      int n;
-      n = read (fileno (s->pipe),
-                (void *) (s->buf + s->buf_tail),
-                target);
-      if (n > 0)
-        {
-          s->buf_tail += n;
-          s->buf[s->buf_tail] = 0;
-        }
+      int c = textclient_getc (s->tc);
+      if (c > 0)
+        s->buf[s->buf_tail++] = (char) c;
       else
-        {
-          XtRemoveInput (s->pipe_id);
-          s->pipe_id = 0;
-          pclose (s->pipe);
-          s->pipe = 0;
-
-          /* If the process didn't print a terminating newline, add one. */
-          if (s->buf_tail > 1 &&
-              s->buf[s->buf_tail-1] != '\n')
-            {
-              s->buf[s->buf_tail++] = '\n';
-              s->buf[s->buf_tail] = 0;
-            }
-
-          /* Then add one more, to make sure there's a sentence break at EOF.
-           */
-          s->buf[s->buf_tail++] = '\n';
-          s->buf[s->buf_tail] = 0;
-
-          /* Set up a timer to re-launch the subproc in a bit. */
-          XtAppAddTimeOut (app, s->subproc_relaunch_delay,
-                           relaunch_generator_timer,
-                           (XtPointer) s);
-        }
+        break;
     }
 }
 
 \f
 /* Window setup and resource loading */
 
-static state *
-init_fontglide (Display *dpy, Window window)
+static void *
+fontglide_init (Display *dpy, Window window)
 {
   XGCValues gcv;
   state *s = (state *) calloc (1, sizeof(*s));
   s->dpy = dpy;
   s->window = window;
+  s->frame_delay = get_integer_resource (dpy, "delay", "Integer");
 
   XGetWindowAttributes (s->dpy, s->window, &s->xgwa);
 
-  s->font_override = get_string_resource ("font", "Font");
+  s->font_override = get_string_resource (dpy, "font", "Font");
   if (s->font_override && (!*s->font_override || *s->font_override == '('))
     s->font_override = 0;
 
-  s->charset = get_string_resource ("fontCharset", "FontCharset");
-  s->border_width = get_integer_resource ("fontBorderWidth", "Integer");
+  s->charset = get_string_resource (dpy, "fontCharset", "FontCharset");
+  s->border_width = get_integer_resource (dpy, "fontBorderWidth", "Integer");
   if (s->border_width < 0 || s->border_width > 20)
     s->border_width = 1;
 
-  s->speed = get_float_resource ("speed", "Float");
+  s->speed = get_float_resource (dpy, "speed", "Float");
   if (s->speed <= 0 || s->speed > 200)
     s->speed = 1;
 
-  s->linger = get_float_resource ("linger", "Float");
+  s->linger = get_float_resource (dpy, "linger", "Float");
   if (s->linger <= 0 || s->linger > 200)
     s->linger = 1;
 
-  s->debug_p = get_boolean_resource ("debug", "Debug");
-  s->trails_p = get_boolean_resource ("trails", "Trails");
+  s->trails_p = get_boolean_resource (dpy, "trails", "Trails");
+
+# ifdef DEBUG
+  s->debug_p = get_boolean_resource (dpy, "debug", "Debug");
+  s->debug_metrics_p = (get_boolean_resource (dpy, "debugMetrics", "Debug")
+                        ? 199 : 0);
+  s->debug_scale = 6;
+
+#  ifdef HAVE_JWXYZ
+  if (s->debug_metrics_p && !s->font_override)
+    s->font_override = "Helvetica Bold 16";
+#  endif
+
+# endif /* DEBUG */
 
-  s->dbuf = get_boolean_resource ("doubleBuffer", "Boolean");
-  s->dbeclear_p = get_boolean_resource ("useDBEClear", "Boolean");
+  s->dbuf = get_boolean_resource (dpy, "doubleBuffer", "Boolean");
+
+# ifdef HAVE_JWXYZ     /* Don't second-guess Quartz's double-buffering */
+  s->dbuf = False;
+# endif
+
+# ifdef DEBUG
+  if (s->debug_metrics_p) s->trails_p = False;
+# endif /* DEBUG */
 
   if (s->trails_p) s->dbuf = False;  /* don't need it in this case */
 
   {
-    char *ss = get_string_resource ("mode", "Mode");
+    const char *ss = get_string_resource (dpy, "mode", "Mode");
     if (!ss || !*ss || !strcasecmp (ss, "random"))
       s->mode = ((random() % 2) ? SCROLL : PAGE);
     else if (!strcasecmp (ss, "scroll"))
       s->mode = SCROLL;
     else if (!strcasecmp (ss, "page"))
       s->mode = PAGE;
+    else if (!strcasecmp (ss, "chars") || !strcasecmp (ss, "char"))
+      s->mode = CHARS;
     else
       {
         fprintf (stderr,
@@ -1491,6 +2267,7 @@ init_fontglide (Display *dpy, Window window)
   if (s->dbuf)
     {
 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
+      s->dbeclear_p = get_boolean_resource (dpy, "useDBEClear", "Boolean");
       if (s->dbeclear_p)
         s->b = xdbe_get_backbuffer (dpy, window, XdbeBackground);
       else
@@ -1511,48 +2288,182 @@ init_fontglide (Display *dpy, Window window)
       s->b = s->window;
     }
 
-  gcv.foreground = get_pixel_resource ("background", "Background",
-                                       s->dpy, s->xgwa.colormap);
+  gcv.foreground = get_pixel_resource (s->dpy, s->xgwa.colormap,
+                                       "background", "Background");
   s->bg_gc = XCreateGC (s->dpy, s->b, GCForeground, &gcv);
 
-  s->subproc_relaunch_delay = 2 * 1000;
-
-  launch_text_generator (s);
-
   s->nsentences = 5; /* #### */
   s->sentences = (sentence **) calloc (s->nsentences, sizeof (sentence *));
   s->spawn_p = True;
 
+  s->early_p = True;
+  s->start_time = time ((time_t *) 0);
+  s->tc = textclient_open (dpy);
+
   return s;
 }
 
 
-\f
-char *progclass = "FontGlide";
+static Bool
+fontglide_event (Display *dpy, Window window, void *closure, XEvent *event)
+{
+# ifdef DEBUG
+  state *s = (state *) closure;
+
+  if (! s->debug_metrics_p)
+    return False;
+  if (event->xany.type == KeyPress)
+    {
+      static const unsigned long max = 0x110000;
+      KeySym keysym;
+      char c = 0;
+      XLookupString (&event->xkey, &c, 1, &keysym, 0);
+
+      if (s->entering_unicode_p > 0)
+        {
+          unsigned digit;
+          unsigned long new_char = 0;
+
+          if (c >= 'a' && c <= 'f')
+            digit = c + 0xa - 'a';
+          else if (c >= 'A' && c <= 'F')
+            digit = c + 0xa - 'A';
+          else if (c >= '0' && c <= '9')
+            digit = c + 0 - '0';
+          else
+            {
+              s->entering_unicode_p = 0;
+              return True;
+            }
+
+          if (s->entering_unicode_p == 1)
+            new_char = 0;
+          else if (s->entering_unicode_p == 2)
+            new_char = s->debug_metrics_p;
+
+          new_char = (new_char << 4) | digit;
+          if (new_char > 0 && new_char < max)
+            {
+              s->debug_metrics_p = new_char;
+              s->entering_unicode_p = 2;
+            }
+          else
+            s->entering_unicode_p = 0;
+          return True;
+        }
+
+      if (c == '\t')
+        s->debug_metrics_antialiasing_p ^= True;
+      else if (c == 3 || c == 27)
+        exit (0);
+      else if (c >= ' ')
+        s->debug_metrics_p = (unsigned char) c;
+      else if (keysym == XK_Left || keysym == XK_Right)
+        {
+          s->debug_metrics_p += (keysym == XK_Left ? -1 : 1);
+          if (s->debug_metrics_p >= max)
+            s->debug_metrics_p = 1;
+          else if (s->debug_metrics_p <= 0)
+            s->debug_metrics_p = max - 1;
+          return True;
+        }
+      else if (keysym == XK_Prior)
+        s->debug_metrics_p = (s->debug_metrics_p + max - 0x80) % max;
+      else if (keysym == XK_Next)
+        s->debug_metrics_p = (s->debug_metrics_p + 0x80) % max;
+      else if (keysym == XK_Up)
+        s->debug_scale++;
+      else if (keysym == XK_Down)
+        s->debug_scale = (s->debug_scale > 1 ? s->debug_scale-1 : 1);
+      else if (keysym == XK_F1)
+        s->entering_unicode_p = 1;
+      else
+        return False;
+      return True;
+    }
+# endif /* DEBUG */
+
+  return False;
+}
+
 
-char *defaults [] = {
+static void
+fontglide_reshape (Display *dpy, Window window, void *closure, 
+                 unsigned int w, unsigned int h)
+{
+  state *s = (state *) closure;
+  XGetWindowAttributes (s->dpy, s->window, &s->xgwa);
+
+  if (s->dbuf && s->ba)
+    {
+      XFreePixmap (s->dpy, s->ba);
+      s->ba = XCreatePixmap (s->dpy, s->window, 
+                             s->xgwa.width, s->xgwa.height,
+                             s->xgwa.depth);
+      XFillRectangle (s->dpy, s->ba, s->bg_gc, 0, 0, 
+                      s->xgwa.width, s->xgwa.height);
+      s->b = s->ba;
+    }
+}
+
+static void
+fontglide_free (Display *dpy, Window window, void *closure)
+{
+  state *s = (state *) closure;
+  textclient_close (s->tc);
+
+#ifdef DEBUG
+  if (s->metrics_xftfont)
+    XftFontClose (s->dpy, s->metrics_xftfont);
+  if (s->metrics_font1)
+    XFreeFont (s->dpy, s->metrics_font1);
+  if (s->metrics_font2 && s->metrics_font1 != s->metrics_font2)
+    XFreeFont (s->dpy, s->metrics_font2);
+  if (s->prev_font_name) free (s->prev_font_name);
+  if (s->next_font_name) free (s->next_font_name);
+  if (s->label_gc) XFreeGC (dpy, s->label_gc);
+#endif
+
+  /* #### there's more to free here */
+
+  free (s);
+}
+
+
+static const char *fontglide_defaults [] = {
   ".background:                #000000",
   ".foreground:                #DDDDDD",
   ".borderColor:       #555555",
   "*delay:             10000",
   "*program:           xscreensaver-text",
+  "*usePty:             false",
   "*mode:               random",
   ".font:               (default)",
+
+  /* I'm not entirely clear on whether the charset of an XLFD has any
+     meaning when Xft is being used. */
   "*fontCharset:        iso8859-1",
+/*"*fontCharset:        iso10646-1", */
+/*"*fontCharset:        *-*",*/
+
   "*fontBorderWidth:    2",
   "*speed:              1.0",
   "*linger:             1.0",
   "*trails:             False",
+# ifdef DEBUG
   "*debug:              False",
+  "*debugMetrics:       False",
+# endif /* DEBUG */
   "*doubleBuffer:      True",
-#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
+# ifdef HAVE_DOUBLE_BUFFER_EXTENSION
   "*useDBE:            True",
   "*useDBEClear:       True",
-#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
+# endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
   0
 };
 
-XrmOptionDescRec options [] = {
+static XrmOptionDescRec fontglide_options [] = {
+  { "-mode",           ".mode",            XrmoptionSepArg, 0 },
   { "-scroll",         ".mode",            XrmoptionNoArg, "scroll" },
   { "-page",           ".mode",            XrmoptionNoArg, "page"   },
   { "-random",         ".mode",            XrmoptionNoArg, "random" },
@@ -1567,22 +2478,12 @@ XrmOptionDescRec options [] = {
   { "-no-trails",      ".trails",          XrmoptionNoArg,  "False" },
   { "-db",             ".doubleBuffer",    XrmoptionNoArg,  "True"  },
   { "-no-db",          ".doubleBuffer",    XrmoptionNoArg,  "False" },
+# ifdef DEBUG
   { "-debug",          ".debug",           XrmoptionNoArg,  "True"  },
+  { "-debug-metrics",  ".debugMetrics",    XrmoptionNoArg,  "True"  },
+# endif /* DEBUG */
   { 0, 0, 0, 0 }
 };
 
 
-void
-screenhack (Display *dpy, Window window)
-{
-  state *s = init_fontglide (dpy, window);
-  int delay = get_integer_resource ("delay", "Integer");
-
-  while (1)
-    {
-      handle_events (s);
-      draw_fontglide (s);
-      XSync (dpy, False);
-      if (delay) usleep (delay);
-    }
-}
+XSCREENSAVER_MODULE ("FontGlide", fontglide)