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

/* Metacity 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 Street - Suite 500, Boston, MA
 * 02110-1335, USA.
 */

/**
 * SECTION:theme
 * @short_description: Making Metacity look pretty
 *
 * The window decorations drawn by Metacity are described by files on disk
 * known internally as "themes" (externally as "window border themes" on
 * http://art.gnome.org/themes/metacity/ or "Metacity 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-private.h"
#include "util-private.h"
#include <meta/gradient.h>
#include <meta/prefs.h>
#include <gtk/gtk.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>

#define GDK_COLOR_RGBA(color)                                           \
                         ((guint32) (0xff                         |     \
                                     ((int)((color).red * 255) << 24)   |    \
                                     ((int)((color).green * 255) << 16) |    \
                                     ((int)((color).blue * 255) << 8)))

#define GDK_COLOR_RGB(color)                                            \
                         ((guint32) (((int)((color).red * 255) << 16)   |    \
                                     ((int)((color).green * 255) << 8)  |    \
                                     ((int)((color).blue * 255))))

#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		(GdkRGBA	 *a,
					 GdkRGBA	 *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,
                 GdkRGBA   *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;
              dg = new_color->green * intensity * 2.0;
              db = new_color->blue * intensity * 2.0;
            }
          else
            {
              /* Go from new_color at intensity = 0.5 to white at intensity = 1.0 */
              dr = new_color->red + (1.0 - new_color->red) * (intensity - 0.5) * 2.0;
              dg = new_color->green + (1.0 - new_color->green) * (intensity - 0.5) * 2.0;
              db = new_color->blue + (1.0 - new_color->blue) * (intensity - 0.5) * 2.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 GdkRGBA *bg,
                 const GdkRGBA *fg,
                 double         alpha,
                 GdkRGBA       *color)
{
  *color = *bg;
  color->red = color->red + (fg->red - color->red) * alpha;
  color->green = color->green + (fg->green - color->green) * alpha;
  color->blue = color->blue + (fg->blue - color->blue) * alpha;
}

/*
 * 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;
}

/**
 * meta_frame_layout_new: (skip)
 *
 * Creates a new, empty MetaFrameLayout. The fields will be set to dummy
 * values.
 *
 * Returns: The newly created MetaFrameLayout.
 */
LOCAL_SYMBOL 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;
}

LOCAL_SYMBOL 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;
}

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

  layout = g_new0 (MetaFrameLayout, 1);

  *layout = *src;

  layout->refcount = 1;

  return layout;
}

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

  layout->refcount += 1;
}

LOCAL_SYMBOL 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);
      free (layout);
    }
}

LOCAL_SYMBOL void
meta_frame_layout_get_borders (const MetaFrameLayout *layout,
                               int                    text_height,
                               MetaFrameFlags         flags,
                               MetaFrameType          type,
                               MetaFrameBorders      *borders)
{
  int buttons_height, title_height, draggable_borders;

  meta_frame_borders_clear (borders);

  /* For a full-screen window, we don't have any borders, visible or not. */
  if (flags & META_FRAME_FULLSCREEN)
    return;

  g_return_if_fail (layout != 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;

  borders->visible.top    = MAX (buttons_height, title_height);
  borders->visible.left   = layout->left_width;
  borders->visible.right  = layout->right_width;
  borders->visible.bottom = layout->bottom_height;

  draggable_borders = meta_prefs_get_draggable_border_width ();

  if (flags & META_FRAME_ALLOWS_TOP_RESIZE)
    {
      if (type != META_FRAME_TYPE_ATTACHED)
        borders->invisible.top = MAX (0, draggable_borders - 2);
    }

  if (flags & META_FRAME_ALLOWS_BOTTOM_RESIZE)
    {
      borders->invisible.bottom = MAX (0, draggable_borders - borders->visible.bottom);
    }

  if (flags & META_FRAME_ALLOWS_LEFT_RESIZE)
    {
      borders->invisible.left   = MAX (0, draggable_borders - borders->visible.left);
    }

  if (flags & META_FRAME_ALLOWS_RIGHT_RESIZE)
    {
      borders->invisible.right   = MAX (0, draggable_borders - borders->visible.right);
    }

  borders->total.left   = borders->invisible.left   + borders->visible.left;
  borders->total.right  = borders->invisible.right  + borders->visible.right;
  borders->total.bottom = borders->invisible.bottom + borders->visible.bottom;
  borders->total.top    = borders->invisible.top    + borders->visible.top;
}

static MetaButtonType
map_button_function_to_type (MetaButtonFunction  function)
{
  switch (function)
    {
    case META_BUTTON_FUNCTION_SHADE:
      return META_BUTTON_TYPE_SHADE;
    case META_BUTTON_FUNCTION_ABOVE:
      return META_BUTTON_TYPE_ABOVE;
    case META_BUTTON_FUNCTION_STICK:
      return META_BUTTON_TYPE_STICK;
    case META_BUTTON_FUNCTION_UNSHADE:
      return META_BUTTON_TYPE_UNSHADE;
    case META_BUTTON_FUNCTION_UNABOVE:
      return META_BUTTON_TYPE_UNABOVE;
    case META_BUTTON_FUNCTION_UNSTICK:
      return META_BUTTON_TYPE_UNSTICK;
    case META_BUTTON_FUNCTION_MENU:
      return META_BUTTON_TYPE_MENU;
    case META_BUTTON_FUNCTION_MINIMIZE:
      return META_BUTTON_TYPE_MINIMIZE;
    case META_BUTTON_FUNCTION_MAXIMIZE:
      return META_BUTTON_TYPE_MAXIMIZE;
    case META_BUTTON_FUNCTION_CLOSE:
      return META_BUTTON_TYPE_CLOSE;
    case META_BUTTON_FUNCTION_LAST:
      return META_BUTTON_TYPE_LAST;
    }

  return META_BUTTON_TYPE_LAST;
}

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 */
}

LOCAL_SYMBOL void
meta_frame_layout_calc_geometry (const MetaFrameLayout  *layout,
                                 int                     text_height,
                                 MetaFrameFlags          flags,
                                 int                     client_width,
                                 int                     client_height,
                                 const MetaButtonLayout *button_layout,
                                 MetaFrameType           type,
                                 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];

  MetaFrameBorders borders;

  meta_frame_layout_get_borders (layout, text_height,
                                 flags, type,
                                 &borders);

  fgeom->borders = borders;

  width = client_width + borders.total.left + borders.total.right;

  height = ((flags & META_FRAME_SHADED) ? 0: client_height) +
    borders.total.top + borders.total.bottom;

  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 = borders.visible.top - 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 (n_left == 1)
        left_bg_rects[i] = &fgeom->left_single_background;
      else if (i == 0)
        left_bg_rects[i] = &fgeom->left_left_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 (n_right == 1)
        right_bg_rects[i] = &fgeom->right_single_background;
      else if (i == (n_right - 1))
        right_bg_rects[i] = &fgeom->right_right_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);
        }
    }

  /* Save the button layout */
  fgeom->button_layout = *button_layout;
  fgeom->n_left_buttons = n_left;
  fgeom->n_right_buttons = n_right;

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

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

  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 ||
          flags & META_FRAME_TILED_LEFT ||
          flags & META_FRAME_TILED_RIGHT)
        {
          rect->clickable.x = rect->visible.x;
          rect->clickable.y = 0;
          rect->clickable.width = rect->visible.width;
          rect->clickable.height = button_height + button_y;

          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 + borders.invisible.left;
  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)
        {
          if (i==0)
            {
              rect->clickable.x = 0;
              rect->clickable.width = button_width + x;
            }
          else
            {
              rect->clickable.x = rect->visible.x;
              rect->clickable.width = button_width;
            }

            rect->clickable.y = 0;
            rect->clickable.height = button_height + button_y;
          }
        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 + borders.invisible.top;
  fgeom->title_rect.width = title_right_edge - fgeom->title_rect.x;
  fgeom->title_rect.height = borders.visible.top - 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 (borders.visible.top + borders.visible.left >= min_size_for_rounding)
    fgeom->top_left_corner_rounded_radius = layout->top_left_corner_rounded_radius;
  if (borders.visible.top + borders.visible.right >= min_size_for_rounding)
    fgeom->top_right_corner_rounded_radius = layout->top_right_corner_rounded_radius;

  if (borders.visible.bottom + borders.visible.left >= min_size_for_rounding)
    fgeom->bottom_left_corner_rounded_radius = layout->bottom_left_corner_rounded_radius;
  if (borders.visible.bottom + borders.visible.right >= min_size_for_rounding)
    fgeom->bottom_right_corner_rounded_radius = layout->bottom_right_corner_rounded_radius;
}

/**
 * meta_gradient_spec_new: (skip)
 *
 */
LOCAL_SYMBOL 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);
}

LOCAL_SYMBOL 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);
  free (spec);
}

LOCAL_SYMBOL GdkPixbuf*
meta_gradient_spec_render (const MetaGradientSpec *spec,
                           GtkStyleContext        *style,
                           int                     width,
                           int                     height)
{
  int n_colors;
  GdkRGBA *colors;
  GSList *tmp;
  int i;
  GdkPixbuf *pixbuf;

  n_colors = g_slist_length (spec->color_specs);

  if (n_colors == 0)
    return NULL;

  colors = g_new (GdkRGBA, n_colors);

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

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

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

  free (colors);

  return pixbuf;
}

LOCAL_SYMBOL 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;
}

/**
 * meta_alpha_gradient_spec_new: (skip)
 *
 */
LOCAL_SYMBOL LOCAL_SYMBOL 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;
}

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

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

/**
 * meta_color_spec_new: (skip)
 *
 */
LOCAL_SYMBOL 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_GTK_CUSTOM:
      size += sizeof (dummy.data.gtkcustom);
      break;

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

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

  spec = calloc (1, size);

  spec->type = type;

  return spec;
}

LOCAL_SYMBOL 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_GTK_CUSTOM:
      free (spec->data.gtkcustom.color_name);
      if (spec->data.gtkcustom.fallback)
        meta_color_spec_free (spec->data.gtkcustom.fallback);
      DEBUG_FILL_STRUCT (&spec->data.gtkcustom);
      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;
    }

  free (spec);
}

/**
 * meta_color_spec_new_from_string: (skip)
 *
 */
LOCAL_SYMBOL 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] == ':' &&
      str[4] == 'c' && str[5] == 'u' && str[6] == 's' && str[7] == 't' &&
      str[8] == 'o' && str[9] == 'm')
    {
      const char *color_name_start, *fallback_str_start, *end;
      char *color_name;
      MetaColorSpec *fallback = NULL;
      static gboolean debug, debug_set = FALSE;

      if (!debug_set)
        {
          debug = g_getenv ("MUFFIN_DISABLE_FALLBACK_COLOR") != NULL;
          debug_set = TRUE;
        }

      if (str[10] != '(')
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("GTK custom color specification must have color name and fallback in parentheses, e.g. gtk:custom(foo,bar); could not parse \"%s\""),
                       str);
          return NULL;
        }

      color_name_start = str + 11;

      fallback_str_start = color_name_start;
      while (*fallback_str_start && *fallback_str_start != ',')
        {
          if (!(g_ascii_isalnum (*fallback_str_start)
                || *fallback_str_start == '-'
                || *fallback_str_start == '_'))
            {
              g_set_error (err, META_THEME_ERROR,
                           META_THEME_ERROR_FAILED,
                           _("Invalid character '%c' in color_name parameter of gtk:custom, only A-Za-z0-9-_ are valid"),
                           *fallback_str_start);
              return NULL;
            }
          fallback_str_start++;
        }
      fallback_str_start++;

      end = strrchr (str, ')');

      if (color_name_start == NULL || fallback_str_start == NULL || end == NULL)
        {
          g_set_error (err, META_THEME_ERROR,
                       META_THEME_ERROR_FAILED,
                       _("Gtk:custom format is \"gtk:custom(color_name,fallback)\", \"%s\" does not fit the format"),
                       str);
          return NULL;
        }

      if (!debug)
        {
          char *fallback_str;
          fallback_str = g_strndup (fallback_str_start,
                                    end - fallback_str_start);
          fallback = meta_color_spec_new_from_string (fallback_str, err);
          free (fallback_str);
        }
      else
        {
          fallback = meta_color_spec_new_from_string ("pink", err);
        }

      if (fallback == NULL)
        return NULL;

      color_name = g_strndup (color_name_start,
                              fallback_str_start - color_name_start - 1);

      spec = meta_color_spec_new (META_COLOR_SPEC_GTK_CUSTOM);
      spec->data.gtkcustom.color_name = color_name;
      spec->data.gtkcustom.fallback = fallback;
    }
  else if (str[0] == 'g' && str[1] == 't' && str[2] == 'k' && str[3] == ':')
    {
      /* GTK color */
      const char *bracket;
      const char *end_bracket;
      char *tmp;
      GtkStateFlags 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);
          free (tmp);
          return NULL;
        }
      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);
          free (tmp);
          return NULL;
        }
      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.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_rgba_parse (&spec->data.basic.color, str))
        {
          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;
}

/**
 * meta_color_spec_new_gtk: (skip)
 *
 */
LOCAL_SYMBOL LOCAL_SYMBOL MetaColorSpec*
meta_color_spec_new_gtk (MetaGtkColorComponent component,
                         GtkStateFlags         state)
{
  MetaColorSpec *spec;

  spec = meta_color_spec_new (META_COLOR_SPEC_GTK);

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

  return spec;
}

static void
get_background_color (GtkStyleContext *context,
                      GtkStateFlags    state,
                      GdkRGBA         *color)
{
  gtk_style_context_get_background_color (context, state, color);

  GdkRGBA empty = {0.0, 0.0, 0.0, 0.0};

  // Sometimes the widget has no background color, so append the background
  // class and ask again.
  if (gdk_rgba_equal(color, &empty))
    {
      gtk_style_context_add_class (context, GTK_STYLE_CLASS_BACKGROUND);
      gtk_style_context_get_background_color (context, state, color);
    }
}

/* Based on set_color() in gtkstyle.c */
#define LIGHTNESS_MULT 1.3
#define DARKNESS_MULT  0.7
LOCAL_SYMBOL void
meta_gtk_style_get_light_color (GtkStyleContext *style,
                                GtkStateFlags    state,
                                GdkRGBA         *color)
{
  get_background_color (style, state, color);
  gtk_style_shade (color, color, LIGHTNESS_MULT);
}

LOCAL_SYMBOL void
meta_gtk_style_get_dark_color (GtkStyleContext *style,
                               GtkStateFlags    state,
                               GdkRGBA         *color)
{
  get_background_color (style, state, color);
  gtk_style_shade (color, color, DARKNESS_MULT);
}

static void
meta_set_color_from_style (GdkRGBA               *color,
                           GtkStyleContext       *context,
                           GtkStateFlags          state,
                           MetaGtkColorComponent  component)
{
  GdkRGBA other;

  switch (component)
    {
    case META_GTK_COLOR_BG:
    case META_GTK_COLOR_BASE:
      get_background_color (context, state, color);
      break;
    case META_GTK_COLOR_FG:
    case META_GTK_COLOR_TEXT:
      gtk_style_context_get_color (context, state, color);
      break;
    case META_GTK_COLOR_TEXT_AA:
      gtk_style_context_get_color (context, state, color);
      meta_set_color_from_style (&other, context, state, META_GTK_COLOR_BASE);

      color->red = (color->red + other.red) / 2;
      color->green = (color->green + other.green) / 2;
      color->blue = (color->blue + other.blue) / 2;
      break;
    case META_GTK_COLOR_MID:
      meta_gtk_style_get_light_color (context, state, color);
      meta_gtk_style_get_dark_color (context, state, &other);

      color->red = (color->red + other.red) / 2;
      color->green = (color->green + other.green) / 2;
      color->blue = (color->blue + other.blue) / 2;
      break;
    case META_GTK_COLOR_LIGHT:
      meta_gtk_style_get_light_color (context, state, color);
      break;
    case META_GTK_COLOR_DARK:
      meta_gtk_style_get_dark_color (context, state, color);
      break;
    case META_GTK_COLOR_LAST:
      g_assert_not_reached ();
      break;
    }
}

static void
meta_set_custom_color_from_style (GdkRGBA         *color,
                                  GtkStyleContext *context,
                                  char            *color_name,
                                  MetaColorSpec   *fallback)
{
  if (!gtk_style_context_lookup_color (context, color_name, color))
    meta_color_spec_render (fallback, context, color);
}

LOCAL_SYMBOL void
meta_color_spec_render (MetaColorSpec   *spec,
                        GtkStyleContext *context,
                        GdkRGBA         *color)
{
  g_return_if_fail (spec != NULL);
  g_return_if_fail (GTK_IS_STYLE_CONTEXT (context));

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

    case META_COLOR_SPEC_GTK:
      meta_set_color_from_style (color,
                                 context,
                                 spec->data.gtk.state,
                                 spec->data.gtk.component);
      break;

    case META_COLOR_SPEC_GTK_CUSTOM:
      meta_set_custom_color_from_style (color,
                                        context,
                                        spec->data.gtkcustom.color_name,
                                        spec->data.gtkcustom.fallback);
      break;

    case META_COLOR_SPEC_BLEND:
      {
        GdkRGBA bg, fg;

        meta_color_spec_render (spec->data.blend.background, context, &bg);
        meta_color_spec_render (spec->data.blend.foreground, context, &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, context,
                                &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)
      free (tokens[i].d.v.name);

  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);
          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);
          free (num_str);
          return FALSE;
        }
    }

  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 Metacity, 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 if (t->d.v.name_quark == env->theme->quark_frame_x_center)
        *result = env->frame_x_center;
      else if (t->d.v.name_quark == env->theme->quark_frame_y_center)
        *result = env->frame_y_center;
      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 if (strcmp (t->d.v.name, "frame_x_center") == 0)
        *result = env->frame_x_center;
      else if (strcmp (t->d.v.name, "frame_y_center") == 0)
        *result = env->frame_y_center;
      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.
 */

/**
 * meta_parse_position_expression: (skip)
 *
 */
LOCAL_SYMBOL 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;
}


/**
 * meta_parse_size_expression: (skip)
 *
 */
LOCAL_SYMBOL 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.
 */
LOCAL_SYMBOL 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))
            {
              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))
            {
              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;
}

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

/**
 * meta_draw_spec_new: (skip)
 *
 */
LOCAL_SYMBOL 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;
}

/**
 * meta_draw_op_new: (skip)
 *
 */
LOCAL_SYMBOL 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 = calloc (1, size);

  op->type = type;

  return op;
}

LOCAL_SYMBOL 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:
      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:
      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);
      if (op->data.title.ellipsize_width)
        meta_draw_spec_free (op->data.title.ellipsize_width);
      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;
    }

  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;
}

/* gdk_pixbuf_scale_simple doesn't do too well with
   scaling a pixel or two up or down, you end up with
   a blurry icon.  Avoid scaling the icon if the scaled
   size difference is within our arbitrary tolerance.  This
   isn't perfect.. if you use a crazy-sized window title font,
   your icons still may end up less than ideal, but for most users,
   this will solve things.

   It would be more effective to avoid scaling at all,
   and load the image from a file directly here (especially
   with SVG images,) but images are used in some themes to
   construct frames and I don't want to affect that.  There
   are also unknown performance implications to loading here
   every time a window is added)
*/

#define PIXBUF_SIZE_TOLERANCE 2

static gboolean
in_tolerance_to (gint size, gint scale_size)
{
  return scale_size <= size + PIXBUF_SIZE_TOLERANCE &&
         scale_size >= size - PIXBUF_SIZE_TOLERANCE;
}

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 (in_tolerance_to (src_w, dest_w) && in_tolerance_to (src_h, dest_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,
                   GtkStyleContext     *context,
                   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)
        {
          GdkRGBA color;

          meta_color_spec_render (op->data.rectangle.color_spec,
                                  context,
                                  &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:
      {
        GdkRGBA color;
        guint32 rgba;
        gboolean has_alpha;

        meta_color_spec_render (op->data.rectangle.color_spec,
                                context,
                                &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,
                                            context, width, height);

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


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

            meta_color_spec_render (op->data.image.colorize_spec,
                                    context, &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:
      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->borders.visible.left;
      env->right_width = info->fgeom->borders.visible.right;
      env->top_height = info->fgeom->borders.visible.top;
      env->bottom_height = info->fgeom->borders.visible.bottom;
      env->frame_x_center = info->fgeom->width / 2 - logical_region.x;
      env->frame_y_center = info->fgeom->height / 2 - logical_region.y;
    }
  else
    {
      env->left_width = 0;
      env->right_width = 0;
      env->top_height = 0;
      env->bottom_height = 0;
      env->frame_x_center = 0;
      env->frame_y_center = 0;
    }

#define META_ICON_WIDTH 32
#define META_ICON_HEIGHT 32
#define META_MINI_ICON_WIDTH 16
#define META_MINI_ICON_HEIGHT 16

  env->mini_icon_width = META_MINI_ICON_WIDTH;
  env->mini_icon_height = META_MINI_ICON_HEIGHT;
  env->icon_width = META_ICON_WIDTH;
  env->icon_height = META_ICON_HEIGHT;

  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,
                            GtkStyleContext     *style_gtk,
                            GtkWidget           *widget,
                            cairo_t             *cr,
                            const MetaDrawInfo  *info,
                            MetaRectangle        rect,
                            MetaPositionExprEnv *env)
{
  GdkRGBA color;

  cairo_save (cr);
  gtk_style_context_save (style_gtk);

  cairo_set_line_width (cr, 1.0);

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

        meta_color_spec_render (op->data.line.color_spec, style_gtk, &color);
        gdk_cairo_set_source_rgba (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; for zero-width lines
             * the generic algorithm produces the right result so we don't
             * need to handle them here.
             */
            if ((y1 == y2 || x1 == x2) && op->data.line.width != 0)
              {
                double offset = op->data.line.width % 2 ? .5 : 0;

                if (y1 == y2)
                  {
                    cairo_move_to (cr, x1, y1 + offset);
                    cairo_line_to (cr, x2, y2 + offset);
                  }
                else
                  {
                    cairo_move_to (cr, x1 + offset, y1);
                    cairo_line_to (cr, x2 + offset, y2);
                  }
              }
            else
              {
                /* zero-width lines include both end-points in X, unlike wide lines */
                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,
                                style_gtk, &color);
        gdk_cairo_set_source_rgba (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, style_gtk, &color);
        gdk_cairo_set_source_rgba (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.)
                      - (.5 * 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,
                                    style_gtk, &color);
            gdk_cairo_set_source_rgba (cr, &color);

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

            pixbuf = draw_op_as_pixbuf (op, style_gtk, 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, style_gtk, 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, style_gtk, 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;
        double angle = 0, size;

        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);

        size = MAX(rwidth, rheight);

        switch (op->data.gtk_arrow.arrow)
          {
          case GTK_ARROW_UP:
            angle = 0;
            break;
          case GTK_ARROW_RIGHT:
            angle = M_PI / 2;
            break;
          case GTK_ARROW_DOWN:
            angle = M_PI;
            break;
          case GTK_ARROW_LEFT:
            angle = 3 * M_PI / 2;
            break;
          case GTK_ARROW_NONE:
            return;
          }

        gtk_style_context_set_state (style_gtk, op->data.gtk_arrow.state);
        gtk_render_arrow (style_gtk, cr, angle, rx, ry, size);
      }
      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_style_context_set_state (style_gtk, op->data.gtk_box.state);
        gtk_render_background (style_gtk, cr, rx, ry, rwidth, rheight);
        gtk_render_frame (style_gtk, cr, 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_style_context_set_state (style_gtk, op->data.gtk_vline.state);
        gtk_render_line (style_gtk, cr, rx, ry1, rx, ry2);
      }
      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, style_gtk, 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;
          PangoRectangle ink_rect, logical_rect;

          meta_color_spec_render (op->data.title.color_spec,
                                  style_gtk, &color);
          gdk_cairo_set_source_rgba (cr, &color);

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

          if (op->data.title.ellipsize_width)
            {
              int ellipsize_width;
              int right_bearing;

              ellipsize_width = parse_x_position_unchecked (op->data.title.ellipsize_width, env);
              /* HACK: parse_x_position_unchecked adds in env->rect.x, subtract out again */
              ellipsize_width -= env->rect.x;

              pango_layout_set_width (info->title_layout, -1);
              pango_layout_get_pixel_extents (info->title_layout,
                                              &ink_rect, &logical_rect);

              /* Pango's idea of ellipsization is with respect to the logical rect.
               * correct for this, by reducing the ellipsization width by the overflow
               * of the un-ellipsized text on the right... it's always the visual
               * right we want regardless of bidi, since since the X we pass in to
               * cairo_move_to() is always the left edge of the line.
               */
              right_bearing = (ink_rect.x + ink_rect.width) - (logical_rect.x + logical_rect.width);
              right_bearing = MAX (right_bearing, 0);

              ellipsize_width -= right_bearing;
              ellipsize_width = MAX (ellipsize_width, 0);

              /* Only ellipsizing when necessary is a performance optimization -
               * pango_layout_set_width() will force a relayout if it isn't the
               * same as the current width of -1.
               */
              if (ellipsize_width < logical_rect.width)
                pango_layout_set_width (info->title_layout, PANGO_SCALE * ellipsize_width);
            }

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

          /* Remove any ellipsization we might have set; will short-circuit
           * if the width is already -1 */
          pango_layout_set_width (info->title_layout, -1);
        }
      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, cr, info,
                                d_rect);
      }
      break;

    case META_DRAW_TILE:
      {
        int rx, ry, rwidth, rheight;
        int tile_xoffset, tile_yoffset;
        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);

        cairo_save (cr);

        cairo_rectangle (cr, rx, ry, rwidth, rheight);
        cairo_clip (cr);

        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, cr, info,
                                        tile);

                tile.y += tile.height;
              }

            tile.x += tile.width;
          }
        cairo_restore (cr);

      }
      break;
    }

  cairo_restore (cr);
  gtk_style_context_restore (style_gtk);
}

LOCAL_SYMBOL void
meta_draw_op_draw_with_style (const MetaDrawOp    *op,
                              GtkStyleContext     *style_gtk,
                              GtkWidget           *widget,
                              cairo_t             *cr,
                              const MetaDrawInfo  *info,
                              MetaRectangle        logical_region)
{
  MetaPositionExprEnv env;

  fill_env (&env, info, logical_region);

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

}

LOCAL_SYMBOL void
meta_draw_op_draw (const MetaDrawOp    *op,
                   GtkWidget           *widget,
                   cairo_t             *cr,
                   const MetaDrawInfo  *info,
                   MetaRectangle        logical_region)
{
  meta_draw_op_draw_with_style (op, gtk_widget_get_style_context (widget),
                                widget, cr, info, logical_region);
}

/**
 * meta_draw_op_list_new: (skip)
 *
 */
LOCAL_SYMBOL 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;
}

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

  op_list->refcount += 1;
}

LOCAL_SYMBOL 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]);

      free (op_list->ops);

      DEBUG_FILL_STRUCT (op_list);
      free (op_list);
    }
}

LOCAL_SYMBOL void
meta_draw_op_list_draw_with_style  (const MetaDrawOpList *op_list,
                                    GtkStyleContext      *style_gtk,
                                    GtkWidget            *widget,
                                    cairo_t              *cr,
                                    const MetaDrawInfo   *info,
                                    MetaRectangle         rect)
{
  int i;
  MetaPositionExprEnv env;

  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.
   */

  cairo_save (cr);

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

      if (op->type == META_DRAW_CLIP)
        {
          cairo_restore (cr);

          cairo_rectangle (cr,
                           parse_x_position_unchecked (op->data.clip.x, &env),
                           parse_y_position_unchecked (op->data.clip.y, &env),
                           parse_size_unchecked (op->data.clip.width, &env),
                           parse_size_unchecked (op->data.clip.height, &env));
          cairo_clip (cr);

          cairo_save (cr);
        }
      else if (gdk_cairo_get_clip_rectangle (cr, NULL))
        {
          meta_draw_op_draw_with_env (op,
                                      style_gtk, widget, cr, info,
                                      rect,
                                      &env);
        }
    }

  cairo_restore (cr);
}

LOCAL_SYMBOL void
meta_draw_op_list_draw  (const MetaDrawOpList *op_list,
                         GtkWidget            *widget,
                         cairo_t              *cr,
                         const MetaDrawInfo   *info,
                         MetaRectangle         rect)

{
  meta_draw_op_list_draw_with_style (op_list, gtk_widget_get_style_context (widget), widget,
                                     cr, info, rect);
}

LOCAL_SYMBOL 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;
}

LOCAL_SYMBOL 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.
 */
LOCAL_SYMBOL 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.
 */
LOCAL_SYMBOL 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.
 */
LOCAL_SYMBOL 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]);
}

LOCAL_SYMBOL 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);
      free (style);
    }
}

static MetaButtonState
map_button_state (MetaButtonType           button_type,
                  const MetaFrameGeometry *fgeom,
                  int                      middle_bg_offset,
                  MetaButtonState          button_states[META_BUTTON_TYPE_LAST])
{
  MetaButtonFunction function = META_BUTTON_FUNCTION_LAST;

  switch (button_type)
    {
    /* First hande functions, which map directly */
    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:
    case META_BUTTON_TYPE_MENU:
    case META_BUTTON_TYPE_MINIMIZE:
    case META_BUTTON_TYPE_MAXIMIZE:
    case META_BUTTON_TYPE_CLOSE:
      return button_states[button_type];

    /* Map position buttons to the corresponding function */
    case META_BUTTON_TYPE_RIGHT_LEFT_BACKGROUND:
    case META_BUTTON_TYPE_RIGHT_SINGLE_BACKGROUND:
      if (fgeom->n_right_buttons > 0)
        function = fgeom->button_layout.right_buttons[0];
      break;
    case META_BUTTON_TYPE_RIGHT_RIGHT_BACKGROUND:
      if (fgeom->n_right_buttons > 0)
        function = fgeom->button_layout.right_buttons[fgeom->n_right_buttons - 1];
      break;
    case META_BUTTON_TYPE_RIGHT_MIDDLE_BACKGROUND:
      if (middle_bg_offset + 1 < fgeom->n_right_buttons)
        function = fgeom->button_layout.right_buttons[middle_bg_offset + 1];
      break;
    case META_BUTTON_TYPE_LEFT_LEFT_BACKGROUND:
    case META_BUTTON_TYPE_LEFT_SINGLE_BACKGROUND:
      if (fgeom->n_left_buttons > 0)
        function = fgeom->button_layout.left_buttons[0];
      break;
    case META_BUTTON_TYPE_LEFT_RIGHT_BACKGROUND:
      if (fgeom->n_left_buttons > 0)
        function = fgeom->button_layout.left_buttons[fgeom->n_left_buttons - 1];
      break;
    case META_BUTTON_TYPE_LEFT_MIDDLE_BACKGROUND:
      if (middle_bg_offset + 1 < fgeom->n_left_buttons)
        function = fgeom->button_layout.left_buttons[middle_bg_offset + 1];
      break;
    case META_BUTTON_TYPE_LAST:
      break;
    }

  if (function != META_BUTTON_FUNCTION_LAST)
    return button_states[map_button_function_to_type (function)];

  return META_BUTTON_STATE_LAST;
}

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 the side buttons if we don't have
   * single button backgrounds, and to middle button
   * backgrounds if we don't have the ones on the sides
   */

  if (op_list == NULL &&
      type == META_BUTTON_TYPE_LEFT_SINGLE_BACKGROUND)
    return get_button (style, META_BUTTON_TYPE_LEFT_LEFT_BACKGROUND, state);

  if (op_list == NULL &&
      type == META_BUTTON_TYPE_RIGHT_SINGLE_BACKGROUND)
    return get_button (style, META_BUTTON_TYPE_RIGHT_RIGHT_BACKGROUND, state);

  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;
}

LOCAL_SYMBOL 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_LEFT_SINGLE_BACKGROUND:
      *rect = fgeom->left_single_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_RIGHT_SINGLE_BACKGROUND:
      *rect = fgeom->right_single_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;
    }
}

LOCAL_SYMBOL LOCAL_SYMBOL void
meta_frame_style_draw_with_style (MetaFrameStyle          *style,
                                  GtkStyleContext         *style_gtk,
                                  GtkWidget               *widget,
                                  cairo_t                 *cr,
                                  const MetaFrameGeometry *fgeom,
                                  int                      client_width,
                                  int                      client_height,
                                  PangoLayout             *title_layout,
                                  int                      text_height,
                                  MetaButtonState          button_states[META_BUTTON_TYPE_LAST])
{
  int i, j;
  GdkRectangle visible_rect;
  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 logical_rect;
  MetaDrawInfo draw_info;
  const MetaFrameBorders *borders;

  borders = &fgeom->borders;

  visible_rect.x = borders->invisible.left;
  visible_rect.y = borders->invisible.top;
  visible_rect.width = fgeom->width - borders->invisible.left - borders->invisible.right;
  visible_rect.height = fgeom->height - borders->invisible.top - borders->invisible.bottom;

  titlebar_rect.x = visible_rect.x;
  titlebar_rect.y = visible_rect.y;
  titlebar_rect.width = visible_rect.width;
  titlebar_rect.height = borders->visible.top;

  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 = visible_rect.x;
  left_edge.y = visible_rect.y + borders->visible.top;
  left_edge.width = borders->visible.left;
  left_edge.height = visible_rect.height - borders->visible.top - borders->visible.bottom;

  right_edge.x = visible_rect.x + visible_rect.width - borders->visible.right;
  right_edge.y = visible_rect.y + borders->visible.top;
  right_edge.width = borders->visible.right;
  right_edge.height = visible_rect.height - borders->visible.top - borders->visible.bottom;

  bottom_edge.x = visible_rect.x;
  bottom_edge.y = visible_rect.y + visible_rect.height - borders->visible.bottom;
  bottom_edge.width = visible_rect.width;
  bottom_edge.height = borders->visible.bottom;

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

  draw_info.title_layout = title_layout;
  draw_info.title_layout_width = title_layout ? logical_rect.width : 0;
  draw_info.title_layout_height = title_layout ? logical_rect.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;

      switch ((MetaFramePiece) i)
        {
        case META_FRAME_PIECE_ENTIRE_BACKGROUND:
          rect = visible_rect;
          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 = visible_rect;
          break;

        case META_FRAME_PIECE_LAST:
          g_assert_not_reached ();
          break;
        }

      cairo_save (cr);

      gdk_cairo_rectangle (cr, &rect);
      cairo_clip (cr);

      if (gdk_cairo_get_clip_rectangle (cr, NULL))
        {
          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,
                                                 cr,
                                                 &draw_info,
                                                 m_rect);
            }
        }

      cairo_restore (cr);

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

          middle_bg_offset = 0;
          j = 0;
          while (j < META_BUTTON_TYPE_LAST)
            {
              MetaButtonState button_state;

              button_rect (j, fgeom, middle_bg_offset, &rect);

              button_state = map_button_state (j, fgeom, middle_bg_offset, button_states);

              op_list = get_button (style, j, button_state);

              if (op_list)
                {
                  cairo_save (cr);
                  gdk_cairo_rectangle (cr, &rect);
                  cairo_clip (cr);

                  if (gdk_cairo_get_clip_rectangle (cr, NULL))
                    {
                      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,
                                                         cr,
                                                         &draw_info,
                                                         m_rect);
                    }

                  cairo_restore (cr);
                }

              /* 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)
                {
                  ++middle_bg_offset;
                }
              else
                {
                  middle_bg_offset = 0;
                  ++j;
                }
            }
        }

      ++i;
    }
}

LOCAL_SYMBOL void
meta_frame_style_draw (MetaFrameStyle          *style,
                       GtkWidget               *widget,
                       cairo_t                 *cr,
                       const MetaFrameGeometry *fgeom,
                       int                      client_width,
                       int                      client_height,
                       PangoLayout             *title_layout,
                       int                      text_height,
                       MetaButtonState          button_states[META_BUTTON_TYPE_LAST])
{
  meta_frame_style_draw_with_style (style, gtk_widget_get_style_context (widget), widget,
                                    cr, fgeom, client_width, client_height,
                                    title_layout, text_height,
                                    button_states);
}

LOCAL_SYMBOL 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]);
}

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

  style_set->refcount += 1;
}

LOCAL_SYMBOL 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->tiled_left_styles);
      free_focus_styles (style_set->tiled_right_styles);
      free_focus_styles (style_set->maximized_and_shaded_styles);
      free_focus_styles (style_set->tiled_left_and_shaded_styles);
      free_focus_styles (style_set->tiled_right_and_shaded_styles);

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

      DEBUG_FILL_STRUCT (style_set);
      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_TILED_LEFT:
            styles = style_set->tiled_left_styles;
            break;
          case META_FRAME_STATE_TILED_RIGHT:
            styles = style_set->tiled_right_styles;
            break;
          case META_FRAME_STATE_MAXIMIZED_AND_SHADED:
            styles = style_set->maximized_and_shaded_styles;
            break;
          case META_FRAME_STATE_TILED_LEFT_AND_SHADED:
            styles = style_set->tiled_left_and_shaded_styles;
            break;
          case META_FRAME_STATE_TILED_RIGHT_AND_SHADED:
            styles = style_set->tiled_right_and_shaded_styles;
            break;
          case META_FRAME_STATE_NORMAL:
          case META_FRAME_STATE_SHADED:
          case META_FRAME_STATE_LAST:
            break;
          }

        style = styles[focus];

        /* Tiled states are optional, try falling back to non-tiled states */
        if (style == NULL)
          {
            if (state == META_FRAME_STATE_TILED_LEFT ||
                state == META_FRAME_STATE_TILED_RIGHT)
              style = get_style (style_set, META_FRAME_STATE_NORMAL,
                                 resize, focus);
            else if (state == META_FRAME_STATE_TILED_LEFT_AND_SHADED ||
                     state == META_FRAME_STATE_TILED_RIGHT_AND_SHADED)
              style = get_style (style_set, META_FRAME_STATE_SHADED,
                                 resize, 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;
}

LOCAL_SYMBOL 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;
}

/**
 * meta_theme_get_current: (skip)
 *
 */
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);
    }
}

/**
 * meta_theme_new: (skip)
 *
 */
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,
                           free,
                           (GDestroyNotify) g_object_unref);

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

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

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

  theme->style_sets_by_name =
    g_hash_table_new_full (g_str_hash,
                           g_str_equal,
                           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");
  theme->quark_frame_x_center = g_quark_from_static_string ("frame_x_center");
  theme->quark_frame_y_center = g_quark_from_static_string ("frame_y_center");
  return theme;
}


void
meta_theme_free (MetaTheme *theme)
{
  int i;

  g_return_if_fail (theme != NULL);

  free (theme->name);
  free (theme->dirname);
  free (theme->filename);
  free (theme->readable_name);
  free (theme->date);
  free (theme->description);
  free (theme->author);
  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);
  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 (i != (int)META_FRAME_TYPE_ATTACHED && 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;
}

/**
 * meta_theme_load_image: (skip)
 *
 */
LOCAL_SYMBOL GdkPixbuf*
meta_theme_load_image (MetaTheme  *theme,
                       const char *filename,
                       guint       scale,
                       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_for_scale (gtk_icon_theme_get_default (),
                                                       filename+6,
                                                       20,
                                                       scale,
                                                       0,
                                                       error);
          if (pixbuf == NULL) return NULL;
         }
      else
        {
          char *full_path;
          gint width, height;

          full_path = g_build_filename (theme->dirname, filename, NULL);

          if (gdk_pixbuf_get_file_info (full_path, &width, &height) == NULL)
            {
              g_free (full_path);
              return NULL;
            }

          width *= scale;
          height *= scale;

          pixbuf = gdk_pixbuf_new_from_file_at_size (full_path, width, height, 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];

  if (style_set == NULL && type == META_FRAME_TYPE_ATTACHED)
    style_set = theme->style_sets_by_type[META_FRAME_TYPE_BORDER];

  /* Right now the parser forces a style set for all other 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 |
                   META_FRAME_TILED_LEFT | META_FRAME_TILED_RIGHT))
    {
    case 0:
      state = META_FRAME_STATE_NORMAL;
      break;
    case META_FRAME_MAXIMIZED:
      state = META_FRAME_STATE_MAXIMIZED;
      break;
    case META_FRAME_TILED_LEFT:
      state = META_FRAME_STATE_TILED_LEFT;
      break;
    case META_FRAME_TILED_RIGHT:
      state = META_FRAME_STATE_TILED_RIGHT;
      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;
    case (META_FRAME_TILED_LEFT | META_FRAME_SHADED):
      state = META_FRAME_STATE_TILED_LEFT_AND_SHADED;
      break;
    case (META_FRAME_TILED_RIGHT | META_FRAME_SHADED):
      state = META_FRAME_STATE_TILED_RIGHT_AND_SHADED;
      break;
    default:
      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:
    case META_FRAME_ALLOWS_BOTTOM_RESIZE:
    case META_FRAME_ALLOWS_TOP_RESIZE:
      resize = META_FRAME_RESIZE_VERTICAL;
      break;
    case META_FRAME_ALLOWS_HORIZONTAL_RESIZE:
    case META_FRAME_ALLOWS_LEFT_RESIZE:
    case META_FRAME_ALLOWS_RIGHT_RESIZE:
      resize = META_FRAME_RESIZE_HORIZONTAL;
      break;
    case (META_FRAME_ALLOWS_VERTICAL_RESIZE | META_FRAME_ALLOWS_HORIZONTAL_RESIZE):
    case (META_FRAME_ALLOWS_LEFT_RESIZE | META_FRAME_ALLOWS_BOTTOM_RESIZE):
    case (META_FRAME_ALLOWS_RIGHT_RESIZE | META_FRAME_ALLOWS_BOTTOM_RESIZE):
    case (META_FRAME_ALLOWS_LEFT_RESIZE | META_FRAME_ALLOWS_TOP_RESIZE):
    case (META_FRAME_ALLOWS_RIGHT_RESIZE | META_FRAME_ALLOWS_TOP_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;
}

LOCAL_SYMBOL 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;
}

LOCAL_SYMBOL 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;
}

LOCAL_SYMBOL void
meta_theme_draw_frame_with_style (MetaTheme              *theme,
                                  GtkStyleContext        *style_gtk,
                                  GtkWidget              *widget,
                                  cairo_t                *cr,
                                  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])
{
  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,
                                   type,
                                   &fgeom,
                                   theme);

  meta_frame_style_draw_with_style (style,
                                    style_gtk,
                                    widget,
                                    cr,
                                    &fgeom,
                                    client_width, client_height,
                                    title_layout,
                                    text_height,
                                    button_states);
}

void
meta_theme_draw_frame (MetaTheme              *theme,
                       GtkWidget              *widget,
                       cairo_t                *cr,
                       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])
{
  meta_theme_draw_frame_with_style (theme, gtk_widget_get_style_context (widget), widget,
                                    cr, type,flags,
                                    client_width, client_height,
                                    title_layout, text_height,
                                    button_layout, button_states);
}

void
meta_theme_get_frame_borders (MetaTheme        *theme,
                              MetaFrameType     type,
                              int               text_height,
                              MetaFrameFlags    flags,
                              MetaFrameBorders *borders)
{
  MetaFrameStyle *style;

  g_return_if_fail (type < META_FRAME_TYPE_LAST);

  style = theme_get_style (theme, type, flags);

  meta_frame_borders_clear (borders);

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

  meta_frame_layout_get_borders (style->layout,
                                 text_height,
                                 flags, type,
                                 borders);
}

LOCAL_SYMBOL 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,
                                   type,
                                   fgeom,
                                   theme);
}

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

LOCAL_SYMBOL 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);
}

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

LOCAL_SYMBOL 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);
}

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

LOCAL_SYMBOL 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);
}

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

LOCAL_SYMBOL LOCAL_SYMBOL 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);
}

LOCAL_SYMBOL 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,
                                                      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;
}

LOCAL_SYMBOL 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;
    }
}

LOCAL_SYMBOL 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,
                                                    free,
                                                    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;
}

LOCAL_SYMBOL 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;
    }
}

LOCAL_SYMBOL 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,
                                                    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
 */
LOCAL_SYMBOL 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;
    }
}


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

  g_return_val_if_fail (gtk_widget_get_realized (widget), NULL);

  style = gtk_widget_get_style_context (widget);
  gtk_style_context_get (style, gtk_style_context_get_state (style),
                         GTK_STYLE_PROPERTY_FONT, &font_desc,
                         NULL);

  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;
}

LOCAL_SYMBOL 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;
}

LOCAL_SYMBOL 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>";
}

LOCAL_SYMBOL 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;
}

LOCAL_SYMBOL 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>";
}

LOCAL_SYMBOL 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 ("left_single_background", str) == 0)
    return META_BUTTON_TYPE_LEFT_SINGLE_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 if (strcmp ("right_single_background", str) == 0)
    return META_BUTTON_TYPE_RIGHT_SINGLE_BACKGROUND;
  else
    return META_BUTTON_TYPE_LAST;
}

LOCAL_SYMBOL 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_LEFT_SINGLE_BACKGROUND:
      return "left_single_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_RIGHT_SINGLE_BACKGROUND:
      return "right_single_background";
    case META_BUTTON_TYPE_LAST:
      break;
    }

  return "<unknown>";
}

LOCAL_SYMBOL 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;
}

LOCAL_SYMBOL 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>";
}

LOCAL_SYMBOL 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 ("tiled_left", str) == 0)
    return META_FRAME_STATE_TILED_LEFT;
  else if (strcmp ("tiled_right", str) == 0)
    return META_FRAME_STATE_TILED_RIGHT;
  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 if (strcmp ("tiled_left_and_shaded", str) == 0)
    return META_FRAME_STATE_TILED_LEFT_AND_SHADED;
  else if (strcmp ("tiled_right_and_shaded", str) == 0)
    return META_FRAME_STATE_TILED_RIGHT_AND_SHADED;
  else
    return META_FRAME_STATE_LAST;
}

LOCAL_SYMBOL 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_TILED_LEFT:
      return "tiled_left";
    case META_FRAME_STATE_TILED_RIGHT:
      return "tiled_right";
    case META_FRAME_STATE_SHADED:
      return "shaded";
    case META_FRAME_STATE_MAXIMIZED_AND_SHADED:
      return "maximized_and_shaded";
    case META_FRAME_STATE_TILED_LEFT_AND_SHADED:
      return "tiled_left_and_shaded";
    case META_FRAME_STATE_TILED_RIGHT_AND_SHADED:
      return "tiled_right_and_shaded";
    case META_FRAME_STATE_LAST:
      break;
    }

  return "<unknown>";
}

LOCAL_SYMBOL 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;
}

LOCAL_SYMBOL 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>";
}

LOCAL_SYMBOL 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;
}

LOCAL_SYMBOL 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>";
}

LOCAL_SYMBOL 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;
  else if (strcmp ("attached", str) == 0)
    return META_FRAME_TYPE_ATTACHED;
#if 0
  else if (strcmp ("toolbar", str) == 0)
    return META_FRAME_TYPE_TOOLBAR;
#endif
  else
    return META_FRAME_TYPE_LAST;
}

/**
 * meta_frame_type_to_string:
 *
 * Converts a frame type enum value to the name string that would
 * appear in the theme definition file.
 *
 * Return value: the string value
 */
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";
    case META_FRAME_TYPE_ATTACHED:
      return "attached";
#if 0
    case META_FRAME_TYPE_TOOLBAR:
      return "toolbar";
#endif
    case  META_FRAME_TYPE_LAST:
      break;
    }

  return "<unknown>";
}

LOCAL_SYMBOL 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;
}

LOCAL_SYMBOL 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>";
}

LOCAL_SYMBOL GtkStateFlags
meta_gtk_state_from_string (const char *str)
{
  if (g_ascii_strcasecmp ("normal", str) == 0)
    return GTK_STATE_FLAG_NORMAL;
  else if (g_ascii_strcasecmp ("prelight", str) == 0)
    return GTK_STATE_FLAG_PRELIGHT;
  else if (g_ascii_strcasecmp ("active", str) == 0)
    return GTK_STATE_FLAG_ACTIVE;
  else if (g_ascii_strcasecmp ("selected", str) == 0)
    return GTK_STATE_FLAG_SELECTED;
  else if (g_ascii_strcasecmp ("insensitive", str) == 0)
    return GTK_STATE_FLAG_INSENSITIVE;
  else if (g_ascii_strcasecmp ("inconsistent", str) == 0)
    return GTK_STATE_FLAG_INCONSISTENT;
  else if (g_ascii_strcasecmp ("focused", str) == 0)
    return GTK_STATE_FLAG_FOCUSED;
  else if (g_ascii_strcasecmp ("backdrop", str) == 0)
    return GTK_STATE_FLAG_BACKDROP;
  else
    return -1; /* hack */
}

LOCAL_SYMBOL const char*
meta_gtk_state_to_string (GtkStateFlags state)
{
  switch (state)
    {
    case GTK_STATE_FLAG_NORMAL:
      return "NORMAL";
    case GTK_STATE_FLAG_PRELIGHT:
      return "PRELIGHT";
    case GTK_STATE_FLAG_ACTIVE:
      return "ACTIVE";
    case GTK_STATE_FLAG_SELECTED:
      return "SELECTED";
    case GTK_STATE_FLAG_INSENSITIVE:
      return "INSENSITIVE";
    case GTK_STATE_FLAG_INCONSISTENT:
      return "INCONSISTENT";
    case GTK_STATE_FLAG_FOCUSED:
      return "FOCUSED";
    case GTK_STATE_FLAG_BACKDROP:
      return "BACKDROP";
#if GTK_MAJOR_VERSION > 3 || (GTK_MAJOR_VERSION == 3 && GTK_MINOR_VERSION >= 7)
    case GTK_STATE_FLAG_DIR_LTR:
      return "DIR_LTR";
    case GTK_STATE_FLAG_DIR_RTL:
      return "DIR_RTL";
#endif
#if GTK_MAJOR_VERSION > 3 || (GTK_MAJOR_VERSION == 3 && GTK_MINOR_VERSION >= 12)
    case GTK_STATE_FLAG_LINK:
      return "LINK";
    case GTK_STATE_FLAG_VISITED:
      return "VISITED";
#endif
#if GTK_MAJOR_VERSION > 3 || (GTK_MAJOR_VERSION == 3 && GTK_MINOR_VERSION >= 14)
    case GTK_STATE_FLAG_CHECKED:
      return "CHECKED";
#endif
    }

  return "<unknown>";
}

LOCAL_SYMBOL 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;
}

LOCAL_SYMBOL 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>";
}

LOCAL_SYMBOL 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;
}

LOCAL_SYMBOL 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.
 */
LOCAL_SYMBOL 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
 */
LOCAL_SYMBOL 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 (GdkRGBA *a,
                 GdkRGBA *b,
                 gdouble   k)
{
  gdouble red;
  gdouble green;
  gdouble blue;

  red = a->red;
  green = a->green;
  blue = a->blue;

  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;
  b->green = green;
  b->blue = blue;
}

/*
 * 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)
{
  GdkRGBA 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:
      {
        GdkRGBA 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
 */
LOCAL_SYMBOL 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 1000;

    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 2000;

    case META_BUTTON_TYPE_LEFT_SINGLE_BACKGROUND:
    case META_BUTTON_TYPE_RIGHT_SINGLE_BACKGROUND:
      return 3003;

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