yorick banner

Home

Manual

Packages

Global Index

Keywords

Quick Reference


/*
 * yeti_gist_gui.i --
 *
 *	Rudimentary Graphic User Interface build on top of Gist.
 *
 *-----------------------------------------------------------------------------
 *
 *	Copyright (C) 2001-2002 Eric THIEBAUT.
 *
 *	This file is part of Yeti.
 *
 *	Yeti is  free software;  you can redistribute  it and/or  modify it
 *	under the terms  of the GNU General Public  License as published by
 *	the Free Software  Foundation; either version 2 of  the License, or
 *	(at your option) any later version.
 *
 *	Yeti is distributed in the hope that it will be useful, but WITHOUT
 *	ANY WARRANTY; without even  the implied warranty of MERCHANTABILITY
 *	or FITNESS  FOR A PARTICULAR  PURPOSE.  See the GNU  General Public
 *	License for more details.
 *
 *	You should have  received a copy of the  GNU General Public License
 *	along with  Yeti (file "COPYING"  in the top source  directory); if
 *	not, write to the Free  Software Foundation, Inc., 59 Temple Place,
 *	Suite 330,  Boston, MA  02111-1307 USA.
 *
 *-----------------------------------------------------------------------------
 *
 * History:
 *	$Id$
 *	$Log$
 */



/* WISH LIST
   [ ] fix computation of cell size(s)
   [ ] use weights for grid cells
   [ ] message box widget
   [ ] list box widget
   [ ] menu widget
   [ ] entry widget (NOT POSSIBLE with basic Gist)
   [ ] config method
   [ ] add fontsize settings to gg_window
   [ ] add gg_get_fontsize routine with DPI keyword (i.e. fontsize is in point)
*/

func gg_dialog (message, label, .., win=, color=, font=)
{
  bw = 70; // button width
  bh = 25; // button height
  pad = 2; // padding space
  bd = 2;  // border width

  fontsize = 20;
  font = gg_get_font(font, "font", "timesBI");
  color = gg_get_color(color, "color", GG_BLUE);
  newlines = where(*pointer(message+"\n\n") == '\n');
  hmsg = max(100, fontsize*numberof(newlines));
  wmsg = max(200, fontsize*max(newlines(dif))*2/3);
  
  while (more_args()) grow, label, next_arg();
  ncols = numberof(label);
  if (ncols < 1 || structof(label)!=string) error, "bad button label(s)";
  Top = gg_toplevel(bd=bd, relief="groove", padx=pad, pady=pad);
  Message = gg_button(text=message, width=wmsg, height=hmsg,
                      bd=1, relief="sunken", 
                      font=font, fontsize=fontsize, justify="CH",
                      bg="white", fg=color);
  gg_manage, Top, 1, 1, Message, padx=pad, pady=pad,
    colspan=ncols+1, anchor="ew";
  Button = gg_button(text=string(0), width=bw, height=bh, bd=bd);
  for (i=1 ; i<=ncols ; ++i) {
    s = label(i);
    gg_manage, Top, i,2, h_set(h_copy(Button), text=s, ident=s),
      padx=3*pad, pady=pad;
  }
  
  __gg_finalize_pass1, Top;
  width = Top._w;
  height = Top._h;

  if (is_void(win)) win = 7;
  winkill, win;
  window, win, wait=1, width=width, height=height;
    h_set, Top, window=win;
  geom = window_geometry(Top.win);
  one_pixel = geom(2);
  xbias = geom(3);
  ybias = geom(4);
  width = long(geom(5));
  height = long(geom(6));
  x0 = xbias + one_pixel*max((width - Top._w)/2, 0);
  y0 = ybias - one_pixel*(height - 1);
  x1 = x0 + one_pixel*(Top._w - 1);
  y1 = y0 + one_pixel*(Top._h - 1);
  h_set, Top, one_pixel=one_pixel, x0=x0, y0=y0, x1=x1, y1=y1;
  __gg_finalize_pass2, Top;
  gg_paint, Top;
  
  for (;;) {
    while (is_void((ms = mouse(-1, 0, ""))))
      ;

    /* Check that button was clicked and released over same widget. */
    clicked = gg_find_clicked(Top, ms);
    if (is_hash(clicked)) {
      ident = h_get(clicked, ident=);
      if (structof(ident) == string) return ident;
    }
  }
}

func gg_pli_tool (z, y, x, win=, width=, height=, dpi=)
{
  /* Get current window geometry. */
  win = current_window();
  if (win < 0) error, "you must create window first";
  geom = window_geometry(win);
  dpi = long(geom(1));
  one_pixel = geom(2);
  xbias = geom(3);
  ybias = geom(4);
  wtop = long(geom(5));
  htop = long(geom(6));

  fma;
  
  /* Parse Z, Y and X. */
  dims = dimsof(z);
  nx = dims(2);
  ny = dims(3);
  if (is_void(x)) {
    x0 = 0.5; x1 = nx+0.5;
  } else {
    x = double(x);
    if (numberof(x) == 2) {
      s = (x(2) - x(1)) / nx;
      x0 = x(1) - 0.5*s;
      x1 = x(2) - 0.5*s;
    } else if (numberof(x) == 1) {
      x0 = x(1) - 0.5;
      x1 = x(1) + nx + 0.5;
    } error, "bad dimension for X";
  }
  if (is_void(y)) {
    y0 = 0.5; y1 = ny+0.5;
  } else {
    y = double(y);
    if (numberof(y) == 2) {
      s = (y(2) - y(1)) / ny;
      y0 = y(1) - 0.5*s;
      y1 = y(2) - 0.5*s;
    } else if (numberof(y) == 1) {
      y0 = y(1) - 0.5;
      y1 = y(1) + ny + 0.5;
    } error, "bad dimension for Y";
  }
  dx = abs(x1 - x0);
  dy = abs(y1 - y0);
  

  bw = 70; // button width
  bh = 25; // button height
  pad = 2; // padding space
  bd = 2;  // border width

  // FIXME: not needed ig gg_get_geometry method
  wgui = 4*(pad + bd) + 7*bw;
  hgui = 8*(pad + bd) + 9*bh;

#if 0
  /* Make viewport. */
  xmargin = bw;
  ymargin = 2*bh;
  height = htop - hgui - 2*ymargin;
  width = wtop - 2*xmargin;
  minsize = 3*dpi;
  if (width < minsize || height < minsize) {
    a = min(double(minsize)/dx, double(minsize)/dy);
    wtop = max(wgui, long(2*xmargin + a*dx + 0.5));
    htop = hgui + long(2*ymargin + a*dy + 0.5);
    winkill, win;
    window, win, dpi=dpi, width=wtop, height=htop, wait=1;
  } else {
    a = min(double(width)/dx, double(height)/dy);
  } 
  vp = array(long, 4);
  wplt = long(a*dx + 0.5);
  hplt = long(a*dy + 0.5);
  xmargin = (wtop - wplt)/2;
  ymargin = (htop - hgui - hplt)/2;
  gg_viewport, xmargin, xmargin+wplt-1, htop-ymargin, htop-ymargin-hplt+1,
    units=2 /* pixel */;
#endif
  

  /* Create widgets. */
  w_top = gg_toplevel(bd=bd, relief="groove", padx=pad, pady=pad);
  w_zoom = gg_button(text="Zoom", width=bw, height=bh, bd=bd);
  w_unzoom = gg_button(text="Unzoom", width=bw, height=bh, bd=bd);
  w_redraw = gg_button(text="Redraw", width=bw, height=bh, bd=bd);
  w_ok = gg_button(text="Ok", width=bw, height=bh, bd=bd);
  w_cancel = gg_button(text="Cancel", width=bw, height=bh, bd=bd);
  w_quit = gg_button(text="Quit", width=bw, height=bh, bd=bd);
  w_message = gg_button(text="", width=7*bw, height=5*bh, bd=1,
                        font="helveticaB", fontsize=14, justify="CT",
                        bg="white", fg="blue", relief="sunken");
  h_set, w_message, text="Try this...";

  /* Manage widgets in a grid (from top to bottom). */
  gg_manage, w_top, 1, 1, w_zoom,    padx=pad, pady=pad;
  gg_manage, w_top, 2, 1, w_unzoom,  padx=pad, pady=pad;
  gg_manage, w_top, 5, 1, w_redraw,  padx=pad, pady=pad;
  gg_manage, w_top, 1, 2, w_message, padx=pad, pady=pad, colspan=5;
  gg_manage, w_top, 1, 3, w_ok,      padx=pad, pady=pad;
  gg_manage, w_top, 5, 3, w_cancel,  padx=pad, pady=pad;

  pli, z, x0, y0, x1, y1;
  gg_paint, w_top;

  do_zoom = 0;
  for (;;) {
    while (is_void((ms = mouse(-1, 0, ""))))
      ;

    /* Check that button was clicked and released over same widget. */
    clicked = gg_find_clicked(w_top, ms);
    if (is_void(clicked)) {
      if (do_zoom) gg_zoom, ms;
    } else {
      if (clicked == w_redraw) {
        lim = limits;
        fma;
        pli, z, x0, y0, x1, y1;
        //limits, lim;
        gg_paint, w_top;
      } else if (clicked == w_zoom) {
        do_zoom = ! do_zoom;
        if (do_zoom) h_set, w_zoom, relief=GG_SUNKEN, fg=GG_RED;
        else         h_set, w_zoom, relief=GG_RAISED, fg=GG_DEFAULT_FG;
        gg_paint, w_zoom;
      } else if (clicked == w_unzoom) {
        fma;
        pli, z, x0, y0, x1, y1;
        limits;
        gg_paint, w_top;
      } else if (clicked == w_cancel) {
        return "cancel";
      } else if (clicked == w_ok) {
        return "ok";
      }
    }
  }
}

func gg_test (dummy, dpi=, width=, height=, win=)
{
  if (is_void(win)) win = current_window();
  if (win < 0) error, "you must create window first";
  //window, win, dpi=dpi, width=width, height=height, wait=1, style="nobox.gs";
  //win = current_window();
  geom = window_geometry(win);

  width = 80;
  height = 30;
  padx = 10;
  pady = 5;

  top = gg_toplevel();

  gg_manage, top, 1, 1, gg_button(text="Button 11", width=width, height=height,
                                  bd=2);
  //gg_paint, g;
  //rdline, prompt="hit [return]";
  gg_manage, top, 1, 2, gg_button(text="Button 12", width=width, height=height,
                                  fg="red", bd=3);
  //gg_paint, g;
  //rdline, prompt="hit [return]";
  gg_manage, top, 2, 2, gg_label(text="Label 22", width=width, height=height,
                                 fg="red", bd=5, relief="flat");
  gg_manage, top, 2, 1, gg_label(text="Label 21", width=width, height=height,
                                 fg="red", bd=3, relief="sunken", bg="white");
  gg_manage, top, 3, 1, gg_label(text="Label 31", width=width, height=height,
                                 fg="red", bd=3, relief="solid", bg="white");
  gg_manage, top, 3, 2, colspan=2,
    gg_label(text="Label 32", width=width, height=height,
             fg="red", bd=3, relief="solid", bg="white");
  gg_manage, top, 4, 1, colspan=1,
    gg_label(text="Label 41", width=width, height=height,
             fg="red", bd=4, relief="groove", bg="cyan");
  gg_manage, top, 5, 1, rowspan=2,
    gg_label(text="Label 51", width=width, height=height,
             fg="blue", bd=4, relief="ridge");
  gg_manage, top, 1, 3, colspan=5, rowspan=1,
    gg_label(text="Message label\nbox with some\nline of text.",
             width=width*5, height=height*3, fontsize=24, font="timesBI",
             fg="blue", bd=4, relief="ridge");

  gg_paint, top;

  return top;
}

/*---------------------------------------------------------------------------*/
/* GENERAL */

/* WIDGET INTERNALS

   (X0,Y0,X1,Y1) = widget bounding box in NDC units (*)
   ONE_PIXEL     = pixel size in NDC units (*)
   _X, _Y        = widget location (top-left) in pixels relative to the
                   top-left corner of its parent (*)
   _W, _H        = actual widget size in pixels (*)
   COL, ROW      = widget position in grid units
   COLSPAN, ROWSPAN = widget size in grid units
   WIDTH, HEIGHT = requested (minimum) widget size in pixels
   PADX, PADY = margins around widget in pixels

   FONTSIZE is in pixel (it is converted back into points when text needs
   to be plotted)

   X0 = PARENT_X0 + ONE_PIXEL*X
   X1 = X0 + ONE_PIXEL*(WIDTH - 1)
   Y1 = PARENT_Y1 + ONE_PIXEL*Y
   Y0 = Y1 - ONE_PIXEL*(HEIGHT - 1)

   COORDINATES

     Gist plotting system 0 only knows about normalized device coordinates
     (NDC) that run from bottom-left to top-right, i.e. unlike window
     systems that use pixel coordinates that run from top-left to
     bottom-right.

   (*) value(s) automatically computed by layout manager */
func gg_paint (widget)
{
  old_sys = plsys(0);
  op = widget.paint;
  op, widget;
  if (old_sys) plsys, old_sys;
}

func gg_no_op (..) {}


local GG_ONE_POINT ;
local GG_ONE_INCH ;
GG_ONE_INCH = 72.27*(GG_ONE_POINT = 0.0013000);
func gg_one_pixel(dpi) { return GG_ONE_INCH/dpi; }
/* DOCUMENT GG_ONE_POINT      -- one point in NDC units;
       -or- GG_ONE_INCH       -- one inch in NDC units;
       -or- gg_one_pixel(dpi) -- get pixel size in NDC units given the
                                 resolution in pixels per inch. */

/*---------------------------------------------------------------------------*/
/* FRAME WIDGET (layout manager). */

func gg_frame (nil, width=, height=, padx=, pady=,
              ident=, bd=, relief=, bg=, fg=, hi=, lo=)
{
  if (! is_void(nil)) error, "unexpected argument";

  /* Don't do h_new(key=gg_get_integer(key),... to make sure to use
     private copy of values. */
  bd = gg_get_integer(bd, "bd", GG_DEFAULT_BD, 0);
  padx = gg_get_integer(pady, "padx", GG_DEFAULT_PAD, 0);
  pady = gg_get_integer(padx, "pady", GG_DEFAULT_PAD, 0);
  mn = min(1, 2*(bd + padx)); /* minimum width */
  width  = gg_get_integer(width,  "width", mn, mn);
  mn = min(1, 2*(bd + pady)); /* minimum height */
  height = gg_get_integer(height, "height", mn, mn);
  bg = gg_get_color(bg, "bg", GG_DEFAULT_BG);
  fg = gg_get_color(fg, "fg", GG_DEFAULT_FG);
  hi = gg_get_color(hi, "hi", GG_DEFAULT_HI);
  lo = gg_get_color(lo, "lo", GG_DEFAULT_LO);
  relief = gg_get_relief(relief, "relief", GG_DEFAULT_FRAME_RELIEF);
  return h_new(class="Frame", paint=__gg_frame_paint, ident=ident,
               width=width, height=height, padx=padx, pady=pady,
               bd=bd, relief=relief, fontsize=fontsize, font=font,
               fg=fg, bg=bg, hi=hi, lo=lo);
}

func gg_toplevel (nil, x=, y=,
                 width=, height=, padx=, pady=,
                 ident=, bd=, relief=, bg=, fg=, hi=, lo=)
{
  /* Coordinates of upper left corner. */
  x = gg_get_integer(x, "x", 0);
  y = gg_get_integer(y, "y", 0);

  /* A toplevel widget is just a frame that knows its location. */
  top = gg_frame(nil, width=width, height=height, padx=padx, pady=pady,
                 ident=ident, bd=bd, relief=relief,
                 bg=bg, fg=fg, hi=hi, lo=lo);
  return h_set(top, class="Toplevel", x=x, y=y, paint=__gg_resize_and_paint);
}

func __gg_frame_paint (this)
{
  gg_draw_3d_rect, this.x0, this.y0, this.x1, this.y1, this.one_pixel,
    this.bd, this.relief, this.bg, this.fg, this.hi, this.lo;
  for (child=this.child ; is_hash(child) ; child=child.next) {
    op = child.paint;
    if (is_func(op)) op, child;
  }
}

func __gg_toplevel_paint (this)
{
  //old_window = current_window();
  window, this.window;
  gg_draw_3d_rect, x0, y0, x1, y1, one_pixel, this.bd, this.relief,
    this.bg, this.fg, this.hi, this.lo;
  gg_draw_3d_rect, this.x0, this.y0, this.x1, this.y1, this.one_pixel,
    this.bd, this.relief, this.bg, this.fg, this.hi, this.lo;
  for (child=this.child ; is_hash(child) ; child=child.next) {
    op = child.paint;
    if (is_func(op)) op, child;
  }
  //if (old_window >= 0) window, old_window;
}

func __gg_resize_and_paint (top)
{
  gg_finalize, top;
  __gg_frame_paint, top;
}

func gg_manage (container, col, row, widget,
               colspan=, rowspan=, padx=, pady=, anchor=, insert=)
/* DOCUMENT gg_manage, container, col, row, widget;
     Make WIDGET a children of widget CONTAINER that will be displayed into
     cell (COL,ROW) of CONTAINER grid.

     Keyword ANCHOR can be used to indicates how WIDGET should be anchored
     in its cell if there is remaining space (see gg_get_anchor).  The
     default anchoring is centered.

     Keywords COLSPAN and ROWSPAN indicates how many colmuns/rows the widget
     will occupy.  The default is COLSPAN=1 and ROWSPAN=1.

     Keywords PADX and PADY indicates how many pixels shoulkd be left around
     the widget in its cell.

     If keyword INSERT is true, WIDGET will be the first children of the
     list and therefore displayed below all other children; otherwise,
     WIDGET will be the topmost one (if children of CONTAINER do not
     overlap, this dictinction is irrelevant).

   SEE ALSO: gg_finalize, gg_forget. */
{
  /* Check container and new children widget. */
  class = h_get(container, class=);
  if (class != "Toplevel" && class != "Frame")
    error, "CONTAINER must be a frame or a toplevel widget";
  other = h_get(container, child=);
  if (! is_void(h_get(widget, next=)))
    error, "widget already managed (call gg_forget before)";

  /* Store grid manager information into widget record. */
  col = gg_get_integer(col, "col", , 1);
  row = gg_get_integer(row, "row", , 1);
  colspan = gg_get_integer(colspan, "colspan", 1, 1);
  rowspan = gg_get_integer(rowspan, "rowspan", 1, 1);
  padx = gg_get_integer(padx, "padx", GG_DEFAULT_PAD, 0);
  pady = gg_get_integer(pady, "pady", GG_DEFAULT_PAD, 0);
  anchor = gg_get_anchor(anchor, "anchor", 0);
  h_set, widget, col=col, row=row, colspan=colspan, rowspan=rowspan,
    padx=padx, pady=pady, anchor=anchor;

  if (is_void(other)) {
    h_set, container, child=widget;
  } else if (insert) {
    /* Insert new children. */
    h_set, widget, next=other;
    h_set, container, child=widget;
  } else {
    /* Append new children. */
    for (;;) {
      next = h_get(other, next=);
      if (is_void(next)) break;
      other = next;
    }
    h_set, other, next=widget;
  }
  h_set, container, paint=__gg_resize_and_paint;
}

func gg_forget (container, widget)
/* DOCUMENT gg_forget, container, widget;
     Remove WIDGET from the descendant list of CONTAINER, WIDGET will
     no longer be displayed for subsquent repaints of CONTAINER.  If
     WIDGET is not a descendant of CONTAINER, nothing is done.

   SEE ALSO: gg_manage. */
{
  other = h_get(container, child=);
  if (other == widget) {
    h_set, container, child=h_pop(widget, next=), paint=__gg_resize_and_paint;
    return;
  }
  for (;;) {
    next = h_get(other, next=);
    if (widget == next) {
      h_set, other, next=h_pop(widget, next=), paint=__gg_resize_and_paint;
      return;
    }
    other = next;
  }
}

func gg_finalize (top, win=, justify=)
{
  /* Check top and get window number. */
  if (h_get(top, class=) != "Toplevel")
    error, "TOP must be a toplevel widget";
  win = gg_get_integer(win, "win", current_window());
  if (win < 0) error, "you must create a window first or use the WIN keyword";

  __gg_finalize_pass1, top;

  /* Make the toplevel widget horizontally centered and vertically anchored
     at bottom. */
  h_set, top, window=win;
  geom = window_geometry(top.win);
  one_pixel = geom(2);
  xbias = geom(3);
  ybias = geom(4);
  width = long(geom(5));
  height = long(geom(6));
  x0 = xbias + one_pixel*max((width - top._w)/2, 0);
  y0 = ybias - one_pixel*(height - 1);
  x1 = x0 + one_pixel*(top._w - 1);
  y1 = y0 + one_pixel*(top._h - 1);
  h_set, top, one_pixel=one_pixel, x0=x0, y0=y0, x1=x1, y1=y1;
  __gg_finalize_pass2, top;
}

func __gg_finalize_pass2 (container)
/* DOCUMENT __gg_finalize_pass2, container;
     Second pass of geometry manager: convert pixel bounding box relative
     to container into absolute NDC.

   SEE ALSO: gg_finalize, __gg_finalize_pass1. */
{
  one_pixel = container.one_pixel;
  xbias = container.x0;
  ybias = container.y1;
  for (child=container.child ; is_hash(child) ; child=child.next) {
    x = child._x;
    y = child._y;
    h_set, child, one_pixel=one_pixel,
      x0=xbias + one_pixel*x,
      y0=ybias - one_pixel*(y + child._h - 1),
      x1=xbias + one_pixel*(x + child._w - 1),
      y1=ybias - one_pixel*y;
    if (child.class == "Frame") __gg_finalize_pass2, child;
  }
  h_set, container, paint=__gg_frame_paint;
}

/* ya = yoff - one_pixel*(_y)
   yb = yoff - one_pixel*(_y + _h - 1)
*/
func __gg_finalize_pass1 (container)
/* DOCUMENT __gg_finalize_pass1, container;
     First pass of geometry manager: compute pixel bounding box relative
     to parent for every children of container.

   SEE ALSO: gg_finalize, __gg_finalize_pass2. */
{
  /* Count number of children and compute sizes of children managed by
     sub-frames. */
  number = 0;
  for (child=container.child ; is_hash(child) ; child=child.next) {
    ++number;
  }
  bd = container.bd;
  if (number == 0) {
    minwidth  = 1 + 2*(bd + container.padx);
    minheight = 1 + 2*(bd + container.pady);  
  } else {
    /* Collect all children parameters needed to compute sizes. */
    col = row = colspan = rowspan = width = height = array(long, number);
    for (i=1, child=container.child ; is_hash(child) ; child=child.next, ++i) {
      col(i) = child.col;
      row(i) = child.row;
      colspan(i) = child.colspan;
      rowspan(i) = child.rowspan;
      if (child.class == "Frame") {
        __gg_finalize_pass1, child;
        width(i) = child._w + 2*child.padx;
        height(i) = child._h + 2*child.pady;
      } else {
        width(i) = child.width + 2*child.padx;
        height(i) = child.height + 2*child.pady;
      }
    }

    /* Compute columns and rows limits in pixels. */
    local xmin, xmax, ymin, xmax;
    minwidth  = __gg_cell_limits(xmin, xmax, col, colspan, width,
                                 bd + container.padx);
    minheight = __gg_cell_limits(ymin, ymax, row, rowspan, height,
                                 bd + container.pady);

    /* Propagate computed sizes for every (direct) children according to
       anchoring. */
    for (child=container.child ; is_hash(child) ; child=child.next) {
      anchor = child.anchor;

      /* Horizontal anchoring. */
      a = (anchor & 3);
      sz = child.width + 2*(padx = child.padx);
      col = child.col;
      mn = xmin(col);
      mx = xmax(col + child.colspan - 1);
      if (a == 0)      /* center */ { x=(mn+mx+1-sz)/2; w=sz;      }
      else if (a == 1) /*   west */ { x=mn;             w=sz;      }
      else if (a == 2) /*   east */ { x=mx-sz+1;        w=sz;      }
      else             /*   fill */ { x=mn;             w=mx-mn+1; }

      /* Vertical anchoring. */
      a = (anchor & 12);
      sz = child.height + 2*(pady = child.pady);
      row = child.row;
      mn = ymin(row);
      mx = ymax(row + child.rowspan - 1);
      if (a == 0)      /* center */ { y=(mn+mx+1-sz)/2; h=sz;      }
      else if (a == 4) /*  north */ { y=mn;             h=sz;      }
      else if (a == 8) /*  south */ { y=mx-sz+1;        h=sz;      }
      else             /*   fill */ { y=mn;             h=mx-mn+1; }

      /* Store pixel bounding box. */
      if (padx) {x += padx; w -= 2*padx; }
      if (pady) {y += pady; h -= 2*pady; }
      h_set, child, _x=x, _y=y, _w=w, _h=h;
    }
  }

  /* Update size of CONTAINER (not its location which is computed by its
     parent). */
  h_set, container, _w=max(container.width,  minwidth),
                    _h=max(container.height, minheight);
}

func __gg_cell_limits (&cmin, &cmax, first, span, size, bd)
/* DOCUMENT __gg_cell_limits, cmin, cmax, first, span, size, bd;
     Compute cell relative limits.

   SEE ALSO: __gg_finalize_pass1. */
{
  last = first + span;
  n = max(last);
  offset = array(bd, n);
  for (k=2; k<=n ; ++k) {
    if (is_array((i = where(last == k))))
      offset(k) = max(size(i) + offset(first(i)));
  }
  for (k=n-1 ; k>=1 ; --k) {
    if (is_array((i = where(first == k))))
      offset(k) = min(offset(last(i)) - size(i));
  }

  cmin = offset(:-1);
  cmax = offset(2:) - 1;
  return offset(0) - offset(1) + 2*bd; /* return total size */
}

/*---------------------------------------------------------------------------*/
/* LABEL WIDGET */

func gg_label (width=, height=, ident=, text=, font=, fontsize=, justify=,
              bd=, relief=, fg=, bg=, hi=, lo=)
{
  /* Don't do h_new(key=gg_get_integer(key),... to make sure to use
     private copy of values. */
  text = gg_get_string(text, "text", "");
  width = gg_get_integer(width, "width", 50, 1);
  height = gg_get_integer(height, "height", 25, 1);
  bd = gg_get_integer(bd, "bd", GG_DEFAULT_BD);
  fontsize = gg_get_integer(fontsize, "fontsize", 12, 4);
  font = gg_get_font(font, "font", GG_DEFAULT_FONT);
  justify = gg_get_justify(justify);
  fg = gg_get_color(fg, "fg", GG_DEFAULT_FG);
  bg = gg_get_color(bg, "bg", GG_DEFAULT_BG);
  hi = gg_get_color(hi, "hi", GG_DEFAULT_HI);
  lo = gg_get_color(lo, "lo", GG_DEFAULT_LO);
  relief = gg_get_relief(relief, "relief", GG_DEFAULT_LABEL_RELIEF);
  bd = min(max(bd, 0), min(width, height)/2); // fix border width
  return h_new(class="Label", paint=__gg_label_paint,
               text=text, ident=ident, width=width, height=height,
               bd=bd, relief=relief, fontsize=fontsize, font=font,
               justify=justify,
               fg=fg, bg=bg, hi=hi, lo=lo);
}

func __gg_label_paint (lab)
{
  one_pixel = lab.one_pixel;
  x0 = lab.x0;
  y0 = lab.y0;
  x1 = lab.x1;
  y1 = lab.y1;
  bd = lab.bd;
  gg_draw_3d_rect, x0, y0, x1, y1, one_pixel, bd, lab.relief,
    lab.bg, lab.fg, lab.hi, lab.lo;
  op = lab.justify;
  if (is_func(op)) op, lab.text, x0, y0, x1, y1, bd, lab.font,
                     (one_pixel/GG_ONE_POINT)*lab.fontsize, lab.fg;
}

/*---------------------------------------------------------------------------*/
/* BUTTON WIDGET */

func gg_button (width=, height=, ident=, text=, font=, fontsize=, justify=,
               bd=, relief=, fg=, bg=, hi=, lo=, callback=)
{
  /* A Button widget inherits most of its capabilities from a Label widget. */
  if (is_void(relief)) relief = GG_DEFAULT_BUTTON_RELIEF;
  btn = gg_label(width=width, height=height, ident=ident, text=text,
                 font=font, fontsize=fontsize, justify=justify,
                 bd=bd, relief=relief, fg=fg, bg=bg, hi=hi, lo=lo);
  return h_set(btn, class="Button", callback=callback);
}

func gg_on_click (wdg, xndc, yndc, btn, mods)
{
  local x0, y0, y1, y1;
  nil = string(0);
  n = 0;
  oldSys = plsys(0);
  for (;;) {
    m = mouse(0, 0, nil);
    if (is_void(m)) break;
    if (n==0) {
      x0 = m(1);
      y0 = m(2);
      n = 1;
    } else if (n==1) {
      x1 = m(1);
      y1 = m(2);
      plg, [y0,y1], [x0,x1], color=GG_XOR, width=0, marks=0 /* type=*/;
      id = numberof(plq());
      p = *(plq(id)(5));
      if (numberof(p) != 3 && p(1) != 2)
        error, "peeked wrong graphic element";
      address = (vert ? p(2) : p(3));
      n = 2;
    } else {
      x = m(1);
      y = m(2);
      plg, [y0,y1], [x0,x1], color=GG_XOR, width=0, marks=0 /* type=*/;
      if (abs(x1-x, y1-y) < abs(x0-x, y0-y)) {
        x1=x; y1=y;
      } else {
        x0=x; y0=y;
      }
      plg, [y0,y1], [x0,x1], color=GG_XOR, width=0, marks=0 /* type=*/;
    }
  }
  plsys, oldSys;
}

/*---------------------------------------------------------------------------*/
/* TOPLEVEL WINDOW */

func gg_viewport (xmin, xmax, ymin, ymax, units=, xopt=, yopt=,
                 fg=, font=, fontsize=, landscape=)
/* DOCUMENT gg_viewport, left, right, bottom, top;
       -or- gg_viewport, vp;
     
   SEE ALSO: window, viewport, set_style. */
{
  require, "style.i";

  /* Get settings for current window. */
  win = current_window();
  if (win < 0) error, "no current window";
  geom = window_geometry(win);
  dpi = long(geom(1));
  one_pixel = geom(2);
  xbias = geom(3);
  ybias = geom(4);
  wtop = long(geom(5));
  htop = long(geom(6));
  
  /* Get coordinates of viewport(s). */
  case = is_void(xmin) + 2*is_void(xmax) + 4*is_void(ymin) + 8*is_void(ymax);
  nvps = 0;
  case;
  if (case == 15) {
    /* Get current viewport. */
    vp = viewport();
    if (max(vp) == min(vp)) {
      xmin = 0.15;
      xmax = 0.85;
      ymin = 0.10;
      ymax = 0.80;
      units = 1;
    } else {
      xmin = vp(1,);
      xmax = vp(2,);
      ymin = vp(3,);
      ymax = vp(4,);
      units = 0;
    }
    nvps = numberof(xmin);
  } else if (case == 14) {
    /* Array of viewport(s) specified. */
    vp = xmin;
    if (is_array(vp) && (dims = dimsof(vp))(1) >= 1 && dims(2) == 4) {
      if ((s = structof(vp))==double || s==float ||
          s==long || s==int || s==short) {
        xmin = vp(1,);
        xmax = vp(2,);
        ymin = vp(3,);
        ymax = vp(4,);
        nvps = numberof(xmin);
      }
    }
  } else if (case == 0) {
    /* XMIN, XMAX, YMIN and YMAX given. */
    if (is_array(xmin) && ! dimsof(xmin)(1) &&
        ((s=structof(xmin))==double || s==float || s==long ||
         s==int || s==short) &&
        is_array(xmax) && ! dimsof(xmax)(1) &&
        ((s=structof(xmax))==double || s==float || s==long ||
         s==int || s==short) &&
        is_array(ymin) && ! dimsof(ymin)(1) &&
        ((s=structof(ymin))==double || s==float || s==long ||
         s==int || s==short) &&
        is_array(ymax) && ! dimsof(ymax)(1) &&
        ((s=structof(ymax))==double || s==float || s==long ||
         s==int || s==short)) {
      nvps = 1;
    }
  }
  if (nvps <= 0) error, "bad viewport specification";

  /* Convert viewport coordinates to NDC. */
  if (units) {
    /* Compute viewport size in pixels, then in NDC. */
    if (units == 1) {
      /* relative units: 0 is min and 1 is max */
      xscl = one_pixel*(wtop - 1.0);
      yscl = one_pixel*(htop - 1.0);
      ymin = 1.0 - ymin;
      ymax = 1.0 - ymax;
    } else if (units == 2) {
      /* pixel units: exchange YMIN and YMAX */
      xscl = yscl = one_pixel;
      //ytmp = ymax; ymax = ymin; ymin = ytmp;
    } else {
      error, "bad value for keyword UNITS (0=NDC, 1=relative, 2=pixels)";
    }
    xmin = xbias + xscl*xmin;
    xmax = xbias + xscl*xmax;
    ymin = ybias - yscl*ymin;
    ymax = ybias - yscl*ymax;
  }
  if (anyof(xmin >= xmax) || anyof(ymin >= ymax)) error, "bad viewport limits";
  
  /* Parse other parameters. */
  if (is_void(xmargin)) xmargin = 2.0;
  if (is_void(ymargin)) ymargin = 2.0;
  if (is_void(fontsize)) fontsize = 8;
  font  = gg_map(gg_get_font,       font,  GG_HELVETICA);
  color = gg_map(gg_get_color,      color, GG_FG);
  xopt  = gg_map(gg_get_axis_flags, xopt,  0x33);
  yopt  = gg_map(gg_get_axis_flags, yopt,  0x33);
  if (nvps>1) {
    /* Multiple viewports: fix per-viewport settings. */
    zero = array(long, dimsof(xmin));
    if (is_void(dimsof(xopt, zero))) error, "bad XOPT dimensions";
    xopt += zero;
    if (is_void(dimsof(yopt, zero))) error, "bad YOPT dimensions";
    yopt += zero;
    if (is_void(dimsof(fontsize, zero))) error, "bad FONTSIZE dimensions";
    fontsize += zero;
    if (is_void(dimsof(font, zero))) error, "bad FONT dimensions";
    font += zero;
    if (is_void(dimsof(color, zero))) error, "bad COLOR dimensions";
    color += zero;
  }
  if (numberof(xopt)     != nvps) error, "bad XOPT dimensions";
  if (numberof(yopt)     != nvps) error, "bad YOPT dimensions";
  if (numberof(fontsize) != nvps) error, "bad FONTSIZE dimensions";
  if (numberof(font)     != nvps) error, "bad FONT dimensions";
  if (numberof(color)    != nvps) error, "bad COLOR dimensions";

  /* Convert font size to points and margins to NDC. */
  fontsize = max(long((one_pixel/GG_ONE_POINT)*fontsize + 0.5), 4);
  xmargin  *= one_pixel;
  ymargin  *= one_pixel;

  /* Build up systems. */
  nHDigits = 5;
  nVDigits = 6;
  tickLen = one_pixel*[5.0, 3.0, 1.0, 0.0, 0.0];
  system = array(GfakeSystem, nvps);
  for (i=1 ; i<=nvps ; ++i) {
    //write, i,   xmin(i), xmax(i), ymin(i), ymax(i);
    fontHeight= fontsize(i);
    xFlags= xopt(i);
    yFlags= yopt(i);
    tickStyle=  GpLineAttribs(color=color(i), type=1, width=1);
    frameStyle= GpLineAttribs(color=color(i), type=1, width=1);
    gridStyle=  GpLineAttribs(color=color(i), type=3, width=1);
    textStyle=  GpTextAttribs(color=color(i), font=font(i), height=fontHeight,
                              alignH=0, alignV=0, opaque=0);
    xLabelOff= (xFlags&0x08 ? max(tickLen) : 0) + fontHeight/1.5;
    yLabelOff= (yFlags&0x08 ? max(tickLen) : 0) + 0.75*fontHeight;
    xTickOff= (xmargin ? xmargin + ((xFlags&0x08) ? max(tickLen) : 0.0) : 0.0);
    yTickOff= (ymargin ? ymargin + ((yFlags&0x08) ? max(tickLen) : 0.0) : 0.0);
    xTickLen= ((xFlags&0x018)==0x018 ? 2.0*tickLen : tickLen);
    yTickLen= ((yFlags&0x018)==0x018 ? 2.0*tickLen : tickLen);
    horiz= GaAxisStyle(nMajor=7.5, nMinor=50, logAdjMajor=1.2, logAdjMinor=1.2,
                       nDigits=nHDigits, gridLevel=1, flags=xFlags,
                       tickOff=xTickOff, labelOff=xLabelOff, tickLen=xTickLen,
                       tickStyle=tickStyle, gridStyle=gridStyle,
                       textStyle=textStyle, xOver=xmax(i),
                       yOver=ymin(i)-fontHeight);
    vert=  GaAxisStyle(nMajor=7.5, nMinor=50, logAdjMajor=1.2, logAdjMinor=1.2,
                       nDigits=nVDigits, gridLevel=1, flags=yFlags,
                       tickOff=yTickOff, labelOff=yLabelOff, tickLen=yTickLen,
                       tickStyle=tickStyle, gridStyle=gridStyle,
                       textStyle=textStyle, xOver=xmin(i),
                       yOver=ymax(i)+fontHeight);
    system(i)= GfakeSystem(viewport=[xmin(i), xmax(i), ymin(i), ymax(i)],
                           ticks=GaTickStyle(horiz=horiz, vert=vert, frame=1,
                                             frameStyle=frameStyle),
                           legend=swrite(format="System %d", i));
  }

  /* layout of the plot legends */
  legends = GeLegendBox();

  /* layout of the contour legends */
  clegends = GeLegendBox();

  /* create window and apply plot-style settings */
  if (is_void(landscape)) landscape=1n;
  set_style, landscape, system, legends, clegends;
  //fma;
}

func gg_window (win, width=, height=, dpi=, display=, private=, dump=, hcp=,
               landscape=, viewport=, units=, font=, size=, color=,
               keep=, xopt=, yopt=, xmargin=, ymargin=)
/* DOCUMENT gg_window, win;
       -or- gg_window;
       -or- gg_window(...);
     Create/switch to graphic window WIN.  This routine allows one to
     create a window of given size with viewport(s) different from the
     default one.  Otherwise its behaviour should mimics that of the
     "window" builtin routine.  If called as a function, the return
     value is an array of 6 double values:
       [WIN, ONE_PIXEL, XMIN, XMAX, YMIN, YMAX]
     where WIN is the window number, ONE_PIXEL is the pixel size in NDC
     units and [XMIN,XMAX,YMIN,YMAX] is the bounding box in NDC units.

   KEYWORDS
     DPI, DISPLAY, PRIVATE, DUMP, HCP - Same options as for the
                 window builtin routine.
     WIDTH, HEIGHT - Window width/height in pixels; default: 450 at 75dpi
                 and 600 at 100dpi.
     LANDSCAPE - Orientation.
     VIEWPORT  - Viewport coordinates: [XMIN, XMAX, YMIN, YMAX].  Several
                 viewports can be specified at the same time, in this
                 case, first dimension of VIEWPORT must be a 4 and
                 XMIN is VIEWPORT(1,..), XMAX is VIEWPORT(2,..) and so on.
     UNITS     - Units for the viewport keyword: 0 for NDC (default),
                 1 for relative, and 2 for pixels.
     FONT      - Font to use to label axes (see gg_get_font); default is
                 Helvetica.
     SIZE      - Text size for axis labels in points; default is 12.
     COLOR     - Axis color (see gg_get_color); default is foreground.
     KEEP      - Keep WIN if it already exists? Otherwise the window is
                 forced to be recreated (this is the default behaviour).
     XOPT      - Options for X-axis of viewport (see gg_get_axis_flags).
     YOPT      - Options for Y-axis of viewport (see gg_get_axis_flags).
     XMARGIN, YMARGIN -

     Note: If you specify multiple viewports, FONT, SIZE, COLOR, XOPT
           and YOPT can by different for each viewport.

   SEE ALSO gg_get_axis_flags, gg_get_color, gg_get_font, window. */
{
  require, "style.i";

  /* DPI can be set by pldefault but we have no way to know this value
     -- see graph.c */
  if (is_void(dpi)) dpi = 100;
  else              dpi = (dpi < 25 ? 25 : (dpi > 300 ? 300 : long(dpi)));

  /* Define some constants -- see gist.h */
  ONE_POINT= 0.0013000;       // one point in NDC units
  ONE_INCH=  72.27*ONE_POINT; // one inch in NDC units
  ONE_PIXEL= ONE_INCH/dpi;    // one pixel in NDC units

  /* Figure out window size in pixels (without top text line) -- called
     "topWidth" and "topHeight" in xfancy.c */
  if (is_void(width)) width = 6*dpi;
  else width = (width <= 31 ? 31 : long(width));
  if (is_void(height)) height = 6*dpi;
  else height = (height <= 31 ? 31 : long(height));

  /* Page size (8.5 x 11 inches) in NDC and pixels -- see xfancy.c */
  if (is_void(landscape)) landscape = (width > height);
  if (landscape) {
    pageWidthNDC = 1.033461;
    pageHeightNDC = 0.798584;
  } else {
    pageWidthNDC = 0.798584;
    pageHeightNDC = 1.033461;
  }
  pageWidth = long(pageWidthNDC/ONE_PIXEL);
  pageHeight = long(pageHeightNDC/ONE_PIXEL);
  if (width > pageWidth) width= pageWidth;
  if (height > pageHeight) height= pageHeight;

  /* Compute offsets (really tricky to figure out!) and bounding viewport
     -- see xfancy.c */
  xoff = (pageWidth - width)/2;
  if (landscape) {
    //yoff = (pageHeight - height)/2;
    yoff = (pageHeight - height + 3)/2;
  } else {
    //yoff = (pageHeight - height) - (pageWidth - height)/2;
    yoff = pageHeight - (pageWidth + height)/2;
  }
  if (xoff < 0) xoff = 0;
  if (yoff < 0) yoff = 0;
  vp0_offsets = ONE_PIXEL*[xoff, xoff, yoff, yoff];
  vp0_pixel = [width, width, height, height] - 1.0;
  vp0_ndc = vp0_offsets + ONE_PIXEL*vp0_pixel;

  /* Fix viewport coordinates and figure out how many viewports to make. */
  if (is_void(viewport)) {
    viewport= [0.15, 0.85, 0.1, 0.8];
    units= 1;
  }
  if (units) {
    /* Compute viewport size in pixels, then in NDC. */
    if (units==1) {
      /* relative units: 0 is min and 1 is max */
      viewport *= vp0_pixel;
    } else if (units!=2) {
      error, "bad value for keyword UNITS (0=NDC, 1=relative, 2=pixels)";
    }
    viewport = vp0_offsets + ONE_PIXEL*viewport;
  }
  if (! is_array(viewport) || (dims= dimsof(viewport))(1) < 1 || dims(2) != 4)
    error, "bad VIEWPORT";
  nvps= numberof(viewport)/4;

    /* Parse other parameters. */
  if (is_void(xmargin)) xmargin= 2.0;
  if (is_void(ymargin)) ymargin= 2.0;
  if (is_void(size)) size= 12;
  font  = gg_map(gg_get_font,       font,  GG_HELVETICA);
  color = gg_map(gg_get_color,      color, GG_FG);
  xopt  = gg_map(gg_get_axis_flags, xopt,  0x33);
  yopt  = gg_map(gg_get_axis_flags, yopt,  0x33);
  if (nvps>1) {
    /* Multiple viewports: fix per-viewport settings. */
    dims= dims(2:);
    dims(1)= numberof(dims)-1;
    zero= array(long, dims);
    if (is_void(dimsof(xopt, zero))) error, "bad XOPT dimensions";
    xopt+= zero;
    if (is_void(dimsof(yopt, zero))) error, "bad YOPT dimensions";
    yopt+= zero;
    if (is_void(dimsof(size, zero))) error, "bad SIZE dimensions";
    size+= zero;
    if (is_void(dimsof(font, zero))) error, "bad FONT dimensions";
    font+= zero;
    if (is_void(dimsof(color, zero))) error, "bad COLOR dimensions";
    color+= zero;
  }
  if (numberof(xopt)!=nvps) error, "bad XOPT dimensions";
  if (numberof(yopt)!=nvps) error, "bad YOPT dimensions";
  if (numberof(size)!=nvps) error, "bad SIZE dimensions";
  if (numberof(font)!=nvps) error, "bad FONT dimensions";
  if (numberof(color)!=nvps) error, "bad COLOR dimensions";

  /* Build up systems. */
  nHDigits= 5;
  nVDigits= 6;
  tickLen= ONE_PIXEL*[5.0, 3.0, 1.0, 0.0, 0.0];
  system= array(GfakeSystem, nvps);
  viewport= viewport(,*);  // concatenate extra dims
  for (i=1 ; i<=nvps ; ++i) {
    fontHeight= size(i)*ONE_POINT;
    xFlags= xopt(i);
    yFlags= yopt(i);
    tickStyle=  GpLineAttribs(color=color(i), type=1, width=1);
    frameStyle= GpLineAttribs(color=color(i), type=1, width=1);
    gridStyle=  GpLineAttribs(color=color(i), type=3, width=1);
    textStyle=  GpTextAttribs(color=color(i), font=font(i), height=fontHeight,
                              alignH=0, alignV=0, opaque=0);
    xLabelOff= (xFlags&0x08 ? max(tickLen) : 0) + fontHeight/1.5;
    yLabelOff= (yFlags&0x08 ? max(tickLen) : 0) + 0.75*fontHeight;
    xTickOff= (xmargin
               ? xmargin*ONE_PIXEL + ((xFlags&0x08) ? max(tickLen) : 0.0)
               : 0.0);
    yTickOff= (ymargin
               ? ymargin*ONE_PIXEL + ((yFlags&0x08) ? max(tickLen) : 0.0)
               : 0.0);
    xTickLen= ((xFlags&0x018)==0x018 ? 2.0*tickLen : tickLen);
    yTickLen= ((yFlags&0x018)==0x018 ? 2.0*tickLen : tickLen);
    horiz= GaAxisStyle(nMajor=7.5, nMinor=50, logAdjMajor=1.2, logAdjMinor=1.2,
                       nDigits=nHDigits, gridLevel=1, flags=xFlags,
                       tickOff=xTickOff, labelOff=xLabelOff, tickLen=xTickLen,
                       tickStyle=tickStyle, gridStyle=gridStyle,
                       textStyle=textStyle, xOver=viewport(2,i),
                       yOver=viewport(3,i)-fontHeight);
    vert=  GaAxisStyle(nMajor=7.5, nMinor=50, logAdjMajor=1.2, logAdjMinor=1.2,
                       nDigits=nVDigits, gridLevel=1, flags=yFlags,
                       tickOff=yTickOff, labelOff=yLabelOff, tickLen=yTickLen,
                       tickStyle=tickStyle, gridStyle=gridStyle,
                       textStyle=textStyle, xOver=viewport(1,i),
                       yOver=viewport(4,i)+fontHeight);
    system(i)= GfakeSystem(viewport=viewport(,i),
                           ticks=GaTickStyle(horiz=horiz, vert=vert, frame=1,
                                             frameStyle=frameStyle),
                           legend=swrite(format="System %d", i));
  }

  /* layout of the plot legends */
  legends= GeLegendBox();

  /* layout of the contour legends */
  clegends= GeLegendBox();

  /* create window and apply plot-style settings */
  if (is_void(win)) win= window();
  if (! keep) winkill, win;
  window, win, wait=1, width=width, height=height,
    display=display, private=private, dump=dump, hcp=hcp, dpi=dpi;
  set_style, landscape, system, legends, clegends;
  fma;
#if 0
  for (i=1;i<=nvps;++i) plbox, viewport(1,i), viewport(3,i), viewport(2,i),
                          viewport(4,i), color="red", system=0;
#endif
  if (! am_subroutine()) {
    result = array(double, 6);
    result(1) = win;
    result(2) = ONE_PIXEL;
    result(3:6) = vp0_ndc;
    return result;
  }
}

/*---------------------------------------------------------------------------*/
/* UTILITIES */

func gg_get_integer (value, name, defvalue, minvalue, maxvalue)
{
  if (is_array(value)) {
    if (dimsof(value)(1)==0 &&
        ((structof(value))==long || s==int || s==short || s==char) &&
        (is_void(minvalue) || value >= minvalue) &&
        (is_void(maxvalue) || value <= maxvalue)) return long(value);
  } else if (is_void(value) && ! is_void(defvalue)) return defvalue;
  if (is_void(name)) msg = "expecting scalar integer";
  else msg = swrite(format="\"%s\" must be a scalar integer", name);
  if (! is_void(minvalue)) msg += swrite(format=" >=%d", minvalue);
  if (! is_void(maxvalue)) msg += swrite(format=" <=%d", maxvalue);
  error, msg;
}

func gg_get_real (value, name, defvalue, minvalue, maxvalue)
{
  if (is_array(value)) {
    if (dimsof(value)(1)==0 &&
        ((structof(value))==double || s==float || s==long ||
         s==int || s==short || s==char) &&
        (is_void(minvalue) || value >= minvalue) &&
        (is_void(maxvalue) || value <= maxvalue)) return double(value);
  } else if (is_void(value) && ! is_void(defvalue)) return defvalue;
  if (is_void(name)) msg = "expecting scalar real";
  else msg = swrite(format="\"%s\" must be a scalar real", name);
  if (! is_void(minvalue)) msg += swrite(format=" >=%d", minvalue);
  if (! is_void(maxvalue)) msg += swrite(format=" <=%d", maxvalue);
  error, msg;
}

func gg_get_string (value, name, defvalue)
{
  if (structof(value)==string && dimsof(value)(1) == 0) return value;
  if (is_void(value) && ! is_void(defvalue)) return defvalue;
  if (is_void(name)) msg = "expecting scalar string";
  else msg = swrite(format="\"%s\" must be a scalar string", name);
  error, msg;
}

func gg_is_ndx (colors) { return (colors >= 0) & (colors < 256); }
func gg_is_rgb (colors) { return (colors < 0) | (colors >= 256); }
func gg_rgb_r (colors)  { return  colors         & 0xff; }
func gg_rgb_g (colors)  { return (colors >>   8) & 0xff; }
func gg_rgb_b (colors)  { return (colors >>  16) & 0xff; }
func gg_rgb (r,g,b)     { return r | (g<<8) | (b<<16) | 0x01000000;}
/* DOCUMENT gg_is_ndx(colors)  -- true where COLORS is indexed
       -or- gg_is_rgb(colors)  -- true where COLORS is RGB
       -or- gg_rgb(r,g,b)      -- convert to RGB color
       -or- gg_rgb_r(colors)   -- red intensity of RGB COLORS
       -or- gg_rgb_g(colors)   -- green intensity of RGB COLORS
       -or- gg_rgb_b(colors)   -- blue intensity of RGB COLORS
     These routines are useful to deal with color values (as integers).
     R, G, and B are the components of RGB colors (0=black, 255=maximum).

  SEE ALSO gg_get_color. */
local GG_BG, GG_FG, GG_BLACK, GG_WHITE, GG_RED, GG_GREEN, GG_BLUE;
local GG_CYAN, GG_MAGENTA, GG_YELLOW, GG_GUI_BG, GG_GUI_FG;
local GG_GUI_HI, GG_GUI_LO, GG_XOR, GG_EXTRA;
local __gg_color_table;
func gg_get_color (value, name, defvalue)
/* DOCUMENT gg_get_color(value, name, defvalue);
     Get color specification from VALUE as an integer value or RGB triplet.
     VALUE can have any value recognized by Yorick for the "color" keyword
     in builtin plotting functions. If VALUE is nil and DEFVALUE is
     specified, DEFVALUE is returned. Optional argument NAME can be used to
     set the name of the parameter for error message.

   SEE ALSO color, gg_get_font, gg_window. */
/* DOCUMENT gg_color, drw, color;
     Set the current color for subsequent drawing on DRW.  This must be
     called not only when the color changes, but also after any graphics
     call to any other window has been made there are two distinct types of
     colors: indexed (<=255) and RGB (bit 0x01000000 set, R in LSB, then G,
     then B).  Additionally, within the indexed colors, there are 16
     reserved colors; the gg_palette function sets the rgb values for the
     other 240 indexed colors (any colors not set by gg_palette are GG_FG).

     The following routines are useful:
        GG_IS_NDX(colors)  true where COLORS is indexed
        GG_IS_RGB(colors)  true where COLORS is RGB
        GG_RGB(r,g,b)      convert to RGB color
        GG_R(colors),
        GG_G(colors),
        GG_B(colors)       R, G, and B components of RGB colors (0 black,
                          255 maximum).

     The reserved indexed colors are:
        255 GG_BG          251 GG_RED         245 GG_GUI_BG
        254 GG_FG          250 GG_GREEN       244 GG_GUI_FG
        253 GG_BLACK       249 GG_BLUE        243 GG_GUI_HI
        252 GG_WHITE       248 GG_CYAN        242 GG_GUI_LO
                           247 GG_MAGENTA     241 GG_XOR
                           246 GG_YELLOW      240 GG_EXTRA
     (GG_BG and GG_FG are "background" and "foreground", which the user
      may be able to define differently on different screens or workspaces).

   SEE ALSO: gg_intro. */
{
  if (is_array(value)) {
    rank = dimsof(value)(1);
    if ((s = structof(value))==long || s==char || s==short || s==int) {
      if (rank==0) return int(value) + 256n*(value < 0);
      if (rank==1 && numberof(value) == 3) return int(value);
    }
    if (s==string && rank==0 && h_has(__gg_color_table, value)) {
      return h_get(__gg_color_table, value);
    }
  } else if (is_void(value) && ! is_void(defvalue)) return defvalue;
  error, (is_void(name) ? "bad color value"
                        : "bad color value for \""+name+"\"");
}
GG_BG      = 255n;
GG_FG      = 254n;
GG_BLACK   = 253n;
GG_WHITE   = 252n;
GG_RED     = 251n;
GG_GREEN   = 250n;
GG_BLUE    = 249n;
GG_CYAN    = 248n;
GG_MAGENTA = 247n;
GG_YELLOW  = 246n;
GG_GUI_BG  = 245n;
GG_GUI_FG  = 244n;
GG_GUI_HI  = 243n;
GG_GUI_LO  = 242n;
GG_XOR     = 241n;
GG_EXTRA   = 240n;
__gg_color_table = h_new(bg=GG_BG, fg=GG_FG, black=GG_BLACK, white=GG_WHITE,
                         red=GG_RED, green=GG_GREEN, blue=GG_BLUE,
                         cyan=GG_CYAN, magenta=GG_MAGENTA, yellow=GG_YELLOW,
                         gui_bg=GG_GUI_BG, gui_fg=GG_GUI_FG,
                         gui_hi=GG_GUI_HI, gui_lo=GG_GUI_LO,
                         xor=GG_XOR, extra=GG_EXTRA);


func gg_get_font (value, name, defvalue)
/* DOCUMENT gg_get_font(value);
     Parse font VALUE which can have any value recognized by Yorick
     for the "font" keyword in builtin plotting functions and return the
     corresponding integer value. If VALUE is nil, the value of keyword
     DEFVALUE is returned. Keyword NAME can be used to set the name of the
     parameter for error message.

   SEE ALSO font, gg_get_color, gg_window. */
{
  if (is_void(value)) return defvalue;
  if (is_array(value) && dimsof(value)(1)==0) {
    if ((s= structof(value))==long) return value;
    if (s==string) {
      n = strmatch(value, "B") | 2*strmatch(value, "I");
      fn = (n==3 ? strpart(value, 1:-2) : (n ? strpart(value, 1:-1) : value));
      if (fn=="courier")    return n;
      if (fn=="times")      return n+4;
      if (fn=="helvetica")  return n+8;
      if (fn=="symbol")     return n+12;
      if (fn=="schoolbook") return n+16;
      error, "bad font name \""+value+"\"";
    }
    if (s==char || s==short || s==int) return long(value);
  }
  error, (is_void(name) ? "bad font value"
                        : "bad font value for \""+name+"\"");
}

func gg_get_axis_flags (value, name, defvalue)
/* DOCUMENT gg_get_axis_flags(value, name, defvalue);
     Parse axis specification VALUE which can be an integer or a string
     where each bits/character toggle an option (see table below). If VALUE
     is nil, the value of keyword DEFVALUE is returned.  Keyword NAME can
     be used to set the name of the parameter for error message.

     char  bit    option
     ----  -----  ----------------------------------------------------
     t     0x001  Draw ticks on bottom or left edge of viewport
     T     0x002  Draw ticks on top or right edge of viewport
     c     0x004  Draw ticks centered on origin in middle of viewport
     i     0x008  Ticks project inward into viewport
     o     0x010  Ticks project outward away from viewport (0x18 for both)
     l     0x020  Draw tick label numbers on bottom or left edge of viewport
     L     0x040  Draw tick label numbers on top or right edge of viewport
     g     0x080  Draw all grid lines down to gridLevel
     z     0x100  Draw single grid line at origin

   SEE ALSO gg_window. */
{
  if (is_void(value)) return defvalue;
  if (is_array(value) && dimsof(value)(1)==0) {
    if ((s= structof(value))==long) return value;
    if (s==string) {
      flags= 0;
      if (strmatch(value, "t")) flags|= 0x001;
      if (strmatch(value, "T")) flags|= 0x002;
      if (strmatch(value, "c")) flags|= 0x004;
      if (strmatch(value, "i")) flags|= 0x008;
      if (strmatch(value, "o")) flags|= 0x010;
      if (strmatch(value, "l")) flags|= 0x020;
      if (strmatch(value, "L")) flags|= 0x040;
      if (strmatch(value, "g")) flags|= 0x080;
      if (strmatch(value, "z")) flags|= 0x100;
      return flags;
    }
    if (s==char || s==short || s==int) return long(value);
  }
  error, (is_void(name) ? "bad axis flags"
                        : "bad axis flags for \""+name+"\"");
}


GG_COURIER    =  0;
GG_TIMES      =  4;
GG_HELVETICA  =  8;
GG_SYMBOL     = 12;
GG_NEWCENTURY = 16;
GG_GUI_FONT   = 20;
GG_BOLD       =  1;
GG_ITALIC     =  2;
GG_OPAQUE     = 32;

func gg_get_anchor (value, name, defvalue)
{
  if (is_void(value)) return defvalue;
  if (is_array(value) && dimsof(value)(1)==0) {
    if ((s = structof(value))==string) {
      flags = 0;
      if (strmatch(value, "w")) flags|= 1;
      if (strmatch(value, "e")) flags|= 2;
      if (strmatch(value, "n")) flags|= 4;
      if (strmatch(value, "w")) flags|= 8;
      return flags;
    }
    if (s==long || s==char || s==short || s==int) return (value&15);
  }
  error, (is_void(name) ? "bad anchor flags"
                        : "bad anchor flags for \""+name+"\"");
}
GG_ANCHOR_CENTER = 0;
GG_ANCHOR_W = 1;
GG_ANCHOR_E = 2;
GG_ANCHOR_N = 4;
GG_ANCHOR_S = 8;

/*---------------------------------------------------------------------------*/
/* TEXT JUSTIFICATION */

/* The default text justification, justify="NN" is normal is both the
   horizontal and vertical directions.  Other possibilities are "L", "C",
   or "R" for the first character, meaning left, center, and right
   horizontal justification, and "T", "C", "H", "A", or "B", meaning top,
   capline, half, baseline, and bottom vertical justification.  The normal
   justification "NN" is equivalent to "LA".  Common values are "LA", "CA",
   and "RA" for garden variety left, center, and right justified text, with
   the y coordinate at the baseline of the last line in the string
   presented to plt.  The characters labeling the right axis of a plot are
   "RH", so that the y value of the text will match the y value of the
   corresponding tick.  Similarly, the characters labeling the bottom axis
   of a plot are "CT".  The justify= may also be a number,
   horizontal+vertical, where horizontal is 0 for "N", 1 for "L", 2 for
   "C", or 3 for "R", and vertical is 0 for "N", 4 for "T", 8 for "C", 12
   for "H", 16 for "A", or 20 for "B".

   |   HORIZONTAL       VERTICAL
   |  0  N  normal     0  N  normal
   |  1  L  left       4  T  top
   |  2  C  center     8  C  capline
   |  3  R  right     12  H  half (center)
   |                  16  A  baseline
   |                  20  B  bottom
   |
   |  ANCHOR  JUSTIFY
   |  l       LH   13
   |  c       CH   14
   |  r       RH   15
   |  tl      LT    5
   |  t       CT    6
   |  tr      RT    7
   |  bl      LB   21
   |  b       CB   22
   |  br RB 23
*/

func __gg_text_l (text, x0, y0, x1, y1, bd, font, height, color) {
  plt, text, x0 + one_pixel*(bd + 1), 0.5*(y0 + y1), justify=13,
    font=font, height=height, color=color, opaque=0; }
func __gg_text_c(text, x0, y0, x1, y1, bd, font, height, color) {
  plt, text, 0.5*(x0 + x1), 0.5*(y0 + y1), justify=14,
    font=font, height=height, color=color, opaque=0; }
func __gg_text_r(text, x0, y0, x1, y1, bd, font, height, color) {
  plt, text, x1 - one_pixel*(bd + 1), 0.5*(y0 + y1), justify=15,
    font=font, height=height, color=color, opaque=0; }
func __gg_text_tl(text, x0, y0, x1, y1, bd, font, height, color) {
  s = one_pixel*(bd + 1);
  plt, text, x0 + s, y1 - s, justify=5,
    font=font, height=height, color=color, opaque=0; }
func __gg_text_t(text, x0, y0, x1, y1, bd, font, height, color) {
  plt, text, 0.5*(x0 + x1), y1 - one_pixel*(bd + 1), justify=6,
    font=font, height=height, color=color, opaque=0; }
func __gg_text_tr(text, x0, y0, x1, y1, bd, font, height, color) {
  s = one_pixel*(bd + 1);
  plt, text, x1 - s, y1 - s, justify=7,
    font=font, height=height, color=color, opaque=0; }
func __gg_text_bl (text, x0, y0, x1, y1, bd, font, height, color) {
  s = one_pixel*(bd + 1);
  plt, text, x0 + s, y0 + s, justify=21,
    font=font, height=height, color=color, opaque=0; }
func __gg_text_b(text, x0, y0, x1, y1, bd, font, height, color) {
  plt, text, 0.5*(x0 + x1), y0 + one_pixel*(bd + 1), justify=22,
    font=font, height=height, color=color, opaque=0; }
func __gg_text_br(text, x0, y0, x1, y1, bd, font, height, color) {
  s = one_pixel*(bd + 1);
  plt, text, x1 - s, y0 + s, justify=23,
    font=font, height=height, color=color, opaque=0; }

local __gg_justify_table ;
func gg_get_justify(value, name, defvalue)
{
  if (is_void(value)) value = "CH";
  if (structof(value) == string && dimsof(value)(1)==0 &&
      h_has(__gg_justify_table, value)) return h_get(__gg_justify_table, value);
  error, "bad JUSTIFY value";
}
__gg_justify_table = h_new(LH=__gg_text_l,  CH=__gg_text_c, RH=__gg_text_r,
                           LT=__gg_text_tl, CT=__gg_text_t, RT=__gg_text_tr,
                           LB=__gg_text_bl, CB=__gg_text_b, RB=__gg_text_br);

/*---------------------------------------------------------------------------*/
/* 3D BORDER AND RELIEF */

local GG_FLAT ;
local GG_SOLID ;
local GG_SUNKEN ;
local GG_RAISED ;
local GG_GROOVE ;
local GG_RIDGE ;
local __gg_relief_table ;
func gg_get_relief(value, name, defvalue)
/* DOCUMENT gg_get_relief(value);
     Get value (as a scalar integer) of relief setting.  If VALUE is nil,
     the value of keyword DEFVALUE is returned; otherwise, VALUE must be
     one of:
       0  GG_FLAT    "flat"
       1  GG_SOLID   "solid"
       2  GG_SUNKEN  "sunken"
       3  GG_RAISED  "raised"
       4  GG_GROOVE  "groove"
       5  GG_RIDGE   "ridge"
     Keyword NAME can be used to set the name of the parameter for error
     message.

   SEE ALSO gg_get_color, gg_get_font, gg_get_integer, gg_get_real,
            gg_get_axis_flags. */
{
  if (is_void(value)) return defvalue;
  if (is_array(value) && dimsof(value)(1)==0) {
    if (((s= structof(value))==long || s==char || s==short || s==int)) {
      if (value >=0 && value <= 5) return long(value);
    } else if (s==string && h_has(__gg_relief_table, value)) {
      return h_get(__gg_relief_table, value);
    }
  }
  error, (is_void(name) ? "bad relief value"
                        : "bad relief value for \""+name+"\"");
}
GG_FLAT     = 0;
GG_SOLID    = 1;
GG_SUNKEN   = 2;
GG_RAISED   = 3;
GG_GROOVE   = 4;
GG_RIDGE    = 5;
__gg_relief_table = h_new(flat=GG_FLAT, solid=GG_SOLID,
                          sunken=GG_SUNKEN, raised=GG_RAISED,
                          groove=GG_GROOVE, ridge=GG_RIDGE);

func gg_draw_3d_rect (x0, y0, x1, y1, one_pixel,
                     bd, relief, bg, fg, hi, lo)
/* DOCUMENT gg_draw_3d_rect, x0, y0, x1, y1, one_pixel,
                             bd, relief, bg, fg, hi, lo;
     Draw 3D rectangle onto current coordinate system.  (X0,Y0,X1,Y1) is
     the bounding box of rectangle (included) in units of current plotting
     system (NDC if plotting system is 0).  ONE_PIXEL is the pixel size in
     units of current plotting system.  BD is the border width in pixels.
     RELIEF is the type of relief (see gg_get_relief) FG is the foreground
     color (used for "solid" border).  BG is the background color (no
     background get drawn if set to GG_EXTRA) HI and LO are the colors used
     to draw bright/dark parts of the 3D border.  All colors (BG, FG, HI
     and LO) must be given as scalar integer.

   SEE ALSO: gg_get_relief. */
{
  draw_border = ((bd > 0)&&(relief != GG_FLAT));
  if (numberof(bg) != 1 || bg != GG_EXTRA) {
    /* Draw background. (FIXME: there is a one pixel offset bug in pli one
       the right and at the bottom). */
    _y0 = y0 - one_pixel;
    _x1 = x1 + one_pixel
    if (draw_border) {
      s = bd*one_pixel;
      pli, array(char(bg), 1, 1), x0+s, _y0+s, _x1-s, y1-s;
    } else {
      pli, array(char(bg), 1, 1), x0, _y0, _x1, y1;
    }
  }
  if (draw_border) {
    /* Draw border. */
    if (relief == GG_SOLID) {
      if (numberof(bg) != 1 || fg != GG_EXTRA) {
        /* Draw a sort of spiral. */
        npts = 1 + bd*4;
        xpts = ypts = array(double, npts);
        s = one_pixel*indgen(0:bd-1);
        xpts(1) = x0;
        xpts(2::4)   = xpts(3::4) = x1 - s;
        xpts(4::4)   = xpts(5::4) = x0 + s;
        ypts(1:-1:4) = ypts(2::4) = y0 + s;
        ypts(3::4)   = ypts(4::4) = y1 - s;
        ypts(0) = y0 + bd*one_pixel;
        plg, ypts, xpts, width=0, color=fg;
      }
    } else if (relief == GG_SUNKEN) {
      __gg_draw_3d_rect_bd, x0, y0, x1, y1, bd, lo, hi;
    } else if (relief == GG_RAISED) {
      __gg_draw_3d_rect_bd, x0, y0, x1, y1, bd, hi, lo;
    } else if (relief == GG_RIDGE) {
      if ((hbd = bd/2) > 0) __gg_draw_3d_rect_bd, x0, y0, x1, y1, hbd, lo, hi;
      h = one_pixel*hbd;
      __gg_draw_3d_rect_bd, x0+h, y0+h, x1-h, y1-h, bd-hbd, hi, lo;
    } else if (relief == GG_GROOVE) {
      if ((hbd = bd/2) > 0) __gg_draw_3d_rect_bd, x0, y0, x1, y1, hbd, hi, lo;
      h = one_pixel*hbd;
      __gg_draw_3d_rect_bd, x0+h, y0+h, x1-h, y1-h, bd-hbd, lo, hi;
    }
  }
}

func __gg_draw3 (x0, y0, x1, y1, x2, y2, color)
{ pldj, [x0, x1], [y0, y1], [x1, x2], [y1, y2], color=color, width=0; }
func __gg_draw_3d_rect_bd (x0, y0, x1, y1, bd, top, bot)
/* DOCUMENT __gg_draw_3d_rect_bd, x0, y0, x1, y1, bd, top, bot;
       -or- __gg_draw3, x0, y0, x1, y1, x2, y2, color;
     Private routines used by gg_draw_3d_rect to paint the 3D border.  Top
     and left sides of border get drawn with color TOP, bottom and right
     sides get drawn with color BOT.

   SEE ALSO: gg_draw_3d_rect. */
{
  /**/extern one_pixel;
  s = one_pixel*indgen(0:bd-1);
  x1 -= s;
  y1 -= s;
  x0 += s;
  y0 += s;
  __gg_draw3, x0,y0, x0,y1, x1,y1, top;
  __gg_draw3, x0+one_pixel,y0,  x1,y0, x1,y1-one_pixel, bot;
}

/*---------------------------------------------------------------------------*/
/* INTERACTION WITH MOUSE */

func gg_find_clicked (top, ms)
/* DOCUMENT gg_find_clicked(top, ms)
     Get widget managed by container TOP which received click event as
     defined by MS.  MS has the same contents/meaning as the result of the
     builtin mouse() function.  A widget is eligible as the receiver of the
     click event if the button press and release events have occured over
     this widget.  The return value may be nil if no children of TOP is
     eligible for the click event.  If several widgets are eligible the
     topmost one is returned.

   SEE ALSO: mouse, gg_find. */
{
  /* Check that button was released over same widget. */
  match = gg_find(top, ms(5), ms(6));
  if (is_hash(match) &&
      (x = ms(7)) >= match.x0 && x <= match.x1 &&
      (y = ms(8)) >= match.y0 && y <= match.y1) return match;
}

func gg_find (widget, xndc, yndc, match)
/* DOCUMENT gg_find(widget, xndc, yndc)
       -or- gg_find(widget, xndc, yndc, match)
     Get topmost widget under NDC coordinates (XNDC,YNDC) starting at
     WIDGET.  Optional argument MATCH is the returned value if no
     descendant of WIDGET, nor WIDGET itself, is found under (XNDC,YNDC).
     If MATCH is the same as WIDGET, it is assumed that WIDGET is under
     (XNDC,YNDC) regardless its actual bounding box (i.e. no check is done
     for WIDGET itself in this case).

   SEE ALSO: mouse, gg_find_clicked. */
{
  if (match != widget) {
    if (xndc < widget.x0 || xndc > widget.x1 ||
        yndc < widget.y0 || yndc > widget.y1) return match;
    match = widget;
  }
  for (widget=widget.child ; is_hash(widget) ; widget=widget.next) {
    if (xndc >= widget.x0 && xndc <= widget.x1 &&
        yndc >= widget.y0 && yndc <= widget.y1) {
      if (h_has(widget, child=)) match = gg_find(widget, xndc, yndc, widget);
      else                       match = widget;
    }
  }
  return match;
}

func gg_zoom (ms, factor)
/* DOCUMENT gg_zoom, ms;
       -or- gg_zoom, ms, zoom_factor;
     Mimics the "zooming with the mouse" feature of Gist interactive windows.
     Argument MS is identical to the return value of the mouse() function:
       MS = [x_pressed,    y_pressed,    x_released,    y_released,
             xndc_pressed, yndc_pressed, xndc_released, yndc_released,
             system, button, modifiers];
     Optional second argument can be used to set the zoom factor to a
     real >=1.0.  With ZOOM_FACTOR=1.0, you can still drag the viewing
     region.

     For instance, the following statements will interactively
     zoom in/out forever:
       for (;;) {
         ms = mouse(-1,2,"");
         if (is_void(ms)) break;
         gg_zoom, ms;
       }

   SEE ALSO: gg_window, limits, mouse, unzoom. */
{
  sys = long(ms(9));
  if (sys == 0) return;
  factor = gg_get_real(factor, "zoom_factor",
                       1.41421356237309504880168872421, 1.0);
  btn = long(ms(10));
  if (btn == 1)      q = 1.0/(s = factor); // zoom in
  else if (btn == 2) s = q = 1.0;          // no zoom, just drag
  else if (btn == 3) s = 1.0/(q = factor); // zoom out
  else return;

  /* EQUATIONS:
   *   New bounds:  x'bnd = (xbnd - xoff)/s  where: bnd = min or max
   *                                                s = factor   for zoom-in
   *                                                    1/factor for zoom-out
   *   New/old widths: w  = xmax - xmin
   *                   w' = x'max - x'min = w/s
   *   Dragging implies: u = (x1 - xmin)/w = (x0 - x'min)/w'
   *                                       = (x0 - (xmin - xoff)/s)/(w/s)
   *   therefore: xoff = x1 - s*x0.
   */

  /* We take care to only change bounds if clicked point (X0,X0) is
     _strictly_ between limits (beware that we may have XMAX <= XMIN).  We
     do clip X1 and Y1 within the bounds which is not really necessary if
     MS is the result of a call to mouse() since this function already does
     that. */
  old_sys = plsys(sys);
  lim = limits(); /* LIM = [XMIN, XMAX, YMIN, YMAX, FLAGS]; */
  x0 = ms(1);
  if ((x0 - (xmin = lim(1)))*(x0 - (xmax = lim(2))) < 0.0) {
    x1 = (xmax >= xmin ? max(xmin, min(xmax, ms(3)))
                       : max(xmax, min(xmin, ms(3))));
    xoff = x1 - s*x0;
    xmod = (xoff != 0.0 || s != 1.0);
    if (xmod) { xmin = (xmin - xoff)*q; xmax = (xmax - xoff)*q; }
  } else xmod = 0n;
  y0 = ms(2);
  if ((y0 - (ymin = lim(3)))*(y0 - (ymax = lim(4))) < 0.0) {
    y1 = (ymax >= ymin ? max(ymin, min(ymax, ms(4)))
                       : max(ymax, min(ymin, ms(4))));
    yoff = y1 - s*y0;
    ymod = (yoff != 0.0 || s != 1.0);
    if (ymod) { ymin = (ymin - yoff)*q; ymax = (ymax - yoff)*q; }
  } else ymod = 0n;
  if (xmod || ymod) limits, xmin, xmax, ymin, ymax;
  if (old_sys != sys) plsys, old_sys;
}

/*---------------------------------------------------------------------------*/
/* UTILITIES */

func gg_map (op, arg, defvalue)
/* DOCUMENT gg_map(op, arg);
       -or- gg_map(op, arg, defvalue);
     Map scalar function OP onto array argument ARG to mimics element-wise
     unary operation.  If ARG is NIL, DEFVALUE is returned.
*/
{
  if ((n= numberof(arg))==0) return defvalue;
  /* use structof to avoid unecessary string duplication for string result */
  out= array(structof((out1= op(arg(1)))), dimsof(arg));
  out(1)= out1;
  for (i=2 ; i<=n ; ++i) out(i)= op(arg(i));
  return out;
}

/*---------------------------------------------------------------------------*/
/* DEFAULT SETTINGS (must be the last one to have all constants defined). */

func gg_default (fg=, bg=, hi=, lo=, bd=, font=, fontsize=, pad=,
                dpi=, buttonrelief=, labelrelief=, framerelief=)
{
  GG_DEFAULT_FG = gg_get_color(fg, "fg", GG_DEFAULT_FG);
  GG_DEFAULT_BG = gg_get_color(bg, "bg", GG_DEFAULT_BG);
  GG_DEFAULT_HI = gg_get_color(hi, "hi", GG_DEFAULT_HI);
  GG_DEFAULT_LO = gg_get_color(lo, "lo", GG_DEFAULT_LO);
  GG_DEFAULT_BD = gg_get_integer(bd, "bd", GG_DEFAULT_BD, 0);
  GG_DEFAULT_PAD = gg_get_integer(padx, "pad", GG_DEFAULT_PAD, 0);
  GG_DEFAULT_DPI = gg_get_integer(dpi, "dpi", GG_DEFAULT_DPI, 25, 300);
  GG_DEFAULT_FONT = gg_get_font(font, "font", GG_DEFAULT_FONT);
  GG_DEFAULT_FONTSIZE = gg_get_integer(fontsize, "fontsize",
                                       GG_DEFAULT_FONTSIZE, 4, 50);
  GG_DEFAULT_BUTTON_RELIEF = gg_get_relief(buttonrelief, "buttonrelief",
                                           GG_DEFAULT_BUTTON_RELIEF);
  GG_DEFAULT_LABEL_RELIEF = gg_get_relief(labelrelief, "labelrelief",
                                          GG_DEFAULT_LABEL_RELIEF);
  GG_DEFAULT_FRAME_RELIEF = gg_get_relief(framerelief, "framerelief",
                                          GG_DEFAULT_FRAME_RELIEF);
}
GG_DEFAULT_FG = [0x00, 0x00, 0x00];
GG_DEFAULT_BG = [0xdc, 0xdc, 0xdc];
GG_DEFAULT_HI = [0xf2, 0xf2, 0xf2];
GG_DEFAULT_LO = [0x84, 0x84, 0x84];
GG_DEFAULT_BD = 3;
GG_DEFAULT_PAD = 2;
GG_DEFAULT_DPI = 150;
GG_DEFAULT_FONT = GG_HELVETICA|GG_BOLD;
GG_DEFAULT_FONTSIZE = 8;
GG_DEFAULT_BUTTON_RELIEF = GG_RAISED;
GG_DEFAULT_LABEL_RELIEF = GG_FLAT;
GG_DEFAULT_FRAME_RELIEF = GG_SUNKEN;

/*---------------------------------------------------------------------------*/