/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */

/* Marco Theme Rendering */

/*
 * Copyright (C) 2001 Havoc Pennington
 *
 * This program 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.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

/**
 * \file theme.c    Making Marco look pretty
 *
 * The window decorations drawn by Marco are described by files on disk
 * known internally as "themes" (externally as "window border themes" on
 * http://art.gnome.org/themes/marco/ or "Marco themes"). This file
 * contains most of the code necessary to support themes; it does not
 * contain the XML parser, which is in theme-parser.c.
 *
 * \bug This is a big file with lots of different subsystems, which might
 * be better split out into separate files.
 */

/**
 * \defgroup tokenizer   The theme expression tokenizer
 *
 * Themes can use a simple expression language to represent the values of
 * things. This is the tokeniser used for that language.
 *
 * \bug We could remove almost all this code by using GScanner instead,
 * but we would also have to find every expression in every existing theme
 * we could and make sure the parse trees were the same.
 */

/**
 * \defgroup parser  The theme expression parser
 *
 * Themes can use a simple expression language to represent the values of
 * things. This is the parser used for that language.
 */

#include <config.h>
#include "theme.h"
#include "theme-parser.h"
#include "util.h"
#include "gradient.h"
#include <gtk/gtk.h>
#include <string.h>
#include <stdlib.h>
#define __USE_XOPEN
#include <math.h>

#define GDK_COLOR_RGBA(color)                                           \
                         ((guint32) (0xff                         |     \
                                     (((color).red / 256) << 24)   |    \
                                     (((color).green / 256) << 16) |    \
                                     (((color).blue / 256) << 8)))

#define GDK_COLOR_RGB(color)                                            \
                         ((guint32) ((((color).red / 256) << 16)   |    \
                                     (((color).green / 256) << 8)  |    \
                                     (((color).blue / 256))))

#define ALPHA_TO_UCHAR(d) ((unsigned char) ((d) * 255))

#define DEBUG_FILL_STRUCT(s) memset ((s), 0xef, sizeof (*(s)))
#define CLAMP_UCHAR(v) ((guchar) (CLAMP (((int)v), (int)0, (int)255)))
#define INTENSITY(r, g, b) ((r) * 0.30 + (g) * 0.59 + (b) * 0.11)

static void gtk_style_shade		(GdkColor	 *a,
					 GdkColor	 *b,
					 gdouble	  k);
static void rgb_to_hls			(gdouble	 *r,
					 gdouble	 *g,
					 gdouble	 *b);
static void hls_to_rgb			(gdouble	 *h,
					 gdouble	 *l,
					 gdouble	 *s);

/**
 * The current theme. (Themes are singleton.)
 */
static MetaTheme *meta_current_theme = NULL;

static GdkPixbuf *
colorize_pixbuf (GdkPixbuf *orig,
                 GdkColor  *new_color)
{
  GdkPixbuf *pixbuf;
  double intensity;
  int x, y;
  const guchar *src;
  guchar *dest;
  int orig_rowstride;
  int dest_rowstride;
  int width, height;
  gboolean has_alpha;
  const guchar *src_pixels;
  guchar *dest_pixels;

  pixbuf = gdk_pixbuf_new (gdk_pixbuf_get_colorspace (orig), gdk_pixbuf_get_has_alpha (orig),
			   gdk_pixbuf_get_bits_per_sample (orig),
			   gdk_pixbuf_get_width (orig), gdk_pixbuf_get_height (orig));

  if (pixbuf == NULL)
    return NULL;

  orig_rowstride = gdk_pixbuf_get_rowstride (orig);
  dest_rowstride = gdk_pixbuf_get_rowstride (pixbuf);
  width = gdk_pixbuf_get_width (pixbuf);
  height = gdk_pixbuf_get_height (pixbuf);
  has_alpha = gdk_pixbuf_get_has_alpha (orig);
  src_pixels = gdk_pixbuf_get_pixels (orig);
  dest_pixels = gdk_pixbuf_get_pixels (pixbuf);

  for (y = 0; y < height; y++)
    {
      src = src_pixels + y * orig_rowstride;
      dest = dest_pixels + y * dest_rowstride;

      for (x = 0; x < width; x++)
        {
          double dr, dg, db;

          intensity = INTENSITY (src[0], src[1], src[2]) / 255.0;

          if (intensity <= 0.5)
            {
              /* Go from black at intensity = 0.0 to new_color at intensity = 0.5 */
              dr = (new_color->red * intensity * 2.0) / 65535.0;
              dg = (new_color->green * intensity * 2.0) / 65535.0;
              db = (new_color->blue * intensity * 2.0) / 65535.0;
            }
          else
            {
              /* Go from new_color at intensity = 0.5 to white at intensity = 1.0 */
              dr = (new_color->red + (65535 - new_color->red) * (intensity - 0.5) * 2.0) / 65535.0;
              dg = (new_color->green + (65535 - new_color->green) * (intensity - 0.5) * 2.0) / 65535.0;
              db = (new_color->blue + (65535 - new_color->blue) * (intensity - 0.5) * 2.0) / 65535.0;
            }

          dest[0] = CLAMP_UCHAR (255 * dr);
          dest[1] = CLAMP_UCHAR (255 * dg);
          dest[2] = CLAMP_UCHAR (255 * db);

          if (has_alpha)
            {
              dest[3] = src[3];
              src += 4;
              dest += 4;
            }
          else
            {
              src += 3;
              dest += 3;
            }
        }
    }

  return pixbuf;
}

static void
color_composite (const GdkColor *bg,
                 const GdkColor *fg,
                 double          alpha_d,
                 GdkColor       *color)
{
  guint16 alpha;

  *color = *bg;
  alpha = alpha_d * 0xffff;
  color->red = color->red + (((fg->red - color->red) * alpha + 0x8000) >> 16);
  color->green = color->green + (((fg->green - color->green) * alpha + 0x8000) >> 16);
  color->blue = color->blue + (((fg->blue - color->blue) * alpha + 0x8000) >> 16);
}

/**
 * Sets all the fields of a border to dummy values.
 *
 * \param border The border whose fields should be reset.
 */
static void
init_border (GtkBorder *border)
{
  border->top = -1;
  border->bottom = -1;
  border->left = -1;
  border->right = -1;
}

/**
 * Creates a new, empty MetaFrameLayout. The fields will be set to dummy
 * values.
 *
 * \return The newly created MetaFrameLayout.
 */
MetaFrameLayout*
meta_frame_layout_new  (void)
{
  MetaFrameLayout *layout;

  layout = g_new0 (MetaFrameLayout, 1);

  layout->refcount = 1;

  /* Fill with -1 values to detect invalid themes */
  layout->left_width = -1;
  layout->right_width = -1;
  layout->bottom_height = -1;

  init_border (&layout->title_border);

  layout->title_vertical_pad = -1;

  layout->right_titlebar_edge = -1;
  layout->left_titlebar_edge = -1;

  layout->button_sizing = META_BUTTON_SIZING_LAST;
  layout->button_aspect = 1.0;
  layout->button_width = -1;
  layout->button_height = -1;

  layout->has_title = TRUE;
  layout->title_scale = 1.0;

  init_border (&layout->button_border);

  return layout;
}

/**
 *
 */
static gboolean
validate_border (const GtkBorder *border,
                 const char     **bad)
{
  *bad = NULL;

  if (border->top < 0)
    *bad = _("top");
  else if (border->bottom < 0)
    *bad = _("bottom");
  else if (border->left < 0)
    *bad = _("left");
  else if (border->right < 0)
    *bad = _("right");

  return *bad == NULL;
}

/**
 * Ensures that the theme supplied a particular dimension. When a
 * MetaFrameLayout is created, all its integer fields are set to -1
 * by meta_frame_layout_new(). After an instance of this type
 * should have been initialised, this function checks that
 * a given field is not still at -1. It is never called directly, but
 * rather via the CHECK_GEOMETRY_VALUE and CHECK_GEOMETRY_BORDER
 * macros.
 *
 * \param      val    The value to check
 * \param      name   The name to use in the error message
 * \param[out] error  Set to an error if val was not initialised
 */
static gboolean
validate_geometry_value (int         val,
                         const char *name,
                         GError    **error)
{
  if (val < 0)
    {
      g_set_error (error, META_THEME_ERROR,
                   META_THEME_ERROR_FRAME_GEOMETRY,
                   _("frame geometry does not specify \"%s\" dimension"),
                   name);
      return FALSE;
    }
  else
    return TRUE;
}

static gboolean
validate_geometry_border (const GtkBorder *border,
                          const char      *name,
                          GError         **error)
{
  const char *bad;

  if (!validate_border (border, &bad))
    {
      g_set_error (error, META_THEME_ERROR,
                   META_THEME_ERROR_FRAME_GEOMETRY,
                   _("frame geometry does not specify dimension \"%s\" for border \"%s\""),
                   bad, name);
      return FALSE;
    }
  else
    return TRUE;
}

gboolean
meta_frame_layout_validate (const MetaFrameLayout *layout,
                            GError               **error)
{
  g_return_val_if_fail (layout != NULL, FALSE);

#define CHECK_GEOMETRY_VALUE(vname) if (!validate_geometry_value (layout->vname, #vname, error)) return FALSE

#define CHECK_GEOMETRY_BORDER(bname) if (!validate_geometry_border (&layout->bname, #bname, error)) return FALSE

  CHECK_GEOMETRY_VALUE (left_width);
  CHECK_GEOMETRY_VALUE (right_width);
  CHECK_GEOMETRY_VALUE (bottom_height);

  CHECK_GEOMETRY_BORDER (title_border);

  CHECK_GEOMETRY_VALUE (title_vertical_pad);

  CHECK_GEOMETRY_VALUE (right_titlebar_edge);
  CHECK_GEOMETRY_VALUE (left_titlebar_edge);

  switch (layout->button_sizing)
    {
    case META_BUTTON_SIZING_ASPECT:
      if (layout->button_aspect < (0.1) ||
          layout->button_aspect > (15.0))
        {
          g_set_error (error, META_THEME_ERROR,
                       META_THEME_ERROR_FRAME_GEOMETRY,
                       _("Button aspect ratio %g is not reasonable"),
                       layout->button_aspect);
          return FALSE;
        }
      break;
    case META_BUTTON_SIZING_FIXED:
      CHECK_GEOMETRY_VALUE (button_width);
      CHECK_GEOMETRY_VALUE (button_height);
      break;
    case META_BUTTON_SIZING_LAST:
      g_set_error (error, META_THEME_ERROR,
                   META_THEME_ERROR_FRAME_GEOMETRY,
                   _("Frame geometry does not specify size of buttons"));
      return FALSE;
    }

  CHECK_GEOMETRY_BORDER (button_border);

  return TRUE;
}

MetaFrameLayout*
meta_frame_layout_copy (const MetaFrameLayout *src)
{
  MetaFrameLayout *layout;

  layout = g_new0 (MetaFrameLayout, 1);

  *layout = *src;

  layout->refcount = 1;

  return layout;
}

void
meta_frame_layout_ref (MetaFrameLayout *layout)
{
  g_return_if_fail (layout != NULL);

  layout->refcount += 1;
}

void
meta_frame_layout_unref (MetaFrameLayout *layout)
{
  g_return_if_fail (layout != NULL);
  g_return_if_fail (layout->refcount > 0);

  layout->refcount -= 1;

  if (layout->refcount == 0)
    {
      DEBUG_FILL_STRUCT (layout);
      g_free (layout);
    }
}

void
meta_frame_layout_get_borders (const MetaFrameLayout *layout,
                               int                    text_height,
                               MetaFrameFlags         flags,
                               int                   *top_height,
                               int                   *bottom_height,
                               int                   *left_width,
                               int                   *right_width)
{
  int buttons_height, title_height;

  g_return_if_fail (top_height != NULL);
  g_return_if_fail (bottom_height != NULL);
  g_return_if_fail (left_width != NULL);
  g_return_if_fail (right_width != NULL);

  if (!layout->has_title)
    text_height = 0;

  buttons_height = layout->button_height +
    layout->button_border.top + layout->button_border.bottom;
  title_height = text_height +
    layout->title_vertical_pad +
    layout->title_border.top + layout->title_border.bottom;

  if (top_height)
    {
      *top_height = MAX (buttons_height, title_height);
    }

  if (left_width)
    *left_width = layout->left_width;
  if (right_width)
    *right_width = layout->right_width;

  if (bottom_height)
    {
      if (flags & META_FRAME_SHADED)
        *bottom_height = 0;
      else
        *bottom_height = layout->bottom_height;
    }

  if (flags & META_FRAME_FULLSCREEN)
    {
      if (top_height)
        *top_height = 0;
      if (bottom_height)
        *bottom_height = 0;
      if (left_width)
        *left_width = 0;
      if (right_width)
        *right_width = 0;
    }
}

static MetaButtonSpace*
rect_for_function (MetaFrameGeometry *fgeom,
                   MetaFrameFlags     flags,
                   MetaButtonFunction function,
                   MetaTheme         *theme)
{

  /* Firstly, check version-specific things. */

  if (META_THEME_ALLOWS(theme, META_THEME_SHADE_STICK_ABOVE_BUTTONS))
    {
      switch (function)
        {
        case META_BUTTON_FUNCTION_SHADE:
          if ((flags & META_FRAME_ALLOWS_SHADE) && !(flags & META_FRAME_SHADED))
            return &fgeom->shade_rect;
          else
            return NULL;
        case META_BUTTON_FUNCTION_ABOVE:
          if (!(flags & META_FRAME_ABOVE))
            return &fgeom->above_rect;
          else
            return NULL;
        case META_BUTTON_FUNCTION_STICK:
          if (!(flags & META_FRAME_STUCK))
            return &fgeom->stick_rect;
          else
            return NULL;
        case META_BUTTON_FUNCTION_UNSHADE:
          if ((flags & META_FRAME_ALLOWS_SHADE) && (flags & META_FRAME_SHADED))
            return &fgeom->unshade_rect;
          else
            return NULL;
        case META_BUTTON_FUNCTION_UNABOVE:
          if (flags & META_FRAME_ABOVE)
            return &fgeom->unabove_rect;
          else
            return NULL;
        case META_BUTTON_FUNCTION_UNSTICK:
          if (flags & META_FRAME_STUCK)
            return &fgeom->unstick_rect;
        default:
          /* just go on to the next switch block */;
        }
    }

  /* now consider the buttons which exist in all versions */

  switch (function)
    {
    case META_BUTTON_FUNCTION_MENU:
      if (flags & META_FRAME_ALLOWS_MENU)
        return &fgeom->menu_rect;
      else
        return NULL;
    case META_BUTTON_FUNCTION_MINIMIZE:
      if (flags & META_FRAME_ALLOWS_MINIMIZE)
        return &fgeom->min_rect;
      else
        return NULL;
    case META_BUTTON_FUNCTION_MAXIMIZE:
      if (flags & META_FRAME_ALLOWS_MAXIMIZE)
        return &fgeom->max_rect;
      else
        return NULL;
    case META_BUTTON_FUNCTION_CLOSE:
      if (flags & META_FRAME_ALLOWS_DELETE)
        return &fgeom->close_rect;
      else
        return NULL;
    case META_BUTTON_FUNCTION_STICK:
    case META_BUTTON_FUNCTION_SHADE:
    case META_BUTTON_FUNCTION_ABOVE:
    case META_BUTTON_FUNCTION_UNSTICK:
    case META_BUTTON_FUNCTION_UNSHADE:
    case META_BUTTON_FUNCTION_UNABOVE:
      /* we are being asked for a >v1 button which hasn't been handled yet,
       * so obviously we're not in a theme which supports that version.
       * therefore, we don't show the button. return NULL and all will
       * be well.
       */
      return NULL;

    case META_BUTTON_FUNCTION_LAST:
      return NULL;
    }

  return NULL;
}

static gboolean
strip_button (MetaButtonSpace *func_rects[MAX_BUTTONS_PER_CORNER],
              GdkRectangle    *bg_rects[MAX_BUTTONS_PER_CORNER],
              int             *n_rects,
              MetaButtonSpace *to_strip)
{
  int i;

  i = 0;
  while (i < *n_rects)
    {
      if (func_rects[i] == to_strip)
        {
          *n_rects -= 1;

          /* shift the other rects back in the array */
          while (i < *n_rects)
            {
              func_rects[i] = func_rects[i+1];
              bg_rects[i] = bg_rects[i+1];

              ++i;
            }

          func_rects[i] = NULL;
          bg_rects[i] = NULL;

          return TRUE;
        }

      ++i;
    }

  return FALSE; /* did not strip anything */
}

void
meta_frame_layout_calc_geometry (const MetaFrameLayout  *layout,
                                 int                     text_height,
                                 MetaFrameFlags          flags,
                                 int                     client_width,
                                 int                     client_height,
                                 const MetaButtonLayout *button_layout,
                                 MetaFrameGeometry      *fgeom,
                                 MetaTheme              *theme)
{
  int i, n_left, n_right, n_left_spacers, n_right_spacers;
  int x;
  int button_y;
  int title_right_edge;
  int width, height;
  int button_width, button_height;
  int min_size_for_rounding;

  /* the left/right rects in order; the max # of rects
   * is the number of button functions
   */
  MetaButtonSpace *left_func_rects[MAX_BUTTONS_PER_CORNER];
  MetaButtonSpace *right_func_rects[MAX_BUTTONS_PER_CORNER];
  GdkRectangle *left_bg_rects[MAX_BUTTONS_PER_CORNER];
  gboolean left_buttons_has_spacer[MAX_BUTTONS_PER_CORNER];
  GdkRectangle *right_bg_rects[MAX_BUTTONS_PER_CORNER];
  gboolean right_buttons_has_spacer[MAX_BUTTONS_PER_CORNER];

  meta_frame_layout_get_borders (layout, text_height,
                                 flags,
                                 &fgeom->top_height,
                                 &fgeom->bottom_height,
                                 &fgeom->left_width,
                                 &fgeom->right_width);

  width = client_width + fgeom->left_width + fgeom->right_width;

  height = ((flags & META_FRAME_SHADED) ? 0: client_height) +
    fgeom->top_height + fgeom->bottom_height;

  fgeom->width = width;
  fgeom->height = height;

  fgeom->top_titlebar_edge = layout->title_border.top;
  fgeom->bottom_titlebar_edge = layout->title_border.bottom;
  fgeom->left_titlebar_edge = layout->left_titlebar_edge;
  fgeom->right_titlebar_edge = layout->right_titlebar_edge;

  /* gcc warnings */
  button_width = -1;
  button_height = -1;

  switch (layout->button_sizing)
    {
    case META_BUTTON_SIZING_ASPECT:
      button_height = fgeom->top_height - layout->button_border.top - layout->button_border.bottom;
      button_width = button_height / layout->button_aspect;
      break;
    case META_BUTTON_SIZING_FIXED:
      button_width = layout->button_width;
      button_height = layout->button_height;
      break;
    case META_BUTTON_SIZING_LAST:
      g_assert_not_reached ();
      break;
    }

  /* FIXME all this code sort of pretends that duplicate buttons
   * with the same function are allowed, but that breaks the
   * code in frames.c, so isn't really allowed right now.
   * Would need left_close_rect, right_close_rect, etc.
   */

  /* Init all button rects to 0, lame hack */
  memset (ADDRESS_OF_BUTTON_RECTS (fgeom), '\0',
          LENGTH_OF_BUTTON_RECTS);

  n_left = 0;
  n_right = 0;
  n_left_spacers = 0;
  n_right_spacers = 0;

  if (!layout->hide_buttons)
    {
      /* Try to fill in rects */
      for (i = 0; i < MAX_BUTTONS_PER_CORNER && button_layout->left_buttons[i] != META_BUTTON_FUNCTION_LAST; i++)
        {
          left_func_rects[n_left] = rect_for_function (fgeom, flags,
                                                       button_layout->left_buttons[i],
                                                       theme);
          if (left_func_rects[n_left] != NULL)
            {
              left_buttons_has_spacer[n_left] = button_layout->left_buttons_has_spacer[i];
              if (button_layout->left_buttons_has_spacer[i])
                ++n_left_spacers;

              ++n_left;
            }
        }

      for (i = 0; i < MAX_BUTTONS_PER_CORNER && button_layout->right_buttons[i] != META_BUTTON_FUNCTION_LAST; i++)
        {
          right_func_rects[n_right] = rect_for_function (fgeom, flags,
                                                         button_layout->right_buttons[i],
                                                         theme);
          if (right_func_rects[n_right] != NULL)
            {
              right_buttons_has_spacer[n_right] = button_layout->right_buttons_has_spacer[i];
              if (button_layout->right_buttons_has_spacer[i])
                ++n_right_spacers;

              ++n_right;
            }
        }
    }

  for (i = 0; i < MAX_BUTTONS_PER_CORNER; i++)
    {
      left_bg_rects[i] = NULL;
      right_bg_rects[i] = NULL;
    }

  for (i = 0; i < n_left; i++)
    {
      if (i == 0) /* For the first button (From left to right) */
        {
          if (n_left > 1) /* Set left_left_background
                             if we have more than one button */
            left_bg_rects[i] = &fgeom->left_left_background;
          else /* No background if we have only one single button */
            left_bg_rects[i] = &fgeom->left_single_background;
        }
      else if (i == (n_left - 1))
        left_bg_rects[i] = &fgeom->left_right_background;
      else
        left_bg_rects[i] = &fgeom->left_middle_backgrounds[i - 1];
    }

  for (i = 0; i < n_right; i++)
    {
      if (i == (n_right - 1)) /* For the first button (From right to left) */
        {
          if (n_right > 1) /* Set right_right_background
                              if we have more than one button */
            right_bg_rects[i] = &fgeom->right_right_background;
          else /* No background if we have only one single button */
            right_bg_rects[i] = &fgeom->right_single_background;
        }
      else if (i == 0)
        right_bg_rects[i] = &fgeom->right_left_background;
      else
        right_bg_rects[i] = &fgeom->right_middle_backgrounds[i - 1];
    }

  /* Be sure buttons fit */
  while (n_left > 0 || n_right > 0)
    {
      int space_used_by_buttons;
      int space_available;

      space_available = fgeom->width - layout->left_titlebar_edge - layout->right_titlebar_edge;

      space_used_by_buttons = 0;

      space_used_by_buttons += button_width * n_left;
      space_used_by_buttons += (button_width * 0.75) * n_left_spacers;
      space_used_by_buttons += layout->button_border.left * n_left;
      space_used_by_buttons += layout->button_border.right * n_left;

      space_used_by_buttons += button_width * n_right;
      space_used_by_buttons += (button_width * 0.75) * n_right_spacers;
      space_used_by_buttons += layout->button_border.left * n_right;
      space_used_by_buttons += layout->button_border.right * n_right;

      if (space_used_by_buttons <= space_available)
        break; /* Everything fits, bail out */

      /* First try to remove separators */
      if (n_left_spacers > 0)
        {
          left_buttons_has_spacer[--n_left_spacers] = FALSE;
          continue;
        }
      else if (n_right_spacers > 0)
        {
          right_buttons_has_spacer[--n_right_spacers] = FALSE;
          continue;
        }

      /* Otherwise we need to shave out a button. Shave
       * above, stick, shade, min, max, close, then menu (menu is most useful);
       * prefer the default button locations.
       */
      if (strip_button (left_func_rects, left_bg_rects,
                        &n_left, &fgeom->above_rect))
        continue;
      else if (strip_button (right_func_rects, right_bg_rects,
                             &n_right, &fgeom->above_rect))
        continue;
      else if (strip_button (left_func_rects, left_bg_rects,
                        &n_left, &fgeom->stick_rect))
        continue;
      else if (strip_button (right_func_rects, right_bg_rects,
                             &n_right, &fgeom->stick_rect))
        continue;
      else if (strip_button (left_func_rects, left_bg_rects,
                        &n_left, &fgeom->shade_rect))
        continue;
      else if (strip_button (right_func_rects, right_bg_rects,
                             &n_right, &fgeom->shade_rect))
        continue;
      else if (strip_button (left_func_rects, left_bg_rects,
                        &n_left, &fgeom->min_rect))
        continue;
      else if (strip_button (right_func_rects, right_bg_rects,
                             &n_right, &fgeom->min_rect))
        continue;
      else if (strip_button (left_func_rects, left_bg_rects,
                             &n_left, &fgeom->max_rect))
        continue;
      else if (strip_button (right_func_rects, right_bg_rects,
                             &n_right, &fgeom->max_rect))
        continue;
      else if (strip_button (left_func_rects, left_bg_rects,
                             &n_left, &fgeom->close_rect))
        continue;
      else if (strip_button (right_func_rects, right_bg_rects,
                             &n_right, &fgeom->close_rect))
        continue;
      else if (strip_button (right_func_rects, right_bg_rects,
                             &n_right, &fgeom->menu_rect))
        continue;
      else if (strip_button (left_func_rects, left_bg_rects,
                             &n_left, &fgeom->menu_rect))
        continue;
      else
        {
          meta_bug ("Could not find a button to strip. n_left = %d n_right = %d\n",
                    n_left, n_right);
        }
    }

  /* center buttons vertically */
  button_y = (fgeom->top_height -
              (button_height + layout->button_border.top + layout->button_border.bottom)) / 2 + layout->button_border.top;

  /* right edge of farthest-right button */
  x = width - layout->right_titlebar_edge;

  i = n_right - 1;
  while (i >= 0)
    {
      MetaButtonSpace *rect;

      if (x < 0) /* if we go negative, leave the buttons we don't get to as 0-width */
        break;

      rect = right_func_rects[i];
      rect->visible.x = x - layout->button_border.right - button_width;
      if (right_buttons_has_spacer[i])
        rect->visible.x -= (button_width * 0.75);

      rect->visible.y = button_y;
      rect->visible.width = button_width;
      rect->visible.height = button_height;

      if (flags & META_FRAME_MAXIMIZED)
        {
          rect->clickable.x = rect->visible.x;
          rect->clickable.y = rect->visible.y;
          rect->clickable.width = button_width;
          rect->clickable.height = button_height;

          if (i == n_right - 1)
            rect->clickable.width += layout->right_titlebar_edge + layout->right_width + layout->button_border.right;

        }
      else
        g_memmove (&(rect->clickable), &(rect->visible), sizeof(rect->clickable));

      *(right_bg_rects[i]) = rect->visible;

      x = rect->visible.x - layout->button_border.left;

      --i;
    }

  /* save right edge of titlebar for later use */
  title_right_edge = x - layout->title_border.right;

  /* Now x changes to be position from the left and we go through
   * the left-side buttons
   */
  x = layout->left_titlebar_edge;
  for (i = 0; i < n_left; i++)
    {
      MetaButtonSpace *rect;

      rect = left_func_rects[i];

      rect->visible.x = x + layout->button_border.left;
      rect->visible.y = button_y;
      rect->visible.width = button_width;
      rect->visible.height = button_height;

      if (flags & META_FRAME_MAXIMIZED)
        {
          rect->clickable.x = rect->visible.x;
          rect->clickable.y = rect->visible.y;
          rect->clickable.width = button_width;
          rect->clickable.height = button_height;
        }
      else
        g_memmove (&(rect->clickable), &(rect->visible), sizeof(rect->clickable));


      x = rect->visible.x + rect->visible.width + layout->button_border.right;
      if (left_buttons_has_spacer[i])
        x += (button_width * 0.75);

      *(left_bg_rects[i]) = rect->visible;
    }

  /* We always fill as much vertical space as possible with title rect,
   * rather than centering it like the buttons
   */
  fgeom->title_rect.x = x + layout->title_border.left;
  fgeom->title_rect.y = layout->title_border.top;
  fgeom->title_rect.width = title_right_edge - fgeom->title_rect.x;
  fgeom->title_rect.height = fgeom->top_height - layout->title_border.top - layout->title_border.bottom;

  /* Nuke title if it won't fit */
  if (fgeom->title_rect.width < 0 ||
      fgeom->title_rect.height < 0)
    {
      fgeom->title_rect.width = 0;
      fgeom->title_rect.height = 0;
    }

  if (flags & META_FRAME_SHADED)
    min_size_for_rounding = 0;
  else
    min_size_for_rounding = 5;

  fgeom->top_left_corner_rounded_radius = 0;
  fgeom->top_right_corner_rounded_radius = 0;
  fgeom->bottom_left_corner_rounded_radius = 0;
  fgeom->bottom_right_corner_rounded_radius = 0;

  if (fgeom->top_height + fgeom->left_width >= min_size_for_rounding)
    fgeom->top_left_corner_rounded_radius = layout->top_left_corner_rounded_radius;
  if (fgeom->top_height + fgeom->right_width >= min_size_for_rounding)
    fgeom->top_right_corner_rounded_radius = layout->top_right_corner_rounded_radius;

  if (fgeom->bottom_height + fgeom->left_width >= min_size_for_rounding)
    fgeom->bottom_left_corner_rounded_radius = layout->bottom_left_corner_rounded_radius;
  if (fgeom->bottom_height + fgeom->right_width >= min_size_for_rounding)
    fgeom->bottom_right_corner_rounded_radius = layout->bottom_right_corner_rounded_radius;
}

MetaGradientSpec*
meta_gradient_spec_new (MetaGradientType type)
{
  MetaGradientSpec *spec;

  spec = g_new (MetaGradientSpec, 1);

  spec->type = type;
  spec->color_specs = NULL;

  return spec;
}

static void
free_color_spec (gpointer spec, gpointer user_data)
{
  meta_color_spec_free (spec);
}

void
meta_gradient_spec_free (MetaGradientSpec *spec)
{
  g_return_if_fail (spec != NULL);

  g_slist_foreach (spec->color_specs, free_color_spec, NULL);
  g_slist_free (spec->color_specs);

  DEBUG_FILL_STRUCT (spec);
  g_free (spec);
}

GdkPixbuf*
meta_gradient_spec_render (const MetaGradientSpec *spec,
                           GtkWidget              *widget,
                           int                     width,
                           int                     height)
{
  int n_colors;
  GdkColor *colors;
  GSList *tmp;
  int i;
  GdkPixbuf *pixbuf;

  n_colors = g_slist_length (spec->color_specs);

  if (n_colors == 0)
    return NULL;

  colors = g_new (GdkColor, n_colors);

  i = 0;
  tmp = spec->color_specs;
  while (tmp != NULL)
    {
      meta_color_spec_render (tmp->data, widget, &colors[i]);

      tmp = tmp->next;
      ++i;
    }

  pixbuf = meta_gradient_create_multi (width, height,
                                       colors, n_colors,
                                       spec->type);

  g_free (colors);

  return pixbuf;
}

gboolean
meta_gradient_spec_validate (MetaGradientSpec *spec,
                             GError          **error)
{
  g_return_val_if_fail (spec != NULL, FALSE);

  if (g_slist_length (spec->color_specs) < 2)
    {
      g_set_error (error, META_THEME_ERROR,
                   META_THEME_ERROR_FAILED,
                   _("Gradients should have at least two colors"));
      return FALSE;
    }

  return TRUE;
}

MetaAlphaGradientSpec*
meta_alpha_gradient_spec_new (MetaGradientType       type,
                              int                    n_alphas)
{
  MetaAlphaGradientSpec *spec;

  g_return_val_if_fail (n_alphas > 0, NULL);

  spec = g_new0 (MetaAlphaGradientSpec, 1);

  spec->type = type;
  spec->alphas = g_new0 (unsigned char, n_alphas);
  spec->n_alphas = n_alphas;

  return spec;
}

void
meta_alpha_gradient_spec_free (MetaAlphaGradientSpec *spec)
{
  g_return_if_fail (spec != NULL);

  g_free (spec->alphas);
  g_free (spec);
}

MetaColorSpec*
meta_color_spec_new (MetaColorSpecType type)
{
  MetaColorSpec *spec;
  MetaColorSpec dummy;
  int size;

  size = G_STRUCT_OFFSET (MetaColorSpec, data);

  switch (type)
    {
    case META_COLOR_SPEC_BASIC:
      size += sizeof (dummy.data.basic);
      break;

    case META_COLOR_SPEC_GTK:
      size += sizeof (dummy.data.gtk);
      break;

    case META_COLOR_SPEC_BLEND:
      size += sizeof (dummy.data.blend);
      break;

    case META_COLOR_SPEC_SHADE:
      size += sizeof (dummy.data.shade);
      break;
    }

  spec = g_malloc0 (size);

  spec->type = type;

  return spec;
}

void
meta_color_spec_free (MetaColorSpec *spec)
{
  g_return_if_fail (spec != NULL);

  switch (spec->type)
    {
    case META_COLOR_SPEC_BASIC:
      DEBUG_FILL_STRUCT (&spec->data.basic);
      break;

    case META_COLOR_SPEC_GTK:
      DEBUG_FILL_STRUCT (&spec->data.gtk);
      break;

    case META_COLOR_SPEC_BLEND:
      if (spec->data.blend.foreground)
        meta_color_spec_free (spec->data.blend.foreground);
      if (spec->data.blend.background)
        meta_color_spec_free (spec->data.blend.background);
      DEBUG_FILL_STRUCT (&spec->data.blend);
      break;

    case META_COLOR_SPEC_SHADE:
      if (spec->data.shade.base)
        meta_color_spec_free (spec->data.shade.base);
      DEBUG_FILL_STRUCT (&spec->data.shade);
      break;
    }

  g_free (spec);
}

MetaColorSpec*
meta_color_spec_new_from_string (const char *str,
                                 GError    **err)
{
  MetaColorSpec *spec;

  spec = NULL;

  if (str[0] == 'g' && str[1] == 't' && str[2] == 'k' && str[3] == ':')
    {
      /* GTK color */
      const char *bracket;
      const char *end_bracket;
      char *tmp;
      GtkStateType state;
      MetaGtkColorComponent component;

      bracket = str;
      while (*bracket && *bracket != '[')
        ++bracket;

      if (*bracket == '\0')
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("GTK color specification must have the state in brackets, e.g. gtk:fg[NORMAL] where NORMAL is the state; could not parse \"%s\""),
                       str);
          return NULL;
        }

      end_bracket = bracket;
      ++end_bracket;
      while (*end_bracket && *end_bracket != ']')
        ++end_bracket;

      if (*end_bracket == '\0')
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("GTK color specification must have a close bracket after the state, e.g. gtk:fg[NORMAL] where NORMAL is the state; could not parse \"%s\""),
                       str);
          return NULL;
        }

      tmp = g_strndup (bracket + 1, end_bracket - bracket - 1);
      state = meta_gtk_state_from_string (tmp);
      if (((int) state) == -1)
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Did not understand state \"%s\" in color specification"),
                       tmp);
          g_free (tmp);
          return NULL;
        }
      g_free (tmp);

      tmp = g_strndup (str + 4, bracket - str - 4);
      component = meta_color_component_from_string (tmp);
      if (component == META_GTK_COLOR_LAST)
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Did not understand color component \"%s\" in color specification"),
                       tmp);
          g_free (tmp);
          return NULL;
        }
      g_free (tmp);

      spec = meta_color_spec_new (META_COLOR_SPEC_GTK);
      spec->data.gtk.state = state;
      spec->data.gtk.component = component;
      g_assert (spec->data.gtk.state < N_GTK_STATES);
      g_assert (spec->data.gtk.component < META_GTK_COLOR_LAST);
    }
  else if (str[0] == 'b' && str[1] == 'l' && str[2] == 'e' && str[3] == 'n' &&
           str[4] == 'd' && str[5] == '/')
    {
      /* blend */
      char **split;
      double alpha;
      char *end;
      MetaColorSpec *fg;
      MetaColorSpec *bg;

      split = g_strsplit (str, "/", 4);

      if (split[0] == NULL || split[1] == NULL ||
          split[2] == NULL || split[3] == NULL)
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Blend format is \"blend/bg_color/fg_color/alpha\", \"%s\" does not fit the format"),
                       str);
          g_strfreev (split);
          return NULL;
        }

      alpha = g_ascii_strtod (split[3], &end);
      if (end == split[3])
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Could not parse alpha value \"%s\" in blended color"),
                       split[3]);
          g_strfreev (split);
          return NULL;
        }

      if (alpha < (0.0 - 1e6) || alpha > (1.0 + 1e6))
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Alpha value \"%s\" in blended color is not between 0.0 and 1.0"),
                       split[3]);
          g_strfreev (split);
          return NULL;
        }

      fg = NULL;
      bg = NULL;

      bg = meta_color_spec_new_from_string (split[1], err);
      if (bg == NULL)
        {
          g_strfreev (split);
          return NULL;
        }

      fg = meta_color_spec_new_from_string (split[2], err);
      if (fg == NULL)
        {
          meta_color_spec_free (bg);
          g_strfreev (split);
          return NULL;
        }

      g_strfreev (split);

      spec = meta_color_spec_new (META_COLOR_SPEC_BLEND);
      spec->data.blend.alpha = alpha;
      spec->data.blend.background = bg;
      spec->data.blend.foreground = fg;
    }
  else if (str[0] == 's' && str[1] == 'h' && str[2] == 'a' && str[3] == 'd' &&
           str[4] == 'e' && str[5] == '/')
    {
      /* shade */
      char **split;
      double factor;
      char *end;
      MetaColorSpec *base;

      split = g_strsplit (str, "/", 3);

      if (split[0] == NULL || split[1] == NULL ||
          split[2] == NULL)
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Shade format is \"shade/base_color/factor\", \"%s\" does not fit the format"),
                       str);
          g_strfreev (split);
          return NULL;
        }

      factor = g_ascii_strtod (split[2], &end);
      if (end == split[2])
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Could not parse shade factor \"%s\" in shaded color"),
                       split[2]);
          g_strfreev (split);
          return NULL;
        }

      if (factor < (0.0 - 1e6))
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Shade factor \"%s\" in shaded color is negative"),
                       split[2]);
          g_strfreev (split);
          return NULL;
        }

      base = NULL;

      base = meta_color_spec_new_from_string (split[1], err);
      if (base == NULL)
        {
          g_strfreev (split);
          return NULL;
        }

      g_strfreev (split);

      spec = meta_color_spec_new (META_COLOR_SPEC_SHADE);
      spec->data.shade.factor = factor;
      spec->data.shade.base = base;
    }
  else
    {
      spec = meta_color_spec_new (META_COLOR_SPEC_BASIC);

      if (!gdk_color_parse (str, &spec->data.basic.color))
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Could not parse color \"%s\""),
                       str);
          meta_color_spec_free (spec);
          return NULL;
        }
    }

  g_assert (spec);

  return spec;
}

MetaColorSpec*
meta_color_spec_new_gtk (MetaGtkColorComponent component,
                         GtkStateType          state)
{
  MetaColorSpec *spec;

  spec = meta_color_spec_new (META_COLOR_SPEC_GTK);

  spec->data.gtk.component = component;
  spec->data.gtk.state = state;

  return spec;
}

void
meta_color_spec_render (MetaColorSpec *spec,
                        GtkWidget     *widget,
                        GdkColor      *color)
{
  GtkStyle *widget_style;

  g_return_if_fail (spec != NULL);
  g_return_if_fail (GTK_IS_WIDGET (widget));

  widget_style = gtk_widget_get_style (widget);
  g_return_if_fail (widget_style != NULL);

  switch (spec->type)
    {
    case META_COLOR_SPEC_BASIC:
      *color = spec->data.basic.color;
      break;

    case META_COLOR_SPEC_GTK:
      switch (spec->data.gtk.component)
        {
        case META_GTK_COLOR_BG:
          *color = widget_style->bg[spec->data.gtk.state];
          break;
        case META_GTK_COLOR_FG:
          *color = widget_style->fg[spec->data.gtk.state];
          break;
        case META_GTK_COLOR_BASE:
          *color = widget_style->base[spec->data.gtk.state];
          break;
        case META_GTK_COLOR_TEXT:
          *color = widget_style->text[spec->data.gtk.state];
          break;
        case META_GTK_COLOR_LIGHT:
          *color = widget_style->light[spec->data.gtk.state];
          break;
        case META_GTK_COLOR_DARK:
          *color = widget_style->dark[spec->data.gtk.state];
          break;
        case META_GTK_COLOR_MID:
          *color = widget_style->mid[spec->data.gtk.state];
          break;
        case META_GTK_COLOR_TEXT_AA:
          *color = widget_style->text_aa[spec->data.gtk.state];
          break;
        case META_GTK_COLOR_LAST:
          g_assert_not_reached ();
          break;
        }
      break;

    case META_COLOR_SPEC_BLEND:
      {
        GdkColor bg, fg;

        meta_color_spec_render (spec->data.blend.background, widget, &bg);
        meta_color_spec_render (spec->data.blend.foreground, widget, &fg);

        color_composite (&bg, &fg, spec->data.blend.alpha,
                         &spec->data.blend.color);

        *color = spec->data.blend.color;
      }
      break;

    case META_COLOR_SPEC_SHADE:
      {
        meta_color_spec_render (spec->data.shade.base, widget,
                                &spec->data.shade.color);

        gtk_style_shade (&spec->data.shade.color,
                         &spec->data.shade.color, spec->data.shade.factor);

        *color = spec->data.shade.color;
      }
      break;
    }
}

/**
 * Represents an operation as a string.
 *
 * \param type  an operation, such as addition
 * \return  a string, such as "+"
 */
static const char*
op_name (PosOperatorType type)
{
  switch (type)
    {
    case POS_OP_ADD:
      return "+";
    case POS_OP_SUBTRACT:
      return "-";
    case POS_OP_MULTIPLY:
      return "*";
    case POS_OP_DIVIDE:
      return "/";
    case POS_OP_MOD:
      return "%";
    case POS_OP_MAX:
      return "`max`";
    case POS_OP_MIN:
      return "`min`";
    case POS_OP_NONE:
      break;
    }

  return "<unknown>";
}

/**
 * Parses a string and returns an operation.
 *
 * \param p  a pointer into a string representing an operation; part of an
 *           expression somewhere, so not null-terminated
 * \param len  set to the length of the string found. Set to 0 if none is.
 * \return  the operation found. If none was, returns POS_OP_NONE.
 */
static PosOperatorType
op_from_string (const char *p,
                int        *len)
{
  *len = 0;

  switch (*p)
    {
    case '+':
      *len = 1;
      return POS_OP_ADD;
    case '-':
      *len = 1;
      return POS_OP_SUBTRACT;
    case '*':
      *len = 1;
      return POS_OP_MULTIPLY;
    case '/':
      *len = 1;
      return POS_OP_DIVIDE;
    case '%':
      *len = 1;
      return POS_OP_MOD;

    case '`':
      if (p[0] == '`' &&
          p[1] == 'm' &&
          p[2] == 'a' &&
          p[3] == 'x' &&
          p[4] == '`')
        {
          *len = 5;
          return POS_OP_MAX;
        }
      else if (p[0] == '`' &&
               p[1] == 'm' &&
               p[2] == 'i' &&
               p[3] == 'n' &&
               p[4] == '`')
        {
          *len = 5;
          return POS_OP_MIN;
        }
    }

  return POS_OP_NONE;
}

/**
 * Frees an array of tokens. All the tokens and their associated memory
 * will be freed.
 *
 * \param tokens  an array of tokens to be freed
 * \param n_tokens  how many tokens are in the array.
 */
static void
free_tokens (PosToken *tokens,
             int       n_tokens)
{
  int i;

  /* n_tokens can be 0 since tokens may have been allocated more than
   * it was initialized
   */

  for (i = 0; i < n_tokens; i++)
    if (tokens[i].type == POS_TOKEN_VARIABLE)
      g_free (tokens[i].d.v.name);

  g_free (tokens);
}

/**
 * Tokenises a number in an expression.
 *
 * \param p  a pointer into a string representing an operation; part of an
 *           expression somewhere, so not null-terminated
 * \param end_return  set to a pointer to the end of the number found; but
 *                    not updated if no number was found at all
 * \param next  set to either an integer or a float token
 * \param[out] err  set to the problem if there was a problem
 * \return TRUE if a valid number was found, FALSE otherwise (and "err" will
 *         have been set)
 *
 * \bug The "while (*start)..." part: what's wrong with strchr-ish things?
 * \bug The name is wrong: it doesn't parse anything.
 * \ingroup tokenizer
 */
static gboolean
parse_number (const char  *p,
              const char **end_return,
              PosToken    *next,
              GError     **err)
{
  const char *start = p;
  char *end;
  gboolean is_float;
  char *num_str;

  while (*p && (*p == '.' || g_ascii_isdigit (*p)))
    ++p;

  if (p == start)
    {
      char buf[7] = { '\0' };
      buf[g_unichar_to_utf8 (g_utf8_get_char (p), buf)] = '\0';
      g_set_error (err, META_THEME_ERROR,
                   META_THEME_ERROR_BAD_CHARACTER,
                   _("Coordinate expression contains character '%s' which is not allowed"),
                   buf);
      return FALSE;
    }

  *end_return = p;

  /* we need this to exclude floats like "1e6" */
  num_str = g_strndup (start, p - start);
  start = num_str;
  is_float = FALSE;
  while (*start)
    {
      if (*start == '.')
        is_float = TRUE;
      ++start;
    }

  if (is_float)
    {
      next->type = POS_TOKEN_DOUBLE;
      next->d.d.val = g_ascii_strtod (num_str, &end);

      if (end == num_str)
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Coordinate expression contains floating point number '%s' which could not be parsed"),
                       num_str);
          g_free (num_str);
          return FALSE;
        }
    }
  else
    {
      next->type = POS_TOKEN_INT;
      next->d.i.val = strtol (num_str, &end, 10);
      if (end == num_str)
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Coordinate expression contains integer '%s' which could not be parsed"),
                       num_str);
          g_free (num_str);
          return FALSE;
        }
    }

  g_free (num_str);

  g_assert (next->type == POS_TOKEN_INT || next->type == POS_TOKEN_DOUBLE);

  return TRUE;
}

/**
 * Whether a variable can validly appear as part of the name of a variable.
 */
#define IS_VARIABLE_CHAR(c) (g_ascii_isalpha ((c)) || (c) == '_')

#if 0
static void
debug_print_tokens (PosToken *tokens,
                    int       n_tokens)
{
  int i;

  for (i = 0; i < n_tokens; i++)
    {
      PosToken *t = &tokens[i];

      g_print (" ");

      switch (t->type)
        {
        case POS_TOKEN_INT:
          g_print ("\"%d\"", t->d.i.val);
          break;
        case POS_TOKEN_DOUBLE:
          g_print ("\"%g\"", t->d.d.val);
          break;
        case POS_TOKEN_OPEN_PAREN:
          g_print ("\"(\"");
          break;
        case POS_TOKEN_CLOSE_PAREN:
          g_print ("\")\"");
          break;
        case POS_TOKEN_VARIABLE:
          g_print ("\"%s\"", t->d.v.name);
          break;
        case POS_TOKEN_OPERATOR:
          g_print ("\"%s\"", op_name (t->d.o.op));
          break;
        }
    }

  g_print ("\n");
}
#endif

/**
 * Tokenises an expression.
 *
 * \param      expr        The expression
 * \param[out] tokens_p    The resulting tokens
 * \param[out] n_tokens_p  The number of resulting tokens
 * \param[out] err  set to the problem if there was a problem
 *
 * \return  True if the expression was successfully tokenised; false otherwise.
 *
 * \ingroup tokenizer
 */
static gboolean
pos_tokenize (const char  *expr,
              PosToken   **tokens_p,
              int         *n_tokens_p,
              GError     **err)
{
  PosToken *tokens;
  int n_tokens;
  int allocated;
  const char *p;

  *tokens_p = NULL;
  *n_tokens_p = 0;

  allocated = 3;
  n_tokens = 0;
  tokens = g_new (PosToken, allocated);

  p = expr;
  while (*p)
    {
      PosToken *next;
      int len;

      if (n_tokens == allocated)
        {
          allocated *= 2;
          tokens = g_renew (PosToken, tokens, allocated);
        }

      next = &tokens[n_tokens];

      switch (*p)
        {
        case '*':
        case '/':
        case '+':
        case '-': /* negative numbers aren't allowed so this is easy */
        case '%':
        case '`':
          next->type = POS_TOKEN_OPERATOR;
          next->d.o.op = op_from_string (p, &len);
          if (next->d.o.op != POS_OP_NONE)
            {
              ++n_tokens;
              p = p + (len - 1); /* -1 since we ++p later */
            }
          else
            {
              g_set_error (err, META_THEME_ERROR,
                           META_THEME_ERROR_FAILED,
                           _("Coordinate expression contained unknown operator at the start of this text: \"%s\""),
                           p);

              goto error;
            }
          break;

        case '(':
          next->type = POS_TOKEN_OPEN_PAREN;
          ++n_tokens;
          break;

        case ')':
          next->type = POS_TOKEN_CLOSE_PAREN;
          ++n_tokens;
          break;

        case ' ':
        case '\t':
        case '\n':
          break;

        default:
          if (IS_VARIABLE_CHAR (*p))
            {
              /* Assume variable */
              const char *start = p;
              while (*p && IS_VARIABLE_CHAR (*p))
                ++p;
              g_assert (p != start);
              next->type = POS_TOKEN_VARIABLE;
              next->d.v.name = g_strndup (start, p - start);
              ++n_tokens;
              --p; /* since we ++p again at the end of while loop */
            }
          else
            {
              /* Assume number */
              const char *end;

              if (!parse_number (p, &end, next, err))
                goto error;

              ++n_tokens;
              p = end - 1; /* -1 since we ++p again at the end of while loop */
            }

          break;
        }

      ++p;
    }

  if (n_tokens == 0)
    {
      g_set_error (err, META_THEME_ERROR,
                   META_THEME_ERROR_FAILED,
                   _("Coordinate expression was empty or not understood"));

      goto error;
    }

  *tokens_p = tokens;
  *n_tokens_p = n_tokens;

  return TRUE;

 error:
  g_assert (err == NULL || *err != NULL);

  free_tokens (tokens, n_tokens);
  return FALSE;
}

/**
 * The type of a PosExpr: either integer, double, or an operation.
 * \ingroup parser
 */
typedef enum
{
  POS_EXPR_INT,
  POS_EXPR_DOUBLE,
  POS_EXPR_OPERATOR
} PosExprType;

/**
 * Type and value of an expression in a parsed sequence. We don't
 * keep expressions in a tree; if this is of type POS_EXPR_OPERATOR,
 * the arguments of the operator will be in the array positions
 * immediately preceding and following this operator; they cannot
 * themselves be operators.
 *
 * \bug operator is char; it should really be of PosOperatorType.
 * \ingroup parser
 */
typedef struct
{
  PosExprType type;
  union
  {
    double double_val;
    int int_val;
    char operator;
  } d;
} PosExpr;

#if 0
static void
debug_print_exprs (PosExpr *exprs,
                   int      n_exprs)
{
  int i;

  for (i = 0; i < n_exprs; i++)
    {
      switch (exprs[i].type)
        {
        case POS_EXPR_INT:
          g_print (" %d", exprs[i].d.int_val);
          break;
        case POS_EXPR_DOUBLE:
          g_print (" %g", exprs[i].d.double_val);
          break;
        case POS_EXPR_OPERATOR:
          g_print (" %s", op_name (exprs[i].d.operator));
          break;
        }
    }
  g_print ("\n");
}
#endif

static gboolean
do_operation (PosExpr *a,
              PosExpr *b,
              PosOperatorType op,
              GError **err)
{
  /* Promote types to double if required */
  if (a->type == POS_EXPR_DOUBLE ||
      b->type == POS_EXPR_DOUBLE)
    {
      if (a->type != POS_EXPR_DOUBLE)
        {
          a->type = POS_EXPR_DOUBLE;
          a->d.double_val = a->d.int_val;
        }
      if (b->type != POS_EXPR_DOUBLE)
        {
          b->type = POS_EXPR_DOUBLE;
          b->d.double_val = b->d.int_val;
        }
    }

  g_assert (a->type == b->type);

  if (a->type == POS_EXPR_INT)
    {
      switch (op)
        {
        case POS_OP_MULTIPLY:
          a->d.int_val = a->d.int_val * b->d.int_val;
          break;
        case POS_OP_DIVIDE:
          if (b->d.int_val == 0)
            {
              g_set_error (err, META_THEME_ERROR,
                           META_THEME_ERROR_DIVIDE_BY_ZERO,
                           _("Coordinate expression results in division by zero"));
              return FALSE;
            }
          a->d.int_val = a->d.int_val / b->d.int_val;
          break;
        case POS_OP_MOD:
          if (b->d.int_val == 0)
            {
              g_set_error (err, META_THEME_ERROR,
                           META_THEME_ERROR_DIVIDE_BY_ZERO,
                           _("Coordinate expression results in division by zero"));
              return FALSE;
            }
          a->d.int_val = a->d.int_val % b->d.int_val;
          break;
        case POS_OP_ADD:
          a->d.int_val = a->d.int_val + b->d.int_val;
          break;
        case POS_OP_SUBTRACT:
          a->d.int_val = a->d.int_val - b->d.int_val;
          break;
        case POS_OP_MAX:
          a->d.int_val = MAX (a->d.int_val, b->d.int_val);
          break;
        case POS_OP_MIN:
          a->d.int_val = MIN (a->d.int_val, b->d.int_val);
          break;
        case POS_OP_NONE:
          g_assert_not_reached ();
          break;
        }
    }
  else if (a->type == POS_EXPR_DOUBLE)
    {
      switch (op)
        {
        case POS_OP_MULTIPLY:
          a->d.double_val = a->d.double_val * b->d.double_val;
          break;
        case POS_OP_DIVIDE:
          if (b->d.double_val == 0.0)
            {
              g_set_error (err, META_THEME_ERROR,
                           META_THEME_ERROR_DIVIDE_BY_ZERO,
                           _("Coordinate expression results in division by zero"));
              return FALSE;
            }
          a->d.double_val = a->d.double_val / b->d.double_val;
          break;
        case POS_OP_MOD:
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_MOD_ON_FLOAT,
                       _("Coordinate expression tries to use mod operator on a floating-point number"));
          return FALSE;
        case POS_OP_ADD:
          a->d.double_val = a->d.double_val + b->d.double_val;
          break;
        case POS_OP_SUBTRACT:
          a->d.double_val = a->d.double_val - b->d.double_val;
          break;
        case POS_OP_MAX:
          a->d.double_val = MAX (a->d.double_val, b->d.double_val);
          break;
        case POS_OP_MIN:
          a->d.double_val = MIN (a->d.double_val, b->d.double_val);
          break;
        case POS_OP_NONE:
          g_assert_not_reached ();
          break;
        }
    }
  else
    g_assert_not_reached ();

  return TRUE;
}

static gboolean
do_operations (PosExpr *exprs,
               int     *n_exprs,
               int      precedence,
               GError **err)
{
  int i;

#if 0
  g_print ("Doing prec %d ops on %d exprs\n", precedence, *n_exprs);
  debug_print_exprs (exprs, *n_exprs);
#endif

  i = 1;
  while (i < *n_exprs)
    {
      gboolean compress;

      /* exprs[i-1] first operand
       * exprs[i]   operator
       * exprs[i+1] second operand
       *
       * we replace first operand with result of mul/div/mod,
       * or skip over operator and second operand if we have
       * an add/subtract
       */

      if (exprs[i-1].type == POS_EXPR_OPERATOR)
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Coordinate expression has an operator \"%s\" where an operand was expected"),
                       op_name (exprs[i-1].d.operator));
          return FALSE;
        }

      if (exprs[i].type != POS_EXPR_OPERATOR)
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Coordinate expression had an operand where an operator was expected"));
          return FALSE;
        }

      if (i == (*n_exprs - 1))
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Coordinate expression ended with an operator instead of an operand"));
          return FALSE;
        }

      g_assert ((i+1) < *n_exprs);

      if (exprs[i+1].type == POS_EXPR_OPERATOR)
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Coordinate expression has operator \"%c\" following operator \"%c\" with no operand in between"),
                       exprs[i+1].d.operator,
                       exprs[i].d.operator);
          return FALSE;
        }

      compress = FALSE;

      switch (precedence)
        {
        case 2:
          switch (exprs[i].d.operator)
            {
            case POS_OP_DIVIDE:
            case POS_OP_MOD:
            case POS_OP_MULTIPLY:
              compress = TRUE;
              if (!do_operation (&exprs[i-1], &exprs[i+1],
                                 exprs[i].d.operator,
                                 err))
                return FALSE;
              break;
            }
          break;
        case 1:
          switch (exprs[i].d.operator)
            {
            case POS_OP_ADD:
            case POS_OP_SUBTRACT:
              compress = TRUE;
              if (!do_operation (&exprs[i-1], &exprs[i+1],
                                 exprs[i].d.operator,
                                 err))
                return FALSE;
              break;
            }
          break;
          /* I have no rationale at all for making these low-precedence */
        case 0:
          switch (exprs[i].d.operator)
            {
            case POS_OP_MAX:
            case POS_OP_MIN:
              compress = TRUE;
              if (!do_operation (&exprs[i-1], &exprs[i+1],
                                 exprs[i].d.operator,
                                 err))
                return FALSE;
              break;
            }
          break;
        }

      if (compress)
        {
          /* exprs[i-1] first operand (now result)
           * exprs[i]   operator
           * exprs[i+1] second operand
           * exprs[i+2] new operator
           *
           * we move new operator just after first operand
           */
          if ((i+2) < *n_exprs)
            {
              g_memmove (&exprs[i], &exprs[i+2],
                         sizeof (PosExpr) * (*n_exprs - i - 2));
            }

          *n_exprs -= 2;
        }
      else
        {
          /* Skip operator and next operand */
          i += 2;
        }
    }

  return TRUE;
}

/**
 * There is a predefined set of variables which can appear in an expression.
 * Here we take a token representing a variable, and return the current value
 * of that variable in a particular environment.
 * (The value is always an integer.)
 *
 * There are supposedly some circumstances in which this function can be
 * called from outside Marco, in which case env->theme will be NULL, and
 * therefore we can't use it to find out quark values, so we do the comparison
 * using strcmp, which is slower.
 *
 * \param t  The token representing a variable
 * \param[out] result  The value of that variable; not set if the token did
 *                     not represent a known variable
 * \param env  The environment within which t should be evaluated
 * \param[out] err  set to the problem if there was a problem
 *
 * \return true if we found the variable asked for, false if we didn't
 *
 * \bug shouldn't t be const?
 * \bug we should perhaps consider some sort of lookup arrangement into an
 *      array; also, the duplication of code is unlovely; perhaps using glib
 *      string hashes instead of quarks would fix both problems?
 * \ingroup parser
 */
static gboolean
pos_eval_get_variable (PosToken                  *t,
                       int                       *result,
                       const MetaPositionExprEnv *env,
                       GError                   **err)
{
  if (env->theme)
    {
      if (t->d.v.name_quark == env->theme->quark_width)
        *result = env->rect.width;
      else if (t->d.v.name_quark == env->theme->quark_height)
        *result = env->rect.height;
      else if (env->object_width >= 0 &&
               t->d.v.name_quark == env->theme->quark_object_width)
        *result = env->object_width;
      else if (env->object_height >= 0 &&
               t->d.v.name_quark == env->theme->quark_object_height)
        *result = env->object_height;
      else if (t->d.v.name_quark == env->theme->quark_left_width)
        *result = env->left_width;
      else if (t->d.v.name_quark == env->theme->quark_right_width)
        *result = env->right_width;
      else if (t->d.v.name_quark == env->theme->quark_top_height)
        *result = env->top_height;
      else if (t->d.v.name_quark == env->theme->quark_bottom_height)
        *result = env->bottom_height;
      else if (t->d.v.name_quark == env->theme->quark_mini_icon_width)
        *result = env->mini_icon_width;
      else if (t->d.v.name_quark == env->theme->quark_mini_icon_height)
        *result = env->mini_icon_height;
      else if (t->d.v.name_quark == env->theme->quark_icon_width)
        *result = env->icon_width;
      else if (t->d.v.name_quark == env->theme->quark_icon_height)
        *result = env->icon_height;
      else if (t->d.v.name_quark == env->theme->quark_title_width)
        *result = env->title_width;
      else if (t->d.v.name_quark == env->theme->quark_title_height)
        *result = env->title_height;
      else
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_UNKNOWN_VARIABLE,
                       _("Coordinate expression had unknown variable or constant \"%s\""),
                       t->d.v.name);
          return FALSE;
        }
    }
  else
    {
      if (strcmp (t->d.v.name, "width") == 0)
        *result = env->rect.width;
      else if (strcmp (t->d.v.name, "height") == 0)
        *result = env->rect.height;
      else if (env->object_width >= 0 &&
               strcmp (t->d.v.name, "object_width") == 0)
        *result = env->object_width;
      else if (env->object_height >= 0 &&
               strcmp (t->d.v.name, "object_height") == 0)
        *result = env->object_height;
      else if (strcmp (t->d.v.name, "left_width") == 0)
        *result = env->left_width;
      else if (strcmp (t->d.v.name, "right_width") == 0)
        *result = env->right_width;
      else if (strcmp (t->d.v.name, "top_height") == 0)
        *result = env->top_height;
      else if (strcmp (t->d.v.name, "bottom_height") == 0)
        *result = env->bottom_height;
      else if (strcmp (t->d.v.name, "mini_icon_width") == 0)
        *result = env->mini_icon_width;
      else if (strcmp (t->d.v.name, "mini_icon_height") == 0)
        *result = env->mini_icon_height;
      else if (strcmp (t->d.v.name, "icon_width") == 0)
        *result = env->icon_width;
      else if (strcmp (t->d.v.name, "icon_height") == 0)
        *result = env->icon_height;
      else if (strcmp (t->d.v.name, "title_width") == 0)
        *result = env->title_width;
      else if (strcmp (t->d.v.name, "title_height") == 0)
        *result = env->title_height;
      else
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_UNKNOWN_VARIABLE,
                       _("Coordinate expression had unknown variable or constant \"%s\""),
                       t->d.v.name);
          return FALSE;
        }
    }

  return TRUE;
}

/**
 * Evaluates a sequence of tokens within a particular environment context,
 * and returns the current value. May recur if parantheses are found.
 *
 * \param tokens  A list of tokens to evaluate.
 * \param n_tokens  How many tokens are in the list.
 * \param env  The environment context in which to evaluate the expression.
 * \param[out] result  The current value of the expression
 *
 * \bug Yes, we really do reparse the expression every time it's evaluated.
 *      We should keep the parse tree around all the time and just
 *      run the new values through it.
 * \ingroup parser
 */
static gboolean
pos_eval_helper (PosToken                   *tokens,
                 int                         n_tokens,
                 const MetaPositionExprEnv  *env,
                 PosExpr                    *result,
                 GError                    **err)
{
  /* Lazy-ass hardcoded limit on number of terms in expression */
#define MAX_EXPRS 32
  int paren_level;
  int first_paren;
  int i;
  PosExpr exprs[MAX_EXPRS];
  int n_exprs;
  int precedence;

  /* Our first goal is to get a list of PosExpr, essentially
   * substituting variables and handling parentheses.
   */

  first_paren = 0;
  paren_level = 0;
  n_exprs = 0;
  for (i = 0; i < n_tokens; i++)
    {
      PosToken *t = &tokens[i];

      if (n_exprs >= MAX_EXPRS)
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Coordinate expression parser overflowed its buffer."));
          return FALSE;
        }

      if (paren_level == 0)
        {
          switch (t->type)
            {
            case POS_TOKEN_INT:
              exprs[n_exprs].type = POS_EXPR_INT;
              exprs[n_exprs].d.int_val = t->d.i.val;
              ++n_exprs;
              break;

            case POS_TOKEN_DOUBLE:
              exprs[n_exprs].type = POS_EXPR_DOUBLE;
              exprs[n_exprs].d.double_val = t->d.d.val;
              ++n_exprs;
              break;

            case POS_TOKEN_OPEN_PAREN:
              ++paren_level;
              if (paren_level == 1)
                first_paren = i;
              break;

            case POS_TOKEN_CLOSE_PAREN:
              g_set_error (err, META_THEME_ERROR,
                           META_THEME_ERROR_BAD_PARENS,
                           _("Coordinate expression had a close parenthesis with no open parenthesis"));
              return FALSE;

            case POS_TOKEN_VARIABLE:
              exprs[n_exprs].type = POS_EXPR_INT;

              /* FIXME we should just dump all this crap
               * in a hash, maybe keep width/height out
               * for optimization purposes
               */
              if (!pos_eval_get_variable (t, &exprs[n_exprs].d.int_val, env, err))
                return FALSE;

              ++n_exprs;
              break;

            case POS_TOKEN_OPERATOR:
              exprs[n_exprs].type = POS_EXPR_OPERATOR;
              exprs[n_exprs].d.operator = t->d.o.op;
              ++n_exprs;
              break;
            }
        }
      else
        {
          g_assert (paren_level > 0);

          switch (t->type)
            {
            case POS_TOKEN_INT:
            case POS_TOKEN_DOUBLE:
            case POS_TOKEN_VARIABLE:
            case POS_TOKEN_OPERATOR:
              break;

            case POS_TOKEN_OPEN_PAREN:
              ++paren_level;
              break;

            case POS_TOKEN_CLOSE_PAREN:
              if (paren_level == 1)
                {
                  /* We closed a toplevel paren group, so recurse */
                  if (!pos_eval_helper (&tokens[first_paren+1],
                                        i - first_paren - 1,
                                        env,
                                        &exprs[n_exprs],
                                        err))
                    return FALSE;

                  ++n_exprs;
                }

              --paren_level;
              break;

            }
        }
    }

  if (paren_level > 0)
    {
      g_set_error (err, META_THEME_ERROR,
                   META_THEME_ERROR_BAD_PARENS,
                   _("Coordinate expression had an open parenthesis with no close parenthesis"));
      return FALSE;
    }

  /* Now we have no parens and no vars; so we just do all the multiplies
   * and divides, then all the add and subtract.
   */
  if (n_exprs == 0)
    {
      g_set_error (err, META_THEME_ERROR,
                   META_THEME_ERROR_FAILED,
                   _("Coordinate expression doesn't seem to have any operators or operands"));
      return FALSE;
    }

  /* precedence 1 ops */
  precedence = 2;
  while (precedence >= 0)
    {
      if (!do_operations (exprs, &n_exprs, precedence, err))
        return FALSE;
      --precedence;
    }

  g_assert (n_exprs == 1);

  *result = *exprs;

  return TRUE;
}

/*
 *   expr = int | double | expr * expr | expr / expr |
 *          expr + expr | expr - expr | (expr)
 *
 *   so very not worth fooling with bison, yet so very painful by hand.
 */
/**
 * Evaluates an expression.
 *
 * \param spec  The expression to evaluate.
 * \param env   The environment context to evaluate the expression in.
 * \param[out] val_p  The integer value of the expression; if the expression
 *                    is of type float, this will be rounded. If we return
 *                    FALSE because the expression is invalid, this will be
 *                    zero.
 * \param[out] err    The error, if anything went wrong.
 *
 * \return  True if we evaluated the expression successfully; false otherwise.
 *
 * \bug Shouldn't spec be const?
 * \ingroup parser
 */
static gboolean
pos_eval (MetaDrawSpec              *spec,
          const MetaPositionExprEnv *env,
          int                       *val_p,
          GError                   **err)
{
  PosExpr expr;

  *val_p = 0;

  if (pos_eval_helper (spec->tokens, spec->n_tokens, env, &expr, err))
    {
      switch (expr.type)
        {
        case POS_EXPR_INT:
          *val_p = expr.d.int_val;
          break;
        case POS_EXPR_DOUBLE:
          *val_p = expr.d.double_val;
          break;
        case POS_EXPR_OPERATOR:
          g_assert_not_reached ();
          break;
        }
      return TRUE;
    }
  else
    {
      return FALSE;
    }
}

/* We always return both X and Y, but only one will be meaningful in
 * most contexts.
 */

gboolean
meta_parse_position_expression (MetaDrawSpec              *spec,
                                const MetaPositionExprEnv *env,
                                int                       *x_return,
                                int                       *y_return,
                                GError                   **err)
{
  /* All positions are in a coordinate system with x, y at the origin.
   * The expression can have -, +, *, / as operators, floating point
   * or integer constants, and the variables "width" and "height" and
   * optionally "object_width" and object_height". Negative numbers
   * aren't allowed.
   */
  int val;

  if (spec->constant)
    val = spec->value;
  else
    {
      if (pos_eval (spec, env, &spec->value, err) == FALSE)
        {
          g_assert (err == NULL || *err != NULL);
          return FALSE;
        }

      val = spec->value;
    }

  if (x_return)
    *x_return = env->rect.x + val;
  if (y_return)
    *y_return = env->rect.y + val;

  return TRUE;
}


gboolean
meta_parse_size_expression (MetaDrawSpec              *spec,
                            const MetaPositionExprEnv *env,
                            int                       *val_return,
                            GError                   **err)
{
  int val;

  if (spec->constant)
    val = spec->value;
  else
    {
      if (pos_eval (spec, env, &spec->value, err) == FALSE)
        {
          g_assert (err == NULL || *err != NULL);
          return FALSE;
        }

      val = spec->value;
    }

  if (val_return)
    *val_return = MAX (val, 1); /* require that sizes be at least 1x1 */

  return TRUE;
}

/* To do this we tokenize, replace variable tokens
 * that are constants, then reassemble. The purpose
 * here is to optimize expressions so we don't do hash
 * lookups to eval them. Obviously it's a tradeoff that
 * slows down theme load times.
 */
gboolean
meta_theme_replace_constants (MetaTheme   *theme,
                              PosToken    *tokens,
                              int          n_tokens,
                              GError     **err)
{
  int i;
  double dval;
  int ival;
  gboolean is_constant = TRUE;

  /* Loop through tokenized string looking for variables to replace */
  for (i = 0; i < n_tokens; i++)
    {
      PosToken *t = &tokens[i];

      if (t->type == POS_TOKEN_VARIABLE)
        {
          if (meta_theme_lookup_int_constant (theme, t->d.v.name, &ival))
            {
              g_free (t->d.v.name);
              t->type = POS_TOKEN_INT;
              t->d.i.val = ival;
            }
          else if (meta_theme_lookup_float_constant (theme, t->d.v.name, &dval))
            {
              g_free (t->d.v.name);
              t->type = POS_TOKEN_DOUBLE;
              t->d.d.val = dval;
            }
          else
            {
              /* If we've found a variable that cannot be replaced then the
                 expression is not a constant expression and we want to
                 replace it with a GQuark */

              t->d.v.name_quark = g_quark_from_string (t->d.v.name);
              is_constant = FALSE;
            }
        }
    }

  return is_constant;
}

static int
parse_x_position_unchecked (MetaDrawSpec              *spec,
                            const MetaPositionExprEnv *env)
{
  int retval;
  GError *error;

  retval = 0;
  error = NULL;
  if (!meta_parse_position_expression (spec, env, &retval, NULL, &error))
    {
      meta_warning (_("Theme contained an expression that resulted in an error: %s\n"),
                    error->message);

      g_error_free (error);
    }

  return retval;
}

static int
parse_y_position_unchecked (MetaDrawSpec              *spec,
                            const MetaPositionExprEnv *env)
{
  int retval;
  GError *error;

  retval = 0;
  error = NULL;
  if (!meta_parse_position_expression (spec, env, NULL, &retval, &error))
    {
      meta_warning (_("Theme contained an expression that resulted in an error: %s\n"),
                    error->message);

      g_error_free (error);
    }

  return retval;
}

static int
parse_size_unchecked (MetaDrawSpec        *spec,
                      MetaPositionExprEnv *env)
{
  int retval;
  GError *error;

  retval = 0;
  error = NULL;
  if (!meta_parse_size_expression (spec, env, &retval, &error))
    {
      meta_warning (_("Theme contained an expression that resulted in an error: %s\n"),
                    error->message);

      g_error_free (error);
    }

  return retval;
}

void
meta_draw_spec_free (MetaDrawSpec *spec)
{
  if (!spec) return;
  free_tokens (spec->tokens, spec->n_tokens);
  g_slice_free (MetaDrawSpec, spec);
}

MetaDrawSpec *
meta_draw_spec_new (MetaTheme  *theme,
                    const char *expr,
                    GError    **error)
{
  MetaDrawSpec *spec;

  spec = g_slice_new0 (MetaDrawSpec);

  pos_tokenize (expr, &spec->tokens, &spec->n_tokens, NULL);

  spec->constant = meta_theme_replace_constants (theme, spec->tokens,
                                                 spec->n_tokens, NULL);
  if (spec->constant)
    {
      gboolean result;

      result = pos_eval (spec, NULL, &spec->value, error);
      if (result == FALSE)
        {
          meta_draw_spec_free (spec);
          return NULL;
        }
    }

  return spec;
}

MetaDrawOp*
meta_draw_op_new (MetaDrawType type)
{
  MetaDrawOp *op;
  MetaDrawOp dummy;
  int size;

  size = G_STRUCT_OFFSET (MetaDrawOp, data);

  switch (type)
    {
    case META_DRAW_LINE:
      size += sizeof (dummy.data.line);
      break;

    case META_DRAW_RECTANGLE:
      size += sizeof (dummy.data.rectangle);
      break;

    case META_DRAW_ARC:
      size += sizeof (dummy.data.arc);
      break;

    case META_DRAW_CLIP:
      size += sizeof (dummy.data.clip);
      break;

    case META_DRAW_TINT:
      size += sizeof (dummy.data.tint);
      break;

    case META_DRAW_GRADIENT:
      size += sizeof (dummy.data.gradient);
      break;

    case META_DRAW_IMAGE:
      size += sizeof (dummy.data.image);
      break;

    case META_DRAW_GTK_ARROW:
      size += sizeof (dummy.data.gtk_arrow);
      break;

    case META_DRAW_GTK_BOX:
      size += sizeof (dummy.data.gtk_box);
      break;

    case META_DRAW_GTK_VLINE:
      size += sizeof (dummy.data.gtk_vline);
      break;

    case META_DRAW_ICON:
      size += sizeof (dummy.data.icon);
      break;

    case META_DRAW_TITLE:
      size += sizeof (dummy.data.title);
      break;
    case META_DRAW_OP_LIST:
      size += sizeof (dummy.data.op_list);
      break;
    case META_DRAW_TILE:
      size += sizeof (dummy.data.tile);
      break;
    }

  op = g_malloc0 (size);

  op->type = type;

  return op;
}

void
meta_draw_op_free (MetaDrawOp *op)
{
  g_return_if_fail (op != NULL);

  switch (op->type)
    {
    case META_DRAW_LINE:
      if (op->data.line.color_spec)
        meta_color_spec_free (op->data.line.color_spec);

      meta_draw_spec_free (op->data.line.x1);
      meta_draw_spec_free (op->data.line.y1);
      meta_draw_spec_free (op->data.line.x2);
      meta_draw_spec_free (op->data.line.y2);
      break;

    case META_DRAW_RECTANGLE:
      if (op->data.rectangle.color_spec)
        g_free (op->data.rectangle.color_spec);

      meta_draw_spec_free (op->data.rectangle.x);
      meta_draw_spec_free (op->data.rectangle.y);
      meta_draw_spec_free (op->data.rectangle.width);
      meta_draw_spec_free (op->data.rectangle.height);
      break;

    case META_DRAW_ARC:
      if (op->data.arc.color_spec)
        g_free (op->data.arc.color_spec);

      meta_draw_spec_free (op->data.arc.x);
      meta_draw_spec_free (op->data.arc.y);
      meta_draw_spec_free (op->data.arc.width);
      meta_draw_spec_free (op->data.arc.height);
      break;

    case META_DRAW_CLIP:
      meta_draw_spec_free (op->data.clip.x);
      meta_draw_spec_free (op->data.clip.y);
      meta_draw_spec_free (op->data.clip.width);
      meta_draw_spec_free (op->data.clip.height);
      break;

    case META_DRAW_TINT:
      if (op->data.tint.color_spec)
        meta_color_spec_free (op->data.tint.color_spec);

      if (op->data.tint.alpha_spec)
        meta_alpha_gradient_spec_free (op->data.tint.alpha_spec);

      meta_draw_spec_free (op->data.tint.x);
      meta_draw_spec_free (op->data.tint.y);
      meta_draw_spec_free (op->data.tint.width);
      meta_draw_spec_free (op->data.tint.height);
      break;

    case META_DRAW_GRADIENT:
      if (op->data.gradient.gradient_spec)
        meta_gradient_spec_free (op->data.gradient.gradient_spec);

      if (op->data.gradient.alpha_spec)
        meta_alpha_gradient_spec_free (op->data.gradient.alpha_spec);

      meta_draw_spec_free (op->data.gradient.x);
      meta_draw_spec_free (op->data.gradient.y);
      meta_draw_spec_free (op->data.gradient.width);
      meta_draw_spec_free (op->data.gradient.height);
      break;

    case META_DRAW_IMAGE:
      if (op->data.image.alpha_spec)
        meta_alpha_gradient_spec_free (op->data.image.alpha_spec);

      if (op->data.image.pixbuf)
        g_object_unref (G_OBJECT (op->data.image.pixbuf));

      if (op->data.image.colorize_spec)
	meta_color_spec_free (op->data.image.colorize_spec);

      if (op->data.image.colorize_cache_pixbuf)
        g_object_unref (G_OBJECT (op->data.image.colorize_cache_pixbuf));

      meta_draw_spec_free (op->data.image.x);
      meta_draw_spec_free (op->data.image.y);
      meta_draw_spec_free (op->data.image.width);
      meta_draw_spec_free (op->data.image.height);
      break;

    case META_DRAW_GTK_ARROW:
      meta_draw_spec_free (op->data.gtk_arrow.x);
      meta_draw_spec_free (op->data.gtk_arrow.y);
      meta_draw_spec_free (op->data.gtk_arrow.width);
      meta_draw_spec_free (op->data.gtk_arrow.height);
      break;

    case META_DRAW_GTK_BOX:
      meta_draw_spec_free (op->data.gtk_box.x);
      meta_draw_spec_free (op->data.gtk_box.y);
      meta_draw_spec_free (op->data.gtk_box.width);
      meta_draw_spec_free (op->data.gtk_box.height);
      break;

    case META_DRAW_GTK_VLINE:
      meta_draw_spec_free (op->data.gtk_vline.x);
      meta_draw_spec_free (op->data.gtk_vline.y1);
      meta_draw_spec_free (op->data.gtk_vline.y2);
      break;

    case META_DRAW_ICON:
      if (op->data.icon.alpha_spec)
        meta_alpha_gradient_spec_free (op->data.icon.alpha_spec);

      meta_draw_spec_free (op->data.icon.x);
      meta_draw_spec_free (op->data.icon.y);
      meta_draw_spec_free (op->data.icon.width);
      meta_draw_spec_free (op->data.icon.height);
      break;

    case META_DRAW_TITLE:
      if (op->data.title.color_spec)
        meta_color_spec_free (op->data.title.color_spec);

      meta_draw_spec_free (op->data.title.x);
      meta_draw_spec_free (op->data.title.y);
      break;

    case META_DRAW_OP_LIST:
      if (op->data.op_list.op_list)
        meta_draw_op_list_unref (op->data.op_list.op_list);

      meta_draw_spec_free (op->data.op_list.x);
      meta_draw_spec_free (op->data.op_list.y);
      meta_draw_spec_free (op->data.op_list.width);
      meta_draw_spec_free (op->data.op_list.height);
      break;

    case META_DRAW_TILE:
      if (op->data.tile.op_list)
        meta_draw_op_list_unref (op->data.tile.op_list);

      meta_draw_spec_free (op->data.tile.x);
      meta_draw_spec_free (op->data.tile.y);
      meta_draw_spec_free (op->data.tile.width);
      meta_draw_spec_free (op->data.tile.height);
      meta_draw_spec_free (op->data.tile.tile_xoffset);
      meta_draw_spec_free (op->data.tile.tile_yoffset);
      meta_draw_spec_free (op->data.tile.tile_width);
      meta_draw_spec_free (op->data.tile.tile_height);
      break;
    }

  g_free (op);
}

static GdkPixbuf*
apply_alpha (GdkPixbuf             *pixbuf,
             MetaAlphaGradientSpec *spec,
             gboolean               force_copy)
{
  GdkPixbuf *new_pixbuf;
  gboolean needs_alpha;

  g_return_val_if_fail (GDK_IS_PIXBUF (pixbuf), NULL);

  needs_alpha = spec && (spec->n_alphas > 1 ||
                         spec->alphas[0] != 0xff);

  if (!needs_alpha)
    return pixbuf;

  if (!gdk_pixbuf_get_has_alpha (pixbuf))
    {
      new_pixbuf = gdk_pixbuf_add_alpha (pixbuf, FALSE, 0, 0, 0);
      g_object_unref (G_OBJECT (pixbuf));
      pixbuf = new_pixbuf;
    }
  else if (force_copy)
    {
      new_pixbuf = gdk_pixbuf_copy (pixbuf);
      g_object_unref (G_OBJECT (pixbuf));
      pixbuf = new_pixbuf;
    }

  g_assert (gdk_pixbuf_get_has_alpha (pixbuf));

  meta_gradient_add_alpha (pixbuf, spec->alphas, spec->n_alphas, spec->type);

  return pixbuf;
}

static GdkPixbuf*
pixbuf_tile (GdkPixbuf *tile,
             int        width,
             int        height)
{
  GdkPixbuf *pixbuf;
  int tile_width;
  int tile_height;
  int i, j;

  tile_width = gdk_pixbuf_get_width (tile);
  tile_height = gdk_pixbuf_get_height (tile);

  pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
                           gdk_pixbuf_get_has_alpha (tile),
                           8, width, height);

  i = 0;
  while (i < width)
    {
      j = 0;
      while (j < height)
        {
          int w, h;

          w = MIN (tile_width, width - i);
          h = MIN (tile_height, height - j);

          gdk_pixbuf_copy_area (tile,
                                0, 0,
                                w, h,
                                pixbuf,
                                i, j);

          j += tile_height;
        }

      i += tile_width;
    }

  return pixbuf;
}

static GdkPixbuf *
replicate_rows (GdkPixbuf  *src,
                int         src_x,
                int         src_y,
                int         width,
                int         height)
{
  unsigned int n_channels = gdk_pixbuf_get_n_channels (src);
  unsigned int src_rowstride = gdk_pixbuf_get_rowstride (src);
  unsigned char *pixels = (gdk_pixbuf_get_pixels (src) + src_y * src_rowstride + src_x
                           * n_channels);
  unsigned char *dest_pixels;
  GdkPixbuf *result;
  unsigned int dest_rowstride;
  int i;

  result = gdk_pixbuf_new (GDK_COLORSPACE_RGB, n_channels == 4, 8,
                           width, height);
  dest_rowstride = gdk_pixbuf_get_rowstride (result);
  dest_pixels = gdk_pixbuf_get_pixels (result);

  for (i = 0; i < height; i++)
    memcpy (dest_pixels + dest_rowstride * i, pixels, n_channels * width);

  return result;
}

static GdkPixbuf *
replicate_cols (GdkPixbuf  *src,
                int         src_x,
                int         src_y,
                int         width,
                int         height)
{
  unsigned int n_channels = gdk_pixbuf_get_n_channels (src);
  unsigned int src_rowstride = gdk_pixbuf_get_rowstride (src);
  unsigned char *pixels = (gdk_pixbuf_get_pixels (src) + src_y * src_rowstride + src_x
                           * n_channels);
  unsigned char *dest_pixels;
  GdkPixbuf *result;
  unsigned int dest_rowstride;
  int i, j;

  result = gdk_pixbuf_new (GDK_COLORSPACE_RGB, n_channels == 4, 8,
                           width, height);
  dest_rowstride = gdk_pixbuf_get_rowstride (result);
  dest_pixels = gdk_pixbuf_get_pixels (result);

  for (i = 0; i < height; i++)
    {
      unsigned char *p = dest_pixels + dest_rowstride * i;
      unsigned char *q = pixels + src_rowstride * i;

      unsigned char r = *(q++);
      unsigned char g = *(q++);
      unsigned char b = *(q++);

      if (n_channels == 4)
        {
          unsigned char a;

          a = *(q++);

          for (j = 0; j < width; j++)
            {
              *(p++) = r;
              *(p++) = g;
              *(p++) = b;
              *(p++) = a;
            }
        }
      else
        {
          for (j = 0; j < width; j++)
            {
              *(p++) = r;
              *(p++) = g;
              *(p++) = b;
            }
        }
    }

  return result;
}

static GdkPixbuf*
scale_and_alpha_pixbuf (GdkPixbuf             *src,
                        MetaAlphaGradientSpec *alpha_spec,
                        MetaImageFillType      fill_type,
                        int                    width,
                        int                    height,
                        gboolean               vertical_stripes,
                        gboolean               horizontal_stripes)
{
  GdkPixbuf *pixbuf;
  GdkPixbuf *temp_pixbuf;

  pixbuf = NULL;

  pixbuf = src;

  if (gdk_pixbuf_get_width (pixbuf) == width &&
      gdk_pixbuf_get_height (pixbuf) == height)
    {
      g_object_ref (G_OBJECT (pixbuf));
    }
  else
    {
      if (fill_type == META_IMAGE_FILL_TILE)
        {
          pixbuf = pixbuf_tile (pixbuf, width, height);
        }
      else
        {
    	  int src_h, src_w, dest_h, dest_w;
          src_h = gdk_pixbuf_get_height (src);
          src_w = gdk_pixbuf_get_width (src);

          /* prefer to replicate_cols if possible, as that
           * is faster (no memory reads)
           */
          if (horizontal_stripes)
            {
              dest_w = gdk_pixbuf_get_width (src);
              dest_h = height;
            }
          else if (vertical_stripes)
            {
              dest_w = width;
              dest_h = gdk_pixbuf_get_height (src);
            }

          else
            {
              dest_w = width;
              dest_h = height;
            }

          if (dest_w == src_w && dest_h == src_h)
            {
              temp_pixbuf = src;
              g_object_ref (G_OBJECT (temp_pixbuf));
            }
          else
            {
              temp_pixbuf = gdk_pixbuf_scale_simple (src,
                                                     dest_w, dest_h,
                                                     GDK_INTERP_BILINEAR);
            }

          /* prefer to replicate_cols if possible, as that
           * is faster (no memory reads)
           */
          if (horizontal_stripes)
            {
              pixbuf = replicate_cols (temp_pixbuf, 0, 0, width, height);
              g_object_unref (G_OBJECT (temp_pixbuf));
            }
          else if (vertical_stripes)
            {
              pixbuf = replicate_rows (temp_pixbuf, 0, 0, width, height);
              g_object_unref (G_OBJECT (temp_pixbuf));
            }
          else
            {
              pixbuf = temp_pixbuf;
            }
        }
    }

  if (pixbuf)
    pixbuf = apply_alpha (pixbuf, alpha_spec, pixbuf == src);

  return pixbuf;
}

static GdkPixbuf*
draw_op_as_pixbuf (const MetaDrawOp    *op,
                   GtkWidget           *widget,
                   const MetaDrawInfo  *info,
                   int                  width,
                   int                  height)
{
  /* Try to get the op as a pixbuf, assuming w/h in the op
   * matches the width/height passed in. return NULL
   * if the op can't be converted to an equivalent pixbuf.
   */
  GdkPixbuf *pixbuf;

  pixbuf = NULL;

  switch (op->type)
    {
    case META_DRAW_LINE:
      break;

    case META_DRAW_RECTANGLE:
      if (op->data.rectangle.filled)
        {
          GdkColor color;

          meta_color_spec_render (op->data.rectangle.color_spec,
                                  widget,
                                  &color);

          pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
                                   FALSE,
                                   8, width, height);

          gdk_pixbuf_fill (pixbuf, GDK_COLOR_RGBA (color));
        }
      break;

    case META_DRAW_ARC:
      break;

    case META_DRAW_CLIP:
      break;

    case META_DRAW_TINT:
      {
        GdkColor color;
        guint32 rgba;
        gboolean has_alpha;

        meta_color_spec_render (op->data.rectangle.color_spec,
                                widget,
                                &color);

        has_alpha =
          op->data.tint.alpha_spec &&
          (op->data.tint.alpha_spec->n_alphas > 1 ||
           op->data.tint.alpha_spec->alphas[0] != 0xff);

        pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
                                 has_alpha,
                                 8, width, height);

        if (!has_alpha)
          {
            rgba = GDK_COLOR_RGBA (color);

            gdk_pixbuf_fill (pixbuf, rgba);
          }
        else if (op->data.tint.alpha_spec->n_alphas == 1)
          {
            rgba = GDK_COLOR_RGBA (color);
            rgba &= ~0xff;
            rgba |= op->data.tint.alpha_spec->alphas[0];

            gdk_pixbuf_fill (pixbuf, rgba);
          }
        else
          {
            rgba = GDK_COLOR_RGBA (color);

            gdk_pixbuf_fill (pixbuf, rgba);

            meta_gradient_add_alpha (pixbuf,
                                     op->data.tint.alpha_spec->alphas,
                                     op->data.tint.alpha_spec->n_alphas,
                                     op->data.tint.alpha_spec->type);
          }
      }
      break;

    case META_DRAW_GRADIENT:
      {
        pixbuf = meta_gradient_spec_render (op->data.gradient.gradient_spec,
                                            widget, width, height);

        pixbuf = apply_alpha (pixbuf,
                              op->data.gradient.alpha_spec,
                              FALSE);
      }
      break;


    case META_DRAW_IMAGE:
      {
	if (op->data.image.colorize_spec)
	  {
	    GdkColor color;

            meta_color_spec_render (op->data.image.colorize_spec,
                                    widget, &color);

            if (op->data.image.colorize_cache_pixbuf == NULL ||
                op->data.image.colorize_cache_pixel != GDK_COLOR_RGB (color))
              {
                if (op->data.image.colorize_cache_pixbuf)
                  g_object_unref (G_OBJECT (op->data.image.colorize_cache_pixbuf));

                /* const cast here */
                ((MetaDrawOp*)op)->data.image.colorize_cache_pixbuf =
                  colorize_pixbuf (op->data.image.pixbuf,
                                   &color);
                ((MetaDrawOp*)op)->data.image.colorize_cache_pixel =
                  GDK_COLOR_RGB (color);
              }

            if (op->data.image.colorize_cache_pixbuf)
              {
                pixbuf = scale_and_alpha_pixbuf (op->data.image.colorize_cache_pixbuf,
                                                 op->data.image.alpha_spec,
                                                 op->data.image.fill_type,
                                                 width, height,
                                                 op->data.image.vertical_stripes,
                                                 op->data.image.horizontal_stripes);
              }
	  }
	else
	  {
	    pixbuf = scale_and_alpha_pixbuf (op->data.image.pixbuf,
                                             op->data.image.alpha_spec,
                                             op->data.image.fill_type,
                                             width, height,
                                             op->data.image.vertical_stripes,
                                             op->data.image.horizontal_stripes);
	  }
        break;
      }

    case META_DRAW_GTK_ARROW:
    case META_DRAW_GTK_BOX:
    case META_DRAW_GTK_VLINE:
      break;

    case META_DRAW_ICON:
      if (info->mini_icon &&
          width <= gdk_pixbuf_get_width (info->mini_icon) &&
          height <= gdk_pixbuf_get_height (info->mini_icon))
        pixbuf = scale_and_alpha_pixbuf (info->mini_icon,
                                         op->data.icon.alpha_spec,
                                         op->data.icon.fill_type,
                                         width, height,
                                         FALSE, FALSE);
      else if (info->icon)
        pixbuf = scale_and_alpha_pixbuf (info->icon,
                                         op->data.icon.alpha_spec,
                                         op->data.icon.fill_type,
                                         width, height,
                                         FALSE, FALSE);
      break;

    case META_DRAW_TITLE:
      break;

    case META_DRAW_OP_LIST:
      break;

    case META_DRAW_TILE:
      break;
    }

  return pixbuf;
}

static void
fill_env (MetaPositionExprEnv *env,
          const MetaDrawInfo  *info,
          MetaRectangle        logical_region)
{
  /* FIXME this stuff could be raised into draw_op_list_draw() probably
   */
  env->rect = logical_region;
  env->object_width = -1;
  env->object_height = -1;
  if (info->fgeom)
    {
      env->left_width = info->fgeom->left_width;
      env->right_width = info->fgeom->right_width;
      env->top_height = info->fgeom->top_height;
      env->bottom_height = info->fgeom->bottom_height;
    }
  else
    {
      env->left_width = 0;
      env->right_width = 0;
      env->top_height = 0;
      env->bottom_height = 0;
    }

  env->mini_icon_width = info->mini_icon ? gdk_pixbuf_get_width (info->mini_icon) : 0;
  env->mini_icon_height = info->mini_icon ? gdk_pixbuf_get_height (info->mini_icon) : 0;
  env->icon_width = info->icon ? gdk_pixbuf_get_width (info->icon) : 0;
  env->icon_height = info->icon ? gdk_pixbuf_get_height (info->icon) : 0;

  env->title_width = info->title_layout_width;
  env->title_height = info->title_layout_height;
  env->theme = meta_current_theme;
}

/* This code was originally rendering anti-aliased using X primitives, and
 * now has been switched to draw anti-aliased using cairo. In general, the
 * closest correspondence between X rendering and cairo rendering is given
 * by offsetting the geometry by 0.5 pixels in both directions before rendering
 * with cairo. This is because X samples at the upper left corner of the
 * pixel while cairo averages over the entire pixel. However, in the cases
 * where the X rendering was an exact rectangle with no "jaggies"
 * we need to be a bit careful about applying the offset. We want to produce
 * the exact same pixel-aligned rectangle, rather than a rectangle with
 * fuzz around the edges.
 */
static void
meta_draw_op_draw_with_env (const MetaDrawOp    *op,
                            GtkStyle            *style_gtk,
                            GtkWidget           *widget,
                            GdkDrawable         *drawable,
                            const GdkRectangle  *clip,
                            const MetaDrawInfo  *info,
                            MetaRectangle        rect,
                            MetaPositionExprEnv *env)
{
  GdkColor color;
  cairo_t *cr;

  cr = gdk_cairo_create (drawable);

  cairo_set_line_width (cr, 1.0);

  if (clip)
    {
      gdk_cairo_rectangle (cr, clip);
      cairo_clip (cr);
    }

  switch (op->type)
    {
    case META_DRAW_LINE:
      {
        int x1, x2, y1, y2;

        meta_color_spec_render (op->data.line.color_spec, widget, &color);
        gdk_cairo_set_source_color (cr, &color);

        if (op->data.line.width > 0)
          cairo_set_line_width (cr, op->data.line.width);

        if (op->data.line.dash_on_length > 0 &&
            op->data.line.dash_off_length > 0)
          {
            double dash_list[2];
            dash_list[0] = op->data.line.dash_on_length;
            dash_list[1] = op->data.line.dash_off_length;
            cairo_set_dash (cr, dash_list, 2, 0);
          }

        x1 = parse_x_position_unchecked (op->data.line.x1, env);
        y1 = parse_y_position_unchecked (op->data.line.y1, env);

        if (!op->data.line.x2 &&
            !op->data.line.y2 &&
            op->data.line.width==0)
          {
            cairo_rectangle (cr, x1, y1, 1, 1);
            cairo_fill (cr);
          }
        else
          {
            if (op->data.line.x2)
              x2 = parse_x_position_unchecked (op->data.line.x2, env);
            else
              x2 = x1;

            if (op->data.line.y2)
              y2 = parse_y_position_unchecked (op->data.line.y2, env);
            else
              y2 = y1;

            /* This is one of the cases where we are matching the exact
             * pixel aligned rectangle produced by X.
             */
            if (y1 == y2 || x1 == x2)
              {
                double offset = (op->data.line.width == 0 ||
                                 op->data.line.width % 2) ? .5 : 0;
                /* X includes end points for lines of width 0 */
                double line_extend = op->data.line.width == 0 ? 1. : 0.;

                if (y1 == y2)
                  {
                    if (x2 < x1)
                    {
                       x1 ^= x2;
                       x2 ^= x1;
                       x1 ^= x2;
                    }
                    cairo_move_to (cr, x1, y1 + offset);
                    cairo_line_to (cr, x2 + line_extend, y2 + offset);
                  }
                else
                  {
                    if (y2 < y1)
                    {
                      y1 ^= y2;
                      y2 ^= y1;
                      y1 ^= y2;
                    }
                    cairo_move_to (cr, x1 + offset, y1);
                    cairo_line_to (cr, x2 + offset, y2 + line_extend);
                  }
              }
            else
              {
                if (op->data.line.width <= 0)
                  {
                    cairo_set_line_cap (cr, CAIRO_LINE_CAP_SQUARE);
                  }
                cairo_move_to (cr, x1 + .5, y1 + .5);
                cairo_line_to (cr, x2 + .5, y2 + .5);
              }
            cairo_stroke (cr);
          }
      }
      break;

    case META_DRAW_RECTANGLE:
      {
        int rx, ry, rwidth, rheight;

        meta_color_spec_render (op->data.rectangle.color_spec, widget, &color);
        gdk_cairo_set_source_color (cr, &color);

        rx = parse_x_position_unchecked (op->data.rectangle.x, env);
        ry = parse_y_position_unchecked (op->data.rectangle.y, env);
        rwidth = parse_size_unchecked (op->data.rectangle.width, env);
        rheight = parse_size_unchecked (op->data.rectangle.height, env);

        /* Filled and stroked rectangles are the other cases
         * we pixel-align to X rasterization
         */
        if (op->data.rectangle.filled)
          {
            cairo_rectangle (cr, rx, ry, rwidth, rheight);
            cairo_fill (cr);
          }
        else
          {
            cairo_rectangle (cr, rx + .5, ry + .5, rwidth, rheight);
            cairo_stroke (cr);
          }
      }
      break;

    case META_DRAW_ARC:
      {
        int rx, ry, rwidth, rheight;
        double start_angle, end_angle;
        double center_x, center_y;

        meta_color_spec_render (op->data.arc.color_spec, widget, &color);
        gdk_cairo_set_source_color (cr, &color);

        rx = parse_x_position_unchecked (op->data.arc.x, env);
        ry = parse_y_position_unchecked (op->data.arc.y, env);
        rwidth = parse_size_unchecked (op->data.arc.width, env);
        rheight = parse_size_unchecked (op->data.arc.height, env);

        start_angle = op->data.arc.start_angle * (M_PI / 180.)
                      - (.25 * M_PI); /* start at 12 instead of 3 oclock */
        end_angle = start_angle + op->data.arc.extent_angle * (M_PI / 180.);
        center_x = rx + (double)rwidth / 2. + .5;
        center_y = ry + (double)rheight / 2. + .5;

        cairo_save (cr);

        cairo_translate (cr, center_x, center_y);
        cairo_scale (cr, (double)rwidth / 2., (double)rheight / 2.);

        if (op->data.arc.extent_angle >= 0)
          cairo_arc (cr, 0, 0, 1, start_angle, end_angle);
        else
          cairo_arc_negative (cr, 0, 0, 1, start_angle, end_angle);

        cairo_restore (cr);

        if (op->data.arc.filled)
          {
            cairo_line_to (cr, center_x, center_y);
            cairo_fill (cr);
          }
        else
          cairo_stroke (cr);
      }
      break;

    case META_DRAW_CLIP:
      break;

    case META_DRAW_TINT:
      {
        int rx, ry, rwidth, rheight;
        gboolean needs_alpha;

        needs_alpha = op->data.tint.alpha_spec &&
          (op->data.tint.alpha_spec->n_alphas > 1 ||
           op->data.tint.alpha_spec->alphas[0] != 0xff);

        rx = parse_x_position_unchecked (op->data.tint.x, env);
        ry = parse_y_position_unchecked (op->data.tint.y, env);
        rwidth = parse_size_unchecked (op->data.tint.width, env);
        rheight = parse_size_unchecked (op->data.tint.height, env);

        if (!needs_alpha)
          {
            meta_color_spec_render (op->data.tint.color_spec, widget, &color);
            gdk_cairo_set_source_color (cr, &color);

            cairo_rectangle (cr, rx, ry, rwidth, rheight);
            cairo_fill (cr);
          }
        else
          {
            GdkPixbuf *pixbuf;

            pixbuf = draw_op_as_pixbuf (op, widget, info,
                                        rwidth, rheight);

            if (pixbuf)
              {
                gdk_cairo_set_source_pixbuf (cr, pixbuf, rx, ry);
                cairo_paint (cr);

                g_object_unref (G_OBJECT (pixbuf));
              }
          }
      }
      break;

    case META_DRAW_GRADIENT:
      {
        int rx, ry, rwidth, rheight;
        GdkPixbuf *pixbuf;

        rx = parse_x_position_unchecked (op->data.gradient.x, env);
        ry = parse_y_position_unchecked (op->data.gradient.y, env);
        rwidth = parse_size_unchecked (op->data.gradient.width, env);
        rheight = parse_size_unchecked (op->data.gradient.height, env);

        pixbuf = draw_op_as_pixbuf (op, widget, info,
                                    rwidth, rheight);

        if (pixbuf)
          {
            gdk_cairo_set_source_pixbuf (cr, pixbuf, rx, ry);
            cairo_paint (cr);

            g_object_unref (G_OBJECT (pixbuf));
          }
      }
      break;

    case META_DRAW_IMAGE:
      {
        int rx, ry, rwidth, rheight;
        GdkPixbuf *pixbuf;

        if (op->data.image.pixbuf)
          {
            env->object_width = gdk_pixbuf_get_width (op->data.image.pixbuf);
            env->object_height = gdk_pixbuf_get_height (op->data.image.pixbuf);
          }

        rwidth = parse_size_unchecked (op->data.image.width, env);
        rheight = parse_size_unchecked (op->data.image.height, env);

        pixbuf = draw_op_as_pixbuf (op, widget, info,
                                    rwidth, rheight);

        if (pixbuf)
          {
            rx = parse_x_position_unchecked (op->data.image.x, env);
            ry = parse_y_position_unchecked (op->data.image.y, env);

            gdk_cairo_set_source_pixbuf (cr, pixbuf, rx, ry);
            cairo_paint (cr);

            g_object_unref (G_OBJECT (pixbuf));
          }
      }
      break;

    case META_DRAW_GTK_ARROW:
      {
        int rx, ry, rwidth, rheight;

        rx = parse_x_position_unchecked (op->data.gtk_arrow.x, env);
        ry = parse_y_position_unchecked (op->data.gtk_arrow.y, env);
        rwidth = parse_size_unchecked (op->data.gtk_arrow.width, env);
        rheight = parse_size_unchecked (op->data.gtk_arrow.height, env);

        gtk_paint_arrow (style_gtk,
                         drawable,
                         op->data.gtk_arrow.state,
                         op->data.gtk_arrow.shadow,
                         (GdkRectangle*) clip,
                         widget,
                         "marco",
                         op->data.gtk_arrow.arrow,
                         op->data.gtk_arrow.filled,
                         rx, ry, rwidth, rheight);
      }
      break;

    case META_DRAW_GTK_BOX:
      {
        int rx, ry, rwidth, rheight;

        rx = parse_x_position_unchecked (op->data.gtk_box.x, env);
        ry = parse_y_position_unchecked (op->data.gtk_box.y, env);
        rwidth = parse_size_unchecked (op->data.gtk_box.width, env);
        rheight = parse_size_unchecked (op->data.gtk_box.height, env);

        gtk_paint_box (style_gtk,
                       drawable,
                       op->data.gtk_box.state,
                       op->data.gtk_box.shadow,
                       (GdkRectangle*) clip,
                       widget,
                       "marco",
                       rx, ry, rwidth, rheight);
      }
      break;

    case META_DRAW_GTK_VLINE:
      {
        int rx, ry1, ry2;

        rx = parse_x_position_unchecked (op->data.gtk_vline.x, env);
        ry1 = parse_y_position_unchecked (op->data.gtk_vline.y1, env);
        ry2 = parse_y_position_unchecked (op->data.gtk_vline.y2, env);

        gtk_paint_vline (style_gtk,
                         drawable,
                         op->data.gtk_vline.state,
                         (GdkRectangle*) clip,
                         widget,
                         "marco",
                         ry1, ry2, rx);
      }
      break;

    case META_DRAW_ICON:
      {
        int rx, ry, rwidth, rheight;
        GdkPixbuf *pixbuf;

        rwidth = parse_size_unchecked (op->data.icon.width, env);
        rheight = parse_size_unchecked (op->data.icon.height, env);

        pixbuf = draw_op_as_pixbuf (op, widget, info,
                                    rwidth, rheight);

        if (pixbuf)
          {
            rx = parse_x_position_unchecked (op->data.icon.x, env);
            ry = parse_y_position_unchecked (op->data.icon.y, env);

            gdk_cairo_set_source_pixbuf (cr, pixbuf, rx, ry);
            cairo_paint (cr);

            g_object_unref (G_OBJECT (pixbuf));
          }
      }
      break;

    case META_DRAW_TITLE:
      if (info->title_layout)
        {
          int rx, ry;

          meta_color_spec_render (op->data.title.color_spec, widget, &color);
          gdk_cairo_set_source_color (cr, &color);

          rx = parse_x_position_unchecked (op->data.title.x, env);
          ry = parse_y_position_unchecked (op->data.title.y, env);

          cairo_move_to (cr, rx, ry);
          pango_cairo_show_layout (cr, info->title_layout);
        }
      break;

    case META_DRAW_OP_LIST:
      {
        MetaRectangle d_rect;

        d_rect.x = parse_x_position_unchecked (op->data.op_list.x, env);
        d_rect.y = parse_y_position_unchecked (op->data.op_list.y, env);
        d_rect.width = parse_size_unchecked (op->data.op_list.width, env);
        d_rect.height = parse_size_unchecked (op->data.op_list.height, env);

        meta_draw_op_list_draw_with_style (op->data.op_list.op_list,
                                           style_gtk, widget, drawable, clip, info,
                                           d_rect);
      }
      break;

    case META_DRAW_TILE:
      {
        int rx, ry, rwidth, rheight;
        int tile_xoffset, tile_yoffset;
        GdkRectangle new_clip;
        MetaRectangle tile;

        rx = parse_x_position_unchecked (op->data.tile.x, env);
        ry = parse_y_position_unchecked (op->data.tile.y, env);
        rwidth = parse_size_unchecked (op->data.tile.width, env);
        rheight = parse_size_unchecked (op->data.tile.height, env);

        new_clip.x = rx;
        new_clip.y = ry;
        new_clip.width = rwidth;
        new_clip.height = rheight;

        if (clip == NULL || gdk_rectangle_intersect ((GdkRectangle*)clip, &new_clip,
                                                     &new_clip))
          {
            tile_xoffset = parse_x_position_unchecked (op->data.tile.tile_xoffset, env);
            tile_yoffset = parse_y_position_unchecked (op->data.tile.tile_yoffset, env);
            /* tile offset should not include x/y */
            tile_xoffset -= rect.x;
            tile_yoffset -= rect.y;

            tile.width = parse_size_unchecked (op->data.tile.tile_width, env);
            tile.height = parse_size_unchecked (op->data.tile.tile_height, env);

            tile.x = rx - tile_xoffset;

            while (tile.x < (rx + rwidth))
              {
                tile.y = ry - tile_yoffset;
                while (tile.y < (ry + rheight))
                  {
                    meta_draw_op_list_draw_with_style (op->data.tile.op_list,
                                                       style_gtk, widget, drawable, &new_clip, info,
                                                       tile);

                    tile.y += tile.height;
                  }

                tile.x += tile.width;
              }
          }
      }
      break;
    }

  cairo_destroy (cr);
}

void
meta_draw_op_draw_with_style (const MetaDrawOp    *op,
                              GtkStyle            *style_gtk,
                              GtkWidget           *widget,
                              GdkDrawable         *drawable,
                              const GdkRectangle  *clip,
                              const MetaDrawInfo  *info,
                              MetaRectangle        logical_region)
{
  MetaPositionExprEnv env;

  g_return_if_fail (style_gtk->colormap == gdk_drawable_get_colormap (drawable));

  fill_env (&env, info, logical_region);

  meta_draw_op_draw_with_env (op, style_gtk, widget, drawable, clip,
                              info, logical_region,
                              &env);

}

void
meta_draw_op_draw (const MetaDrawOp    *op,
                   GtkWidget           *widget,
                   GdkDrawable         *drawable,
                   const GdkRectangle  *clip,
                   const MetaDrawInfo  *info,
                   MetaRectangle        logical_region)
{
  meta_draw_op_draw_with_style (op, gtk_widget_get_style (widget), widget,
                                drawable, clip, info, logical_region);
}

MetaDrawOpList*
meta_draw_op_list_new (int n_preallocs)
{
  MetaDrawOpList *op_list;

  g_return_val_if_fail (n_preallocs >= 0, NULL);

  op_list = g_new (MetaDrawOpList, 1);

  op_list->refcount = 1;
  op_list->n_allocated = n_preallocs;
  op_list->ops = g_new (MetaDrawOp*, op_list->n_allocated);
  op_list->n_ops = 0;

  return op_list;
}

void
meta_draw_op_list_ref (MetaDrawOpList *op_list)
{
  g_return_if_fail (op_list != NULL);

  op_list->refcount += 1;
}

void
meta_draw_op_list_unref (MetaDrawOpList *op_list)
{
  g_return_if_fail (op_list != NULL);
  g_return_if_fail (op_list->refcount > 0);

  op_list->refcount -= 1;

  if (op_list->refcount == 0)
    {
      int i;

      for (i = 0; i < op_list->n_ops; i++)
        meta_draw_op_free (op_list->ops[i]);

      g_free (op_list->ops);

      DEBUG_FILL_STRUCT (op_list);
      g_free (op_list);
    }
}

void
meta_draw_op_list_draw_with_style  (const MetaDrawOpList *op_list,
                                    GtkStyle             *style_gtk,
                                    GtkWidget            *widget,
                                    GdkDrawable          *drawable,
                                    const GdkRectangle   *clip,
                                    const MetaDrawInfo   *info,
                                    MetaRectangle         rect)
{
  int i;
  GdkRectangle active_clip;
  GdkRectangle orig_clip;
  MetaPositionExprEnv env;

  g_return_if_fail (style_gtk->colormap == gdk_drawable_get_colormap (drawable));

  if (op_list->n_ops == 0)
    return;

  fill_env (&env, info, rect);

  /* FIXME this can be optimized, potentially a lot, by
   * compressing multiple ops when possible. For example,
   * anything convertible to a pixbuf can be composited
   * client-side, and putting a color tint over a pixbuf
   * can be done without creating the solid-color pixbuf.
   *
   * To implement this my plan is to have the idea of a
   * compiled draw op (with the string expressions already
   * evaluated), we make an array of those, and then fold
   * adjacent items when possible.
   */
  if (clip)
    {
      orig_clip = *clip;
    }
  else
    {
      orig_clip.x = rect.x;
      orig_clip.y = rect.y;
      orig_clip.width = rect.width;
      orig_clip.height = rect.height;
    }

  active_clip = orig_clip;

  for (i = 0; i < op_list->n_ops; i++)
    {
      MetaDrawOp *op = op_list->ops[i];

      if (op->type == META_DRAW_CLIP)
        {
          active_clip.x = parse_x_position_unchecked (op->data.clip.x, &env);
          active_clip.y = parse_y_position_unchecked (op->data.clip.y, &env);
          active_clip.width = parse_size_unchecked (op->data.clip.width, &env);
          active_clip.height = parse_size_unchecked (op->data.clip.height, &env);

          gdk_rectangle_intersect (&orig_clip, &active_clip, &active_clip);
        }
      else if (active_clip.width > 0 &&
               active_clip.height > 0)
        {
          meta_draw_op_draw_with_env (op,
                                      style_gtk, widget, drawable, &active_clip, info,
                                      rect,
                                      &env);
        }
    }
}

void
meta_draw_op_list_draw  (const MetaDrawOpList *op_list,
                         GtkWidget            *widget,
                         GdkDrawable          *drawable,
                         const GdkRectangle   *clip,
                         const MetaDrawInfo   *info,
                         MetaRectangle         rect)

{
  meta_draw_op_list_draw_with_style (op_list, gtk_widget_get_style (widget), widget,
                                     drawable, clip, info, rect);
}

void
meta_draw_op_list_append (MetaDrawOpList       *op_list,
                          MetaDrawOp           *op)
{
  if (op_list->n_ops == op_list->n_allocated)
    {
      op_list->n_allocated *= 2;
      op_list->ops = g_renew (MetaDrawOp*, op_list->ops, op_list->n_allocated);
    }

  op_list->ops[op_list->n_ops] = op;
  op_list->n_ops += 1;
}

gboolean
meta_draw_op_list_validate (MetaDrawOpList    *op_list,
                            GError           **error)
{
  g_return_val_if_fail (op_list != NULL, FALSE);

  /* empty lists are OK, nothing else to check really */

  return TRUE;
}

/* This is not done in validate, since we wouldn't know the name
 * of the list to report the error. It might be nice to
 * store names inside the list sometime.
 */
gboolean
meta_draw_op_list_contains (MetaDrawOpList    *op_list,
                            MetaDrawOpList    *child)
{
  int i;

  /* mmm, huge tree recursion */

  for (i = 0; i < op_list->n_ops; i++)
    {
      if (op_list->ops[i]->type == META_DRAW_OP_LIST)
        {
          if (op_list->ops[i]->data.op_list.op_list == child)
            return TRUE;

          if (meta_draw_op_list_contains (op_list->ops[i]->data.op_list.op_list,
                                          child))
            return TRUE;
        }
      else if (op_list->ops[i]->type == META_DRAW_TILE)
        {
          if (op_list->ops[i]->data.tile.op_list == child)
            return TRUE;

          if (meta_draw_op_list_contains (op_list->ops[i]->data.tile.op_list,
                                          child))
            return TRUE;
        }
    }

  return FALSE;
}

/**
 * Constructor for a MetaFrameStyle.
 *
 * \param parent  The parent style. Data not filled in here will be
 *                looked for in the parent style, and in its parent
 *                style, and so on.
 *
 * \return The newly-constructed style.
 */
MetaFrameStyle*
meta_frame_style_new (MetaFrameStyle *parent)
{
  MetaFrameStyle *style;

  style = g_new0 (MetaFrameStyle, 1);

  style->refcount = 1;

  /* Default alpha is fully opaque */
  style->window_background_alpha = 255;

  style->parent = parent;
  if (parent)
    meta_frame_style_ref (parent);

  return style;
}

/**
 * Increases the reference count of a frame style.
 * If the style is NULL, this is a no-op.
 *
 * \param style  The style.
 */
void
meta_frame_style_ref (MetaFrameStyle *style)
{
  g_return_if_fail (style != NULL);

  style->refcount += 1;
}

static void
free_button_ops (MetaDrawOpList *op_lists[META_BUTTON_TYPE_LAST][META_BUTTON_STATE_LAST])
{
  int i, j;

  for (i = 0; i < META_BUTTON_TYPE_LAST; i++)
    for (j = 0; j < META_BUTTON_STATE_LAST; j++)
      if (op_lists[i][j])
        meta_draw_op_list_unref (op_lists[i][j]);
}

void
meta_frame_style_unref (MetaFrameStyle *style)
{
  g_return_if_fail (style != NULL);
  g_return_if_fail (style->refcount > 0);

  style->refcount -= 1;

  if (style->refcount == 0)
    {
      int i;

      free_button_ops (style->buttons);

      for (i = 0; i < META_FRAME_PIECE_LAST; i++)
        if (style->pieces[i])
          meta_draw_op_list_unref (style->pieces[i]);

      if (style->layout)
        meta_frame_layout_unref (style->layout);

      if (style->window_background_color)
        meta_color_spec_free (style->window_background_color);

      /* we hold a reference to any parent style */
      if (style->parent)
        meta_frame_style_unref (style->parent);

      DEBUG_FILL_STRUCT (style);
      g_free (style);
    }
}

static MetaDrawOpList*
get_button (MetaFrameStyle *style,
            MetaButtonType  type,
            MetaButtonState state)
{
  MetaDrawOpList *op_list;
  MetaFrameStyle *parent;

  parent = style;
  op_list = NULL;
  while (parent && op_list == NULL)
    {
      op_list = parent->buttons[type][state];
      parent = parent->parent;
    }

  /* We fall back to middle button backgrounds if we don't
   * have the ones on the sides
   */

  if (op_list == NULL &&
      (type == META_BUTTON_TYPE_LEFT_LEFT_BACKGROUND ||
       type == META_BUTTON_TYPE_LEFT_RIGHT_BACKGROUND))
    return get_button (style, META_BUTTON_TYPE_LEFT_MIDDLE_BACKGROUND,
                       state);

  if (op_list == NULL &&
      (type == META_BUTTON_TYPE_RIGHT_LEFT_BACKGROUND ||
       type == META_BUTTON_TYPE_RIGHT_RIGHT_BACKGROUND))
    return get_button (style, META_BUTTON_TYPE_RIGHT_MIDDLE_BACKGROUND,
                       state);

  /* We fall back to normal if no prelight */
  if (op_list == NULL &&
      state == META_BUTTON_STATE_PRELIGHT)
    return get_button (style, type, META_BUTTON_STATE_NORMAL);

  return op_list;
}

gboolean
meta_frame_style_validate (MetaFrameStyle    *style,
                           guint              current_theme_version,
                           GError           **error)
{
  int i, j;

  g_return_val_if_fail (style != NULL, FALSE);
  g_return_val_if_fail (style->layout != NULL, FALSE);

  for (i = 0; i < META_BUTTON_TYPE_LAST; i++)
    {
      /* for now the "positional" buttons are optional */
      if (i >= META_BUTTON_TYPE_CLOSE)
        {
          for (j = 0; j < META_BUTTON_STATE_LAST; j++)
            {
              if (get_button (style, i, j) == NULL &&
                  meta_theme_earliest_version_with_button (i) <= current_theme_version
                  )
                {
                  g_set_error (error, META_THEME_ERROR,
                               META_THEME_ERROR_FAILED,
                               _("<button function=\"%s\" state=\"%s\" draw_ops=\"whatever\"/> must be specified for this frame style"),
                               meta_button_type_to_string (i),
                               meta_button_state_to_string (j));
                  return FALSE;
                }
            }
        }
    }

  return TRUE;
}

static void
button_rect (MetaButtonType           type,
             const MetaFrameGeometry *fgeom,
             int                      middle_background_offset,
             GdkRectangle            *rect)
{
  switch (type)
    {
    case META_BUTTON_TYPE_LEFT_LEFT_BACKGROUND:
      *rect = fgeom->left_left_background;
      break;

    case META_BUTTON_TYPE_LEFT_MIDDLE_BACKGROUND:
      *rect = fgeom->left_middle_backgrounds[middle_background_offset];
      break;

    case META_BUTTON_TYPE_LEFT_RIGHT_BACKGROUND:
      *rect = fgeom->left_right_background;
      break;

    case META_BUTTON_TYPE_RIGHT_LEFT_BACKGROUND:
      *rect = fgeom->right_left_background;
      break;

    case META_BUTTON_TYPE_RIGHT_MIDDLE_BACKGROUND:
      *rect = fgeom->right_middle_backgrounds[middle_background_offset];
      break;

    case META_BUTTON_TYPE_RIGHT_RIGHT_BACKGROUND:
      *rect = fgeom->right_right_background;
      break;

    case META_BUTTON_TYPE_CLOSE:
      *rect = fgeom->close_rect.visible;
      break;

    case META_BUTTON_TYPE_SHADE:
      *rect = fgeom->shade_rect.visible;
      break;

    case META_BUTTON_TYPE_UNSHADE:
      *rect = fgeom->unshade_rect.visible;
      break;

    case META_BUTTON_TYPE_ABOVE:
      *rect = fgeom->above_rect.visible;
      break;

    case META_BUTTON_TYPE_UNABOVE:
      *rect = fgeom->unabove_rect.visible;
      break;

    case META_BUTTON_TYPE_STICK:
      *rect = fgeom->stick_rect.visible;
      break;

    case META_BUTTON_TYPE_UNSTICK:
      *rect = fgeom->unstick_rect.visible;
      break;

    case META_BUTTON_TYPE_MAXIMIZE:
      *rect = fgeom->max_rect.visible;
      break;

    case META_BUTTON_TYPE_MINIMIZE:
      *rect = fgeom->min_rect.visible;
      break;

    case META_BUTTON_TYPE_MENU:
      *rect = fgeom->menu_rect.visible;
      break;

    case META_BUTTON_TYPE_LAST:
      g_assert_not_reached ();
      break;
    }
}

void
meta_frame_style_draw_with_style (MetaFrameStyle          *style,
                                  GtkStyle                *style_gtk,
                                  GtkWidget               *widget,
                                  GdkDrawable             *drawable,
                                  int                      x_offset,
                                  int                      y_offset,
                                  const GdkRectangle      *clip,
                                  const MetaFrameGeometry *fgeom,
                                  int                      client_width,
                                  int                      client_height,
                                  PangoLayout             *title_layout,
                                  int                      text_height,
                                  MetaButtonState          button_states[META_BUTTON_TYPE_LAST],
                                  GdkPixbuf               *mini_icon,
                                  GdkPixbuf               *icon)
{
  int i, j;
  GdkRectangle titlebar_rect;
  GdkRectangle left_titlebar_edge;
  GdkRectangle right_titlebar_edge;
  GdkRectangle bottom_titlebar_edge;
  GdkRectangle top_titlebar_edge;
  GdkRectangle left_edge, right_edge, bottom_edge;
  PangoRectangle extents;
  MetaDrawInfo draw_info;

  g_return_if_fail (style_gtk->colormap == gdk_drawable_get_colormap (drawable));

  titlebar_rect.x = 0;
  titlebar_rect.y = 0;
  titlebar_rect.width = fgeom->width;
  titlebar_rect.height = fgeom->top_height;

  left_titlebar_edge.x = titlebar_rect.x;
  left_titlebar_edge.y = titlebar_rect.y + fgeom->top_titlebar_edge;
  left_titlebar_edge.width = fgeom->left_titlebar_edge;
  left_titlebar_edge.height = titlebar_rect.height - fgeom->top_titlebar_edge - fgeom->bottom_titlebar_edge;

  right_titlebar_edge.y = left_titlebar_edge.y;
  right_titlebar_edge.height = left_titlebar_edge.height;
  right_titlebar_edge.width = fgeom->right_titlebar_edge;
  right_titlebar_edge.x = titlebar_rect.x + titlebar_rect.width - right_titlebar_edge.width;

  top_titlebar_edge.x = titlebar_rect.x;
  top_titlebar_edge.y = titlebar_rect.y;
  top_titlebar_edge.width = titlebar_rect.width;
  top_titlebar_edge.height = fgeom->top_titlebar_edge;

  bottom_titlebar_edge.x = titlebar_rect.x;
  bottom_titlebar_edge.width = titlebar_rect.width;
  bottom_titlebar_edge.height = fgeom->bottom_titlebar_edge;
  bottom_titlebar_edge.y = titlebar_rect.y + titlebar_rect.height - bottom_titlebar_edge.height;

  left_edge.x = 0;
  left_edge.y = fgeom->top_height;
  left_edge.width = fgeom->left_width;
  left_edge.height = fgeom->height - fgeom->top_height - fgeom->bottom_height;

  right_edge.x = fgeom->width - fgeom->right_width;
  right_edge.y = fgeom->top_height;
  right_edge.width = fgeom->right_width;
  right_edge.height = fgeom->height - fgeom->top_height - fgeom->bottom_height;

  bottom_edge.x = 0;
  bottom_edge.y = fgeom->height - fgeom->bottom_height;
  bottom_edge.width = fgeom->width;
  bottom_edge.height = fgeom->bottom_height;

  if (title_layout)
    pango_layout_get_pixel_extents (title_layout,
                                    NULL, &extents);

  draw_info.mini_icon = mini_icon;
  draw_info.icon = icon;
  draw_info.title_layout = title_layout;
  draw_info.title_layout_width = title_layout ? extents.width : 0;
  draw_info.title_layout_height = title_layout ? extents.height : 0;
  draw_info.fgeom = fgeom;

  /* The enum is in the order the pieces should be rendered. */
  i = 0;
  while (i < META_FRAME_PIECE_LAST)
    {
      GdkRectangle rect;
      GdkRectangle combined_clip;

      switch ((MetaFramePiece) i)
        {
        case META_FRAME_PIECE_ENTIRE_BACKGROUND:
          rect.x = 0;
          rect.y = 0;
          rect.width = fgeom->width;
          rect.height = fgeom->height;
          break;

        case META_FRAME_PIECE_TITLEBAR:
          rect = titlebar_rect;
          break;

        case META_FRAME_PIECE_LEFT_TITLEBAR_EDGE:
          rect = left_titlebar_edge;
          break;

        case META_FRAME_PIECE_RIGHT_TITLEBAR_EDGE:
          rect = right_titlebar_edge;
          break;

        case META_FRAME_PIECE_TOP_TITLEBAR_EDGE:
          rect = top_titlebar_edge;
          break;

        case META_FRAME_PIECE_BOTTOM_TITLEBAR_EDGE:
          rect = bottom_titlebar_edge;
          break;

        case META_FRAME_PIECE_TITLEBAR_MIDDLE:
          rect.x = left_titlebar_edge.x + left_titlebar_edge.width;
          rect.y = top_titlebar_edge.y + top_titlebar_edge.height;
          rect.width = titlebar_rect.width - left_titlebar_edge.width -
            right_titlebar_edge.width;
          rect.height = titlebar_rect.height - top_titlebar_edge.height - bottom_titlebar_edge.height;
          break;

        case META_FRAME_PIECE_TITLE:
          rect = fgeom->title_rect;
          break;

        case META_FRAME_PIECE_LEFT_EDGE:
          rect = left_edge;
          break;

        case META_FRAME_PIECE_RIGHT_EDGE:
          rect = right_edge;
          break;

        case META_FRAME_PIECE_BOTTOM_EDGE:
          rect = bottom_edge;
          break;

        case META_FRAME_PIECE_OVERLAY:
          rect.x = 0;
          rect.y = 0;
          rect.width = fgeom->width;
          rect.height = fgeom->height;
          break;

        case META_FRAME_PIECE_LAST:
          g_assert_not_reached ();
          break;
        }

      rect.x += x_offset;
      rect.y += y_offset;

      if (clip == NULL)
        combined_clip = rect;
      else
        gdk_rectangle_intersect ((GdkRectangle*) clip, /* const cast */
                                 &rect,
                                 &combined_clip);

      if (combined_clip.width > 0 && combined_clip.height > 0)
        {
          MetaDrawOpList *op_list;
          MetaFrameStyle *parent;

          parent = style;
          op_list = NULL;
          while (parent && op_list == NULL)
            {
              op_list = parent->pieces[i];
              parent = parent->parent;
            }

          if (op_list)
            {
              MetaRectangle m_rect;
              m_rect = meta_rect (rect.x, rect.y, rect.width, rect.height);
              meta_draw_op_list_draw_with_style (op_list,
                                                 style_gtk,
                                                 widget,
                                                 drawable,
                                                 &combined_clip,
                                                 &draw_info,
                                                 m_rect);
            }
        }


      /* Draw buttons just before overlay */
      if ((i + 1) == META_FRAME_PIECE_OVERLAY)
        {
          int middle_bg_offset;

          middle_bg_offset = 0;
          j = 0;
          while (j < META_BUTTON_TYPE_LAST)
            {
              button_rect (j, fgeom, middle_bg_offset, &rect);

              rect.x += x_offset;
              rect.y += y_offset;

              if (clip == NULL)
                combined_clip = rect;
              else
                gdk_rectangle_intersect ((GdkRectangle*) clip, /* const cast */
                                         &rect,
                                         &combined_clip);

              if (combined_clip.width > 0 && combined_clip.height > 0)
                {
                  MetaDrawOpList *op_list;

                  op_list = get_button (style, j, button_states[j]);

                  if (op_list)
                    {
                      MetaRectangle m_rect;
                      m_rect = meta_rect (rect.x, rect.y,
                                          rect.width, rect.height);
                      meta_draw_op_list_draw_with_style (op_list,
                                                         style_gtk,
                                                         widget,
                                                         drawable,
                                                         &combined_clip,
                                                         &draw_info,
                                                         m_rect);
                    }
                }

              /* MIDDLE_BACKGROUND type may get drawn more than once */
              if ((j == META_BUTTON_TYPE_RIGHT_MIDDLE_BACKGROUND ||
                   j == META_BUTTON_TYPE_LEFT_MIDDLE_BACKGROUND) &&
                  (middle_bg_offset < (MAX_MIDDLE_BACKGROUNDS - 1)))
                {
                  ++middle_bg_offset;
                }
              else
                {
                  middle_bg_offset = 0;
                  ++j;
                }
            }
        }

      ++i;
    }
}

void
meta_frame_style_draw (MetaFrameStyle          *style,
                       GtkWidget               *widget,
                       GdkDrawable             *drawable,
                       int                      x_offset,
                       int                      y_offset,
                       const GdkRectangle      *clip,
                       const MetaFrameGeometry *fgeom,
                       int                      client_width,
                       int                      client_height,
                       PangoLayout             *title_layout,
                       int                      text_height,
                       MetaButtonState          button_states[META_BUTTON_TYPE_LAST],
                       GdkPixbuf               *mini_icon,
                       GdkPixbuf               *icon)
{
  meta_frame_style_draw_with_style (style, gtk_widget_get_style (widget), widget,
                                    drawable, x_offset, y_offset,
                                    clip, fgeom, client_width, client_height,
                                    title_layout, text_height,
                                    button_states, mini_icon, icon);
}

MetaFrameStyleSet*
meta_frame_style_set_new (MetaFrameStyleSet *parent)
{
  MetaFrameStyleSet *style_set;

  style_set = g_new0 (MetaFrameStyleSet, 1);

  style_set->parent = parent;
  if (parent)
    meta_frame_style_set_ref (parent);

  style_set->refcount = 1;

  return style_set;
}

static void
free_focus_styles (MetaFrameStyle *focus_styles[META_FRAME_FOCUS_LAST])
{
  int i;

  for (i = 0; i < META_FRAME_FOCUS_LAST; i++)
    if (focus_styles[i])
      meta_frame_style_unref (focus_styles[i]);
}

void
meta_frame_style_set_ref (MetaFrameStyleSet *style_set)
{
  g_return_if_fail (style_set != NULL);

  style_set->refcount += 1;
}

void
meta_frame_style_set_unref (MetaFrameStyleSet *style_set)
{
  g_return_if_fail (style_set != NULL);
  g_return_if_fail (style_set->refcount > 0);

  style_set->refcount -= 1;

  if (style_set->refcount == 0)
    {
      int i;

      for (i = 0; i < META_FRAME_RESIZE_LAST; i++)
        {
          free_focus_styles (style_set->normal_styles[i]);
          free_focus_styles (style_set->shaded_styles[i]);
        }

      free_focus_styles (style_set->maximized_styles);
      free_focus_styles (style_set->maximized_and_shaded_styles);

      if (style_set->parent)
        meta_frame_style_set_unref (style_set->parent);

      DEBUG_FILL_STRUCT (style_set);
      g_free (style_set);
    }
}


static MetaFrameStyle*
get_style (MetaFrameStyleSet *style_set,
           MetaFrameState     state,
           MetaFrameResize    resize,
           MetaFrameFocus     focus)
{
  MetaFrameStyle *style;

  style = NULL;

  switch (state)
    {
    case META_FRAME_STATE_NORMAL:
    case META_FRAME_STATE_SHADED:
      {
        if (state == META_FRAME_STATE_SHADED)
          style = style_set->shaded_styles[resize][focus];
        else
          style = style_set->normal_styles[resize][focus];

        /* Try parent if we failed here */
        if (style == NULL && style_set->parent)
          style = get_style (style_set->parent, state, resize, focus);

        /* Allow people to omit the vert/horz/none resize modes */
        if (style == NULL &&
            resize != META_FRAME_RESIZE_BOTH)
          style = get_style (style_set, state, META_FRAME_RESIZE_BOTH, focus);
      }
      break;
    default:
      {
        MetaFrameStyle **styles;

        styles = NULL;

        switch (state)
          {
          case META_FRAME_STATE_MAXIMIZED:
            styles = style_set->maximized_styles;
            break;
          case META_FRAME_STATE_MAXIMIZED_AND_SHADED:
            styles = style_set->maximized_and_shaded_styles;
            break;
          case META_FRAME_STATE_NORMAL:
          case META_FRAME_STATE_SHADED:
          case META_FRAME_STATE_LAST:
            g_assert_not_reached ();
            break;
          }

        style = styles[focus];

        /* Try parent if we failed here */
        if (style == NULL && style_set->parent)
          style = get_style (style_set->parent, state, resize, focus);
      }
    }

  return style;
}

static gboolean
check_state  (MetaFrameStyleSet *style_set,
              MetaFrameState     state,
              GError           **error)
{
  int i;

  for (i = 0; i < META_FRAME_FOCUS_LAST; i++)
    {
      if (get_style (style_set, state,
                     META_FRAME_RESIZE_NONE, i) == NULL)
        {
          /* Translators: This error occurs when a <frame> tag is missing
           * in theme XML.  The "<frame ...>" is intended as a noun phrase,
           * and the "missing" qualifies it.  You should translate "whatever".
           */
          g_set_error (error, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Missing <frame state=\"%s\" resize=\"%s\" focus=\"%s\" style=\"whatever\"/>"),
                       meta_frame_state_to_string (state),
                       meta_frame_resize_to_string (META_FRAME_RESIZE_NONE),
                       meta_frame_focus_to_string (i));
          return FALSE;
        }
    }

  return TRUE;
}

gboolean
meta_frame_style_set_validate  (MetaFrameStyleSet *style_set,
                                GError           **error)
{
  int i, j;

  g_return_val_if_fail (style_set != NULL, FALSE);

  for (i = 0; i < META_FRAME_RESIZE_LAST; i++)
    for (j = 0; j < META_FRAME_FOCUS_LAST; j++)
      if (get_style (style_set, META_FRAME_STATE_NORMAL, i, j) == NULL)
        {
          g_set_error (error, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Missing <frame state=\"%s\" resize=\"%s\" focus=\"%s\" style=\"whatever\"/>"),
                       meta_frame_state_to_string (META_FRAME_STATE_NORMAL),
                       meta_frame_resize_to_string (i),
                       meta_frame_focus_to_string (j));
          return FALSE;
        }

  if (!check_state (style_set, META_FRAME_STATE_SHADED, error))
    return FALSE;

  if (!check_state (style_set, META_FRAME_STATE_MAXIMIZED, error))
    return FALSE;

  if (!check_state (style_set, META_FRAME_STATE_MAXIMIZED_AND_SHADED, error))
    return FALSE;

  return TRUE;
}

MetaTheme*
meta_theme_get_current (void)
{
  return meta_current_theme;
}

void
meta_theme_set_current (const char *name,
                        gboolean    force_reload)
{
  MetaTheme *new_theme;
  GError *err;

  meta_topic (META_DEBUG_THEMES, "Setting current theme to \"%s\"\n", name);

  if (!force_reload &&
      meta_current_theme &&
      strcmp (name, meta_current_theme->name) == 0)
    return;

  err = NULL;
  new_theme = meta_theme_load (name, &err);

  if (new_theme == NULL)
    {
      meta_warning (_("Failed to load theme \"%s\": %s\n"),
                    name, err->message);
      g_error_free (err);
    }
  else
    {
      if (meta_current_theme)
        meta_theme_free (meta_current_theme);

      meta_current_theme = new_theme;

      meta_topic (META_DEBUG_THEMES, "New theme is \"%s\"\n", meta_current_theme->name);
    }
}

MetaTheme*
meta_theme_new (void)
{
  MetaTheme *theme;

  theme = g_new0 (MetaTheme, 1);

  theme->images_by_filename =
    g_hash_table_new_full (g_str_hash,
                           g_str_equal,
                           g_free,
                           (GDestroyNotify) g_object_unref);

  theme->layouts_by_name =
    g_hash_table_new_full (g_str_hash,
                           g_str_equal,
                           g_free,
                           (GDestroyNotify) meta_frame_layout_unref);

  theme->draw_op_lists_by_name =
    g_hash_table_new_full (g_str_hash,
                           g_str_equal,
                           g_free,
                           (GDestroyNotify) meta_draw_op_list_unref);

  theme->styles_by_name =
    g_hash_table_new_full (g_str_hash,
                           g_str_equal,
                           g_free,
                           (GDestroyNotify) meta_frame_style_unref);

  theme->style_sets_by_name =
    g_hash_table_new_full (g_str_hash,
                           g_str_equal,
                           g_free,
                           (GDestroyNotify) meta_frame_style_set_unref);

  /* Create our variable quarks so we can look up variables without
     having to strcmp for the names */
  theme->quark_width = g_quark_from_static_string ("width");
  theme->quark_height = g_quark_from_static_string ("height");
  theme->quark_object_width = g_quark_from_static_string ("object_width");
  theme->quark_object_height = g_quark_from_static_string ("object_height");
  theme->quark_left_width = g_quark_from_static_string ("left_width");
  theme->quark_right_width = g_quark_from_static_string ("right_width");
  theme->quark_top_height = g_quark_from_static_string ("top_height");
  theme->quark_bottom_height = g_quark_from_static_string ("bottom_height");
  theme->quark_mini_icon_width = g_quark_from_static_string ("mini_icon_width");
  theme->quark_mini_icon_height = g_quark_from_static_string ("mini_icon_height");
  theme->quark_icon_width = g_quark_from_static_string ("icon_width");
  theme->quark_icon_height = g_quark_from_static_string ("icon_height");
  theme->quark_title_width = g_quark_from_static_string ("title_width");
  theme->quark_title_height = g_quark_from_static_string ("title_height");
  return theme;
}


void
meta_theme_free (MetaTheme *theme)
{
  int i;

  g_return_if_fail (theme != NULL);

  g_free (theme->name);
  g_free (theme->dirname);
  g_free (theme->filename);
  g_free (theme->readable_name);
  g_free (theme->date);
  g_free (theme->description);
  g_free (theme->author);
  g_free (theme->copyright);

  /* be more careful when destroying the theme hash tables,
     since they are only constructed as needed, and may be NULL. */
  if (theme->integer_constants)
    g_hash_table_destroy (theme->integer_constants);
  if (theme->images_by_filename)
    g_hash_table_destroy (theme->images_by_filename);
  if (theme->layouts_by_name)
    g_hash_table_destroy (theme->layouts_by_name);
  if (theme->draw_op_lists_by_name)
    g_hash_table_destroy (theme->draw_op_lists_by_name);
  if (theme->styles_by_name)
    g_hash_table_destroy (theme->styles_by_name);
  if (theme->style_sets_by_name)
    g_hash_table_destroy (theme->style_sets_by_name);

  for (i = 0; i < META_FRAME_TYPE_LAST; i++)
    if (theme->style_sets_by_type[i])
      meta_frame_style_set_unref (theme->style_sets_by_type[i]);

  DEBUG_FILL_STRUCT (theme);
  g_free (theme);
}

gboolean
meta_theme_validate (MetaTheme *theme,
                     GError   **error)
{
  int i;

  g_return_val_if_fail (theme != NULL, FALSE);

  /* FIXME what else should be checked? */

  g_assert (theme->name);

  if (theme->readable_name == NULL)
    {
      /* Translators: This error means that a necessary XML tag (whose name
       * is given in angle brackets) was not found in a given theme (whose
       * name is given second, in quotation marks).
       */
      g_set_error (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
                   _("No <%s> set for theme \"%s\""), "name", theme->name);
      return FALSE;
    }

  if (theme->author == NULL)
    {
      g_set_error (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
                   _("No <%s> set for theme \"%s\""), "author", theme->name);
      return FALSE;
    }

  if (theme->date == NULL)
    {
      g_set_error (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
                   _("No <%s> set for theme \"%s\""), "date", theme->name);
      return FALSE;
    }

  if (theme->description == NULL)
    {
      g_set_error (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
                   _("No <%s> set for theme \"%s\""), "description", theme->name);
      return FALSE;
    }

  if (theme->copyright == NULL)
    {
      g_set_error (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
                   _("No <%s> set for theme \"%s\""), "copyright", theme->name);
      return FALSE;
    }

  for (i = 0; i < (int)META_FRAME_TYPE_LAST; i++)
    if (theme->style_sets_by_type[i] == NULL)
      {
        g_set_error (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
                     _("No frame style set for window type \"%s\" in theme \"%s\", add a <window type=\"%s\" style_set=\"whatever\"/> element"),
                     meta_frame_type_to_string (i),
                     theme->name,
                     meta_frame_type_to_string (i));

        return FALSE;
      }

  return TRUE;
}

GdkPixbuf*
meta_theme_load_image (MetaTheme  *theme,
                       const char *filename,
                       guint size_of_theme_icons,
                       GError    **error)
{
  GdkPixbuf *pixbuf;

  pixbuf = g_hash_table_lookup (theme->images_by_filename,
                                filename);

  if (pixbuf == NULL)
    {

      if (g_str_has_prefix (filename, "theme:") &&
          META_THEME_ALLOWS (theme, META_THEME_IMAGES_FROM_ICON_THEMES))
        {
          pixbuf = gtk_icon_theme_load_icon (
              gtk_icon_theme_get_default (),
              filename+6,
              size_of_theme_icons,
              0,
              error);
          if (pixbuf == NULL) return NULL;
         }
      else
        {
          char *full_path;
          full_path = g_build_filename (theme->dirname, filename, NULL);

          pixbuf = gdk_pixbuf_new_from_file (full_path, error);
          if (pixbuf == NULL)
            {
              g_free (full_path);
              return NULL;
            }

          g_free (full_path);
        }
      g_hash_table_replace (theme->images_by_filename,
                            g_strdup (filename),
                            pixbuf);
    }

  g_assert (pixbuf);

  g_object_ref (G_OBJECT (pixbuf));

  return pixbuf;
}

static MetaFrameStyle*
theme_get_style (MetaTheme     *theme,
                 MetaFrameType  type,
                 MetaFrameFlags flags)
{
  MetaFrameState state;
  MetaFrameResize resize;
  MetaFrameFocus focus;
  MetaFrameStyle *style;
  MetaFrameStyleSet *style_set;

  style_set = theme->style_sets_by_type[type];

  /* Right now the parser forces a style set for all types,
   * but this fallback code is here in case I take that out.
   */
  if (style_set == NULL)
    style_set = theme->style_sets_by_type[META_FRAME_TYPE_NORMAL];
  if (style_set == NULL)
    return NULL;

  switch (flags & (META_FRAME_MAXIMIZED | META_FRAME_SHADED))
    {
    case 0:
      state = META_FRAME_STATE_NORMAL;
      break;
    case META_FRAME_MAXIMIZED:
      state = META_FRAME_STATE_MAXIMIZED;
      break;
    case META_FRAME_SHADED:
      state = META_FRAME_STATE_SHADED;
      break;
    case (META_FRAME_MAXIMIZED | META_FRAME_SHADED):
      state = META_FRAME_STATE_MAXIMIZED_AND_SHADED;
      break;
    default:
      g_assert_not_reached ();
      state = META_FRAME_STATE_LAST; /* compiler */
      break;
    }

  switch (flags & (META_FRAME_ALLOWS_VERTICAL_RESIZE | META_FRAME_ALLOWS_HORIZONTAL_RESIZE))
    {
    case 0:
      resize = META_FRAME_RESIZE_NONE;
      break;
    case META_FRAME_ALLOWS_VERTICAL_RESIZE:
      resize = META_FRAME_RESIZE_VERTICAL;
      break;
    case META_FRAME_ALLOWS_HORIZONTAL_RESIZE:
      resize = META_FRAME_RESIZE_HORIZONTAL;
      break;
    case (META_FRAME_ALLOWS_VERTICAL_RESIZE | META_FRAME_ALLOWS_HORIZONTAL_RESIZE):
      resize = META_FRAME_RESIZE_BOTH;
      break;
    default:
      g_assert_not_reached ();
      resize = META_FRAME_RESIZE_LAST; /* compiler */
      break;
    }

  /* re invert the styles used for focus/unfocussed while flashing a frame */
  if (((flags & META_FRAME_HAS_FOCUS) && !(flags & META_FRAME_IS_FLASHING))
      || (!(flags & META_FRAME_HAS_FOCUS) && (flags & META_FRAME_IS_FLASHING)))
    focus = META_FRAME_FOCUS_YES;
  else
    focus = META_FRAME_FOCUS_NO;

  style = get_style (style_set, state, resize, focus);

  return style;
}

MetaFrameStyle*
meta_theme_get_frame_style (MetaTheme     *theme,
                            MetaFrameType  type,
                            MetaFrameFlags flags)
{
  MetaFrameStyle *style;

  g_return_val_if_fail (type < META_FRAME_TYPE_LAST, NULL);

  style = theme_get_style (theme, type, flags);

  return style;
}

double
meta_theme_get_title_scale (MetaTheme     *theme,
                            MetaFrameType  type,
                            MetaFrameFlags flags)
{
  MetaFrameStyle *style;

  g_return_val_if_fail (type < META_FRAME_TYPE_LAST, 1.0);

  style = theme_get_style (theme, type, flags);

  /* Parser is not supposed to allow this currently */
  if (style == NULL)
    return 1.0;

  return style->layout->title_scale;
}

void
meta_theme_draw_frame_with_style (MetaTheme              *theme,
                                  GtkStyle               *style_gtk,
                                  GtkWidget              *widget,
                                  GdkDrawable            *drawable,
                                  const GdkRectangle     *clip,
                                  int                     x_offset,
                                  int                     y_offset,
                                  MetaFrameType           type,
                                  MetaFrameFlags          flags,
                                  int                     client_width,
                                  int                     client_height,
                                  PangoLayout            *title_layout,
                                  int                     text_height,
                                  const MetaButtonLayout *button_layout,
                                  MetaButtonState         button_states[META_BUTTON_TYPE_LAST],
                                  GdkPixbuf              *mini_icon,
                                  GdkPixbuf              *icon)
{
  MetaFrameGeometry fgeom;
  MetaFrameStyle *style;

  g_return_if_fail (type < META_FRAME_TYPE_LAST);

  style = theme_get_style (theme, type, flags);

  /* Parser is not supposed to allow this currently */
  if (style == NULL)
    return;

  meta_frame_layout_calc_geometry (style->layout,
                                   text_height,
                                   flags,
                                   client_width, client_height,
                                   button_layout,
                                   &fgeom,
                                   theme);

  meta_frame_style_draw_with_style (style,
                                    style_gtk,
                                    widget,
                                    drawable,
                                    x_offset, y_offset,
                                    clip,
                                    &fgeom,
                                    client_width, client_height,
                                    title_layout,
                                    text_height,
                                    button_states,
                                    mini_icon, icon);
}

void
meta_theme_draw_frame (MetaTheme              *theme,
                       GtkWidget              *widget,
                       GdkDrawable            *drawable,
                       const GdkRectangle     *clip,
                       int                     x_offset,
                       int                     y_offset,
                       MetaFrameType           type,
                       MetaFrameFlags          flags,
                       int                     client_width,
                       int                     client_height,
                       PangoLayout            *title_layout,
                       int                     text_height,
                       const MetaButtonLayout *button_layout,
                       MetaButtonState         button_states[META_BUTTON_TYPE_LAST],
                       GdkPixbuf              *mini_icon,
                       GdkPixbuf              *icon)
{
  meta_theme_draw_frame_with_style (theme, gtk_widget_get_style (widget), widget,
                                    drawable, clip, x_offset, y_offset, type,flags,
                                    client_width, client_height,
                                    title_layout, text_height,
                                    button_layout, button_states,
                                    mini_icon, icon);
}

void
meta_theme_draw_frame_by_name (MetaTheme              *theme,
                               GtkWidget              *widget,
                               GdkDrawable            *drawable,
                               const GdkRectangle     *clip,
                               int                     x_offset,
                               int                     y_offset,
                               const gchar             *style_name,
                               MetaFrameFlags          flags,
                               int                     client_width,
                               int                     client_height,
                               PangoLayout            *title_layout,
                               int                     text_height,
                               const MetaButtonLayout *button_layout,
                               MetaButtonState         button_states[META_BUTTON_TYPE_LAST],
                               GdkPixbuf              *mini_icon,
                               GdkPixbuf              *icon)
{
  MetaFrameGeometry fgeom;
  MetaFrameStyle *style;

  style = meta_theme_lookup_style (theme, style_name);

  /* Parser is not supposed to allow this currently */
  if (style == NULL)
    return;

  meta_frame_layout_calc_geometry (style->layout,
                                   text_height,
                                   flags,
                                   client_width, client_height,
                                   button_layout,
                                   &fgeom,
                                   theme);

  meta_frame_style_draw (style,
                         widget,
                         drawable,
                         x_offset, y_offset,
                         clip,
                         &fgeom,
                         client_width, client_height,
                         title_layout,
                         text_height,
                         button_states,
                         mini_icon, icon);
}

void
meta_theme_get_frame_borders (MetaTheme      *theme,
                              MetaFrameType   type,
                              int             text_height,
                              MetaFrameFlags  flags,
                              int            *top_height,
                              int            *bottom_height,
                              int            *left_width,
                              int            *right_width)
{
  MetaFrameStyle *style;

  g_return_if_fail (type < META_FRAME_TYPE_LAST);

  if (top_height)
    *top_height = 0;
  if (bottom_height)
    *bottom_height = 0;
  if (left_width)
    *left_width = 0;
  if (right_width)
    *right_width = 0;

  style = theme_get_style (theme, type, flags);

  /* Parser is not supposed to allow this currently */
  if (style == NULL)
    return;

  meta_frame_layout_get_borders (style->layout,
                                 text_height,
                                 flags,
                                 top_height, bottom_height,
                                 left_width, right_width);
}

void
meta_theme_calc_geometry (MetaTheme              *theme,
                          MetaFrameType           type,
                          int                     text_height,
                          MetaFrameFlags          flags,
                          int                     client_width,
                          int                     client_height,
                          const MetaButtonLayout *button_layout,
                          MetaFrameGeometry      *fgeom)
{
  MetaFrameStyle *style;

  g_return_if_fail (type < META_FRAME_TYPE_LAST);

  style = theme_get_style (theme, type, flags);

  /* Parser is not supposed to allow this currently */
  if (style == NULL)
    return;

  meta_frame_layout_calc_geometry (style->layout,
                                   text_height,
                                   flags,
                                   client_width, client_height,
                                   button_layout,
                                   fgeom,
                                   theme);
}

MetaFrameLayout*
meta_theme_lookup_layout (MetaTheme         *theme,
                          const char        *name)
{
  return g_hash_table_lookup (theme->layouts_by_name, name);
}

void
meta_theme_insert_layout (MetaTheme         *theme,
                          const char        *name,
                          MetaFrameLayout   *layout)
{
  meta_frame_layout_ref (layout);
  g_hash_table_replace (theme->layouts_by_name, g_strdup (name), layout);
}

MetaDrawOpList*
meta_theme_lookup_draw_op_list (MetaTheme         *theme,
                                const char        *name)
{
  return g_hash_table_lookup (theme->draw_op_lists_by_name, name);
}

void
meta_theme_insert_draw_op_list (MetaTheme         *theme,
                                const char        *name,
                                MetaDrawOpList    *op_list)
{
  meta_draw_op_list_ref (op_list);
  g_hash_table_replace (theme->draw_op_lists_by_name, g_strdup (name), op_list);
}

MetaFrameStyle*
meta_theme_lookup_style (MetaTheme         *theme,
                         const char        *name)
{
  return g_hash_table_lookup (theme->styles_by_name, name);
}

void
meta_theme_insert_style (MetaTheme         *theme,
                         const char        *name,
                         MetaFrameStyle    *style)
{
  meta_frame_style_ref (style);
  g_hash_table_replace (theme->styles_by_name, g_strdup (name), style);
}

MetaFrameStyleSet*
meta_theme_lookup_style_set (MetaTheme         *theme,
                             const char        *name)
{
  return g_hash_table_lookup (theme->style_sets_by_name, name);
}

void
meta_theme_insert_style_set    (MetaTheme         *theme,
                                const char        *name,
                                MetaFrameStyleSet *style_set)
{
  meta_frame_style_set_ref (style_set);
  g_hash_table_replace (theme->style_sets_by_name, g_strdup (name), style_set);
}

static gboolean
first_uppercase (const char *str)
{
  return g_ascii_isupper (*str);
}

gboolean
meta_theme_define_int_constant (MetaTheme   *theme,
                                const char  *name,
                                int          value,
                                GError     **error)
{
  if (theme->integer_constants == NULL)
    theme->integer_constants = g_hash_table_new_full (g_str_hash,
                                                      g_str_equal,
                                                      g_free,
                                                      NULL);

  if (!first_uppercase (name))
    {
      g_set_error (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
                   _("User-defined constants must begin with a capital letter; \"%s\" does not"),
                   name);
      return FALSE;
    }

  if (g_hash_table_lookup_extended (theme->integer_constants, name, NULL, NULL))
    {
      g_set_error (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
                   _("Constant \"%s\" has already been defined"),
                   name);

      return FALSE;
    }

  g_hash_table_insert (theme->integer_constants,
                       g_strdup (name),
                       GINT_TO_POINTER (value));

  return TRUE;
}

gboolean
meta_theme_lookup_int_constant (MetaTheme   *theme,
                                const char  *name,
                                int         *value)
{
  gpointer old_value;

  *value = 0;

  if (theme->integer_constants == NULL)
    return FALSE;

  if (g_hash_table_lookup_extended (theme->integer_constants,
                                    name, NULL, &old_value))
    {
      *value = GPOINTER_TO_INT (old_value);
      return TRUE;
    }
  else
    {
      return FALSE;
    }
}

gboolean
meta_theme_define_float_constant (MetaTheme   *theme,
                                  const char  *name,
                                  double       value,
                                  GError     **error)
{
  double *d;

  if (theme->float_constants == NULL)
    theme->float_constants = g_hash_table_new_full (g_str_hash,
                                                    g_str_equal,
                                                    g_free,
                                                    g_free);

  if (!first_uppercase (name))
    {
      g_set_error (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
                   _("User-defined constants must begin with a capital letter; \"%s\" does not"),
                   name);
      return FALSE;
    }

  if (g_hash_table_lookup_extended (theme->float_constants, name, NULL, NULL))
    {
      g_set_error (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
                   _("Constant \"%s\" has already been defined"),
                   name);

      return FALSE;
    }

  d = g_new (double, 1);
  *d = value;

  g_hash_table_insert (theme->float_constants,
                       g_strdup (name), d);

  return TRUE;
}

gboolean
meta_theme_lookup_float_constant (MetaTheme   *theme,
                                  const char  *name,
                                  double      *value)
{
  double *d;

  *value = 0.0;

  if (theme->float_constants == NULL)
    return FALSE;

  d = g_hash_table_lookup (theme->float_constants, name);

  if (d)
    {
      *value = *d;
      return TRUE;
    }
  else
    {
      return FALSE;
    }
}

gboolean
meta_theme_define_color_constant (MetaTheme   *theme,
                                  const char  *name,
                                  const char  *value,
                                  GError     **error)
{
  if (theme->color_constants == NULL)
    theme->color_constants = g_hash_table_new_full (g_str_hash,
                                                    g_str_equal,
                                                    g_free,
                                                    NULL);

  if (!first_uppercase (name))
    {
      g_set_error (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
                   _("User-defined constants must begin with a capital letter; \"%s\" does not"),
                   name);
      return FALSE;
    }

  if (g_hash_table_lookup_extended (theme->color_constants, name, NULL, NULL))
    {
      g_set_error (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
                   _("Constant \"%s\" has already been defined"),
                   name);

      return FALSE;
    }

  g_hash_table_insert (theme->color_constants,
                       g_strdup (name),
                       g_strdup (value));

  return TRUE;
}

/**
 * Looks up a colour constant.
 *
 * \param theme  the theme containing the constant
 * \param name  the name of the constant
 * \param value  [out] the string representation of the colour, or NULL if it
 *               doesn't exist
 * \return  TRUE if it exists, FALSE otherwise
 */
gboolean
meta_theme_lookup_color_constant (MetaTheme   *theme,
                                  const char  *name,
                                  char       **value)
{
  char *result;

  *value = NULL;

  if (theme->color_constants == NULL)
    return FALSE;

  result = g_hash_table_lookup (theme->color_constants, name);

  if (result)
    {
      *value = result;
      return TRUE;
    }
  else
    {
      return FALSE;
    }
}


PangoFontDescription*
meta_gtk_widget_get_font_desc (GtkWidget *widget,
                               double     scale,
			       const PangoFontDescription *override)
{
  PangoFontDescription *font_desc;

  g_return_val_if_fail (gtk_widget_get_realized (widget), NULL);

  font_desc = pango_font_description_copy (gtk_widget_get_style (widget)->font_desc);

  if (override)
    pango_font_description_merge (font_desc, override, TRUE);

  pango_font_description_set_size (font_desc,
                                   MAX (pango_font_description_get_size (font_desc) * scale, 1));

  return font_desc;
}

/**
 * Returns the height of the letters in a particular font.
 *
 * \param font_desc  the font
 * \param context  the context of the font
 * \return  the height of the letters
 */
int
meta_pango_font_desc_get_text_height (const PangoFontDescription *font_desc,
                                      PangoContext         *context)
{
  PangoFontMetrics *metrics;
  PangoLanguage *lang;
  int retval;

  lang = pango_context_get_language (context);
  metrics = pango_context_get_metrics (context, font_desc, lang);

  retval = PANGO_PIXELS (pango_font_metrics_get_ascent (metrics) +
                         pango_font_metrics_get_descent (metrics));

  pango_font_metrics_unref (metrics);

  return retval;
}

MetaGtkColorComponent
meta_color_component_from_string (const char *str)
{
  if (strcmp ("fg", str) == 0)
    return META_GTK_COLOR_FG;
  else if (strcmp ("bg", str) == 0)
    return META_GTK_COLOR_BG;
  else if (strcmp ("light", str) == 0)
    return META_GTK_COLOR_LIGHT;
  else if (strcmp ("dark", str) == 0)
    return META_GTK_COLOR_DARK;
  else if (strcmp ("mid", str) == 0)
    return META_GTK_COLOR_MID;
  else if (strcmp ("text", str) == 0)
    return META_GTK_COLOR_TEXT;
  else if (strcmp ("base", str) == 0)
    return META_GTK_COLOR_BASE;
  else if (strcmp ("text_aa", str) == 0)
    return META_GTK_COLOR_TEXT_AA;
  else
    return META_GTK_COLOR_LAST;
}

const char*
meta_color_component_to_string (MetaGtkColorComponent component)
{
  switch (component)
    {
    case META_GTK_COLOR_FG:
      return "fg";
    case META_GTK_COLOR_BG:
      return "bg";
    case META_GTK_COLOR_LIGHT:
      return "light";
    case META_GTK_COLOR_DARK:
      return "dark";
    case META_GTK_COLOR_MID:
      return "mid";
    case META_GTK_COLOR_TEXT:
      return "text";
    case META_GTK_COLOR_BASE:
      return "base";
    case META_GTK_COLOR_TEXT_AA:
      return "text_aa";
    case META_GTK_COLOR_LAST:
      break;
    }

  return "<unknown>";
}

MetaButtonState
meta_button_state_from_string (const char *str)
{
  if (strcmp ("normal", str) == 0)
    return META_BUTTON_STATE_NORMAL;
  else if (strcmp ("pressed", str) == 0)
    return META_BUTTON_STATE_PRESSED;
  else if (strcmp ("prelight", str) == 0)
    return META_BUTTON_STATE_PRELIGHT;
  else
    return META_BUTTON_STATE_LAST;
}

const char*
meta_button_state_to_string (MetaButtonState state)
{
  switch (state)
    {
    case META_BUTTON_STATE_NORMAL:
      return "normal";
    case META_BUTTON_STATE_PRESSED:
      return "pressed";
    case META_BUTTON_STATE_PRELIGHT:
      return "prelight";
    case META_BUTTON_STATE_LAST:
      break;
    }

  return "<unknown>";
}

MetaButtonType
meta_button_type_from_string (const char *str, MetaTheme *theme)
{
  if (META_THEME_ALLOWS(theme, META_THEME_SHADE_STICK_ABOVE_BUTTONS))
    {
      if (strcmp ("shade", str) == 0)
        return META_BUTTON_TYPE_SHADE;
      else if (strcmp ("above", str) == 0)
        return META_BUTTON_TYPE_ABOVE;
      else if (strcmp ("stick", str) == 0)
        return META_BUTTON_TYPE_STICK;
      else if (strcmp ("unshade", str) == 0)
        return META_BUTTON_TYPE_UNSHADE;
      else if (strcmp ("unabove", str) == 0)
        return META_BUTTON_TYPE_UNABOVE;
      else if (strcmp ("unstick", str) == 0)
        return META_BUTTON_TYPE_UNSTICK;
     }

  if (strcmp ("close", str) == 0)
    return META_BUTTON_TYPE_CLOSE;
  else if (strcmp ("maximize", str) == 0)
    return META_BUTTON_TYPE_MAXIMIZE;
  else if (strcmp ("minimize", str) == 0)
    return META_BUTTON_TYPE_MINIMIZE;
  else if (strcmp ("menu", str) == 0)
    return META_BUTTON_TYPE_MENU;
  else if (strcmp ("left_left_background", str) == 0)
    return META_BUTTON_TYPE_LEFT_LEFT_BACKGROUND;
  else if (strcmp ("left_middle_background", str) == 0)
    return META_BUTTON_TYPE_LEFT_MIDDLE_BACKGROUND;
  else if (strcmp ("left_right_background", str) == 0)
    return META_BUTTON_TYPE_LEFT_RIGHT_BACKGROUND;
  else if (strcmp ("right_left_background", str) == 0)
    return META_BUTTON_TYPE_RIGHT_LEFT_BACKGROUND;
  else if (strcmp ("right_middle_background", str) == 0)
    return META_BUTTON_TYPE_RIGHT_MIDDLE_BACKGROUND;
  else if (strcmp ("right_right_background", str) == 0)
    return META_BUTTON_TYPE_RIGHT_RIGHT_BACKGROUND;
  else
    return META_BUTTON_TYPE_LAST;
}

const char*
meta_button_type_to_string (MetaButtonType type)
{
  switch (type)
    {
    case META_BUTTON_TYPE_CLOSE:
      return "close";
    case META_BUTTON_TYPE_MAXIMIZE:
      return "maximize";
    case META_BUTTON_TYPE_MINIMIZE:
      return "minimize";
    case META_BUTTON_TYPE_SHADE:
     return "shade";
    case META_BUTTON_TYPE_ABOVE:
      return "above";
    case META_BUTTON_TYPE_STICK:
      return "stick";
    case META_BUTTON_TYPE_UNSHADE:
      return "unshade";
    case META_BUTTON_TYPE_UNABOVE:
      return "unabove";
    case META_BUTTON_TYPE_UNSTICK:
      return "unstick";
     case META_BUTTON_TYPE_MENU:
      return "menu";
    case META_BUTTON_TYPE_LEFT_LEFT_BACKGROUND:
      return "left_left_background";
    case META_BUTTON_TYPE_LEFT_MIDDLE_BACKGROUND:
      return "left_middle_background";
    case META_BUTTON_TYPE_LEFT_RIGHT_BACKGROUND:
      return "left_right_background";
    case META_BUTTON_TYPE_RIGHT_LEFT_BACKGROUND:
      return "right_left_background";
    case META_BUTTON_TYPE_RIGHT_MIDDLE_BACKGROUND:
      return "right_middle_background";
    case META_BUTTON_TYPE_RIGHT_RIGHT_BACKGROUND:
      return "right_right_background";
    case META_BUTTON_TYPE_LAST:
      break;
    }

  return "<unknown>";
}

MetaFramePiece
meta_frame_piece_from_string (const char *str)
{
  if (strcmp ("entire_background", str) == 0)
    return META_FRAME_PIECE_ENTIRE_BACKGROUND;
  else if (strcmp ("titlebar", str) == 0)
    return META_FRAME_PIECE_TITLEBAR;
  else if (strcmp ("titlebar_middle", str) == 0)
    return META_FRAME_PIECE_TITLEBAR_MIDDLE;
  else if (strcmp ("left_titlebar_edge", str) == 0)
    return META_FRAME_PIECE_LEFT_TITLEBAR_EDGE;
  else if (strcmp ("right_titlebar_edge", str) == 0)
    return META_FRAME_PIECE_RIGHT_TITLEBAR_EDGE;
  else if (strcmp ("top_titlebar_edge", str) == 0)
    return META_FRAME_PIECE_TOP_TITLEBAR_EDGE;
  else if (strcmp ("bottom_titlebar_edge", str) == 0)
    return META_FRAME_PIECE_BOTTOM_TITLEBAR_EDGE;
  else if (strcmp ("title", str) == 0)
    return META_FRAME_PIECE_TITLE;
  else if (strcmp ("left_edge", str) == 0)
    return META_FRAME_PIECE_LEFT_EDGE;
  else if (strcmp ("right_edge", str) == 0)
    return META_FRAME_PIECE_RIGHT_EDGE;
  else if (strcmp ("bottom_edge", str) == 0)
    return META_FRAME_PIECE_BOTTOM_EDGE;
  else if (strcmp ("overlay", str) == 0)
    return META_FRAME_PIECE_OVERLAY;
  else
    return META_FRAME_PIECE_LAST;
}

const char*
meta_frame_piece_to_string (MetaFramePiece piece)
{
  switch (piece)
    {
    case META_FRAME_PIECE_ENTIRE_BACKGROUND:
      return "entire_background";
    case META_FRAME_PIECE_TITLEBAR:
      return "titlebar";
    case META_FRAME_PIECE_TITLEBAR_MIDDLE:
      return "titlebar_middle";
    case META_FRAME_PIECE_LEFT_TITLEBAR_EDGE:
      return "left_titlebar_edge";
    case META_FRAME_PIECE_RIGHT_TITLEBAR_EDGE:
      return "right_titlebar_edge";
    case META_FRAME_PIECE_TOP_TITLEBAR_EDGE:
      return "top_titlebar_edge";
    case META_FRAME_PIECE_BOTTOM_TITLEBAR_EDGE:
      return "bottom_titlebar_edge";
    case META_FRAME_PIECE_TITLE:
      return "title";
    case META_FRAME_PIECE_LEFT_EDGE:
      return "left_edge";
    case META_FRAME_PIECE_RIGHT_EDGE:
      return "right_edge";
    case META_FRAME_PIECE_BOTTOM_EDGE:
      return "bottom_edge";
    case META_FRAME_PIECE_OVERLAY:
      return "overlay";
    case META_FRAME_PIECE_LAST:
      break;
    }

  return "<unknown>";
}

MetaFrameState
meta_frame_state_from_string (const char *str)
{
  if (strcmp ("normal", str) == 0)
    return META_FRAME_STATE_NORMAL;
  else if (strcmp ("maximized", str) == 0)
    return META_FRAME_STATE_MAXIMIZED;
  else if (strcmp ("shaded", str) == 0)
    return META_FRAME_STATE_SHADED;
  else if (strcmp ("maximized_and_shaded", str) == 0)
    return META_FRAME_STATE_MAXIMIZED_AND_SHADED;
  else
    return META_FRAME_STATE_LAST;
}

const char*
meta_frame_state_to_string (MetaFrameState state)
{
  switch (state)
    {
    case META_FRAME_STATE_NORMAL:
      return "normal";
    case META_FRAME_STATE_MAXIMIZED:
      return "maximized";
    case META_FRAME_STATE_SHADED:
      return "shaded";
    case META_FRAME_STATE_MAXIMIZED_AND_SHADED:
      return "maximized_and_shaded";
    case META_FRAME_STATE_LAST:
      break;
    }

  return "<unknown>";
}

MetaFrameResize
meta_frame_resize_from_string (const char *str)
{
  if (strcmp ("none", str) == 0)
    return META_FRAME_RESIZE_NONE;
  else if (strcmp ("vertical", str) == 0)
    return META_FRAME_RESIZE_VERTICAL;
  else if (strcmp ("horizontal", str) == 0)
    return META_FRAME_RESIZE_HORIZONTAL;
  else if (strcmp ("both", str) == 0)
    return META_FRAME_RESIZE_BOTH;
  else
    return META_FRAME_RESIZE_LAST;
}

const char*
meta_frame_resize_to_string (MetaFrameResize resize)
{
  switch (resize)
    {
    case META_FRAME_RESIZE_NONE:
      return "none";
    case META_FRAME_RESIZE_VERTICAL:
      return "vertical";
    case META_FRAME_RESIZE_HORIZONTAL:
      return "horizontal";
    case META_FRAME_RESIZE_BOTH:
      return "both";
    case META_FRAME_RESIZE_LAST:
      break;
    }

  return "<unknown>";
}

MetaFrameFocus
meta_frame_focus_from_string (const char *str)
{
  if (strcmp ("no", str) == 0)
    return META_FRAME_FOCUS_NO;
  else if (strcmp ("yes", str) == 0)
    return META_FRAME_FOCUS_YES;
  else
    return META_FRAME_FOCUS_LAST;
}

const char*
meta_frame_focus_to_string (MetaFrameFocus focus)
{
  switch (focus)
    {
    case META_FRAME_FOCUS_NO:
      return "no";
    case META_FRAME_FOCUS_YES:
      return "yes";
    case META_FRAME_FOCUS_LAST:
      break;
    }

  return "<unknown>";
}

MetaFrameType
meta_frame_type_from_string (const char *str)
{
  if (strcmp ("normal", str) == 0)
    return META_FRAME_TYPE_NORMAL;
  else if (strcmp ("dialog", str) == 0)
    return META_FRAME_TYPE_DIALOG;
  else if (strcmp ("modal_dialog", str) == 0)
    return META_FRAME_TYPE_MODAL_DIALOG;
  else if (strcmp ("utility", str) == 0)
    return META_FRAME_TYPE_UTILITY;
  else if (strcmp ("menu", str) == 0)
    return META_FRAME_TYPE_MENU;
  else if (strcmp ("border", str) == 0)
    return META_FRAME_TYPE_BORDER;
#if 0
  else if (strcmp ("toolbar", str) == 0)
    return META_FRAME_TYPE_TOOLBAR;
#endif
  else
    return META_FRAME_TYPE_LAST;
}

const char*
meta_frame_type_to_string (MetaFrameType type)
{
  switch (type)
    {
    case META_FRAME_TYPE_NORMAL:
      return "normal";
    case META_FRAME_TYPE_DIALOG:
      return "dialog";
    case META_FRAME_TYPE_MODAL_DIALOG:
      return "modal_dialog";
    case META_FRAME_TYPE_UTILITY:
      return "utility";
    case META_FRAME_TYPE_MENU:
      return "menu";
    case META_FRAME_TYPE_BORDER:
      return "border";
#if 0
    case META_FRAME_TYPE_TOOLBAR:
      return "toolbar";
#endif
    case  META_FRAME_TYPE_LAST:
      break;
    }

  return "<unknown>";
}

MetaGradientType
meta_gradient_type_from_string (const char *str)
{
  if (strcmp ("vertical", str) == 0)
    return META_GRADIENT_VERTICAL;
  else if (strcmp ("horizontal", str) == 0)
    return META_GRADIENT_HORIZONTAL;
  else if (strcmp ("diagonal", str) == 0)
    return META_GRADIENT_DIAGONAL;
  else
    return META_GRADIENT_LAST;
}

const char*
meta_gradient_type_to_string (MetaGradientType type)
{
  switch (type)
    {
    case META_GRADIENT_VERTICAL:
      return "vertical";
    case META_GRADIENT_HORIZONTAL:
      return "horizontal";
    case META_GRADIENT_DIAGONAL:
      return "diagonal";
    case META_GRADIENT_LAST:
      break;
    }

  return "<unknown>";
}

GtkStateType
meta_gtk_state_from_string (const char *str)
{
  if (strcmp ("normal", str) == 0 || strcmp ("NORMAL", str) == 0)
    return GTK_STATE_NORMAL;
  else if (strcmp ("prelight", str) == 0 || strcmp ("PRELIGHT", str) == 0)
    return GTK_STATE_PRELIGHT;
  else if (strcmp ("active", str) == 0 || strcmp ("ACTIVE", str) == 0)
    return GTK_STATE_ACTIVE;
  else if (strcmp ("selected", str) == 0 || strcmp ("SELECTED", str) == 0)
    return GTK_STATE_SELECTED;
  else if (strcmp ("insensitive", str) == 0 || strcmp ("INSENSITIVE", str) == 0)
    return GTK_STATE_INSENSITIVE;
  else
    return -1; /* hack */
}

const char*
meta_gtk_state_to_string (GtkStateType state)
{
  switch (state)
    {
    case GTK_STATE_NORMAL:
      return "NORMAL";
    case GTK_STATE_PRELIGHT:
      return "PRELIGHT";
    case GTK_STATE_ACTIVE:
      return "ACTIVE";
    case GTK_STATE_SELECTED:
      return "SELECTED";
    case GTK_STATE_INSENSITIVE:
      return "INSENSITIVE";
    }

  return "<unknown>";
}

GtkShadowType
meta_gtk_shadow_from_string (const char *str)
{
  if (strcmp ("none", str) == 0)
    return GTK_SHADOW_NONE;
  else if (strcmp ("in", str) == 0)
    return GTK_SHADOW_IN;
  else if (strcmp ("out", str) == 0)
    return GTK_SHADOW_OUT;
  else if (strcmp ("etched_in", str) == 0)
    return GTK_SHADOW_ETCHED_IN;
  else if (strcmp ("etched_out", str) == 0)
    return GTK_SHADOW_ETCHED_OUT;
  else
    return -1;
}

const char*
meta_gtk_shadow_to_string (GtkShadowType shadow)
{
  switch (shadow)
    {
    case GTK_SHADOW_NONE:
      return "none";
    case GTK_SHADOW_IN:
      return "in";
    case GTK_SHADOW_OUT:
      return "out";
    case GTK_SHADOW_ETCHED_IN:
      return "etched_in";
    case GTK_SHADOW_ETCHED_OUT:
      return "etched_out";
    }

  return "<unknown>";
}

GtkArrowType
meta_gtk_arrow_from_string (const char *str)
{
  if (strcmp ("up", str) == 0)
    return GTK_ARROW_UP;
  else if (strcmp ("down", str) == 0)
    return GTK_ARROW_DOWN;
  else if (strcmp ("left", str) == 0)
    return GTK_ARROW_LEFT;
  else if (strcmp ("right", str) == 0)
    return GTK_ARROW_RIGHT;
  else if (strcmp ("none", str) == 0)
    return GTK_ARROW_NONE;
  else
    return -1;
}

const char*
meta_gtk_arrow_to_string (GtkArrowType arrow)
{
  switch (arrow)
    {
    case GTK_ARROW_UP:
      return "up";
    case GTK_ARROW_DOWN:
      return "down";
    case GTK_ARROW_LEFT:
      return "left";
    case GTK_ARROW_RIGHT:
      return "right";
    case GTK_ARROW_NONE:
      return "none";
    }

  return "<unknown>";
}

/**
 * Returns a fill_type from a string.  The inverse of
 * meta_image_fill_type_to_string().
 *
 * \param str  a string representing a fill_type
 * \result  the fill_type, or -1 if it represents no fill_type.
 */
MetaImageFillType
meta_image_fill_type_from_string (const char *str)
{
  if (strcmp ("tile", str) == 0)
    return META_IMAGE_FILL_TILE;
  else if (strcmp ("scale", str) == 0)
    return META_IMAGE_FILL_SCALE;
  else
    return -1;
}

/**
 * Returns a string representation of a fill_type.  The inverse of
 * meta_image_fill_type_from_string().
 *
 * \param fill_type  the fill type
 * \result  a string representing that type
 */
const char*
meta_image_fill_type_to_string (MetaImageFillType fill_type)
{
  switch (fill_type)
    {
    case META_IMAGE_FILL_TILE:
      return "tile";
    case META_IMAGE_FILL_SCALE:
      return "scale";
    }

  return "<unknown>";
}

/**
 * Takes a colour "a", scales the lightness and saturation by a certain amount,
 * and sets "b" to the resulting colour.
 * gtkstyle.c cut-and-pastage.
 *
 * \param a  the starting colour
 * \param b  [out] the resulting colour
 * \param k  amount to scale lightness and saturation by
 */
static void
gtk_style_shade (GdkColor *a,
                 GdkColor *b,
                 gdouble   k)
{
  gdouble red;
  gdouble green;
  gdouble blue;

  red = (gdouble) a->red / 65535.0;
  green = (gdouble) a->green / 65535.0;
  blue = (gdouble) a->blue / 65535.0;

  rgb_to_hls (&red, &green, &blue);

  green *= k;
  if (green > 1.0)
    green = 1.0;
  else if (green < 0.0)
    green = 0.0;

  blue *= k;
  if (blue > 1.0)
    blue = 1.0;
  else if (blue < 0.0)
    blue = 0.0;

  hls_to_rgb (&red, &green, &blue);

  b->red = red * 65535.0;
  b->green = green * 65535.0;
  b->blue = blue * 65535.0;
}

/**
 * Converts a red/green/blue triplet to a hue/lightness/saturation triplet.
 *
 * \param r  on input, red; on output, hue
 * \param g  on input, green; on output, lightness
 * \param b  on input, blue; on output, saturation
 */
static void
rgb_to_hls (gdouble *r,
            gdouble *g,
            gdouble *b)
{
  gdouble min;
  gdouble max;
  gdouble red;
  gdouble green;
  gdouble blue;
  gdouble h, l, s;
  gdouble delta;

  red = *r;
  green = *g;
  blue = *b;

  if (red > green)
    {
      if (red > blue)
        max = red;
      else
        max = blue;

      if (green < blue)
        min = green;
      else
        min = blue;
    }
  else
    {
      if (green > blue)
        max = green;
      else
        max = blue;

      if (red < blue)
        min = red;
      else
        min = blue;
    }

  l = (max + min) / 2;
  s = 0;
  h = 0;

  if (max != min)
    {
      if (l <= 0.5)
        s = (max - min) / (max + min);
      else
        s = (max - min) / (2 - max - min);

      delta = max -min;
      if (red == max)
        h = (green - blue) / delta;
      else if (green == max)
        h = 2 + (blue - red) / delta;
      else if (blue == max)
        h = 4 + (red - green) / delta;

      h *= 60;
      if (h < 0.0)
        h += 360;
    }

  *r = h;
  *g = l;
  *b = s;
}

/**
 * Converts a hue/lightness/saturation triplet to a red/green/blue triplet.
 *
 * \param h  on input, hue; on output, red
 * \param l  on input, lightness; on output, green
 * \param s  on input, saturation; on output, blue
 */
static void
hls_to_rgb (gdouble *h,
            gdouble *l,
            gdouble *s)
{
  gdouble hue;
  gdouble lightness;
  gdouble saturation;
  gdouble m1, m2;
  gdouble r, g, b;

  lightness = *l;
  saturation = *s;

  if (lightness <= 0.5)
    m2 = lightness * (1 + saturation);
  else
    m2 = lightness + saturation - lightness * saturation;
  m1 = 2 * lightness - m2;

  if (saturation == 0)
    {
      *h = lightness;
      *l = lightness;
      *s = lightness;
    }
  else
    {
      hue = *h + 120;
      while (hue > 360)
        hue -= 360;
      while (hue < 0)
        hue += 360;

      if (hue < 60)
        r = m1 + (m2 - m1) * hue / 60;
      else if (hue < 180)
        r = m2;
      else if (hue < 240)
        r = m1 + (m2 - m1) * (240 - hue) / 60;
      else
        r = m1;

      hue = *h;
      while (hue > 360)
        hue -= 360;
      while (hue < 0)
        hue += 360;

      if (hue < 60)
        g = m1 + (m2 - m1) * hue / 60;
      else if (hue < 180)
        g = m2;
      else if (hue < 240)
        g = m1 + (m2 - m1) * (240 - hue) / 60;
      else
        g = m1;

      hue = *h - 120;
      while (hue > 360)
        hue -= 360;
      while (hue < 0)
        hue += 360;

      if (hue < 60)
        b = m1 + (m2 - m1) * hue / 60;
      else if (hue < 180)
        b = m2;
      else if (hue < 240)
        b = m1 + (m2 - m1) * (240 - hue) / 60;
      else
        b = m1;

      *h = r;
      *l = g;
      *s = b;
    }
}

#if 0
/* These are some functions I'm saving to use in optimizing
 * MetaDrawOpList, namely to pre-composite pixbufs on client side
 * prior to rendering to the server
 */
static void
draw_bg_solid_composite (const MetaTextureSpec *bg,
                         const MetaTextureSpec *fg,
                         double                 alpha,
                         GtkWidget             *widget,
                         GdkDrawable           *drawable,
                         const GdkRectangle    *clip,
                         MetaTextureDrawMode    mode,
                         double                 xalign,
                         double                 yalign,
                         int                    x,
                         int                    y,
                         int                    width,
                         int                    height)
{
  GdkColor bg_color;

  g_assert (bg->type == META_TEXTURE_SOLID);
  g_assert (fg->type != META_TEXTURE_COMPOSITE);
  g_assert (fg->type != META_TEXTURE_SHAPE_LIST);

  meta_color_spec_render (bg->data.solid.color_spec,
                          widget,
                          &bg_color);

  switch (fg->type)
    {
    case META_TEXTURE_SOLID:
      {
        GdkColor fg_color;

        meta_color_spec_render (fg->data.solid.color_spec,
                                widget,
                                &fg_color);

        color_composite (&bg_color, &fg_color,
                         alpha, &fg_color);

        draw_color_rectangle (widget, drawable, &fg_color, clip,
                              x, y, width, height);
      }
      break;

    case META_TEXTURE_GRADIENT:
      /* FIXME I think we could just composite all the colors in
       * the gradient prior to generating the gradient?
       */
      /* FALL THRU */
    case META_TEXTURE_IMAGE:
      {
        GdkPixbuf *pixbuf;
        GdkPixbuf *composited;

        pixbuf = meta_texture_spec_render (fg, widget, mode, 255,
                                           width, height);

        if (pixbuf == NULL)
          return;

        composited = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
                                     gdk_pixbuf_get_has_alpha (pixbuf), 8,
                                     gdk_pixbuf_get_width (pixbuf),
                                     gdk_pixbuf_get_height (pixbuf));

        if (composited == NULL)
          {
            g_object_unref (G_OBJECT (pixbuf));
            return;
          }

        gdk_pixbuf_composite_color (pixbuf,
                                    composited,
                                    0, 0,
                                    gdk_pixbuf_get_width (pixbuf),
                                    gdk_pixbuf_get_height (pixbuf),
                                    0.0, 0.0, /* offsets */
                                    1.0, 1.0, /* scale */
                                    GDK_INTERP_BILINEAR,
                                    255 * alpha,
                                    0, 0,     /* check offsets */
                                    0,        /* check size */
                                    GDK_COLOR_RGB (bg_color),
                                    GDK_COLOR_RGB (bg_color));

        /* Need to draw background since pixbuf is not
         * necessarily covering the whole thing
         */
        draw_color_rectangle (widget, drawable, &bg_color, clip,
                              x, y, width, height);

        render_pixbuf_aligned (drawable, clip, composited,
                               xalign, yalign,
                               x, y, width, height);

        g_object_unref (G_OBJECT (pixbuf));
        g_object_unref (G_OBJECT (composited));
      }
      break;

    case META_TEXTURE_BLANK:
    case META_TEXTURE_COMPOSITE:
    case META_TEXTURE_SHAPE_LIST:
      g_assert_not_reached ();
      break;
    }
}

static void
draw_bg_gradient_composite (const MetaTextureSpec *bg,
                            const MetaTextureSpec *fg,
                            double                 alpha,
                            GtkWidget             *widget,
                            GdkDrawable           *drawable,
                            const GdkRectangle    *clip,
                            MetaTextureDrawMode    mode,
                            double                 xalign,
                            double                 yalign,
                            int                    x,
                            int                    y,
                            int                    width,
                            int                    height)
{
  g_assert (bg->type == META_TEXTURE_GRADIENT);
  g_assert (fg->type != META_TEXTURE_COMPOSITE);
  g_assert (fg->type != META_TEXTURE_SHAPE_LIST);

  switch (fg->type)
    {
    case META_TEXTURE_SOLID:
    case META_TEXTURE_GRADIENT:
    case META_TEXTURE_IMAGE:
      {
        GdkPixbuf *bg_pixbuf;
        GdkPixbuf *fg_pixbuf;
        GdkPixbuf *composited;
        int fg_width, fg_height;

        bg_pixbuf = meta_texture_spec_render (bg, widget, mode, 255,
                                              width, height);

        if (bg_pixbuf == NULL)
          return;

        fg_pixbuf = meta_texture_spec_render (fg, widget, mode, 255,
                                              width, height);

        if (fg_pixbuf == NULL)
          {
            g_object_unref (G_OBJECT (bg_pixbuf));
            return;
          }

        /* gradients always fill the entire target area */
        g_assert (gdk_pixbuf_get_width (bg_pixbuf) == width);
        g_assert (gdk_pixbuf_get_height (bg_pixbuf) == height);

        composited = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
                                     gdk_pixbuf_get_has_alpha (bg_pixbuf), 8,
                                     gdk_pixbuf_get_width (bg_pixbuf),
                                     gdk_pixbuf_get_height (bg_pixbuf));

        if (composited == NULL)
          {
            g_object_unref (G_OBJECT (bg_pixbuf));
            g_object_unref (G_OBJECT (fg_pixbuf));
            return;
          }

        fg_width = gdk_pixbuf_get_width (fg_pixbuf);
        fg_height = gdk_pixbuf_get_height (fg_pixbuf);

        /* If we wanted to be all cool we could deal with the
         * offsets and try to composite only in the clip rectangle,
         * but I just don't care enough to figure it out.
         */

        gdk_pixbuf_composite (fg_pixbuf,
                              composited,
                              x + (width - fg_width) * xalign,
                              y + (height - fg_height) * yalign,
                              gdk_pixbuf_get_width (fg_pixbuf),
                              gdk_pixbuf_get_height (fg_pixbuf),
                              0.0, 0.0, /* offsets */
                              1.0, 1.0, /* scale */
                              GDK_INTERP_BILINEAR,
                              255 * alpha);

        gdk_cairo_set_source_pixbuf (cr, composited, x, y);
        cairo_paint (cr);

        g_object_unref (G_OBJECT (bg_pixbuf));
        g_object_unref (G_OBJECT (fg_pixbuf));
        g_object_unref (G_OBJECT (composited));
      }
      break;

    case META_TEXTURE_BLANK:
    case META_TEXTURE_SHAPE_LIST:
    case META_TEXTURE_COMPOSITE:
      g_assert_not_reached ();
      break;
    }
}
#endif

/**
 * Returns the earliest version of the theme format which required support
 * for a particular button.  (For example, "shade" first appeared in v2, and
 * "close" in v1.)
 *
 * \param type  the button type
 * \return  the number of the theme format
 */
guint
meta_theme_earliest_version_with_button (MetaButtonType type)
{
  switch (type)
    {
    case META_BUTTON_TYPE_CLOSE:
    case META_BUTTON_TYPE_MAXIMIZE:
    case META_BUTTON_TYPE_MINIMIZE:
    case META_BUTTON_TYPE_MENU:
    case META_BUTTON_TYPE_LEFT_LEFT_BACKGROUND:
    case META_BUTTON_TYPE_LEFT_MIDDLE_BACKGROUND:
    case META_BUTTON_TYPE_LEFT_RIGHT_BACKGROUND:
    case META_BUTTON_TYPE_RIGHT_LEFT_BACKGROUND:
    case META_BUTTON_TYPE_RIGHT_MIDDLE_BACKGROUND:
    case META_BUTTON_TYPE_RIGHT_RIGHT_BACKGROUND:
      return 1;

    case META_BUTTON_TYPE_SHADE:
    case META_BUTTON_TYPE_ABOVE:
    case META_BUTTON_TYPE_STICK:
    case META_BUTTON_TYPE_UNSHADE:
    case META_BUTTON_TYPE_UNABOVE:
    case META_BUTTON_TYPE_UNSTICK:
      return 2;

    default:
      meta_warning("Unknown button %d\n", type);
      return 1;
    }
}