/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ /* * eek-theme-node.c: style information for one node in a tree of themed objects * * Copyright 2008-2010 Red Hat, Inc. * Copyright 2009 Steve Frécinaux * Copyright 2009, 2010 Florian Müllner * Copyright 2010 Adel Gadllah * Copyright 2010 Giovanni Campagna * Copyright 2010-2011 Daiki Ueno * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 2.1 of * the License, or (at your option) any later version. * * This program is distributed in the hope it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif /* HAVE_CONFIG_H */ #include #include #include #include "eek-theme-context.h" #include "eek-theme-node.h" #include "eek-theme-private.h" struct _EekThemeNode { GObject parent; EekThemeContext *context; EekThemeNode *parent_node; EekTheme *theme; PangoFontDescription *font_desc; EekGradientType background_gradient_type; EekColor background_color; EekColor background_gradient_end; EekColor foreground_color; EekColor border_color[4]; int border_width[4]; int border_radius[4]; GType element_type; char *element_id; char *element_class; char *pseudo_class; char *inline_style; CRDeclaration **properties; int n_properties; /* We hold onto these separately so we can destroy them on finalize */ CRDeclaration *inline_properties; guint properties_computed : 1; guint geometry_computed : 1; guint background_computed : 1; guint foreground_computed : 1; }; struct _EekThemeNodeClass { GObjectClass parent_class; }; G_DEFINE_TYPE (EekThemeNode, eek_theme_node, G_TYPE_OBJECT); #define EEK_THEME_NODE_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE ((obj), EEK_TYPE_THEME_NODE, EekThemeNodePrivate)) static void eek_theme_node_init (EekThemeNode *node); static void eek_theme_node_class_init (EekThemeNodeClass *klass); static void eek_theme_node_dispose (GObject *object); static void eek_theme_node_finalize (GObject *object); static const EekColor BLACK_COLOR = { 0, 0, 0, 0xff }; static const EekColor TRANSPARENT_COLOR = { 0, 0, 0, 0 }; #if 0 // ===== REMOVAL NOTICE ==== expires by: 2019-08-20 -Wunused-const-variable ===== static const EekColor DEFAULT_SUCCESS_COLOR = { 0x4e, 0x9a, 0x06, 0xff }; static const EekColor DEFAULT_WARNING_COLOR = { 0xf5, 0x79, 0x3e, 0xff }; static const EekColor DEFAULT_ERROR_COLOR = { 0xcc, 0x00, 0x00, 0xff }; #endif // ===== REMOVAL NOTICE ==== expires by: 2019-08-20 -Wunused-const-variable ===== static void eek_theme_node_init (EekThemeNode *self) { } static void eek_theme_node_class_init (EekThemeNodeClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = eek_theme_node_dispose; object_class->finalize = eek_theme_node_finalize; } static void eek_theme_node_dispose (GObject *gobject) { EekThemeNode *node = EEK_THEME_NODE (gobject); if (node->context) { g_object_unref (node->context); node->context = NULL; } if (node->theme) { g_object_unref (node->theme); node->theme = NULL; } if (node->parent_node) { g_object_unref (node->parent_node); node->parent_node = NULL; } G_OBJECT_CLASS (eek_theme_node_parent_class)->dispose (gobject); } static void eek_theme_node_finalize (GObject *object) { EekThemeNode *node = EEK_THEME_NODE (object); g_free (node->element_id); g_free (node->element_class); g_free (node->pseudo_class); g_free (node->inline_style); if (node->properties) { g_free (node->properties); node->properties = NULL; node->n_properties = 0; } if (node->inline_properties) { /* This destroys the list, not just the head of the list */ cr_declaration_destroy (node->inline_properties); } if (node->font_desc) { pango_font_description_free (node->font_desc); node->font_desc = NULL; } G_OBJECT_CLASS (eek_theme_node_parent_class)->finalize (object); } /** * eek_theme_node_new: * @parent_node: (allow-none): the parent node of this node * @theme: (allow-none): a theme (stylesheet set) that overrides the * theme inherited from the parent node * @element_type: the type of the GObject represented by this node * in the tree (corresponding to an element if we were theming an XML * document. %G_TYPE_NONE means this style was created for the stage * actor and matches a selector element name of 'stage'. * @element_id: (allow-none): the ID to match CSS rules against * @element_class: (allow-none): a whitespace-separated list of classes * to match CSS rules against * @pseudo_class: (allow-none): a whitespace-separated list of pseudo-classes * (like 'hover' or 'visited') to match CSS rules against * * Creates a new #EekThemeNode. Once created, a node is immutable. Of any * of the attributes of the node (like the @element_class) change the node * and its child nodes must be destroyed and recreated. * * Return value: (transfer full): the theme node */ EekThemeNode * eek_theme_node_new (EekThemeContext *context, EekThemeNode *parent_node, EekTheme *theme, GType element_type, const char *element_id, const char *element_class, const char *pseudo_class, const char *inline_style) { EekThemeNode *node; g_return_val_if_fail (EEK_IS_THEME_CONTEXT (context), NULL); g_return_val_if_fail (parent_node == NULL || EEK_IS_THEME_NODE (parent_node), NULL); node = g_object_new (EEK_TYPE_THEME_NODE, NULL); node->context = g_object_ref (context); if (parent_node != NULL) node->parent_node = g_object_ref (parent_node); else node->parent_node = NULL; if (theme == NULL && parent_node != NULL) theme = eek_theme_node_get_theme (parent_node); if (theme != NULL) node->theme = g_object_ref (theme); node->element_type = element_type; node->element_id = g_strdup (element_id); node->element_class = g_strdup (element_class); node->pseudo_class = g_strdup (pseudo_class); node->inline_style = g_strdup (inline_style); return node; } /** * eek_theme_node_get_parent: * @node: a #EekThemeNode * * Gets the parent themed element node. * * Return value: (transfer none): the parent #EekThemeNode, or %NULL if this * is the root node of the tree of theme elements. */ EekThemeNode * eek_theme_node_get_parent (EekThemeNode *node) { g_return_val_if_fail (EEK_IS_THEME_NODE (node), NULL); return node->parent_node; } /** * eek_theme_node_get_theme: * @node: a #EekThemeNode * * Gets the theme stylesheet set that styles this node * * Return value: (transfer none): the theme stylesheet set */ EekTheme * eek_theme_node_get_theme (EekThemeNode *node) { g_return_val_if_fail (EEK_IS_THEME_NODE (node), NULL); return node->theme; } GType eek_theme_node_get_element_type (EekThemeNode *node) { g_return_val_if_fail (EEK_IS_THEME_NODE (node), G_TYPE_NONE); return node->element_type; } const char * eek_theme_node_get_element_id (EekThemeNode *node) { g_return_val_if_fail (EEK_IS_THEME_NODE (node), NULL); return node->element_id; } const char * eek_theme_node_get_element_class (EekThemeNode *node) { g_return_val_if_fail (EEK_IS_THEME_NODE (node), NULL); return node->element_class; } const char * eek_theme_node_get_pseudo_class (EekThemeNode *node) { g_return_val_if_fail (EEK_IS_THEME_NODE (node), NULL); return node->pseudo_class; } static void ensure_properties (EekThemeNode *node) { if (!node->properties_computed) { GPtrArray *properties = NULL; node->properties_computed = TRUE; if (node->theme) properties = _eek_theme_get_matched_properties (node->theme, node); if (node->inline_style) { CRDeclaration *cur_decl; if (!properties) properties = g_ptr_array_new (); node->inline_properties = _eek_theme_parse_declaration_list (node->inline_style); for (cur_decl = node->inline_properties; cur_decl; cur_decl = cur_decl->next) g_ptr_array_add (properties, cur_decl); } if (properties) { node->n_properties = properties->len; node->properties = (CRDeclaration **)g_ptr_array_free (properties, FALSE); } } } typedef enum { VALUE_FOUND, VALUE_NOT_FOUND, VALUE_INHERIT } GetFromTermResult; static gboolean term_is_none (CRTerm *term) { return (term->type == TERM_IDENT && strcmp (term->content.str->stryng->str, "none") == 0); } static gboolean term_is_transparent (CRTerm *term) { return (term->type == TERM_IDENT && strcmp (term->content.str->stryng->str, "transparent") == 0); } static GetFromTermResult get_color_from_rgba_term (CRTerm *term, EekColor *color) { CRTerm *arg = term->ext_content.func_param; CRNum *num; double r = 0, g = 0, b = 0, a = 0; int i; for (i = 0; i < 4; i++) { double value; if (arg == NULL) return VALUE_NOT_FOUND; if ((i == 0 && arg->the_operator != NO_OP) || (i > 0 && arg->the_operator != COMMA)) return VALUE_NOT_FOUND; if (arg->type != TERM_NUMBER) return VALUE_NOT_FOUND; num = arg->content.num; /* For simplicity, we convert a,r,g,b to [0,1.0] floats and then * convert them back below. Then when we set them on a cairo content * we convert them back to floats, and then cairo converts them * back to integers to pass them to X, and so forth... */ if (i < 3) { if (num->type == NUM_PERCENTAGE) value = num->val / 100; else if (num->type == NUM_GENERIC) value = num->val / 255; else return VALUE_NOT_FOUND; } else { if (num->type != NUM_GENERIC) return VALUE_NOT_FOUND; value = num->val; } value = CLAMP (value, 0, 1); switch (i) { case 0: r = value; break; case 1: g = value; break; case 2: b = value; break; case 3: a = value; break; } arg = arg->next; } color->red = CLAMP(r, 0.0, 1.0); color->green = CLAMP(g, 0.0, 1.0); color->blue = CLAMP(b, 0.0, 1.0); color->alpha = CLAMP(a, 0.0, 1.0); return VALUE_FOUND; } static GetFromTermResult get_color_from_term (EekThemeNode *node, CRTerm *term, EekColor *color) { CRRgb rgb; enum CRStatus status; /* Since libcroco doesn't know about rgba colors, it can't handle * the transparent keyword */ if (term_is_transparent (term)) { *color = TRANSPARENT_COLOR; return VALUE_FOUND; } /* rgba () colors - a CSS3 addition, are not supported by libcroco, * but they are parsed as a "function", so we can emulate the * functionality. * * libcroco < 0.6.2 has a bug where functions starting with 'r' are * misparsed. We workaround this by pre-converting 'rgba' to 'RGBA' * before parsing the stylesheet. Since libcroco isn't * case-insensitive (a bug), it's fine with functions starting with * 'R'. (In theory, we should be doing a case-insensitive compare * everywhere, not just here, but that doesn't make much sense when * the built-in parsing of libcroco is case-sensitive and things * like 10PX don't work.) */ else if (term->type == TERM_FUNCTION && term->content.str && term->content.str->stryng && term->content.str->stryng->str && g_ascii_strcasecmp (term->content.str->stryng->str, "rgba") == 0) { return get_color_from_rgba_term (term, color); } status = cr_rgb_set_from_term (&rgb, term); if (status != CR_OK) return VALUE_NOT_FOUND; if (rgb.inherit) return VALUE_INHERIT; if (rgb.is_percentage) cr_rgb_compute_from_percentage (&rgb); color->red = rgb.red / (gdouble)0xff; color->green = rgb.green / (gdouble)0xff; color->blue = rgb.blue / (gdouble)0xff; color->alpha = 1.0; return VALUE_FOUND; } /** * eek_theme_node_lookup_color: * @node: a #EekThemeNode * @property_name: The name of the color property * @inherit: if %TRUE, if a value is not found for the property on the * node, then it will be looked up on the parent node, and then on the * parent's parent, and so forth. Note that if the property has a * value of 'inherit' it will be inherited even if %FALSE is passed * in for @inherit; this only affects the default behavior for inheritance. * @color: location to store the color that was determined. * If the property is not found, the value in this location * will not be changed. * * Generically looks up a property containing a single color value. When * specific getters (like eek_theme_node_get_background_color()) exist, they * should be used instead. They are cached, so more efficient, and have * handling for shortcut properties and other details of CSS. * * Return value: %TRUE if the property was found in the properties for this * theme node (or in the properties of parent nodes when inheriting.) */ gboolean eek_theme_node_lookup_color (EekThemeNode *node, const char *property_name, gboolean inherit, EekColor *color) { int i; g_return_val_if_fail (EEK_IS_THEME_NODE(node), FALSE); ensure_properties (node); for (i = node->n_properties - 1; i >= 0; i--) { CRDeclaration *decl = node->properties[i]; if (strcmp (decl->property->stryng->str, property_name) == 0) { GetFromTermResult result = get_color_from_term (node, decl->value, color); if (result == VALUE_FOUND) { return TRUE; } else if (result == VALUE_INHERIT) { if (node->parent_node) return eek_theme_node_lookup_color (node->parent_node, property_name, inherit, color); else break; } } } if (inherit && node->parent_node) return eek_theme_node_lookup_color (node->parent_node, property_name, inherit, color); return FALSE; } /** * eek_theme_node_get_color: * @node: a #EekThemeNode * @property_name: The name of the color property * @color: location to store the color that was determined. * * Generically looks up a property containing a single color value. When * specific getters (like eek_theme_node_get_background_color()) exist, they * should be used instead. They are cached, so more efficient, and have * handling for shortcut properties and other details of CSS. * * If @property_name is not found, a warning will be logged and a * default color returned. * * See also eek_theme_node_lookup_color(), which provides more options, * and lets you handle the case where the theme does not specify the * indicated color. */ void eek_theme_node_get_color (EekThemeNode *node, const char *property_name, EekColor *color) { if (!eek_theme_node_lookup_color (node, property_name, FALSE, color)) { g_warning ("Did not find color property '%s'", property_name); memset (color, 0, sizeof (EekColor)); } } /** * eek_theme_node_lookup_double: * @node: a #EekThemeNode * @property_name: The name of the numeric property * @inherit: if %TRUE, if a value is not found for the property on the * node, then it will be looked up on the parent node, and then on the * parent's parent, and so forth. Note that if the property has a * value of 'inherit' it will be inherited even if %FALSE is passed * in for @inherit; this only affects the default behavior for inheritance. * @value: (out): location to store the value that was determined. * If the property is not found, the value in this location * will not be changed. * * Generically looks up a property containing a single numeric value * without units. * * See also eek_theme_node_get_double(), which provides a simpler API. * * Return value: %TRUE if the property was found in the properties for this * theme node (or in the properties of parent nodes when inheriting.) */ gboolean eek_theme_node_lookup_double (EekThemeNode *node, const char *property_name, gboolean inherit, double *value) { gboolean result = FALSE; int i; g_return_val_if_fail (EEK_IS_THEME_NODE(node), FALSE); ensure_properties (node); for (i = node->n_properties - 1; i >= 0; i--) { CRDeclaration *decl = node->properties[i]; if (strcmp (decl->property->stryng->str, property_name) == 0) { CRTerm *term = decl->value; if (term->type != TERM_NUMBER || term->content.num->type != NUM_GENERIC) continue; *value = term->content.num->val; result = TRUE; break; } } if (!result && inherit && node->parent_node) result = eek_theme_node_lookup_double (node->parent_node, property_name, inherit, value); return result; } /** * eek_theme_node_get_double: * @node: a #EekThemeNode * @property_name: The name of the numeric property * * Generically looks up a property containing a single numeric value * without units. * * See also eek_theme_node_lookup_double(), which provides more options, * and lets you handle the case where the theme does not specify the * indicated value. * * Return value: the value found. If @property_name is not * found, a warning will be logged and 0 will be returned. */ gdouble eek_theme_node_get_double (EekThemeNode *node, const char *property_name) { gdouble value; if (eek_theme_node_lookup_double (node, property_name, FALSE, &value)) return value; else { g_warning ("Did not find double property '%s'", property_name); return 0.0; } } static const PangoFontDescription * get_parent_font (EekThemeNode *node) { if (node->parent_node) return eek_theme_node_get_font (node->parent_node); else return eek_theme_context_get_font (node->context); } static GetFromTermResult get_length_from_term (EekThemeNode *node, CRTerm *term, gboolean use_parent_font, gdouble *length) { CRNum *num; enum { ABSOLUTE, POINTS, FONT_RELATIVE, } type = ABSOLUTE; double multiplier = 1.0; if (term->type != TERM_NUMBER) { g_warning ("Ignoring length property that isn't a number"); return VALUE_NOT_FOUND; } num = term->content.num; switch (num->type) { case NUM_LENGTH_PX: type = ABSOLUTE; multiplier = 1; break; case NUM_LENGTH_PT: type = POINTS; multiplier = 1; break; case NUM_LENGTH_IN: type = POINTS; multiplier = 72; break; case NUM_LENGTH_CM: type = POINTS; multiplier = 72. / 2.54; break; case NUM_LENGTH_MM: type = POINTS; multiplier = 72. / 25.4; break; case NUM_LENGTH_PC: type = POINTS; multiplier = 12. / 25.4; break; case NUM_LENGTH_EM: { type = FONT_RELATIVE; multiplier = 1; break; } case NUM_LENGTH_EX: { /* Doing better would require actually resolving the font description * to a specific font, and Pango doesn't have an ex metric anyways, * so we'd have to try and synthesize it by complicated means. * * The 0.5em is the CSS spec suggested thing to use when nothing * better is available. */ type = FONT_RELATIVE; multiplier = 0.5; break; } case NUM_INHERIT: return VALUE_INHERIT; case NUM_AUTO: g_warning ("'auto' not supported for lengths"); return VALUE_NOT_FOUND; case NUM_GENERIC: { if (num->val != 0) { g_warning ("length values must specify a unit"); return VALUE_NOT_FOUND; } else { type = ABSOLUTE; multiplier = 0; } break; } case NUM_PERCENTAGE: g_warning ("percentage lengths not currently supported"); return VALUE_NOT_FOUND; case NUM_ANGLE_DEG: case NUM_ANGLE_RAD: case NUM_ANGLE_GRAD: case NUM_TIME_MS: case NUM_TIME_S: case NUM_FREQ_HZ: case NUM_FREQ_KHZ: case NUM_UNKNOWN_TYPE: case NB_NUM_TYPE: g_warning ("Ignoring invalid type of number of length property"); return VALUE_NOT_FOUND; } switch (type) { case ABSOLUTE: *length = num->val * multiplier; break; case POINTS: { double resolution = eek_theme_context_get_resolution (node->context); *length = num->val * multiplier * (resolution / 72.); } break; case FONT_RELATIVE: { const PangoFontDescription *desc; double font_size; if (use_parent_font) desc = get_parent_font (node); else desc = eek_theme_node_get_font (node); font_size = (double)pango_font_description_get_size (desc) / PANGO_SCALE; if (pango_font_description_get_size_is_absolute (desc)) { *length = num->val * multiplier * font_size; } else { double resolution = eek_theme_context_get_resolution (node->context); *length = num->val * multiplier * (resolution / 72.) * font_size; } } break; default: g_assert_not_reached (); } return VALUE_FOUND; } static GetFromTermResult get_length_from_term_int (EekThemeNode *node, CRTerm *term, gboolean use_parent_font, gint *length) { double value; GetFromTermResult result; result = get_length_from_term (node, term, use_parent_font, &value); if (result == VALUE_FOUND) *length = (int) (0.5 + value); return result; } static GetFromTermResult get_length_internal (EekThemeNode *node, const char *property_name, const char *suffixed, gdouble *length) { int i; g_return_val_if_fail (EEK_IS_THEME_NODE(node), FALSE); ensure_properties (node); for (i = node->n_properties - 1; i >= 0; i--) { CRDeclaration *decl = node->properties[i]; if (strcmp (decl->property->stryng->str, property_name) == 0 || (suffixed != NULL && strcmp (decl->property->stryng->str, suffixed) == 0)) { GetFromTermResult result = get_length_from_term (node, decl->value, FALSE, length); if (result != VALUE_NOT_FOUND) return result; } } return VALUE_NOT_FOUND; } /** * eek_theme_node_lookup_length: * @node: a #EekThemeNode * @property_name: The name of the length property * @inherit: if %TRUE, if a value is not found for the property on the * node, then it will be looked up on the parent node, and then on the * parent's parent, and so forth. Note that if the property has a * value of 'inherit' it will be inherited even if %FALSE is passed * in for @inherit; this only affects the default behavior for inheritance. * @length: (out): location to store the length that was determined. * If the property is not found, the value in this location * will not be changed. The returned length is resolved * to pixels. * * Generically looks up a property containing a single length value. When * specific getters (like eek_theme_node_get_border_width()) exist, they * should be used instead. They are cached, so more efficient, and have * handling for shortcut properties and other details of CSS. * * See also eek_theme_node_get_length(), which provides a simpler API. * * Return value: %TRUE if the property was found in the properties for this * theme node (or in the properties of parent nodes when inheriting.) */ gboolean eek_theme_node_lookup_length (EekThemeNode *node, const char *property_name, gboolean inherit, gdouble *length) { GetFromTermResult result; g_return_val_if_fail (EEK_IS_THEME_NODE(node), FALSE); result = get_length_internal (node, property_name, NULL, length); if (result == VALUE_FOUND) return TRUE; else if (result == VALUE_INHERIT) inherit = TRUE; if (inherit && node->parent_node && eek_theme_node_lookup_length (node->parent_node, property_name, inherit, length)) return TRUE; else return FALSE; } /** * eek_theme_node_get_length: * @node: a #EekThemeNode * @property_name: The name of the length property * * Generically looks up a property containing a single length value. When * specific getters (like eek_theme_node_get_border_width()) exist, they * should be used instead. They are cached, so more efficient, and have * handling for shortcut properties and other details of CSS. * * Unlike eek_theme_node_get_color() and eek_theme_node_get_double(), * this does not print a warning if the property is not found; it just * returns 0. * * See also eek_theme_node_lookup_length(), which provides more options. * * Return value: the length, in pixels, or 0 if the property was not found. */ gdouble eek_theme_node_get_length (EekThemeNode *node, const char *property_name) { gdouble length; if (eek_theme_node_lookup_length (node, property_name, FALSE, &length)) return length; else return 0.0; } static void do_border_radius_term (EekThemeNode *node, CRTerm *term, gboolean topleft, gboolean topright, gboolean bottomright, gboolean bottomleft) { int value; g_return_if_fail (EEK_IS_THEME_NODE (node)); if (get_length_from_term_int (node, term, FALSE, &value) != VALUE_FOUND) return; if (topleft) node->border_radius[EEK_CORNER_TOPLEFT] = value; if (topright) node->border_radius[EEK_CORNER_TOPRIGHT] = value; if (bottomright) node->border_radius[EEK_CORNER_BOTTOMRIGHT] = value; if (bottomleft) node->border_radius[EEK_CORNER_BOTTOMLEFT] = value; } static void do_border_radius (EekThemeNode *node, CRDeclaration *decl) { const char *property_name = decl->property->stryng->str + 13; /* Skip 'border-radius' */ if (strcmp (property_name, "") == 0) { /* Slight deviation ... if we don't understand some of the terms and understand others, * then we set the ones we understand and ignore the others instead of ignoring the * whole thing */ if (decl->value == NULL) /* 0 values */ return; else if (decl->value->next == NULL) /* 1 value */ { do_border_radius_term (node, decl->value, TRUE, TRUE, TRUE, TRUE); /* all corners */ return; } else if (decl->value->next->next == NULL) /* 2 values */ { do_border_radius_term (node, decl->value, TRUE, FALSE, TRUE, FALSE); /* topleft/bottomright */ do_border_radius_term (node, decl->value->next, FALSE, TRUE, FALSE, TRUE); /* topright/bottomleft */ } else if (decl->value->next->next->next == NULL) /* 3 values */ { do_border_radius_term (node, decl->value, TRUE, FALSE, FALSE, FALSE); /* topleft */ do_border_radius_term (node, decl->value->next, FALSE, TRUE, FALSE, TRUE); /* topright/bottomleft */ do_border_radius_term (node, decl->value->next->next, FALSE, FALSE, TRUE, FALSE); /* bottomright */ } else if (decl->value->next->next->next->next == NULL) /* 4 values */ { do_border_radius_term (node, decl->value, TRUE, FALSE, FALSE, FALSE); /* topleft */ do_border_radius_term (node, decl->value->next, FALSE, TRUE, FALSE, FALSE); /* topright */ do_border_radius_term (node, decl->value->next->next, FALSE, FALSE, TRUE, FALSE); /* bottomright */ do_border_radius_term (node, decl->value->next->next->next, FALSE, FALSE, FALSE, TRUE); /* bottomleft */ } else { g_warning ("Too many values for border-radius property"); return; } } else { if (decl->value == NULL || decl->value->next != NULL) return; if (strcmp (property_name, "-topleft") == 0) do_border_radius_term (node, decl->value, TRUE, FALSE, FALSE, FALSE); else if (strcmp (property_name, "-topright") == 0) do_border_radius_term (node, decl->value, FALSE, TRUE, FALSE, FALSE); else if (strcmp (property_name, "-bottomright") == 0) do_border_radius_term (node, decl->value, FALSE, FALSE, TRUE, FALSE); else if (strcmp (property_name, "-bottomleft") == 0) do_border_radius_term (node, decl->value, FALSE, FALSE, FALSE, TRUE); } } static void do_border_property (EekThemeNode *node, CRDeclaration *decl) { const char *property_name = decl->property->stryng->str + 6; /* Skip 'border' */ EekSide side = (EekSide)-1; EekColor color; gboolean color_set = FALSE; int width = 0; /* suppress warning */ gboolean width_set = FALSE; int j; g_return_if_fail (EEK_IS_THEME_NODE (node)); if (g_str_has_prefix (property_name, "-radius")) { do_border_radius (node, decl); return; } if (g_str_has_prefix (property_name, "-left")) { side = EEK_SIDE_LEFT; property_name += 5; } else if (g_str_has_prefix (property_name, "-right")) { side = EEK_SIDE_RIGHT; property_name += 6; } else if (g_str_has_prefix (property_name, "-top")) { side = EEK_SIDE_TOP; property_name += 4; } else if (g_str_has_prefix (property_name, "-bottom")) { side = EEK_SIDE_BOTTOM; property_name += 7; } if (strcmp (property_name, "") == 0) { /* Set value for width/color/style in any order */ CRTerm *term; for (term = decl->value; term; term = term->next) { GetFromTermResult result; if (term->type == TERM_IDENT) { const char *ident = term->content.str->stryng->str; if (strcmp (ident, "none") == 0 || strcmp (ident, "hidden") == 0) { width = 0; width_set = TRUE; continue; } else if (strcmp (ident, "solid") == 0) { /* The only thing we support */ continue; } else if (strcmp (ident, "dotted") == 0 || strcmp (ident, "dashed") == 0 || strcmp (ident, "double") == 0 || strcmp (ident, "groove") == 0 || strcmp (ident, "ridge") == 0 || strcmp (ident, "inset") == 0 || strcmp (ident, "outset") == 0) { /* Treat the same as solid */ continue; } /* Presumably a color, fall through */ } if (term->type == TERM_NUMBER) { result = get_length_from_term_int (node, term, FALSE, &width); if (result != VALUE_NOT_FOUND) { width_set = result == VALUE_FOUND; continue; } } result = get_color_from_term (node, term, &color); if (result != VALUE_NOT_FOUND) { color_set = result == VALUE_FOUND; continue; } } } else if (strcmp (property_name, "-color") == 0) { if (decl->value == NULL || decl->value->next != NULL) return; if (get_color_from_term (node, decl->value, &color) == VALUE_FOUND) /* Ignore inherit */ color_set = TRUE; } else if (strcmp (property_name, "-width") == 0) { if (decl->value == NULL || decl->value->next != NULL) return; if (get_length_from_term_int (node, decl->value, FALSE, &width) == VALUE_FOUND) /* Ignore inherit */ width_set = TRUE; } if (side == (EekSide)-1) { for (j = 0; j < 4; j++) { if (color_set) node->border_color[j] = color; if (width_set) node->border_width[j] = width; } } else { if (color_set) node->border_color[side] = color; if (width_set) node->border_width[side] = width; } } void _eek_theme_node_ensure_geometry (EekThemeNode *node) { int i, j; g_return_if_fail (EEK_IS_THEME_NODE (node)); if (node->geometry_computed) return; node->geometry_computed = TRUE; ensure_properties (node); for (j = 0; j < 4; j++) { node->border_width[j] = 0; node->border_color[j] = TRANSPARENT_COLOR; } for (i = 0; i < node->n_properties; i++) { CRDeclaration *decl = node->properties[i]; const char *property_name = decl->property->stryng->str; if (g_str_has_prefix (property_name, "border")) do_border_property (node, decl); } } int eek_theme_node_get_border_width (EekThemeNode *node, EekSide side) { g_return_val_if_fail (EEK_IS_THEME_NODE (node), 0.); g_return_val_if_fail (side >= EEK_SIDE_TOP && side <= EEK_SIDE_LEFT, 0.); _eek_theme_node_ensure_geometry (node); return node->border_width[side]; } int eek_theme_node_get_border_radius (EekThemeNode *node, EekCorner corner) { g_return_val_if_fail (EEK_IS_THEME_NODE (node), 0.); g_return_val_if_fail (corner >= EEK_CORNER_TOPLEFT && corner <= EEK_CORNER_BOTTOMLEFT, 0.); _eek_theme_node_ensure_geometry (node); return node->border_radius[corner]; } static GetFromTermResult get_background_color_from_term (EekThemeNode *node, CRTerm *term, EekColor *color) { GetFromTermResult result = get_color_from_term (node, term, color); if (result == VALUE_NOT_FOUND) { if (term_is_transparent (term)) { *color = TRANSPARENT_COLOR; return VALUE_FOUND; } } return result; } void _eek_theme_node_ensure_background (EekThemeNode *node) { int i; if (node->background_computed) return; node->background_computed = TRUE; node->background_color = TRANSPARENT_COLOR; node->background_gradient_type = EEK_GRADIENT_NONE; ensure_properties (node); for (i = 0; i < node->n_properties; i++) { CRDeclaration *decl = node->properties[i]; const char *property_name = decl->property->stryng->str; if (g_str_has_prefix (property_name, "background")) property_name += 10; else continue; if (strcmp (property_name, "") == 0) { /* We're very liberal here ... if we recognize any term in the expression we take it, and * we ignore the rest. The actual specification is: * * background: [<'background-color'> || <'background-image'> || <'background-repeat'> || <'background-attachment'> || <'background-position'>] | inherit */ CRTerm *term; /* background: property sets all terms to specified or default values */ node->background_color = TRANSPARENT_COLOR; for (term = decl->value; term; term = term->next) { GetFromTermResult result = get_background_color_from_term (node, term, &node->background_color); if (result == VALUE_FOUND) { /* color stored in node->background_color */ } else if (result == VALUE_INHERIT) { if (node->parent_node) { eek_theme_node_get_background_color (node->parent_node, &node->background_color); } } else if (term_is_none (term)) { /* leave node->background_color as transparent */ } } } else if (strcmp (property_name, "-color") == 0) { GetFromTermResult result; if (decl->value == NULL || decl->value->next != NULL) continue; result = get_background_color_from_term (node, decl->value, &node->background_color); if (result == VALUE_FOUND) { /* color stored in node->background_color */ } else if (result == VALUE_INHERIT) { if (node->parent_node) eek_theme_node_get_background_color (node->parent_node, &node->background_color); } } else if (strcmp (property_name, "-gradient-direction") == 0) { CRTerm *term = decl->value; if (strcmp (term->content.str->stryng->str, "vertical") == 0) { node->background_gradient_type = EEK_GRADIENT_VERTICAL; } else if (strcmp (term->content.str->stryng->str, "horizontal") == 0) { node->background_gradient_type = EEK_GRADIENT_HORIZONTAL; } else if (strcmp (term->content.str->stryng->str, "radial") == 0) { node->background_gradient_type = EEK_GRADIENT_RADIAL; } else if (strcmp (term->content.str->stryng->str, "none") == 0) { node->background_gradient_type = EEK_GRADIENT_NONE; } else { g_warning ("Unrecognized background-gradient-direction \"%s\"", term->content.str->stryng->str); } } else if (strcmp (property_name, "-gradient-start") == 0) { get_color_from_term (node, decl->value, &node->background_color); } else if (strcmp (property_name, "-gradient-end") == 0) { get_color_from_term (node, decl->value, &node->background_gradient_end); } } } void eek_theme_node_get_background_color (EekThemeNode *node, EekColor *color) { g_return_if_fail (EEK_IS_THEME_NODE (node)); _eek_theme_node_ensure_background (node); *color = node->background_color; } void eek_theme_node_get_border_color (EekThemeNode *node, EekSide side, EekColor *color) { g_return_if_fail (EEK_IS_THEME_NODE (node)); g_return_if_fail (side >= EEK_SIDE_TOP && side <= EEK_SIDE_LEFT); _eek_theme_node_ensure_geometry (node); *color = node->border_color[side]; } void eek_theme_node_get_foreground_color (EekThemeNode *node, EekColor *color) { g_assert (EEK_IS_THEME_NODE (node)); if (!node->foreground_computed) { int i; node->foreground_computed = TRUE; ensure_properties (node); for (i = node->n_properties - 1; i >= 0; i--) { CRDeclaration *decl = node->properties[i]; if (strcmp (decl->property->stryng->str, "color") == 0) { GetFromTermResult result = get_color_from_term (node, decl->value, &node->foreground_color); if (result == VALUE_FOUND) goto out; else if (result == VALUE_INHERIT) break; } } if (node->parent_node) eek_theme_node_get_foreground_color (node->parent_node, &node->foreground_color); else node->foreground_color = BLACK_COLOR; /* default to black */ } out: *color = node->foreground_color; } void eek_theme_node_get_background_gradient (EekThemeNode *node, EekGradientType *type, EekColor *start, EekColor *end) { g_assert (EEK_IS_THEME_NODE (node)); _eek_theme_node_ensure_background (node); *type = node->background_gradient_type; if (*type != EEK_GRADIENT_NONE) { *start = node->background_color; *end = node->background_gradient_end; } } static gboolean font_family_from_terms (CRTerm *term, char **family) { GString *family_string; gboolean result = FALSE; gboolean last_was_quoted = FALSE; if (!term) return FALSE; family_string = g_string_new (NULL); while (term) { if (term->type != TERM_STRING && term->type != TERM_IDENT) { goto out; } if (family_string->len > 0) { if (term->the_operator != COMMA && term->the_operator != NO_OP) goto out; /* Can concatenate two bare words, but not two quoted strings */ if ((term->the_operator == NO_OP && last_was_quoted) || term->type == TERM_STRING) goto out; if (term->the_operator == NO_OP) g_string_append (family_string, " "); else g_string_append (family_string, ", "); } else { if (term->the_operator != NO_OP) goto out; } g_string_append (family_string, term->content.str->stryng->str); term = term->next; } result = TRUE; out: if (result) { *family = g_string_free (family_string, FALSE); return TRUE; } else { *family = g_string_free (family_string, TRUE); return FALSE; } } /* In points */ static int font_sizes[] = { 6 * 1024, /* xx-small */ 8 * 1024, /* x-small */ 10 * 1024, /* small */ 12 * 1024, /* medium */ 16 * 1024, /* large */ 20 * 1024, /* x-large */ 24 * 1024, /* xx-large */ }; static gboolean font_size_from_term (EekThemeNode *node, CRTerm *term, double *size) { if (term->type == TERM_IDENT) { double resolution = eek_theme_context_get_resolution (node->context); /* We work in integers to avoid double comparisons when converting back * from a size in pixels to a logical size. */ int size_points = (int)(0.5 + *size * (72. / resolution)); if (strcmp (term->content.str->stryng->str, "xx-small") == 0) size_points = font_sizes[0]; else if (strcmp (term->content.str->stryng->str, "x-small") == 0) size_points = font_sizes[1]; else if (strcmp (term->content.str->stryng->str, "small") == 0) size_points = font_sizes[2]; else if (strcmp (term->content.str->stryng->str, "medium") == 0) size_points = font_sizes[3]; else if (strcmp (term->content.str->stryng->str, "large") == 0) size_points = font_sizes[4]; else if (strcmp (term->content.str->stryng->str, "x-large") == 0) size_points = font_sizes[5]; else if (strcmp (term->content.str->stryng->str, "xx-large") == 0) size_points = font_sizes[6]; else if (strcmp (term->content.str->stryng->str, "smaller") == 0) { /* Find the standard size equal to or smaller than the current size */ int i = 0; while (i <= 6 && font_sizes[i] < size_points) i++; if (i > 6) { /* original size greater than any standard size */ size_points = (int)(0.5 + size_points / 1.2); } else { /* Go one smaller than that, if possible */ if (i > 0) i--; size_points = font_sizes[i]; } } else if (strcmp (term->content.str->stryng->str, "larger") == 0) { /* Find the standard size equal to or larger than the current size */ int i = 6; while (i >= 0 && font_sizes[i] > size_points) i--; if (i < 0) /* original size smaller than any standard size */ i = 0; /* Go one larger than that, if possible */ if (i < 6) i++; size_points = font_sizes[i]; } else { return FALSE; } *size = size_points * (resolution / 72.); return TRUE; } else if (term->type == TERM_NUMBER && term->content.num->type == NUM_PERCENTAGE) { *size *= term->content.num->val / 100.; return TRUE; } else if (get_length_from_term (node, term, TRUE, size) == VALUE_FOUND) { /* Convert from pixels to Pango units */ *size *= 1024; return TRUE; } return FALSE; } static gboolean font_weight_from_term (CRTerm *term, PangoWeight *weight, gboolean *weight_absolute) { if (term->type == TERM_NUMBER) { int weight_int; /* The spec only allows numeric weights from 100-900, though Pango * will handle any number. We just let anything through. */ if (term->content.num->type != NUM_GENERIC) return FALSE; weight_int = (int)(0.5 + term->content.num->val); *weight = weight_int; *weight_absolute = TRUE; } else if (term->type == TERM_IDENT) { /* FIXME: handle INHERIT */ if (strcmp (term->content.str->stryng->str, "bold") == 0) { *weight = PANGO_WEIGHT_BOLD; *weight_absolute = TRUE; } else if (strcmp (term->content.str->stryng->str, "normal") == 0) { *weight = PANGO_WEIGHT_NORMAL; *weight_absolute = TRUE; } else if (strcmp (term->content.str->stryng->str, "bolder") == 0) { *weight = PANGO_WEIGHT_BOLD; *weight_absolute = FALSE; } else if (strcmp (term->content.str->stryng->str, "lighter") == 0) { *weight = PANGO_WEIGHT_LIGHT; *weight_absolute = FALSE; } else { return FALSE; } } else { return FALSE; } return TRUE; } static gboolean font_style_from_term (CRTerm *term, PangoStyle *style) { if (term->type != TERM_IDENT) return FALSE; /* FIXME: handle INHERIT */ if (strcmp (term->content.str->stryng->str, "normal") == 0) *style = PANGO_STYLE_NORMAL; else if (strcmp (term->content.str->stryng->str, "oblique") == 0) *style = PANGO_STYLE_OBLIQUE; else if (strcmp (term->content.str->stryng->str, "italic") == 0) *style = PANGO_STYLE_ITALIC; else return FALSE; return TRUE; } static gboolean font_variant_from_term (CRTerm *term, PangoVariant *variant) { if (term->type != TERM_IDENT) return FALSE; /* FIXME: handle INHERIT */ if (strcmp (term->content.str->stryng->str, "normal") == 0) *variant = PANGO_VARIANT_NORMAL; else if (strcmp (term->content.str->stryng->str, "small-caps") == 0) *variant = PANGO_VARIANT_SMALL_CAPS; else return FALSE; return TRUE; } const PangoFontDescription * eek_theme_node_get_font (EekThemeNode *node) { /* Initialized despite _set flags to suppress compiler warnings */ PangoStyle font_style = PANGO_STYLE_NORMAL; gboolean font_style_set = FALSE; PangoVariant variant = PANGO_VARIANT_NORMAL; gboolean variant_set = FALSE; PangoWeight weight = PANGO_WEIGHT_NORMAL; gboolean weight_absolute = TRUE; gboolean weight_set = FALSE; double size = 0.; gboolean size_set = FALSE; char *family = NULL; double parent_size; int i; if (node->font_desc) return node->font_desc; node->font_desc = pango_font_description_copy (get_parent_font (node)); parent_size = pango_font_description_get_size (node->font_desc); if (!pango_font_description_get_size_is_absolute (node->font_desc)) { double resolution = eek_theme_context_get_resolution (node->context); parent_size *= (resolution / 72.); } ensure_properties (node); for (i = 0; i < node->n_properties; i++) { CRDeclaration *decl = node->properties[i]; if (strcmp (decl->property->stryng->str, "font") == 0) { PangoStyle tmp_style = PANGO_STYLE_NORMAL; PangoVariant tmp_variant = PANGO_VARIANT_NORMAL; PangoWeight tmp_weight = PANGO_WEIGHT_NORMAL; gboolean tmp_weight_absolute = TRUE; double tmp_size; CRTerm *term = decl->value; /* A font specification starts with node/variant/weight * in any order. Each is allowed to be specified only once, * but we don't enforce that. */ for (; term; term = term->next) { if (font_style_from_term (term, &tmp_style)) continue; if (font_variant_from_term (term, &tmp_variant)) continue; if (font_weight_from_term (term, &tmp_weight, &tmp_weight_absolute)) continue; break; } /* The size is mandatory */ if (term == NULL || term->type != TERM_NUMBER) { g_warning ("Size missing from font property"); continue; } tmp_size = parent_size; if (!font_size_from_term (node, term, &tmp_size)) { g_warning ("Couldn't parse size in font property"); continue; } term = term->next; if (term != NULL && term->type && TERM_NUMBER && term->the_operator == DIVIDE) { /* Ignore line-height specification */ term = term->next; } /* the font family is mandatory - it is a comma-separated list of * names. */ if (!font_family_from_terms (term, &family)) { g_warning ("Couldn't parse family in font property"); continue; } font_style = tmp_style; font_style_set = TRUE; weight = tmp_weight; weight_absolute = tmp_weight_absolute; weight_set = TRUE; variant = tmp_variant; variant_set = TRUE; size = tmp_size; size_set = TRUE; } else if (strcmp (decl->property->stryng->str, "font-family") == 0) { if (!font_family_from_terms (decl->value, &family)) { g_warning ("Couldn't parse family in font property"); continue; } } else if (strcmp (decl->property->stryng->str, "font-weight") == 0) { if (decl->value == NULL || decl->value->next != NULL) continue; if (font_weight_from_term (decl->value, &weight, &weight_absolute)) weight_set = TRUE; } else if (strcmp (decl->property->stryng->str, "font-style") == 0) { if (decl->value == NULL || decl->value->next != NULL) continue; if (font_style_from_term (decl->value, &font_style)) font_style_set = TRUE; } else if (strcmp (decl->property->stryng->str, "font-variant") == 0) { if (decl->value == NULL || decl->value->next != NULL) continue; if (font_variant_from_term (decl->value, &variant)) variant_set = TRUE; } else if (strcmp (decl->property->stryng->str, "font-size") == 0) { gdouble tmp_size; if (decl->value == NULL || decl->value->next != NULL) continue; tmp_size = parent_size; if (font_size_from_term (node, decl->value, &tmp_size)) { size = tmp_size; size_set = TRUE; } } } if (family) { pango_font_description_set_family (node->font_desc, family); g_free (family); } if (size_set) pango_font_description_set_absolute_size (node->font_desc, size); if (weight_set) { if (!weight_absolute) { /* bolder/lighter are supposed to switch between available styles, but with * font substitution, that gets to be a pretty fuzzy concept. So we use * a fixed step of 200. (The spec says 100, but that might not take us from * normal to bold. */ PangoWeight old_weight = pango_font_description_get_weight (node->font_desc); if (weight == PANGO_WEIGHT_BOLD) weight = old_weight + 200; else weight = old_weight - 200; if (weight < 100) weight = 100; if (weight > 900) weight = 900; } pango_font_description_set_weight (node->font_desc, weight); } if (font_style_set) pango_font_description_set_style (node->font_desc, font_style); if (variant_set) pango_font_description_set_variant (node->font_desc, variant); return node->font_desc; }