/*
* Copyright (C) 2001 Havoc Pennington
* Copyright (C) 2016 Alberts Muktupāvels
*
* 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, see .
*/
#include "config.h"
#include
#include "meta-color.h"
#include "meta-color-private.h"
#include "meta-color-spec-private.h"
#include "meta-theme.h"
enum _MetaColorSpecType
{
META_COLOR_SPEC_BASIC,
META_COLOR_SPEC_GTK,
META_COLOR_SPEC_GTK_CUSTOM,
META_COLOR_SPEC_BLEND,
META_COLOR_SPEC_SHADE
};
enum _MetaGtkColorComponent
{
META_GTK_COLOR_FG,
META_GTK_COLOR_BG,
META_GTK_COLOR_LIGHT,
META_GTK_COLOR_DARK,
META_GTK_COLOR_MID,
META_GTK_COLOR_TEXT,
META_GTK_COLOR_BASE,
META_GTK_COLOR_TEXT_AA,
META_GTK_COLOR_LAST
};
struct _MetaColorSpec
{
MetaColorSpecType type;
union
{
struct {
GdkRGBA color;
} basic;
struct {
MetaGtkColorComponent component;
GtkStateFlags state;
} gtk;
struct {
gchar *color_name;
MetaColorSpec *fallback;
} gtkcustom;
struct {
MetaColorSpec *foreground;
MetaColorSpec *background;
gdouble alpha;
GdkRGBA color;
} blend;
struct {
MetaColorSpec *base;
gdouble factor;
GdkRGBA color;
} shade;
} data;
};
static void
set_color_from_style (GdkRGBA *color,
GtkStyleContext *context,
GtkStateFlags state,
MetaGtkColorComponent component)
{
GdkRGBA other;
gtk_style_context_set_state (context, state);
switch (component)
{
case META_GTK_COLOR_BG:
case META_GTK_COLOR_BASE:
meta_color_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);
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_color_get_light_color (context, state, color);
meta_color_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_color_get_light_color (context, state, color);
break;
case META_GTK_COLOR_DARK:
meta_color_get_dark_color (context, state, color);
break;
case META_GTK_COLOR_LAST:
default:
g_assert_not_reached ();
break;
}
}
static void
color_composite (const GdkRGBA *bg,
const GdkRGBA *fg,
gdouble 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;
color->alpha = color->alpha + (fg->alpha - color->alpha) * alpha;
}
static void
set_custom_color_from_style (GdkRGBA *color,
GtkStyleContext *context,
const gchar *color_name,
MetaColorSpec *fallback)
{
if (!gtk_style_context_lookup_color (context, color_name, color))
meta_color_spec_render (fallback, context, color);
}
static MetaGtkColorComponent
meta_color_component_from_string (const gchar *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;
}
MetaColorSpec *
meta_color_spec_new (MetaColorSpecType type)
{
MetaColorSpec *spec;
MetaColorSpec dummy;
gint 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;
default:
break;
}
spec = g_malloc0 (size);
spec->type = type;
return spec;
}
MetaColorSpec *
meta_color_spec_new_from_string (const gchar *str,
GError **error)
{
MetaColorSpec *spec;
spec = NULL;
if (strncmp (str, "gtk:custom", 10) == 0)
{
const gchar *color_name_start;
const gchar *fallback_str_start;
const gchar *end;
gchar *fallback_str;
MetaColorSpec *fallback;
gchar *color_name;
if (str[10] != '(')
{
g_set_error (error, 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 (error, 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++;
}
if (*fallback_str_start != '\0')
fallback_str_start++;
end = strrchr (str, ')');
if (*color_name_start == '\0' ||
*fallback_str_start == '\0' ||
end == NULL)
{
g_set_error (error, 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;
}
fallback_str = g_strndup (fallback_str_start, end - fallback_str_start);
fallback = meta_color_spec_new_from_string (fallback_str, error);
g_free (fallback_str);
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 (strncmp (str, "gtk:", 4) == 0)
{
const gchar *bracket;
const gchar *end_bracket;
gchar *tmp;
GtkStateFlags state;
MetaGtkColorComponent component;
bracket = str;
while (*bracket && *bracket != '[')
++bracket;
if (*bracket == '\0')
{
g_set_error (error, 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 (error, 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);
if (!meta_gtk_state_from_string (tmp, &state))
{
g_set_error (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
_("Did not understand state '%s' in color specification"),
tmp);
g_free (tmp);
return NULL;
}
g_free (tmp);
tmp = g_strndup (str + 4, bracket - str - 4);
component = meta_color_component_from_string (tmp);
if (component == META_GTK_COLOR_LAST)
{
g_set_error (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
_("Did not understand color component '%s' in color specification"),
tmp);
g_free (tmp);
return NULL;
}
g_free (tmp);
spec = meta_color_spec_new (META_COLOR_SPEC_GTK);
spec->data.gtk.state = state;
spec->data.gtk.component = component;
g_assert (spec->data.gtk.component < META_GTK_COLOR_LAST);
}
else if (strncmp (str, "blend/", 6) == 0)
{
gchar **split;
gdouble alpha;
gchar *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 (error, 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 (error, 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 (error, 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], error);
if (bg == NULL)
{
g_strfreev (split);
return NULL;
}
fg = meta_color_spec_new_from_string (split[2], error);
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 (strncmp (str, "shade/", 6) == 0)
{
gchar **split;
gdouble factor;
gchar *end;
MetaColorSpec *base;
split = g_strsplit (str, "/", 3);
if (split[0] == NULL || split[1] == NULL || split[2] == NULL)
{
g_set_error (error, 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 (error, 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 (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
_("Shade factor '%s' in shaded color is negative"),
split[2]);
g_strfreev (split);
return NULL;
}
base = meta_color_spec_new_from_string (split[1], error);
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 (error, META_THEME_ERROR, META_THEME_ERROR_FAILED,
_("Could not parse color '%s'"), str);
meta_color_spec_free (spec);
return NULL;
}
}
g_assert (spec);
return spec;
}
MetaColorSpec *
meta_color_spec_new_gtk (MetaGtkColorComponent component,
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;
}
void
meta_color_spec_free (MetaColorSpec *spec)
{
g_return_if_fail (spec != NULL);
switch (spec->type)
{
case META_COLOR_SPEC_BASIC:
break;
case META_COLOR_SPEC_GTK:
break;
case META_COLOR_SPEC_GTK_CUSTOM:
if (spec->data.gtkcustom.color_name)
g_free (spec->data.gtkcustom.color_name);
if (spec->data.gtkcustom.fallback)
meta_color_spec_free (spec->data.gtkcustom.fallback);
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);
break;
case META_COLOR_SPEC_SHADE:
if (spec->data.shade.base)
meta_color_spec_free (spec->data.shade.base);
break;
default:
break;
}
g_free (spec);
}
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:
set_color_from_style (color, context, spec->data.gtk.state,
spec->data.gtk.component);
break;
case META_COLOR_SPEC_GTK_CUSTOM:
set_custom_color_from_style (color, context,
spec->data.gtkcustom.color_name,
spec->data.gtkcustom.fallback);
break;
case META_COLOR_SPEC_BLEND:
{
GdkRGBA bg;
GdkRGBA 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);
meta_color_shade (&spec->data.shade.color, spec->data.shade.factor,
&spec->data.shade.color);
*color = spec->data.shade.color;
}
break;
default:
break;
}
}
/**
* meta_gtk_state_from_string:
* @str: state string
* @state: (out): location to store #GtkStateFlags
*
* Convert string to #GtkStateFlags
*
* Returns: %TRUE if state string was valid, %FALSE otherwise
*/
gboolean
meta_gtk_state_from_string (const gchar *str,
GtkStateFlags *state)
{
if (g_ascii_strcasecmp ("normal", str) == 0)
*state = GTK_STATE_FLAG_NORMAL;
else if (g_ascii_strcasecmp ("prelight", str) == 0)
*state = GTK_STATE_FLAG_PRELIGHT;
else if (g_ascii_strcasecmp ("active", str) == 0)
*state = GTK_STATE_FLAG_ACTIVE;
else if (g_ascii_strcasecmp ("selected", str) == 0)
*state = GTK_STATE_FLAG_SELECTED;
else if (g_ascii_strcasecmp ("insensitive", str) == 0)
*state = GTK_STATE_FLAG_INSENSITIVE;
else if (g_ascii_strcasecmp ("inconsistent", str) == 0)
*state = GTK_STATE_FLAG_INCONSISTENT;
else if (g_ascii_strcasecmp ("focused", str) == 0)
*state = GTK_STATE_FLAG_FOCUSED;
else if (g_ascii_strcasecmp ("backdrop", str) == 0)
*state = GTK_STATE_FLAG_BACKDROP;
else
return FALSE;
return TRUE;
}